[builders] Fix transitive local TS dep externalization in step bundles#1609
[builders] Fix transitive local TS dep externalization in step bundles#1609VaguelySerious wants to merge 4 commits intomainfrom
Conversation
🦋 Changeset detectedLatest commit: c089357 The changes in this PR will be included in the next version bump. This PR includes changesets to release 18 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests💻 Local Development (2 failed)nitro-stable (2 failed):
📋 Other (2 failed)e2e-local-dev-tanstack-start-stable (2 failed):
Details by Category✅ ▲ Vercel Production
❌ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
❌ 📋 Other
❌ Some E2E test jobs failed:
Check the workflow run for details. |
📊 Benchmark Results
workflow with no steps💻 Local Development
workflow with 1 step💻 Local Development
workflow with 10 sequential steps💻 Local Development
workflow with 25 sequential steps💻 Local Development
workflow with 50 sequential steps💻 Local Development
Promise.all with 10 concurrent steps💻 Local Development
Promise.all with 25 concurrent steps💻 Local Development
Promise.all with 50 concurrent steps💻 Local Development
Promise.race with 10 concurrent steps💻 Local Development
Promise.race with 25 concurrent steps💻 Local Development
Promise.race with 50 concurrent steps💻 Local Development
workflow with 10 sequential data payload steps (10KB)💻 Local Development
workflow with 25 sequential data payload steps (10KB)💻 Local Development
workflow with 50 sequential data payload steps (10KB)💻 Local Development
workflow with 10 concurrent data payload steps (10KB)💻 Local Development
workflow with 25 concurrent data payload steps (10KB)💻 Local Development
workflow with 50 concurrent data payload steps (10KB)💻 Local Development
Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
stream pipeline with 5 transform steps (1MB)💻 Local Development
10 parallel streams (1MB each)💻 Local Development
fan-out fan-in 10 streams (1MB each)💻 Local Development
SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
|
TooTallNate
left a comment
There was a problem hiding this comment.
Two well-targeted fixes for the same underlying problem (transitive local TS deps not getting bundled). Verified each piece:
Discover plugin resolver config. The previous promisify(enhancedResolveOriginal) used the library's defaults, which don't include TS extensions. So await enhancedResolve(args.resolveDir, './helpers') would fail when the file is helpers.ts, the if (resolved) branch never fired, and importParents stayed empty for that edge. The new enhancedResolveOriginal.create({ extensions: [...], ... }) config matches the swc plugin's resolver, with the right comment explaining why ("important for parentHasChild() graph lookups"). The added symlinks: true is also subtly important for monorepo setups where local packages resolve via symlinks.
Swc plugin descendant check. The old parentHasChild(normalizedResolvedPath, normalizedEntry) check only handled "this file is an ancestor of an entry," which catches top-level wrappers but misses "this file is a transitive child of an entry" — exactly the helpers/constants case. The new inverse check fixes that with a clear comment.
Tests are well-targeted. Confirmed by reverting just the two production files on this branch and re-running the new tests — both fail without the fix:
discover-entries-esbuild-plugin.test.ts"tracks extensionless relative imports" fails becauseimportParents.get(workflow).has(constants)returnsfalse.swc-esbuild-plugin.test.ts"bundles transitive local dependencies" fails because the output containsimport { MSG } from "../shared/constants.ts"instead of the inlined"world".
So the tests are validating the bug, not just exercising the new code path. All 137 builder tests pass on the branch.
Discovery runs first in production. Verified base-builder.ts:244-260 runs createDiscoverEntriesPlugin in its own esbuild pass before any createSwcPlugin pass. So importParents is populated by the time the new descendant check runs.
PR description's note about broadening onResolve filter from jsTsRegex to /.*/ doesn't match the diff — that change is already on main from an earlier commit. Probably worth tightening the description, but no behavior issue.
Two issues caused local transitive dependencies to be externalized instead of bundled in step/workflow bundles: 1. The discover-entries plugin's onResolve filter only matched imports with explicit file extensions (jsTsRegex). Extensionless imports like `./helpers` were never tracked in the import graph. 2. The swc plugin only checked if a file was an ancestor of an entry (parentHasChild(resolved, entry)) but never checked if it was a descendant (parentHasChild(entry, resolved)). So even with a correct import graph, transitive local deps got externalized. Fixes: configure enhanced-resolve with TS extensions, broaden the onResolve filter to catch all relative imports, and add the reverse parentHasChild check. Add explicit symlinks: true to the discover-entries resolver to align with swc-esbuild-plugin's NODE_ESM_RESOLVE_OPTIONS for monorepos with symlinked packages. Closes #1179 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Closes #1179
Fixes transitive local TypeScript dependencies being externalized instead of bundled in step/workflow bundles, which caused native ESM crashes in Nuxt dev (and other frameworks) when extensionless relative imports like
./helperscouldn't be resolved by Node.Root cause (two issues):
onResolvefilter only matched imports with explicit file extensions (jsTsRegex). Extensionless imports like./helperswere never tracked inimportParents, leaving the dependency graph incomplete.parentHasChild(resolved, entry)), but never checked if it was a descendant (parentHasChild(entry, resolved)). So even with a correct import graph, transitive local deps got externalized.Fix:
enhanced-resolvein the discover plugin with TS extensions so extensionless imports resolve correctlyonResolvefilter fromjsTsRegexto/^[./]/(relative imports only) to track extensionless local depsparentHasChild(entry, resolved)check in the swc plugin so descendants of entries are bundledTest plan
discover-entries-esbuild-plugin.test.tsswc-esbuild-plugin.test.ts🤖 Generated with Claude Code