@callstack/repack-dev-server 5.4.0-canary-20260822202143 → 5.4.0-canary-20260831210904
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugins/devtools/devtoolsPlugin.js +5 -2
- package/dist/plugins/symbolicate/Symbolicator.d.ts +0 -5
- package/dist/plugins/symbolicate/Symbolicator.js +58 -48
- package/dist/plugins/symbolicate/logSymbolicatedStackFrame.d.ts +3 -0
- package/dist/plugins/symbolicate/logSymbolicatedStackFrame.js +42 -0
- package/dist/plugins/symbolicate/sybmolicatePlugin.js +2 -0
- package/dist/types.d.ts +6 -0
- package/dist/utils/symbolication.d.ts +10 -0
- package/dist/utils/symbolication.js +51 -0
- package/package.json +1 -1
|
@@ -27,8 +27,11 @@ async function devtoolsPlugin(instance, { delegate }) {
|
|
|
27
27
|
url: '/open-stack-frame',
|
|
28
28
|
handler: async (request, reply) => {
|
|
29
29
|
const { file, lineNumber } = parseRequestBody(request.body);
|
|
30
|
-
const
|
|
31
|
-
|
|
30
|
+
const openedRemotely = await delegate.devTools?.openStackFrame?.(file, lineNumber);
|
|
31
|
+
if (!openedRemotely) {
|
|
32
|
+
const filepath = delegate.devTools?.resolveProjectPath(file) ?? file;
|
|
33
|
+
launchEditor(`${filepath}:${lineNumber}`, process.env.REACT_EDITOR);
|
|
34
|
+
}
|
|
32
35
|
reply.send('OK');
|
|
33
36
|
},
|
|
34
37
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { FastifyBaseLogger } from 'fastify';
|
|
2
|
-
import { SourceMapConsumer } from 'source-map';
|
|
3
2
|
import type { ReactNativeStackFrame, SymbolicatorDelegate, SymbolicatorResults } from './types.js';
|
|
4
3
|
/**
|
|
5
4
|
* Class for transforming stack traces from React Native application with using Source Map.
|
|
@@ -18,10 +17,6 @@ export declare class Symbolicator {
|
|
|
18
17
|
* @returns Inferred platform or `undefined` if cannot infer.
|
|
19
18
|
*/
|
|
20
19
|
static inferPlatformFromStack(stack: ReactNativeStackFrame[]): string | undefined;
|
|
21
|
-
/**
|
|
22
|
-
* Cache with initialized `SourceMapConsumer` to improve symbolication performance.
|
|
23
|
-
*/
|
|
24
|
-
sourceMapConsumerCache: Record<string, SourceMapConsumer>;
|
|
25
20
|
/**
|
|
26
21
|
* Constructs new `Symbolicator` instance.
|
|
27
22
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { URL } from 'node:url';
|
|
2
2
|
import { codeFrameColumns } from '@babel/code-frame';
|
|
3
3
|
import { SourceMapConsumer } from 'source-map';
|
|
4
|
+
import { isGeneratedBundleFrame, isSymbolicatableFrame, normalizeInvalidWebpackSourceUrls, } from '../../utils/symbolication.js';
|
|
4
5
|
/**
|
|
5
6
|
* Class for transforming stack traces from React Native application with using Source Map.
|
|
6
7
|
* Raw stack frames produced by React Native, points to some location from the bundle
|
|
@@ -40,10 +41,6 @@ export class Symbolicator {
|
|
|
40
41
|
*/
|
|
41
42
|
constructor(delegate) {
|
|
42
43
|
this.delegate = delegate;
|
|
43
|
-
/**
|
|
44
|
-
* Cache with initialized `SourceMapConsumer` to improve symbolication performance.
|
|
45
|
-
*/
|
|
46
|
-
this.sourceMapConsumerCache = {};
|
|
47
44
|
}
|
|
48
45
|
/**
|
|
49
46
|
* Process raw React Native stack frames and transform them using Source Maps.
|
|
@@ -59,72 +56,78 @@ export class Symbolicator {
|
|
|
59
56
|
*/
|
|
60
57
|
async process(logger, stack) {
|
|
61
58
|
logger.debug({ msg: 'Filtering out unnecessary frames' });
|
|
62
|
-
const frames =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
}
|
|
59
|
+
const frames = stack.filter(isSymbolicatableFrame);
|
|
60
|
+
// A Symbolicator instance is shared by the route. Keep consumers local to
|
|
61
|
+
// one request so concurrent call-stack and component-stack requests cannot
|
|
62
|
+
// destroy or read each other's source maps.
|
|
63
|
+
const sourceMapConsumers = new Map();
|
|
69
64
|
try {
|
|
70
65
|
logger.debug({ msg: 'Processing frames', frames });
|
|
71
66
|
const processedFrames = [];
|
|
72
67
|
for (const frame of frames) {
|
|
73
|
-
|
|
68
|
+
try {
|
|
69
|
+
if (!sourceMapConsumers.has(frame.file)) {
|
|
70
|
+
logger.debug({
|
|
71
|
+
msg: 'Loading raw source map data',
|
|
72
|
+
fileUrl: frame.file,
|
|
73
|
+
});
|
|
74
|
+
const rawSourceMap = await this.delegate.getSourceMap(frame.file);
|
|
75
|
+
logger.debug({
|
|
76
|
+
msg: 'Creating source map instance',
|
|
77
|
+
fileUrl: frame.file,
|
|
78
|
+
sourceMapLength: rawSourceMap.length,
|
|
79
|
+
});
|
|
80
|
+
const sourceMapConsumer = await new SourceMapConsumer(normalizeInvalidWebpackSourceUrls(rawSourceMap));
|
|
81
|
+
logger.debug({
|
|
82
|
+
msg: 'Saving source map instance into cache',
|
|
83
|
+
fileUrl: frame.file,
|
|
84
|
+
});
|
|
85
|
+
sourceMapConsumers.set(frame.file, sourceMapConsumer);
|
|
86
|
+
}
|
|
74
87
|
logger.debug({
|
|
75
|
-
msg: '
|
|
76
|
-
|
|
88
|
+
msg: 'Symbolicating frame',
|
|
89
|
+
frame,
|
|
77
90
|
});
|
|
78
|
-
const
|
|
91
|
+
const processedFrame = this.processFrame(frame, sourceMapConsumers);
|
|
79
92
|
logger.debug({
|
|
80
|
-
msg: '
|
|
81
|
-
|
|
82
|
-
sourceMapLength: rawSourceMap.length,
|
|
93
|
+
msg: 'Finished symbolicating frame',
|
|
94
|
+
frame,
|
|
83
95
|
});
|
|
84
|
-
|
|
96
|
+
processedFrames.push(processedFrame);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// Match Metro's best-effort behavior: one unavailable or malformed
|
|
100
|
+
// source map must not discard frames that can still be symbolicated.
|
|
85
101
|
logger.debug({
|
|
86
|
-
msg: '
|
|
102
|
+
msg: 'Failed to symbolicate frame',
|
|
87
103
|
fileUrl: frame.file,
|
|
104
|
+
error: error.message,
|
|
88
105
|
});
|
|
89
|
-
|
|
106
|
+
processedFrames.push({ ...frame, collapse: false });
|
|
90
107
|
}
|
|
91
|
-
logger.debug({
|
|
92
|
-
msg: 'Symbolicating frame',
|
|
93
|
-
frame,
|
|
94
|
-
});
|
|
95
|
-
const processedFrame = this.processFrame(frame);
|
|
96
|
-
logger.debug({
|
|
97
|
-
msg: 'Finished symbolicating frame',
|
|
98
|
-
frame,
|
|
99
|
-
});
|
|
100
|
-
processedFrames.push(processedFrame);
|
|
101
108
|
}
|
|
102
|
-
const codeFrame = (await this.getCodeFrame(logger, processedFrames)) ?? null;
|
|
109
|
+
const codeFrame = (await this.getCodeFrame(logger, processedFrames, frames, sourceMapConsumers)) ?? null;
|
|
103
110
|
logger.debug({
|
|
104
111
|
msg: 'Finished symbolicating frames',
|
|
105
112
|
processedFrames,
|
|
106
113
|
codeFrame,
|
|
107
114
|
});
|
|
108
|
-
return {
|
|
109
|
-
stack: processedFrames,
|
|
110
|
-
codeFrame,
|
|
111
|
-
};
|
|
115
|
+
return { stack: processedFrames, codeFrame };
|
|
112
116
|
}
|
|
113
117
|
finally {
|
|
114
|
-
for (const
|
|
115
|
-
|
|
116
|
-
delete this.sourceMapConsumerCache[key];
|
|
118
|
+
for (const consumer of sourceMapConsumers.values()) {
|
|
119
|
+
consumer.destroy();
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
|
-
processFrame(frame) {
|
|
121
|
-
if (
|
|
123
|
+
processFrame(frame, sourceMapConsumers) {
|
|
124
|
+
if (frame.lineNumber == null || frame.column == null) {
|
|
122
125
|
return {
|
|
123
126
|
...frame,
|
|
124
127
|
collapse: false,
|
|
125
128
|
};
|
|
126
129
|
}
|
|
127
|
-
const consumer =
|
|
130
|
+
const consumer = sourceMapConsumers.get(frame.file);
|
|
128
131
|
if (!consumer) {
|
|
129
132
|
return {
|
|
130
133
|
...frame,
|
|
@@ -152,16 +155,19 @@ export class Symbolicator {
|
|
|
152
155
|
};
|
|
153
156
|
}
|
|
154
157
|
return {
|
|
155
|
-
lineNumber: lookup.line
|
|
156
|
-
column: lookup.column
|
|
158
|
+
lineNumber: lookup.line ?? frame.lineNumber,
|
|
159
|
+
column: lookup.column ?? frame.column,
|
|
157
160
|
file: lookup.source,
|
|
158
161
|
methodName: lookup.name || frame.methodName,
|
|
159
162
|
collapse: false,
|
|
160
163
|
};
|
|
161
164
|
}
|
|
162
|
-
async getCodeFrame(logger, processedFrames) {
|
|
163
|
-
for (const frame of processedFrames) {
|
|
164
|
-
if (frame.collapse ||
|
|
165
|
+
async getCodeFrame(logger, processedFrames, inputFrames, sourceMapConsumers) {
|
|
166
|
+
for (const [index, frame] of processedFrames.entries()) {
|
|
167
|
+
if (frame.collapse || frame.lineNumber == null || frame.column == null) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (isGeneratedBundleFrame(frame)) {
|
|
165
171
|
continue;
|
|
166
172
|
}
|
|
167
173
|
if (!this.delegate.shouldIncludeFrame(frame)) {
|
|
@@ -172,8 +178,12 @@ export class Symbolicator {
|
|
|
172
178
|
frame,
|
|
173
179
|
});
|
|
174
180
|
try {
|
|
181
|
+
const consumer = sourceMapConsumers.get(inputFrames[index]?.file);
|
|
182
|
+
const embeddedSource = consumer?.sourceContentFor(frame.file, true);
|
|
183
|
+
const source = embeddedSource ??
|
|
184
|
+
(await this.delegate.getSource(frame.file)).toString();
|
|
175
185
|
return {
|
|
176
|
-
content: codeFrameColumns(
|
|
186
|
+
content: codeFrameColumns(source, {
|
|
177
187
|
start: { column: frame.column, line: frame.lineNumber },
|
|
178
188
|
}, { forceColor: true }),
|
|
179
189
|
location: {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { FastifyBaseLogger } from 'fastify';
|
|
2
|
+
import type { ReactNativeStackFrame, SymbolicatorResults } from './types.js';
|
|
3
|
+
export declare function logSymbolicatedStackFrame(logger: FastifyBaseLogger, inputStack: ReactNativeStackFrame[], results: SymbolicatorResults): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isGeneratedBundleFrame, isSymbolicatableFrame, } from '../../utils/symbolication.js';
|
|
2
|
+
const RUNTIME_ERROR_METHODS = new Set([
|
|
3
|
+
'react-stack-bottom-frame',
|
|
4
|
+
'renderWithHooks',
|
|
5
|
+
'beginWork',
|
|
6
|
+
'performUnitOfWork',
|
|
7
|
+
]);
|
|
8
|
+
function isRuntimeErrorStack(stack) {
|
|
9
|
+
return stack.some((frame) => RUNTIME_ERROR_METHODS.has(frame.methodName));
|
|
10
|
+
}
|
|
11
|
+
const REMOTE_SOURCE_PATH_PREFIX = '/__repack_source__/';
|
|
12
|
+
function getRemoteName(file) {
|
|
13
|
+
if (!file) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const filename = new URL(file, 'file://').pathname.split('/').pop() ?? '';
|
|
17
|
+
return filename.match(/\.([^.]+)\.chunk\.bundle$/)?.[1];
|
|
18
|
+
}
|
|
19
|
+
function getPrintableFile(file, inputFile) {
|
|
20
|
+
const sourceUrl = new URL(file, 'file://');
|
|
21
|
+
if (sourceUrl.pathname.startsWith(REMOTE_SOURCE_PATH_PREFIX)) {
|
|
22
|
+
const source = decodeURIComponent(sourceUrl.pathname.slice(REMOTE_SOURCE_PATH_PREFIX.length)).replace(/^\[projectRoot(?:\^\d+)?\][\\/]/, '');
|
|
23
|
+
return `${getRemoteName(inputFile) ?? sourceUrl.host}/${source}`;
|
|
24
|
+
}
|
|
25
|
+
return file.replace(/^\[projectRoot(?:\^\d+)?\][\\/]/, '');
|
|
26
|
+
}
|
|
27
|
+
export function logSymbolicatedStackFrame(logger, inputStack, results) {
|
|
28
|
+
if (!isRuntimeErrorStack(inputStack)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const frameIndex = results.stack.findIndex((stackFrame) => !isGeneratedBundleFrame(stackFrame));
|
|
32
|
+
const frame = results.stack[frameIndex];
|
|
33
|
+
if (!frame?.file || frame.lineNumber == null) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const inputFrames = inputStack.filter(isSymbolicatableFrame);
|
|
37
|
+
const file = getPrintableFile(frame.file, inputFrames[frameIndex]?.file);
|
|
38
|
+
logger.info({
|
|
39
|
+
msg: `Symbolicated stack frame: ${file}:${frame.lineNumber}:${frame.column ?? 0}`,
|
|
40
|
+
methodName: frame.methodName,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fastifyPlugin from 'fastify-plugin';
|
|
2
|
+
import { logSymbolicatedStackFrame } from './logSymbolicatedStackFrame.js';
|
|
2
3
|
import { Symbolicator } from './Symbolicator.js';
|
|
3
4
|
function getStackFromRequestBody(request) {
|
|
4
5
|
let body;
|
|
@@ -25,6 +26,7 @@ async function symbolicatePlugin(instance, { delegate, }) {
|
|
|
25
26
|
else {
|
|
26
27
|
request.log.debug({ msg: 'Starting symbolication', platform, stack });
|
|
27
28
|
const results = await symbolicator.process(request.log, stack);
|
|
29
|
+
logSymbolicatedStackFrame(request.log, stack, results);
|
|
28
30
|
reply.send(results);
|
|
29
31
|
}
|
|
30
32
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -127,6 +127,12 @@ export declare namespace Server {
|
|
|
127
127
|
* @returns The resolved project path.
|
|
128
128
|
*/
|
|
129
129
|
resolveProjectPath: (filepath: string) => string;
|
|
130
|
+
/**
|
|
131
|
+
* Open a stack frame owned by another development server.
|
|
132
|
+
*
|
|
133
|
+
* @returns Whether the frame was handled remotely.
|
|
134
|
+
*/
|
|
135
|
+
openStackFrame?: (filepath: string, lineNumber: number) => boolean | Promise<boolean>;
|
|
130
136
|
}
|
|
131
137
|
/**
|
|
132
138
|
* Delegate with implementation for messages used in route handlers.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RawIndexMap, RawSourceMap } from 'source-map';
|
|
2
|
+
interface StackFrameLike {
|
|
3
|
+
file: string | null;
|
|
4
|
+
}
|
|
5
|
+
export declare function normalizeInvalidWebpackSourceUrls(rawSourceMap: string | Buffer): string | RawSourceMap | RawIndexMap;
|
|
6
|
+
export declare function isGeneratedBundleFrame(frame: StackFrameLike): boolean;
|
|
7
|
+
export declare function isSymbolicatableFrame<T extends StackFrameLike>(frame: T): frame is T & {
|
|
8
|
+
file: string;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { URL } from 'node:url';
|
|
2
|
+
export function normalizeInvalidWebpackSourceUrls(rawSourceMap) {
|
|
3
|
+
const sourceMapText = rawSourceMap.toString();
|
|
4
|
+
if (!sourceMapText.includes('webpack://')) {
|
|
5
|
+
return sourceMapText;
|
|
6
|
+
}
|
|
7
|
+
const sourceMap = JSON.parse(sourceMapText);
|
|
8
|
+
let invalidSourceIndex = 0;
|
|
9
|
+
const normalize = (map) => {
|
|
10
|
+
if (!map || typeof map !== 'object') {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const current = map;
|
|
14
|
+
if (Array.isArray(current.sources)) {
|
|
15
|
+
current.sources = current.sources.map((source) => {
|
|
16
|
+
if (typeof source !== 'string') {
|
|
17
|
+
return source;
|
|
18
|
+
}
|
|
19
|
+
const normalizedSource = source.replace(/^webpack:\/\/([^/|]+)\|\/?/, 'webpack://$1/');
|
|
20
|
+
if (!normalizedSource.startsWith('webpack://')) {
|
|
21
|
+
return normalizedSource;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
new URL(normalizedSource);
|
|
25
|
+
return normalizedSource;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Some generated Module Federation runtime modules use their source
|
|
29
|
+
// text as a webpack URL. A single invalid URL makes source-map reject
|
|
30
|
+
// the complete map, including otherwise valid application sources.
|
|
31
|
+
return `webpack://invalid-source/${invalidSourceIndex++}`;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
for (const section of current.sections ?? []) {
|
|
36
|
+
normalize(section.map);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
normalize(sourceMap);
|
|
40
|
+
// SourceMapConsumer accepts parsed maps. Returning the object avoids
|
|
41
|
+
// serializing it here only for the consumer to parse it again.
|
|
42
|
+
return sourceMap;
|
|
43
|
+
}
|
|
44
|
+
export function isGeneratedBundleFrame(frame) {
|
|
45
|
+
return Boolean(frame.file &&
|
|
46
|
+
(frame.file.includes('.bundle') || frame.file.includes('.hot-update.js')));
|
|
47
|
+
}
|
|
48
|
+
export function isSymbolicatableFrame(frame) {
|
|
49
|
+
return Boolean(frame.file &&
|
|
50
|
+
(frame.file.startsWith('http') || isGeneratedBundleFrame(frame)));
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@callstack/repack-dev-server",
|
|
3
3
|
"description": "A bundler-agnostic development server for React Native applications as part of @callstack/repack.",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "5.4.0-canary-
|
|
5
|
+
"version": "5.4.0-canary-20260831210904",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|