@callstack/repack-dev-server 5.4.0-canary-20260822202143 → 5.4.0-canary-20260913161840

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.
@@ -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 filepath = delegate.devTools?.resolveProjectPath(file) ?? file;
31
- launchEditor(`${filepath}:${lineNumber}`, process.env.REACT_EDITOR);
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
- for (const frame of stack) {
64
- const { file } = frame;
65
- if (file?.startsWith('http')) {
66
- frames.push(frame);
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
- if (!this.sourceMapConsumerCache[frame.file]) {
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: 'Loading raw source map data',
76
- fileUrl: frame.file,
88
+ msg: 'Symbolicating frame',
89
+ frame,
77
90
  });
78
- const rawSourceMap = await this.delegate.getSourceMap(frame.file);
91
+ const processedFrame = this.processFrame(frame, sourceMapConsumers);
79
92
  logger.debug({
80
- msg: 'Creating source map instance',
81
- fileUrl: frame.file,
82
- sourceMapLength: rawSourceMap.length,
93
+ msg: 'Finished symbolicating frame',
94
+ frame,
83
95
  });
84
- const sourceMapConsumer = await new SourceMapConsumer(rawSourceMap.toString());
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: 'Saving source map instance into cache',
102
+ msg: 'Failed to symbolicate frame',
87
103
  fileUrl: frame.file,
104
+ error: error.message,
88
105
  });
89
- this.sourceMapConsumerCache[frame.file] = sourceMapConsumer;
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 key in this.sourceMapConsumerCache) {
115
- this.sourceMapConsumerCache[key].destroy();
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 (!frame.lineNumber || !frame.column) {
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 = this.sourceMapConsumerCache[frame.file];
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 || frame.lineNumber,
156
- column: lookup.column || frame.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 || !frame.lineNumber || !frame.column) {
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((await this.delegate.getSource(frame.file)).toString(), {
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-20260822202143",
5
+ "version": "5.4.0-canary-20260913161840",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",