Skip to content

fix: symbolication and open-in-editor for Module Federation stack frames#1407

Draft
dannyhw wants to merge 7 commits into
mainfrom
dannyhw/symbolication-fixes
Draft

fix: symbolication and open-in-editor for Module Federation stack frames#1407
dannyhw wants to merge 7 commits into
mainfrom
dannyhw/symbolication-fixes

Conversation

@dannyhw

@dannyhw dannyhw commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes stack-frame symbolication and open-in-editor for Module Federation setups, prompted by a customer report ("console shows the location in the bundle map instead of the actual source file") on an MFv2 shell + multiple MFE dev servers.

Tests first: the E2E suite in tests/integration/e2e/symbolication.test.ts boots the real tester-federation-v2 host + mini-app dev servers and was committed failing (2 pass / 4 fail on main); the fix commits turn it 6/6 green.

Root causes fixed

  1. MFv2 host maps crash SourceMapConsumer. The MF runtime emits a corrupt webpack:// source name (containing kilobytes of raw runtime code) into the host bundle map; new URL() throws Invalid URL during consumer construction, so on an MFv2 host every stack — even host-only — failed with a 500.
  2. One foreign frame poisons the whole request. Any frame from a bundle the local compiler didn't build (e.g. an MF remote chunk served by another dev server, __federation_expose_X.<remote>.chunk.bundle) made compiler.getSourceMap throw, failing the entire /symbolicate request — host frames included.
  3. /symbolicate and /open-stack-frame don't round-trip. Bundlers percent-encode ^ in source map paths, so symbolication returns parent-escape tokens as [projectRoot%5E2]/node_modules/.... resolveProjectPath only matched the literal ^, and launch-editor silently ignores nonexistent paths — tapping a node_modules frame did nothing, with no error anywhere.
  4. Generated column 0 treated as missing (falsy check), skipping symbolication of valid positions.

Also: the response stack was not 1:1 with the request (non-http frames were dropped). React Native's LogBox rejects symbolication results with a different number of frames, so any stack containing e.g. [native code] frames failed client-side.

Changes

@callstack/repack-dev-server

  • Sanitize URL-unparseable source names before constructing SourceMapConsumer (indices preserved so mappings stay aligned).
  • Per-frame isolation: a frame whose map can't be loaded passes through unchanged with a warn log naming the bundle; the response stack is always 1:1 with the request (Metro semantics).
  • Frames that failed to symbolicate can no longer anchor the code frame (previously rendered a garbage snippet from the raw minified bundle at generated coordinates).
  • Collapse frames by resolved source path — mirroring @react-native/metro-config's INTERNAL_CALLSITES_REGEX approach — plus the source map ignoreList/x_google_ignoreList field. Replaces nothing (there was no collapsing before); never matches on method names.
  • Nullish position guards so column 0 symbolicates.
  • /open-stack-frame: log unresolvable paths, and pass an error callback to launchEditor with an actionable hint (set REACT_EDITOR) — both failure modes were previously silent.

@callstack/repack

  • New fetchDeclaredSourceMap in commands/common, wired into both rspack and webpack start delegates: when the compiler has no map for a URL, fetch the map the bundle declares via its //# sourceMappingURL= comment (which Re.Pack appends to every served bundle, including MF containers/chunks), resolved relative to the bundle URL. Responses are validated as source maps before use; lookups (including misses) are memoized with a short TTL. Only the declared reference is trusted — no filename guessing, matching how browsers and symbolication services (e.g. Sentry) resolve maps.
  • coerceToHttpUrl handles scheme-less frame URLs (localhost:8081/..., 10.0.2.2:8081/...); note new URL('localhost:8081/x') parses successfully with localhost: as the scheme, so coercion cannot rely on parse errors.
  • resolveProjectPath decodes URL-encoded [projectRoot%5EN] tokens (token-only, not the whole path).

Test plan

  • tests/integration/e2e/symbolication.test.ts (new, committed failing first): boots both tester-federation-v2 dev servers, derives frame coordinates from real source maps, observes editor launches via a fake-editor shim. Covers: host-only MFv2 stack, remote chunk frame on the host, mixed stack, [projectRoot] editor round-trip, node_modules (%5E token) editor round-trip, plus two control cases pinning pre-existing behavior.
  • Unit tests: Symbolicator (sanitization, isolation, 1:1 pass-through, ignoreList collapse, column 0, code-frame anchoring, per-request map caching), fetchDeclaredSourceMap/coerceToHttpUrl (declared-reference resolution, validation, memoization, scheme-less URLs, bare-filename rejection), resolveProjectPath (encoded tokens).
  • Full monorepo pnpm test: 10/10 tasks green.
  • The tester-federation-v2 mini-app config gained chunkFilename: '[name].MiniApp.chunk.bundle' to mirror the reported setup's chunk naming.

Deferred follow-ups

  • Configurable additional project roots for cross-package [projectRoot] resolution (monorepos where remotes live in sibling packages — needs a config surface discussion).
  • Upstream issue to rspack for the corrupt MFv2 webpack:// source name (root cause of fix: initialize React Devtools before React Refresh #1 above; the sanitization here is defensive).
  • Upstream PR to launch-editor for resolving editor CLIs installed outside PATH (Homebrew, JetBrains Toolbox).

dannyhw added 4 commits July 7, 2026 15:19
Boots the tester-federation-v2 host and mini-app dev servers and covers
stack-frame symbolication plus /open-stack-frame end to end. The mini-app
config mirrors a real-world MFv2 setup where remote chunks are named
[name].<remote>.chunk.bundle and served by a separate dev server.

Currently-failing cases document known bugs:
- a host-only stack on an MFv2 host fails with 'Invalid URL' because the
  MF runtime emits a corrupt webpack:// source name into the host map
- a frame from a federated remote chunk fails the whole /symbolicate
  request with 500 because the host compiler has no map for it
- one foreign frame poisons symbolication of host-owned frames
- node_modules frames symbolicate to URL-encoded parent-escape tokens
  ([projectRoot%5E2]/...) which /open-stack-frame cannot resolve, so the
  editor silently never opens

Passing cases pin the workflows that must not regress: symbolication of
frames the server's own compiler built, and open-stack-frame for plain
[projectRoot] paths.
- A failure to load one frame's source map (e.g. a Module Federation
  remote chunk served by another dev server) no longer fails the whole
  /symbolicate request with a 500 — the failed frame passes through
  unchanged and the failure is logged at warn with the bundle URL.
- The response stack is now always 1:1 with the request, mirroring
  Metro; LogBox rejects symbolication results with a different number
  of frames, so dropped frames broke client-side symbolication.
- Source names that are not URL-parseable are sanitized before
  constructing SourceMapConsumer. Module Federation v2 emits a corrupt
  webpack:// source name (raw runtime code) into host bundle maps,
  which previously threw 'Invalid URL' and killed symbolication of
  every stack on MFv2 hosts, including host-only stacks.
- Frames whose position failed to resolve can no longer anchor the
  code frame, which previously rendered a garbage snippet from the raw
  minified bundle.
- Frames are collapsed by resolved source path (mirroring React
  Native's default Metro config) and by the source map ignoreList /
  x_google_ignoreList field.
- Generated column 0 is a valid position and now symbolicates instead
  of being treated as missing.
- Editor launch failures in /open-stack-frame are logged with an
  actionable hint (set REACT_EDITOR) instead of being silently
  swallowed; unresolved file paths are logged too, since launch-editor
  silently ignores files that do not exist.
…ncoded project-root tokens

- getSourceMap delegates (rspack and webpack start commands) now fall
  back to the source map a bundle declares for itself via its
  //# sourceMappingURL= comment, resolved relative to the bundle URL,
  when the local compiler has no map for it. This makes stacks from
  Module Federation remote containers/chunks served by other dev
  servers symbolicate on the host. Responses are validated as source
  maps before use and lookups (including misses) are memoized with a
  short TTL. Only the declared reference is trusted — no filename
  guessing.
- coerceToHttpUrl handles scheme-less frame URLs (localhost:8081/...,
  10.0.2.2:8081/...) — note new URL('localhost:8081/x') parses with
  'localhost:' as the scheme, so coercion cannot rely on parse errors.
- resolveProjectPath decodes URL-encoded parent-escape tokens:
  bundlers percent-encode ^ in source map paths, so /symbolicate
  returns [projectRoot%5E2]/... which previously failed to resolve,
  making open-stack-frame a silent no-op for node_modules frames.
- Fail fast with a clear error when the test ports are already in use:
  a stale dev server from an aborted run answers requests with the
  wrong environment and produces unexplainable failures.
- Kill the detached server process groups on process exit, since
  afterAll does not run when the test runner is killed mid-run.
- Give the fake-editor poll a generous timeout for fully parallel test
  runs on loaded machines.
@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
repack-website Ready Ready Preview, Comment Jul 7, 2026 10:39pm

Request Review

@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0f33990

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@callstack/repack-dev-server Patch
@callstack/repack Patch
@callstack/repack-plugin-expo-modules Patch
@callstack/repack-plugin-nativewind Patch
@callstack/repack-plugin-reanimated Patch
@callstack/repack-init Patch

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

Comment thread packages/dev-server/src/plugins/devtools/devtoolsPlugin.ts Dismissed
… E2E CI load

- Reject open-stack-frame bodies where file is not a string, contains
  NUL bytes, or lineNumber is not numeric, and declare the plugin's
  @fastify/sensible dependency.
- Run the E2E dev servers with --platform ios: the suite exercises a
  single platform and compiling both doubled the load on shared CI
  runners, pushing an unrelated tester-app bundle test past its
  timeout.
- Stop test servers with SIGKILL — a SIGTERM caught mid-compilation
  left an orphan holding the port for later runs.
Repeated identical requests (double taps, a client stuck retrying)
each spawned an editor process; launches of the same frame within 1s
are now no-ops.
@dannyhw

dannyhw commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

CodeQL findings on the new /open-stack-frame code, both evaluated:

  • js/path-injection (alert 17) — dismissed as by-design; see the inline thread. The endpoint's purpose is opening the client-named stack-frame file in the developer's editor (same trust model as the RN CLI middleware this plugin cites). Response is constant regardless of file existence; body is validated (string, no NUL bytes, numeric line).
  • js/missing-rate-limiting (alert 16) — dismissed with a mitigation rather than a rate-limiter dependency: the genuine hazard is that each request can spawn an editor process, so repeated identical launches (double taps, a client stuck retrying) are now suppressed within a 1s window (0f33990). A recognized rate-limiting middleware would have to be scoped carefully away from the legitimately high-frequency dev-server routes (bundle serving, HMR, symbolicate) for no additional benefit on a development-only server — Metro and the RN CLI ship the equivalent endpoints without rate limiting.

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.

2 participants