-
-
Notifications
You must be signed in to change notification settings - Fork 8.9k
fix(compiler-sfc): make transformAssetUrl
compatible with Node.js subpath imports patterns
#13045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Size ReportBundles
Usages
|
@vue/compiler-core
@vue/compiler-dom
@vue/compiler-sfc
@vue/compiler-ssr
@vue/reactivity
@vue/runtime-core
@vue/runtime-dom
@vue/server-renderer
@vue/shared
vue
@vue/compat
commit: |
/ecosystem-ci run |
📝 Ran ecosystem CI: Open
|
LGTM |
WalkthroughAdds support for transforming Node.js subpath import-style URLs beginning with “#” in asset URLs by updating relative URL detection and transform logic, refines hash-fragment skipping to only SVG fragment refs, introduces an import registration helper, adjusts hoisting for path+hash cases, and adds a test for “#src/…”. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor SFC as SFC Template
participant T as transformAssetUrl
participant U as isRelativeUrl
participant R as resolveOrRegisterImport
participant C as Context(hoist/imports)
SFC->>T: Encounter asset URL (e.g., "#src/assets/vue.svg")
T->>U: isRelativeUrl(url)
U-->>T: true (with '#' now treated as relative)
alt SVG <use> with href starting "#"
T-->>SFC: Skip transform (hash fragment)
else Other tags/attrs
alt url has path and/or hash
opt path part exists
T->>R: register path import
R->>C: reuse or add _imports_N
R-->>T: { name, exp }
end
opt hash part exists
T->>R: register hash import (if treated as source)
R-->>T: { name, exp }
end
alt hoist combined (path + hash) and hoist enabled
T->>C: hoist or reuse _hoisted_N
C-->>T: hoisted expression
end
T-->>SFC: Transformed expression
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
transformAssetUrl
compatible with Node.js subpath imports patternstransformAssetUrl
compatible with Node.js subpath imports patterns
transformAssetUrl
compatible with Node.js subpath imports patternstransformAssetUrl
compatible with Node.js subpath imports patterns
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (4)
packages/compiler-sfc/src/template/transformAssetUrl.ts (3)
206-215
: Only-hash case: treat bare '#' as empty, not an importA literal
src="#"
(placeholder) would now generateimport _imports_0 from '#';
, which is invalid. Guard forhash.length === 1
and return''
.Apply this diff:
- if (!path && hash) { + if (!path && hash) { + if (hash.length === 1) { + return createSimpleExpression(`''`, false, loc, ConstantTypes.CAN_STRINGIFY) + } const { exp } = resolveOrRegisterImport(hash, loc, context) return exp }
223-238
: Const type for dynamic concat may be too optimistic
name + '${hash}'
isn’t a static string; usingConstantTypes.CAN_STRINGIFY
could mislead later passes. ConsiderConstantTypes.NOT_CONSTANT
here to reflect dynamism.Apply this diff:
- const finalExp = createSimpleExpression( + const finalExp = createSimpleExpression( hashExp, false, loc, - ConstantTypes.CAN_STRINGIFY, + ConstantTypes.NOT_CONSTANT, )
240-258
: Linear scan for hoist reuse is fine; optional memoizationThe O(n) scan over
context.hoists
is acceptable here. If this becomes hot, consider a small map keyed byhashExp
to avoid repeated scans.packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts (1)
117-121
: Good coverage for subpath import pathsTest asserts emitted import for
#src/...
as intended.Add two follow-ups:
- Dedupe encoded vs decoded paths:
+ test('dedupes encoded vs decoded asset paths', () => { + const { code } = compileWithAssetUrls( + `<div><img src="./foo%20bar.png"/><img src="./foo bar.png"/></div>` + ) + expect(code.match(/import _imports_\d+ from '\.\/foo bar\.png'/g)?.length).toBe(1) + })
- Bare
#
placeholder doesn’t import:+ test('does not import bare hash placeholder', () => { + const { code } = compileWithAssetUrls(`<img src="#" />`) + expect(code).not.toMatch(`from '#'`) + })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
packages/compiler-sfc/__tests__/templateTransformAssetUrl.spec.ts
(1 hunks)packages/compiler-sfc/src/template/templateUtils.ts
(1 hunks)packages/compiler-sfc/src/template/transformAssetUrl.ts
(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/compiler-sfc/src/template/transformAssetUrl.ts (2)
packages/compiler-sfc/src/template/templateUtils.ts (2)
isExternalUrl
(15-17)isDataUrl
(20-22)packages/compiler-core/src/ast.ts (4)
SourceLocation
(77-81)SimpleExpressionNode
(225-247)createSimpleExpression
(685-698)ExpressionNode
(91-91)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: test / e2e-test
- GitHub Check: test / unit-test-windows
🔇 Additional comments (4)
packages/compiler-sfc/src/template/templateUtils.ts (1)
6-11
: Adding '#' to relative URL check aligns with subpath importsChange looks correct and scoped. The transform now considers
#
-prefixed specifiers as relative while true hash fragments are filtered in transformAssetUrl. No further action.packages/compiler-sfc/src/template/transformAssetUrl.ts (3)
104-118
: Limit hash-fragment skip to hrefs — good guardrailScoping the “hash fragment” exclusion to
<use>
+ (href
|xlink:href
) avoids skipping Node-style#
imports elsewhere. This is the right fix.
217-221
: Path-only import branch — LGTMStraightforward reuse of the new helper; maintains prior behavior.
260-262
: Hoisting path+hash expression — LGTMUses existing hoist API correctly and returns the hoisted node.
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 } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Dedupe mismatch when paths differ only by encoding; also guard decode errors
findIndex(i => i.path === source)
compares encoded input against a decoded value you push (decodeURIComponent(source)
), so 'foo%20bar.svg'
and 'foo bar.svg'
won’t dedupe. Also, decodeURIComponent
can throw on malformed encodings.
Refactor to normalize once, use it for lookup and push, and catch decode errors.
Apply this diff:
- const existingIndex = context.imports.findIndex(i => i.path === source)
+ // Normalize once for stable matching (handles 'foo%20bar.svg' vs 'foo bar.svg')
+ let normalized = source
+ try {
+ normalized = decodeURIComponent(source)
+ } catch {
+ // keep original if not valid percent-encoding
+ }
+ const existingIndex = context.imports.findIndex(i => i.path === normalized)
@@
- context.imports.push({
- exp,
- path: decodeURIComponent(source),
- })
+ context.imports.push({
+ exp,
+ path: normalized,
+ })
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
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 } | |
} | |
function resolveOrRegisterImport( | |
source: string, | |
loc: SourceLocation, | |
context: TransformContext, | |
): { | |
name: string | |
exp: SimpleExpressionNode | |
} { | |
// Normalize once for stable matching (handles 'foo%20bar.svg' vs 'foo bar.svg') | |
let normalized = source | |
try { | |
normalized = decodeURIComponent(source) | |
} catch { | |
// keep original if not valid percent-encoding | |
} | |
const existingIndex = context.imports.findIndex(i => i.path === normalized) | |
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, | |
) | |
context.imports.push({ | |
exp, | |
path: normalized, | |
}) | |
return { name, exp } | |
} |
🤖 Prompt for AI Agents
In packages/compiler-sfc/src/template/transformAssetUrl.ts around lines 163 to
195, the import dedupe logic currently compares the raw source against a decoded
path and calls decodeURIComponent without handling errors; normalize the source
once into a safePath variable by attempting decodeURIComponent(source) inside a
try/catch (on decode error, fall back to the original source), use safePath for
both the findIndex lookup and for the path stored on the pushed import, and keep
the created exp/name logic unchanged so the same normalized path dedupes
correctly even when inputs differ only by encoding.
Fixes #9919
The implementation is quite straightforward as shown in the first commit.
However I found the
getImportsExpressionExp
is getting more and more complex, so I refactored it to improve readability.I can revert that commit if it presents a hurdle for merging this PR. But IMO, with so many existing test cases, the refactoring is quite safe.
Summary by CodeRabbit
New Features
Bug Fixes
Tests