Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/symbolication-stack-frames.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@callstack/repack-dev-server": patch
"@callstack/repack": patch
---

Fix stack-frame symbolication and open-in-editor for Module Federation setups:

- Symbolication no longer fails the whole request when one frame's source map is unavailable (e.g. a federated remote chunk served by another dev server) — failed frames pass through unchanged, mirroring Metro, and the response stack is always 1:1 with the request.
- Source maps for bundles the local compiler didn't build are now fetched via the `//# sourceMappingURL=` comment the bundle declares, resolved relative to the bundle URL and validated before use.
- Symbolication survives corrupt source names that Module Federation v2 emits into host bundle maps (previously `Invalid URL` failed every stack on MFv2 hosts).
- Frames are collapsed by resolved source path (like React Native's default Metro config) and the source map `ignoreList`/`x_google_ignoreList` field, instead of not at all.
- `/open-stack-frame` now decodes URL-encoded `[projectRoot^N]` tokens (returned as `[projectRoot%5E2]/...` by symbolication), so tapping frames that resolve into `node_modules` opens the editor instead of silently doing nothing.
- Editor launch failures are logged with an actionable hint (set `REACT_EDITOR`) instead of being silently swallowed; frames at generated column 0 symbolicate correctly.
1 change: 1 addition & 0 deletions apps/tester-federation-v2/configs/rspack.mini-app.mts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default Repack.defineRspackConfig((env) => {
output: {
path: '[context]/build/mini-app/[platform]',
uniqueName: 'MF2Tester-MiniApp',
chunkFilename: '[name].MiniApp.chunk.bundle',
},
module: {
rules: [
Expand Down
56 changes: 54 additions & 2 deletions packages/dev-server/src/plugins/devtools/devtoolsPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fs from 'node:fs';
import path from 'node:path';
import type { FastifyInstance } from 'fastify';
import fastifyPlugin from 'fastify-plugin';
import launchEditor from 'launch-editor';
Expand All @@ -19,10 +21,17 @@ function parseRequestBody<T>(body: unknown): T {
throw new Error(`Unsupported body type: ${typeof body}`);
}

const SAME_FRAME_SUPPRESSION_MS = 1000;

async function devtoolsPlugin(
instance: FastifyInstance,
{ delegate }: { delegate: Server.Delegate }
) {
// Repeated identical open-stack-frame requests (double taps, a client
// stuck retrying) would each spawn an editor process; suppress launches
// of the same frame in quick succession.
let lastLaunchedFrame: string | undefined;
let lastLaunchedAt = 0;
// reference implementation in `@react-native-community/cli-server-api`:
// https://github.com/react-native-community/cli/blob/46436a12478464752999d34ed86adf3212348007/packages/cli-server-api/src/openURLMiddleware.ts
instance.route({
Expand All @@ -44,8 +53,51 @@ async function devtoolsPlugin(
const { file, lineNumber } = parseRequestBody<OpenStackFrameRequestBody>(
request.body
);
if (
typeof file !== 'string' ||
file.includes('\0') ||
!Number.isFinite(Number(lineNumber))
) {
reply.badRequest('Invalid open-stack-frame request body');
return;
}
const filepath = delegate.devTools?.resolveProjectPath(file) ?? file;
launchEditor(`${filepath}:${lineNumber}`, process.env.REACT_EDITOR);

const frame = `${filepath}:${lineNumber}`;
if (
frame === lastLaunchedFrame &&
Date.now() - lastLaunchedAt < SAME_FRAME_SUPPRESSION_MS
) {
reply.send('OK');
return;
}
lastLaunchedFrame = frame;
lastLaunchedAt = Date.now();

// launch-editor silently ignores files that don't exist, so surface
// resolution failures here — otherwise tapping a stack frame does
// nothing with no trace of why.
if (!fs.existsSync(filepath)) {
Comment thread
dannyhw marked this conversation as resolved.
Dismissed
request.log.warn(
`Could not open ${file} in your editor: it resolved to ` +
`${filepath}, which does not exist.`
);
reply.send('OK');
return;
}

launchEditor(
`${filepath}:${lineNumber}`,
process.env.REACT_EDITOR,
(fileName, errorMessage) => {
request.log.warn(
`Could not open ${path.basename(fileName)} in your editor` +
`${errorMessage ? `: ${errorMessage}` : ''}. Set the ` +
'REACT_EDITOR environment variable (e.g. REACT_EDITOR=code) ' +
'and restart the dev server.'
);
}
);
reply.send('OK');
},
});
Expand All @@ -62,5 +114,5 @@ async function devtoolsPlugin(

export default fastifyPlugin(devtoolsPlugin, {
name: 'devtools-plugin',
dependencies: ['wss-plugin'],
dependencies: ['@fastify/sensible', 'wss-plugin'],
});
Loading
Loading