-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
fix(nuxt): do not tree-shake composables within other composables #33153
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
Conversation
|
|
|
Coincidentally, the recent changes came in handy right away. 😅 |
|
Pesky autofix... wouldn't it be possible to prevent it from updating inline snapshots? I feel like it might be quite easy to miss that a snapshot was updated in a larger PR. |
@nuxt/kit
nuxt
@nuxt/rspack-builder
@nuxt/schema
@nuxt/vite-builder
@nuxt/webpack-builder
commit: |
CodSpeed Performance ReportMerging #33153 will not alter performanceComparing Summary
Footnotes |
|
(in the interim I've ignored errors when calling |
# Conflicts: # packages/nuxt/src/core/plugins/tree-shake.ts
danielroe
left a comment
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.
one thought - now that we are using AST-aware parsing, could we safely remove the composable entirely rather than adding false && /*@__PURE__*/?
WalkthroughThe AST walker’s enter callback now performs the AST overwrite without a surrounding try/catch: it calls overwrite(...) directly and immediately invokes this.skip(). The unconditional replacement string remains Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Pre-merge checks (4 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. 📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
⏰ 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). (16)
✨ 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. Comment |
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: 0
🧹 Nitpick comments (3)
packages/nuxt/src/core/plugins/tree-shake.ts (2)
86-86: Wrap the replacement in parentheses to preserve operator precedence in all contexts.Helps when the transformed call appears inside larger expressions (arguments, logical chains, ternaries).
Apply:
- s.overwrite(node.start, node.end, ` false && /*@__PURE__*/ ${functionName}${code.slice(node.callee.end, node.end)}`) + s.overwrite(node.start, node.end, `(false && /*@__PURE__*/ ${functionName}${code.slice(node.callee.end, node.end)})`)
85-85: Follow-through on the TODO: consult the real auto-imports registry.Wire an optional predicate into plugin options so we don’t rely solely on name heuristics.
Example API shape:
interface TreeShakeComposablesPluginOptions { sourcemap?: boolean composables: Record<ImportPath, string[]> isAutoImported?: (name: string) => boolean }And in the guard (near Line 81):
if (!scopeTrackerNode && !(allComposableNames.has(functionName) || options.isAutoImported?.(functionName))) { return }packages/nuxt/test/tree-shake.test.ts (1)
71-94: Great regression test for nested composables — confirms new skip behaviour.Consider adding an async-nested variant to cover the reported pattern with async arrows.
You could duplicate this case with:
onMounted(async () => { onMounted(() => {}) })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
packages/nuxt/package.jsonis excluded by!**/package.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!pnpm-lock.yaml
📒 Files selected for processing (2)
packages/nuxt/src/core/plugins/tree-shake.ts(1 hunks)packages/nuxt/test/tree-shake.test.ts(3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Follow standard TypeScript conventions and best practices
Files:
packages/nuxt/src/core/plugins/tree-shake.tspackages/nuxt/test/tree-shake.test.ts
**/*.{test,spec}.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Write unit tests for core functionality using
vitest
Files:
packages/nuxt/test/tree-shake.test.ts
⏰ 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). (1)
- GitHub Check: typecheck (windows-latest, bundler)
🔇 Additional comments (4)
packages/nuxt/src/core/plugins/tree-shake.ts (1)
85-87: Skipping children after overwrite prevents overlapping edits — good fix.Calling this.skip() right after s.overwrite() cleanly avoids re-walking nested nodes and the “Cannot split a chunk that has already been edited” error path.
packages/nuxt/test/tree-shake.test.ts (3)
95-119: Shadowing tests: formatting-only changes look fine.Assertions still validate correct non-shaking of locally shadowed identifiers.
135-141: Variable shadowing test updates look good.Behavioural expectations unchanged; snapshot still exercises the guard.
71-94: Drop oxc-walker dependency bump check
Repository contains no direct “oxc-walker” dependency in any package.json; this verification isn’t applicable.Likely an incorrect or invalid review comment.
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: 0
🧹 Nitpick comments (2)
packages/nuxt/src/core/plugins/tree-shake.ts (1)
47-52: Guard against non-statement contexts (potential syntax break).If a tree-shakable composable were ever used in a non-statement position (e.g., as an argument, in a sequence, or assigned), a raw
s.remove(node.start, node.end)could yield invalid JS (e.g.,foo(, 1)orconst x = ;). Today you only include lifecycle composables, so this is unlikely, but worth hardening.Suggested approach (optional):
- Operate at
ExpressionStatementlevel: only remove when the parent is anExpressionStatementwhoseexpressionis the targetCallExpression.- Otherwise, neutralise with a benign expression (e.g.,
void 0) to keep syntax valid.I can send a follow-up PR to reframe the visitor to match
ExpressionStatementand handle the fallback path if you’d like.Would you confirm that
options.composablesis guaranteed to list only lifecycle-style, statement-only calls in Nuxt 4.x?Also applies to: 85-87
packages/nuxt/test/tree-shake.test.ts (1)
5-10: Add a few safety tests for non-statement contexts (optional).To guard against future additions to
composables, consider adding cases like:
foo(onMounted(() => {}))const x = onMounted(() => {})[onMounted(() => {}), 1]These will catch accidental invalid code generation if the plugin ever targets non-statement usages.
Happy to draft these tests if you want.
Also applies to: 66-81
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
packages/nuxt/src/core/plugins/tree-shake.ts(1 hunks)packages/nuxt/test/tree-shake.test.ts(4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Follow standard TypeScript conventions and best practices
Files:
packages/nuxt/src/core/plugins/tree-shake.tspackages/nuxt/test/tree-shake.test.ts
**/*.{test,spec}.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Write unit tests for core functionality using
vitest
Files:
packages/nuxt/test/tree-shake.test.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: GalacticHypernova
PR: nuxt/nuxt#26468
File: packages/nuxt/src/components/plugins/loader.ts:24-24
Timestamp: 2024-11-05T15:22:54.759Z
Learning: In `packages/nuxt/src/components/plugins/loader.ts`, the references to `resolve` and `distDir` are legacy code from before Nuxt used the new unplugin VFS and will be removed.
📚 Learning: 2024-11-05T15:22:54.759Z
Learnt from: GalacticHypernova
PR: nuxt/nuxt#26468
File: packages/nuxt/src/components/plugins/loader.ts:24-24
Timestamp: 2024-11-05T15:22:54.759Z
Learning: In `packages/nuxt/src/components/plugins/loader.ts`, the references to `resolve` and `distDir` are legacy code from before Nuxt used the new unplugin VFS and will be removed.
Applied to files:
packages/nuxt/src/core/plugins/tree-shake.ts
⏰ 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). (14)
- GitHub Check: test-fixtures (windows-latest, dev, vite, default, manifest-off, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, built, webpack, async, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-off, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, built, vite, default, manifest-on, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, js, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, built, vite, default, manifest-off, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-off, json, lts/-1)
- GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-on, json, lts/-1)
- GitHub Check: release-pkg-pr-new
- GitHub Check: test-size
- GitHub Check: test-benchmark
- GitHub Check: typecheck (ubuntu-latest, bundler)
- GitHub Check: code
🔇 Additional comments (6)
packages/nuxt/src/core/plugins/tree-shake.ts (1)
85-87: Good fix: remove and skip to avoid double edits.Directly removing the node and calling
this.skip()prevents nested composables from being traversed and re-edited, addressing the “already been edited” error and matching the new tests.packages/nuxt/test/tree-shake.test.ts (5)
17-17: Snapshot update aligns with new removal strategy.Expecting only
console.log('Hello World')is consistent with direct removal of the call expression.
66-81: Nice regression test for nested composables.This accurately protects against double-editing by ensuring we stop walking once the outer call is removed.
82-106: Whitespace-only churn.Looks like minor formatting; no action needed.
108-127: Whitespace-only churn.Looks like minor formatting; no action needed.
1-10: Pin oxc-walker and re-run snapshots
No references tooxc-walkerfound in any package.json; ensure you add the required version to dependencies and regenerate snapshots.
|
@danielroe I pushed some changes based on coderabbit's comment
Can you think of any problems with this implementation? |
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 (6)
packages/nuxt/src/core/plugins/tree-shake.ts (2)
86-90: Remove the entire statement to avoid stray semicolons/artefactsWhen the parent is an ExpressionStatement, removing only the CallExpression can leave a dangling “;” or whitespace. Remove the whole statement.
- if (parent?.type === 'ExpressionStatement') { - s.remove(node.start, node.end) + if (parent?.type === 'ExpressionStatement') { + s.remove(parent.start, parent.end) } else { s.overwrite(node.start, node.end, 'void 0') }
91-91: Early return after mutation for clarityAfter mutation + this.skip(), return to make intent explicit and avoid any accidental fallthrough in future edits.
- this.skip() + this.skip() + returnpackages/nuxt/test/tree-shake.test.ts (4)
66-79: Good coverage for nested composablesThis reproduces the original failure mode and verifies no double-edit; looks solid. Consider also a variant where the inner call uses auto-imports from '#imports' (not via alias) to widen coverage.
140-155: Assignment neutralisation verifieda = void 0 maintains runtime shape. Consider adding a semicolon-style variant to ensure no stray “;” when statements end with semicolons.
157-173: Logical/conditional contexts covered wellThese cases are easy to get wrong; snapshots look right. Please add:
- return onMounted() → return void 0
- await onMounted() → await void 0
11-18: Add tests for optional chaining and parenthesised statementsTwo extra edge cases frequently appear in user code:
- onMounted?.(() => {}) in expression/argument positions.
- (onMounted()) as a standalone parenthesised expression statement.
These ensure the callee-detection and ExpressionStatement removal behave as intended.
Proposed additions (append near related blocks):
+ it('should handle optional chaining calls', () => { + const code = ` + import { onMounted } from '#imports' + onMounted?.() + test(onMounted?.()) + ` + const { code: result } = transformPlugin.transform.handler(code, 'test.js') + expect(clean(result)).toMatchInlineSnapshot(` + "import { onMounted } from '#imports' + test(void 0)" + `) + }) + + it('should handle parenthesised expression statements', () => { + const code = ` + import { onMounted } from 'vue' + (onMounted()) + ` + const { code: result } = transformPlugin.transform.handler(code, 'test.js') + expect(clean(result)).toMatchInlineSnapshot(` + "import { onMounted } from 'vue'" + `) + }) + + it('should handle return/await contexts', () => { + const code = ` + import { onMounted } from 'vue' + async function f () { + return onMounted() + } + async function g () { + await onMounted() + } + ` + const { code: result } = transformPlugin.transform.handler(code, 'test.js') + expect(clean(result)).toMatchInlineSnapshot(` + "import { onMounted } from 'vue' + async function f() { + return void 0 + } + async function g() { + await void 0 + }" + `) + })Also applies to: 128-137, 140-155, 157-173, 175-185
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
packages/nuxt/package.jsonis excluded by!**/package.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!pnpm-lock.yaml
📒 Files selected for processing (2)
packages/nuxt/src/core/plugins/tree-shake.ts(2 hunks)packages/nuxt/test/tree-shake.test.ts(5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Follow standard TypeScript conventions and best practices
Files:
packages/nuxt/test/tree-shake.test.tspackages/nuxt/src/core/plugins/tree-shake.ts
**/*.{test,spec}.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Write unit tests for core functionality using
vitest
Files:
packages/nuxt/test/tree-shake.test.ts
⏰ 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: build
- GitHub Check: code
🔇 Additional comments (5)
packages/nuxt/test/tree-shake.test.ts (5)
17-17: Snapshot update looks correctResult no longer includes the previous false && wrapper; matches new transform.
81-105: Shadowing test reads wellConfirms local declaration prevents tree-shake; behaviour matches plugin logic.
128-137: Argument-position neutralisation verifiedUsing void 0 in parameter position is correct and avoids syntax traps.
175-185: Sequence expression case is correctReplacing the inner element with void 0 preserves evaluation order.
5-10: Pinoxc-walker≥0.4.0
Tests rely on enter(node, parent), added in oxc-walker 0.4.0 (PR #126 merged 8 Sep 2025). Verify yourpackage.jsonand lockfiles contain"oxc-walker": "^0.4.0", adding or bumping if missing.
🔗 Linked issue
fix #33151
📚 Description
This fixes an issue where the same chunk could get edited more times, if there were tree-shakable composables within a tree-shakable composable.
It should also bring a very slight performance improvement, since we're not walking the body of the composable once we determine that it should be tree-shaken.
oxc-walker🚧 TODO
oxc-walkeris updated