diff --git a/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts index a1f62f43a3c..0bbbf57a08c 100644 --- a/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts +++ b/packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts @@ -114,6 +114,12 @@ describe('compiler sfc: transform asset url', () => { expect(code).toMatch(`"xlink:href": "#myCircle"`) }) + // #9919 + test('should transform subpath import paths', () => { + const { code } = compileWithAssetUrls(``) + expect(code).toContain(`_imports_0 from '#src/assets/vue.svg'`) + }) + test('should allow for full base URLs, with paths', () => { const { code } = compileWithAssetUrls(``, { base: 'http://localhost:3000/src/', diff --git a/packages/compiler-sfc/src/template/templateUtils.ts b/packages/compiler-sfc/src/template/templateUtils.ts index c1414d1ecbd..9e579fcdb67 100644 --- a/packages/compiler-sfc/src/template/templateUtils.ts +++ b/packages/compiler-sfc/src/template/templateUtils.ts @@ -3,7 +3,12 @@ import { isString } from '@vue/shared' export function isRelativeUrl(url: string): boolean { const firstChar = url.charAt(0) - return firstChar === '.' || firstChar === '~' || firstChar === '@' + return ( + firstChar === '.' || + firstChar === '~' || + firstChar === '@' || + firstChar === '#' + ) } const externalRE = /^(https?:)?\/\// diff --git a/packages/compiler-sfc/src/template/transformAssetUrl.ts b/packages/compiler-sfc/src/template/transformAssetUrl.ts index 6291e21bbba..ee409d5c8c9 100644 --- a/packages/compiler-sfc/src/template/transformAssetUrl.ts +++ b/packages/compiler-sfc/src/template/transformAssetUrl.ts @@ -101,13 +101,19 @@ export const transformAssetUrl: NodeTransform = ( const assetAttrs = (attrs || []).concat(wildCardAttrs || []) node.props.forEach((attr, index) => { + const isHashFragment = + node.tag === 'use' && + attr.type === NodeTypes.ATTRIBUTE && + (attr.name === 'href' || attr.name === 'xlink:href') && + attr.value?.content[0] === '#' + if ( attr.type !== NodeTypes.ATTRIBUTE || !assetAttrs.includes(attr.name) || !attr.value || isExternalUrl(attr.value.content) || isDataUrl(attr.value.content) || - attr.value.content[0] === '#' || + isHashFragment || (!options.includeAbsolute && !isRelativeUrl(attr.value.content)) ) { return @@ -147,70 +153,110 @@ export const transformAssetUrl: NodeTransform = ( } } +/** + * Resolves or registers an import for the given source path + * @param source - Path to resolve import for + * @param loc - Source location + * @param context - Transform context + * @returns Object containing import name and expression + */ +function resolveOrRegisterImport( + source: string, + loc: SourceLocation, + context: TransformContext, +): { + name: string + exp: SimpleExpressionNode +} { + const existingIndex = context.imports.findIndex(i => i.path === source) + if (existingIndex > -1) { + return { + name: `_imports_${existingIndex}`, + exp: context.imports[existingIndex].exp as SimpleExpressionNode, + } + } + + const name = `_imports_${context.imports.length}` + const exp = createSimpleExpression( + name, + false, + loc, + ConstantTypes.CAN_STRINGIFY, + ) + + // We need to ensure the path is not encoded (to %2F), + // so we decode it back in case it is encoded + context.imports.push({ + exp, + path: decodeURIComponent(source), + }) + + return { name, exp } +} + +/** + * Transforms asset URLs into import expressions or string literals + */ function getImportsExpressionExp( path: string | null, hash: string | null, loc: SourceLocation, context: TransformContext, ): ExpressionNode { - if (path) { - let name: string - let exp: SimpleExpressionNode - const existingIndex = context.imports.findIndex(i => i.path === path) - if (existingIndex > -1) { - name = `_imports_${existingIndex}` - exp = context.imports[existingIndex].exp as SimpleExpressionNode - } else { - name = `_imports_${context.imports.length}` - exp = createSimpleExpression( - name, - false, - loc, - ConstantTypes.CAN_STRINGIFY, - ) - - // We need to ensure the path is not encoded (to %2F), - // so we decode it back in case it is encoded - context.imports.push({ - exp, - path: decodeURIComponent(path), - }) - } + // Neither path nor hash - return empty string + if (!path && !hash) { + return createSimpleExpression(`''`, false, loc, ConstantTypes.CAN_STRINGIFY) + } - if (!hash) { - return exp - } + // Only hash without path - treat hash as the import source (likely a subpath import) + if (!path && hash) { + const { exp } = resolveOrRegisterImport(hash, loc, context) + return exp + } + + // Only path without hash - straightforward import + if (path && !hash) { + const { exp } = resolveOrRegisterImport(path, loc, context) + return exp + } + + // At this point, we know we have both path and hash components + const { name } = resolveOrRegisterImport(path!, loc, context) + + // Combine path import with hash + const hashExp = `${name} + '${hash}'` + const finalExp = createSimpleExpression( + hashExp, + false, + loc, + ConstantTypes.CAN_STRINGIFY, + ) + + // No hoisting needed + if (!context.hoistStatic) { + return finalExp + } - const hashExp = `${name} + '${hash}'` - const finalExp = createSimpleExpression( - hashExp, + // Check for existing hoisted expression + const existingHoistIndex = context.hoists.findIndex(h => { + return ( + h && + h.type === NodeTypes.SIMPLE_EXPRESSION && + !h.isStatic && + h.content === hashExp + ) + }) + + // Return existing hoisted expression if found + if (existingHoistIndex > -1) { + return createSimpleExpression( + `_hoisted_${existingHoistIndex + 1}`, false, loc, ConstantTypes.CAN_STRINGIFY, ) - - if (!context.hoistStatic) { - return finalExp - } - - const existingHoistIndex = context.hoists.findIndex(h => { - return ( - h && - h.type === NodeTypes.SIMPLE_EXPRESSION && - !h.isStatic && - h.content === hashExp - ) - }) - if (existingHoistIndex > -1) { - return createSimpleExpression( - `_hoisted_${existingHoistIndex + 1}`, - false, - loc, - ConstantTypes.CAN_STRINGIFY, - ) - } - return context.hoist(finalExp) - } else { - return createSimpleExpression(`''`, false, loc, ConstantTypes.CAN_STRINGIFY) } + + // Hoist the expression and return the hoisted expression + return context.hoist(finalExp) }