Skip to content

Commit ed1003f

Browse files
authored
fix: symbolicate Module Federation remote stack frames (#1434)
1 parent b7a8611 commit ed1003f

12 files changed

Lines changed: 1089 additions & 60 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@callstack/repack": patch
3+
"@callstack/repack-dev-server": patch
4+
---
5+
6+
Fix development symbolication for Module Federation host and remote bundles. The host now follows a remote bundle's declared source map, invalid generated webpack source URLs no longer invalidate an otherwise usable map, symbolication continues when an individual frame cannot be mapped, and code frames use the matching source map's embedded source content. The dev server also logs the first useful symbolicated runtime frame as a fallback when opening the source file from the device is delayed.

‎packages/dev-server/src/plugins/devtools/devtoolsPlugin.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,14 @@ async function devtoolsPlugin(
4444
const { file, lineNumber } = parseRequestBody<OpenStackFrameRequestBody>(
4545
request.body
4646
);
47-
const filepath = delegate.devTools?.resolveProjectPath(file) ?? file;
48-
launchEditor(`${filepath}:${lineNumber}`, process.env.REACT_EDITOR);
47+
const openedRemotely = await delegate.devTools?.openStackFrame?.(
48+
file,
49+
lineNumber
50+
);
51+
if (!openedRemotely) {
52+
const filepath = delegate.devTools?.resolveProjectPath(file) ?? file;
53+
launchEditor(`${filepath}:${lineNumber}`, process.env.REACT_EDITOR);
54+
}
4955
reply.send('OK');
5056
},
5157
});

‎packages/dev-server/src/plugins/symbolicate/Symbolicator.ts‎

Lines changed: 80 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { URL } from 'node:url';
22
import { codeFrameColumns } from '@babel/code-frame';
33
import type { FastifyBaseLogger } from 'fastify';
44
import { SourceMapConsumer } from 'source-map';
5+
import {
6+
isGeneratedBundleFrame,
7+
isSymbolicatableFrame,
8+
normalizeInvalidWebpackSourceUrls,
9+
} from '../../utils/symbolication.js';
510
import type {
611
CodeFrame,
712
InputStackFrame,
@@ -45,11 +50,6 @@ export class Symbolicator {
4550
}
4651
}
4752

48-
/**
49-
* Cache with initialized `SourceMapConsumer` to improve symbolication performance.
50-
*/
51-
sourceMapConsumerCache: Record<string, SourceMapConsumer> = {};
52-
5353
/**
5454
* Constructs new `Symbolicator` instance.
5555
*
@@ -75,86 +75,99 @@ export class Symbolicator {
7575
): Promise<SymbolicatorResults> {
7676
logger.debug({ msg: 'Filtering out unnecessary frames' });
7777

78-
const frames: InputStackFrame[] = [];
79-
for (const frame of stack) {
80-
const { file } = frame;
81-
if (file?.startsWith('http')) {
82-
frames.push(frame as InputStackFrame);
83-
}
84-
}
78+
const frames = stack.filter(isSymbolicatableFrame);
8579

80+
// A Symbolicator instance is shared by the route. Keep consumers local to
81+
// one request so concurrent call-stack and component-stack requests cannot
82+
// destroy or read each other's source maps.
83+
const sourceMapConsumers = new Map<string, SourceMapConsumer>();
8684
try {
8785
logger.debug({ msg: 'Processing frames', frames });
8886

8987
const processedFrames: StackFrame[] = [];
9088
for (const frame of frames) {
91-
if (!this.sourceMapConsumerCache[frame.file]) {
92-
logger.debug({
93-
msg: 'Loading raw source map data',
94-
fileUrl: frame.file,
95-
});
89+
try {
90+
if (!sourceMapConsumers.has(frame.file)) {
91+
logger.debug({
92+
msg: 'Loading raw source map data',
93+
fileUrl: frame.file,
94+
});
95+
96+
const rawSourceMap = await this.delegate.getSourceMap(frame.file);
9697

97-
const rawSourceMap = await this.delegate.getSourceMap(frame.file);
98+
logger.debug({
99+
msg: 'Creating source map instance',
100+
fileUrl: frame.file,
101+
sourceMapLength: rawSourceMap.length,
102+
});
103+
const sourceMapConsumer = await new SourceMapConsumer(
104+
normalizeInvalidWebpackSourceUrls(rawSourceMap)
105+
);
106+
107+
logger.debug({
108+
msg: 'Saving source map instance into cache',
109+
fileUrl: frame.file,
110+
});
111+
sourceMapConsumers.set(frame.file, sourceMapConsumer);
112+
}
98113

99114
logger.debug({
100-
msg: 'Creating source map instance',
101-
fileUrl: frame.file,
102-
sourceMapLength: rawSourceMap.length,
115+
msg: 'Symbolicating frame',
116+
frame,
103117
});
104-
const sourceMapConsumer = await new SourceMapConsumer(
105-
rawSourceMap.toString()
106-
);
118+
const processedFrame = this.processFrame(frame, sourceMapConsumers);
107119

108120
logger.debug({
109-
msg: 'Saving source map instance into cache',
121+
msg: 'Finished symbolicating frame',
122+
frame,
123+
});
124+
processedFrames.push(processedFrame);
125+
} catch (error) {
126+
// Match Metro's best-effort behavior: one unavailable or malformed
127+
// source map must not discard frames that can still be symbolicated.
128+
logger.debug({
129+
msg: 'Failed to symbolicate frame',
110130
fileUrl: frame.file,
131+
error: (error as Error).message,
111132
});
112-
this.sourceMapConsumerCache[frame.file] = sourceMapConsumer;
133+
processedFrames.push({ ...frame, collapse: false });
113134
}
114-
115-
logger.debug({
116-
msg: 'Symbolicating frame',
117-
frame,
118-
});
119-
const processedFrame = this.processFrame(frame);
120-
121-
logger.debug({
122-
msg: 'Finished symbolicating frame',
123-
frame,
124-
});
125-
processedFrames.push(processedFrame);
126135
}
127136

128137
const codeFrame =
129-
(await this.getCodeFrame(logger, processedFrames)) ?? null;
138+
(await this.getCodeFrame(
139+
logger,
140+
processedFrames,
141+
frames,
142+
sourceMapConsumers
143+
)) ?? null;
130144

131145
logger.debug({
132146
msg: 'Finished symbolicating frames',
133147
processedFrames,
134148
codeFrame,
135149
});
136150

137-
return {
138-
stack: processedFrames,
139-
codeFrame,
140-
};
151+
return { stack: processedFrames, codeFrame };
141152
} finally {
142-
for (const key in this.sourceMapConsumerCache) {
143-
this.sourceMapConsumerCache[key].destroy();
144-
delete this.sourceMapConsumerCache[key];
153+
for (const consumer of sourceMapConsumers.values()) {
154+
consumer.destroy();
145155
}
146156
}
147157
}
148158

149-
private processFrame(frame: InputStackFrame): StackFrame {
150-
if (!frame.lineNumber || !frame.column) {
159+
private processFrame(
160+
frame: InputStackFrame,
161+
sourceMapConsumers: Map<string, SourceMapConsumer>
162+
): StackFrame {
163+
if (frame.lineNumber == null || frame.column == null) {
151164
return {
152165
...frame,
153166
collapse: false,
154167
};
155168
}
156169

157-
const consumer = this.sourceMapConsumerCache[frame.file];
170+
const consumer = sourceMapConsumers.get(frame.file);
158171
if (!consumer) {
159172
return {
160173
...frame,
@@ -186,8 +199,8 @@ export class Symbolicator {
186199
}
187200

188201
return {
189-
lineNumber: lookup.line || frame.lineNumber,
190-
column: lookup.column || frame.column,
202+
lineNumber: lookup.line ?? frame.lineNumber,
203+
column: lookup.column ?? frame.column,
191204
file: lookup.source,
192205
methodName: lookup.name || frame.methodName,
193206
collapse: false,
@@ -196,10 +209,16 @@ export class Symbolicator {
196209

197210
private async getCodeFrame(
198211
logger: FastifyBaseLogger,
199-
processedFrames: StackFrame[]
212+
processedFrames: StackFrame[],
213+
inputFrames: InputStackFrame[],
214+
sourceMapConsumers: Map<string, SourceMapConsumer>
200215
): Promise<CodeFrame | undefined> {
201-
for (const frame of processedFrames) {
202-
if (frame.collapse || !frame.lineNumber || !frame.column) {
216+
for (const [index, frame] of processedFrames.entries()) {
217+
if (frame.collapse || frame.lineNumber == null || frame.column == null) {
218+
continue;
219+
}
220+
221+
if (isGeneratedBundleFrame(frame)) {
203222
continue;
204223
}
205224

@@ -213,9 +232,15 @@ export class Symbolicator {
213232
});
214233

215234
try {
235+
const consumer = sourceMapConsumers.get(inputFrames[index]?.file);
236+
const embeddedSource = consumer?.sourceContentFor(frame.file, true);
237+
const source =
238+
embeddedSource ??
239+
(await this.delegate.getSource(frame.file)).toString();
240+
216241
return {
217242
content: codeFrameColumns(
218-
(await this.delegate.getSource(frame.file)).toString(),
243+
source,
219244
{
220245
start: { column: frame.column, line: frame.lineNumber },
221246
},

0 commit comments

Comments
 (0)