Skip to content

Turbopack: resolve loader source map sources against the compilation root#95727

Open
martijnwalraven wants to merge 2 commits into
vercel:canaryfrom
martijnwalraven:fix/turbopack-loader-sourcemap-base
Open

Turbopack: resolve loader source map sources against the compilation root#95727
martijnwalraven wants to merge 2 commits into
vercel:canaryfrom
martijnwalraven:fix/turbopack-loader-sourcemap-base

Conversation

@martijnwalraven

Copy link
Copy Markdown

What?

Two dev-time symbolication fixes for Turbopack + webpack-style loaders:

  • Relative sources in a source map returned by a webpack loader are now resolved against the compilation root the loader was handed (rootContext), instead of against the transformed file's own directory.
  • /__nextjs_original-stack-frames no longer rejects a successfully traced frame when reading the original source content fails — the mapped frame is returned without a code frame instead.

Why?

Motivating case: next-intl's message extractor in a pnpm monorepo. A Next 16 app lives at apps/web with turbopack.root set to the workspace root. next-intl (4.13, experimental.extract) registers a webpack-style loader rule under Turbopack (next-intl/extractor/extractionLoader, content condition useExtracted|getExtracted). For each matching file the loader runs swc with sourceFileName: path.relative(rootContext, resourcePath) — e.g. src/components/i18n/language-toggle.tsx, relative to the rootContext Turbopack handed it (apps/web) — so the returned map's sources are rootContext-relative, the convention webpack itself uses (NormalModule contextifies loader-returned map sources against options.context).

Turbopack then resolves those sources against a different base — the transformed file's own directory — in resolve_source_map_sources, so the served chunk map carries a doubled path:

file:///…/apps/web/src/components/i18n/src/components/i18n/language-toggle.tsx

Every dev-time consumer of that map degrades, but only for the loader-transformed files — sibling files in the same chunk map correctly, which makes it look flaky. A browser exception thrown in language-toggle.tsx traces correctly, then /__nextjs_original-stack-frames fails the whole frame with

Cannot find source for asset apps/web/src/components/i18n/src/components/i18n/language-toggle.tsx

because the code-frame read failure rejects a frame whose mapping had already succeeded. Overlay code frames and external dev symbolication consumers (e.g. Sentry's dev symbolication processor, which falls back silently to minified frames on rejection) lose original locations for exactly the files the loader touched.

Turbopack defines both sides of the contract here: it passes the loader runner cwd = the project path (exposed to loaders as rootContext, turbopack/crates/turbopack-node/js/src/transforms/webpack-loaders.ts), then interprets the returned map against the resource directory (turbopack/crates/turbopack-node/src/transforms/webpack.rs).

Generalized reproduction (no next-intl needed): next dev --turbopack in a workspace where turbopack.root is above the app dir, with any loader rule returning a map with rootContext-relative sources; fetch the chunk's .map and observe the doubled entry, or POST the frame to /__nextjs_original-stack-frames and observe the rejection.

How?

  • resolve_source_map_sources takes an explicit base for relative sources; None keeps the source-map-spec default (the map's location, i.e. the origin's directory), used by the inline-map and .map-file callers. The webpack-loader transform passes the same project_path it hands the loader runner.
  • batchedTraceSource catches getSourceForAsset failures and returns the traced frame with source = null.
  • Unit test added to test_resolve_source_map_sources covering the loader-shaped case (rootContext-relative source + explicit base).

Test plan: cargo test -p turbopack-core --lib source_map (new case + existing cases green); cargo check clean on turbopack-node and turbopack-ecmascript; rustfmt clean. The defect itself is reproduced live against v16.3.0-canary.82 (these files are identical at the canary tip); the fixed binary has not been exercised against that live setup end to end.

…root

A webpack loader emits relative `sources` against the compilation
root it is handed (`rootContext`, wired to the transform's cwd), not
against the transformed file's own directory. Resolving them
origin-relative doubled the path for any loader whose map carries
root-relative sources (e.g. next-intl's extractor calling swc with
`sourceFileName: path.relative(rootContext, resourcePath)`), and
/__nextjs_original-stack-frames then failed those frames with
"Cannot find source for asset ..." after a correct trace.

resolve_source_map_sources gains an explicit base for relative
sources (None keeps the spec default — the map's own location); the
webpack-loader transform passes the same project_path it hands the
loader runner as cwd/rootContext.
batchedTraceSource awaited project.getSourceForAsset inline, so a
failed source read rejected the whole frame in
/__nextjs_original-stack-frames — discarding a successful sourcemap
trace and downgrading symbolication consumers to minified frames. The
mapped frame is still valuable without its code frame: degrade to
source = null instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant