@@ -13,6 +13,7 @@ import {
1313 isDataUrl ,
1414 isExternalUrl ,
1515 isRelativeUrl ,
16+ normalizeDecodedImportPath ,
1617 parseUrl ,
1718} from './templateUtils'
1819import { isArray } from '@vue/shared'
@@ -34,14 +35,20 @@ export interface AssetURLOptions {
3435 tags ?: AssetURLTagConfig
3536}
3637
38+ // Built-in attrs that always represent resource URLs. `use` is intentionally
39+ // omitted because its hash-only values may still be fragment references.
40+ const resourceUrlTagConfig : AssetURLTagConfig = {
41+ video : [ 'src' , 'poster' ] ,
42+ source : [ 'src' ] ,
43+ img : [ 'src' ] ,
44+ image : [ 'xlink:href' , 'href' ] ,
45+ }
46+
3747export const defaultAssetUrlOptions : Required < AssetURLOptions > = {
3848 base : null ,
3949 includeAbsolute : false ,
4050 tags : {
41- video : [ 'src' , 'poster' ] ,
42- source : [ 'src' ] ,
43- img : [ 'src' ] ,
44- image : [ 'xlink:href' , 'href' ] ,
51+ ...resourceUrlTagConfig ,
4552 use : [ 'xlink:href' , 'href' ] ,
4653 } ,
4754}
@@ -69,6 +76,10 @@ export const createAssetUrlTransformWithOptions = (
6976 ( transformAssetUrl as Function ) ( node , context , options )
7077}
7178
79+ function canTransformHashImport ( tag : string , attrName : string ) : boolean {
80+ return ! ! resourceUrlTagConfig [ tag ] ?. includes ( attrName )
81+ }
82+
7283/**
7384 * A `@vue/compiler-core` plugin that transforms relative asset urls into
7485 * either imports or absolute urls.
@@ -104,17 +115,24 @@ export const transformAssetUrl: NodeTransform = (
104115 if (
105116 attr . type !== NodeTypes . ATTRIBUTE ||
106117 ! assetAttrs . includes ( attr . name ) ||
107- ! attr . value ||
108- isExternalUrl ( attr . value . content ) ||
109- isDataUrl ( attr . value . content ) ||
110- attr . value . content [ 0 ] === '#' ||
111- ( ! options . includeAbsolute && ! isRelativeUrl ( attr . value . content ) )
118+ ! attr . value
112119 ) {
113120 return
114121 }
115122
116- const url = parseUrl ( attr . value . content )
117- if ( options . base && attr . value . content [ 0 ] === '.' ) {
123+ const urlValue = attr . value . content
124+ const isHashOnlyValue = urlValue [ 0 ] === '#'
125+ if (
126+ isExternalUrl ( urlValue ) ||
127+ isDataUrl ( urlValue ) ||
128+ ( isHashOnlyValue && ! canTransformHashImport ( node . tag , attr . name ) ) ||
129+ ( ! options . includeAbsolute && ! isRelativeUrl ( urlValue ) )
130+ ) {
131+ return
132+ }
133+
134+ const url = parseUrl ( urlValue )
135+ if ( options . base && urlValue [ 0 ] === '.' ) {
118136 // explicit base - directly rewrite relative urls into absolute url
119137 // to avoid generating extra imports
120138 // Allow for full hostnames provided in options.base
@@ -147,70 +165,113 @@ export const transformAssetUrl: NodeTransform = (
147165 }
148166}
149167
168+ /**
169+ * Resolves or registers an import for the given source path
170+ * @param source - Path to resolve import for
171+ * @param loc - Source location
172+ * @param context - Transform context
173+ * @returns Object containing import name and expression
174+ */
175+ function resolveOrRegisterImport (
176+ source : string ,
177+ loc : SourceLocation ,
178+ context : TransformContext ,
179+ ) : {
180+ name : string
181+ exp : SimpleExpressionNode
182+ } {
183+ const normalizedSource = normalizeDecodedImportPath ( source )
184+ const existingIndex = context . imports . findIndex (
185+ i => i . path === normalizedSource ,
186+ )
187+ if ( existingIndex > - 1 ) {
188+ return {
189+ name : `_imports_${ existingIndex } ` ,
190+ exp : context . imports [ existingIndex ] . exp as SimpleExpressionNode ,
191+ }
192+ }
193+
194+ const name = `_imports_${ context . imports . length } `
195+ const exp = createSimpleExpression (
196+ name ,
197+ false ,
198+ loc ,
199+ ConstantTypes . CAN_STRINGIFY ,
200+ )
201+
202+ // We need to ensure the path is not encoded (to %2F),
203+ // so we decode it back in case it is encoded
204+ context . imports . push ( {
205+ exp,
206+ path : normalizedSource ,
207+ } )
208+
209+ return { name, exp }
210+ }
211+
212+ /**
213+ * Transforms asset URLs into import expressions or string literals
214+ */
150215function getImportsExpressionExp (
151216 path : string | null ,
152217 hash : string | null ,
153218 loc : SourceLocation ,
154219 context : TransformContext ,
155220) : ExpressionNode {
156- if ( path ) {
157- let name : string
158- let exp : SimpleExpressionNode
159- const existingIndex = context . imports . findIndex ( i => i . path === path )
160- if ( existingIndex > - 1 ) {
161- name = `_imports_${ existingIndex } `
162- exp = context . imports [ existingIndex ] . exp as SimpleExpressionNode
163- } else {
164- name = `_imports_${ context . imports . length } `
165- exp = createSimpleExpression (
166- name ,
167- false ,
168- loc ,
169- ConstantTypes . CAN_STRINGIFY ,
170- )
171-
172- // We need to ensure the path is not encoded (to %2F),
173- // so we decode it back in case it is encoded
174- context . imports . push ( {
175- exp,
176- path : decodeURIComponent ( path ) ,
177- } )
178- }
221+ // Neither path nor hash - return empty string
222+ if ( ! path && ! hash ) {
223+ return createSimpleExpression ( `''` , false , loc , ConstantTypes . CAN_STRINGIFY )
224+ }
179225
180- if ( ! hash ) {
181- return exp
182- }
226+ // Only hash without path - treat hash as the import source (likely a subpath import)
227+ if ( ! path && hash ) {
228+ const { exp } = resolveOrRegisterImport ( hash , loc , context )
229+ return exp
230+ }
231+
232+ // Only path without hash - straightforward import
233+ if ( path && ! hash ) {
234+ const { exp } = resolveOrRegisterImport ( path , loc , context )
235+ return exp
236+ }
237+
238+ // At this point, we know we have both path and hash components
239+ const { name } = resolveOrRegisterImport ( path ! , loc , context )
240+
241+ // Combine path import with hash
242+ const hashExp = `${ name } + '${ hash } '`
243+ const finalExp = createSimpleExpression (
244+ hashExp ,
245+ false ,
246+ loc ,
247+ ConstantTypes . CAN_STRINGIFY ,
248+ )
183249
184- const hashExp = `${ name } + '${ hash } '`
185- const finalExp = createSimpleExpression (
186- hashExp ,
250+ // No hoisting needed
251+ if ( ! context . hoistStatic ) {
252+ return finalExp
253+ }
254+
255+ // Check for existing hoisted expression
256+ const existingHoistIndex = context . hoists . findIndex ( h => {
257+ return (
258+ h &&
259+ h . type === NodeTypes . SIMPLE_EXPRESSION &&
260+ ! h . isStatic &&
261+ h . content === hashExp
262+ )
263+ } )
264+
265+ // Return existing hoisted expression if found
266+ if ( existingHoistIndex > - 1 ) {
267+ return createSimpleExpression (
268+ `_hoisted_${ existingHoistIndex + 1 } ` ,
187269 false ,
188270 loc ,
189271 ConstantTypes . CAN_STRINGIFY ,
190272 )
191-
192- if ( ! context . hoistStatic ) {
193- return finalExp
194- }
195-
196- const existingHoistIndex = context . hoists . findIndex ( h => {
197- return (
198- h &&
199- h . type === NodeTypes . SIMPLE_EXPRESSION &&
200- ! h . isStatic &&
201- h . content === hashExp
202- )
203- } )
204- if ( existingHoistIndex > - 1 ) {
205- return createSimpleExpression (
206- `_hoisted_${ existingHoistIndex + 1 } ` ,
207- false ,
208- loc ,
209- ConstantTypes . CAN_STRINGIFY ,
210- )
211- }
212- return context . hoist ( finalExp )
213- } else {
214- return createSimpleExpression ( `''` , false , loc , ConstantTypes . CAN_STRINGIFY )
215273 }
274+
275+ // Hoist the expression and return the hoisted expression
276+ return context . hoist ( finalExp )
216277}
0 commit comments