Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/fix-federated-source-maps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@callstack/repack": patch
"@callstack/repack-dev-server": patch
---

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.
135 changes: 80 additions & 55 deletions packages/dev-server/src/plugins/symbolicate/Symbolicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { URL } from 'node:url';
import { codeFrameColumns } from '@babel/code-frame';
import type { FastifyBaseLogger } from 'fastify';
import { SourceMapConsumer } from 'source-map';
import {
isGeneratedBundleFrame,
isSymbolicatableFrame,
normalizeInvalidWebpackSourceUrls,
} from '../../utils/symbolication.js';
import type {
CodeFrame,
InputStackFrame,
Expand Down Expand Up @@ -45,11 +50,6 @@ export class Symbolicator {
}
}

/**
* Cache with initialized `SourceMapConsumer` to improve symbolication performance.
*/
sourceMapConsumerCache: Record<string, SourceMapConsumer> = {};

/**
* Constructs new `Symbolicator` instance.
*
Expand All @@ -75,86 +75,99 @@ export class Symbolicator {
): Promise<SymbolicatorResults> {
logger.debug({ msg: 'Filtering out unnecessary frames' });

const frames: InputStackFrame[] = [];
for (const frame of stack) {
const { file } = frame;
if (file?.startsWith('http')) {
frames.push(frame as InputStackFrame);
}
}
const frames = stack.filter(isSymbolicatableFrame);

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

const processedFrames: StackFrame[] = [];
for (const frame of frames) {
if (!this.sourceMapConsumerCache[frame.file]) {
logger.debug({
msg: 'Loading raw source map data',
fileUrl: frame.file,
});
try {
if (!sourceMapConsumers.has(frame.file)) {
logger.debug({
msg: 'Loading raw source map data',
fileUrl: frame.file,
});

const rawSourceMap = await this.delegate.getSourceMap(frame.file);

const rawSourceMap = await this.delegate.getSourceMap(frame.file);
logger.debug({
msg: 'Creating source map instance',
fileUrl: frame.file,
sourceMapLength: rawSourceMap.length,
});
const sourceMapConsumer = await new SourceMapConsumer(
normalizeInvalidWebpackSourceUrls(rawSourceMap)
);

logger.debug({
msg: 'Saving source map instance into cache',
fileUrl: frame.file,
});
sourceMapConsumers.set(frame.file, sourceMapConsumer);
}

logger.debug({
msg: 'Creating source map instance',
fileUrl: frame.file,
sourceMapLength: rawSourceMap.length,
msg: 'Symbolicating frame',
frame,
});
const sourceMapConsumer = await new SourceMapConsumer(
rawSourceMap.toString()
);
const processedFrame = this.processFrame(frame, sourceMapConsumers);

logger.debug({
msg: 'Saving source map instance into cache',
msg: 'Finished symbolicating frame',
frame,
});
processedFrames.push(processedFrame);
} catch (error) {
// Match Metro's best-effort behavior: one unavailable or malformed
// source map must not discard frames that can still be symbolicated.
logger.debug({
msg: 'Failed to symbolicate frame',
fileUrl: frame.file,
error: (error as Error).message,
});
this.sourceMapConsumerCache[frame.file] = sourceMapConsumer;
processedFrames.push({ ...frame, collapse: false });
}

logger.debug({
msg: 'Symbolicating frame',
frame,
});
const processedFrame = this.processFrame(frame);

logger.debug({
msg: 'Finished symbolicating frame',
frame,
});
processedFrames.push(processedFrame);
}

const codeFrame =
(await this.getCodeFrame(logger, processedFrames)) ?? null;
(await this.getCodeFrame(
logger,
processedFrames,
frames,
sourceMapConsumers
)) ?? null;

logger.debug({
msg: 'Finished symbolicating frames',
processedFrames,
codeFrame,
});

return {
stack: processedFrames,
codeFrame,
};
return { stack: processedFrames, codeFrame };
} finally {
for (const key in this.sourceMapConsumerCache) {
this.sourceMapConsumerCache[key].destroy();
delete this.sourceMapConsumerCache[key];
for (const consumer of sourceMapConsumers.values()) {
consumer.destroy();
}
}
}

private processFrame(frame: InputStackFrame): StackFrame {
if (!frame.lineNumber || !frame.column) {
private processFrame(
frame: InputStackFrame,
sourceMapConsumers: Map<string, SourceMapConsumer>
): StackFrame {
if (frame.lineNumber == null || frame.column == null) {
return {
...frame,
collapse: false,
};
}

const consumer = this.sourceMapConsumerCache[frame.file];
const consumer = sourceMapConsumers.get(frame.file);
if (!consumer) {
return {
...frame,
Expand Down Expand Up @@ -186,8 +199,8 @@ export class Symbolicator {
}

return {
lineNumber: lookup.line || frame.lineNumber,
column: lookup.column || frame.column,
lineNumber: lookup.line ?? frame.lineNumber,
column: lookup.column ?? frame.column,
file: lookup.source,
methodName: lookup.name || frame.methodName,
collapse: false,
Expand All @@ -196,10 +209,16 @@ export class Symbolicator {

private async getCodeFrame(
logger: FastifyBaseLogger,
processedFrames: StackFrame[]
processedFrames: StackFrame[],
inputFrames: InputStackFrame[],
sourceMapConsumers: Map<string, SourceMapConsumer>
): Promise<CodeFrame | undefined> {
for (const frame of processedFrames) {
if (frame.collapse || !frame.lineNumber || !frame.column) {
for (const [index, frame] of processedFrames.entries()) {
if (frame.collapse || frame.lineNumber == null || frame.column == null) {
continue;
}

if (isGeneratedBundleFrame(frame)) {
continue;
}

Expand All @@ -213,9 +232,15 @@ export class Symbolicator {
});

try {
const consumer = sourceMapConsumers.get(inputFrames[index]?.file);
const embeddedSource = consumer?.sourceContentFor(frame.file, true);
const source =
embeddedSource ??
(await this.delegate.getSource(frame.file)).toString();

return {
content: codeFrameColumns(
(await this.delegate.getSource(frame.file)).toString(),
source,
{
start: { column: frame.column, line: frame.lineNumber },
},
Expand Down
Loading
Loading