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)
}