This commit is contained in:
Haoqun Jiang 2025-05-05 20:38:29 +00:00 committed by GitHub
commit 38a6c6a250
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 111 additions and 54 deletions

View File

@ -114,6 +114,12 @@ describe('compiler sfc: transform asset url', () => {
expect(code).toMatch(`"xlink:href": "#myCircle"`) expect(code).toMatch(`"xlink:href": "#myCircle"`)
}) })
// #9919
test('should transform subpath import paths', () => {
const { code } = compileWithAssetUrls(`<img src="#src/assets/vue.svg" />`)
expect(code).toContain(`_imports_0 from '#src/assets/vue.svg'`)
})
test('should allow for full base URLs, with paths', () => { test('should allow for full base URLs, with paths', () => {
const { code } = compileWithAssetUrls(`<img src="./logo.png" />`, { const { code } = compileWithAssetUrls(`<img src="./logo.png" />`, {
base: 'http://localhost:3000/src/', base: 'http://localhost:3000/src/',

View File

@ -3,7 +3,12 @@ import { isString } from '@vue/shared'
export function isRelativeUrl(url: string): boolean { export function isRelativeUrl(url: string): boolean {
const firstChar = url.charAt(0) const firstChar = url.charAt(0)
return firstChar === '.' || firstChar === '~' || firstChar === '@' return (
firstChar === '.' ||
firstChar === '~' ||
firstChar === '@' ||
firstChar === '#'
)
} }
const externalRE = /^(https?:)?\/\// const externalRE = /^(https?:)?\/\//

View File

@ -101,13 +101,19 @@ export const transformAssetUrl: NodeTransform = (
const assetAttrs = (attrs || []).concat(wildCardAttrs || []) const assetAttrs = (attrs || []).concat(wildCardAttrs || [])
node.props.forEach((attr, index) => { 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 ( if (
attr.type !== NodeTypes.ATTRIBUTE || attr.type !== NodeTypes.ATTRIBUTE ||
!assetAttrs.includes(attr.name) || !assetAttrs.includes(attr.name) ||
!attr.value || !attr.value ||
isExternalUrl(attr.value.content) || isExternalUrl(attr.value.content) ||
isDataUrl(attr.value.content) || isDataUrl(attr.value.content) ||
attr.value.content[0] === '#' || isHashFragment ||
(!options.includeAbsolute && !isRelativeUrl(attr.value.content)) (!options.includeAbsolute && !isRelativeUrl(attr.value.content))
) { ) {
return 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( function getImportsExpressionExp(
path: string | null, path: string | null,
hash: string | null, hash: string | null,
loc: SourceLocation, loc: SourceLocation,
context: TransformContext, context: TransformContext,
): ExpressionNode { ): ExpressionNode {
if (path) { // Neither path nor hash - return empty string
let name: string if (!path && !hash) {
let exp: SimpleExpressionNode return createSimpleExpression(`''`, false, loc, ConstantTypes.CAN_STRINGIFY)
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), // Only hash without path - treat hash as the import source (likely a subpath import)
// so we decode it back in case it is encoded if (!path && hash) {
context.imports.push({ const { exp } = resolveOrRegisterImport(hash, loc, context)
exp, return exp
path: decodeURIComponent(path), }
})
}
if (!hash) { // Only path without hash - straightforward import
return exp if (path && !hash) {
} const { exp } = resolveOrRegisterImport(path, loc, context)
return exp
}
const hashExp = `${name} + '${hash}'` // At this point, we know we have both path and hash components
const finalExp = createSimpleExpression( const { name } = resolveOrRegisterImport(path!, loc, context)
hashExp,
// 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
}
// 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, false,
loc, loc,
ConstantTypes.CAN_STRINGIFY, 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)
} }