@callstack/repack-dev-server 5.3.0 → 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.
@@ -10,6 +10,6 @@ export declare function createServer(config: Server.Config): Promise<{
10
10
  start: () => Promise<void>;
11
11
  stop: () => Promise<void>;
12
12
  instance: Fastify.FastifyInstance<import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, Fastify.FastifyBaseLogger, Fastify.FastifyTypeProviderDefault> & PromiseLike<Fastify.FastifyInstance<import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, Fastify.FastifyBaseLogger, Fastify.FastifyTypeProviderDefault>> & {
13
- __linterBrands: "SafePromiseLike";
13
+ __linterBrands: 'SafePromiseLike';
14
14
  };
15
15
  }>;
@@ -21,7 +21,6 @@ import { normalizeOptions } from './utils/normalizeOptions.js';
21
21
  * @returns `start` and `stop` functions as well as an underlying Fastify `instance`.
22
22
  */
23
23
  export async function createServer(config) {
24
- // biome-ignore lint/style/useConst: needed in fastify constructor
25
24
  let delegate;
26
25
  const options = normalizeOptions(config.options);
27
26
  /** Fastify instance powering the development server. */
@@ -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: {
@@ -1,3 +1,3 @@
1
- export { default } from './sybmolicatePlugin.js';
2
1
  export * from './Symbolicator.js';
2
+ export { default } from './sybmolicatePlugin.js';
3
3
  export * from './types.js';
@@ -1,3 +1,3 @@
1
- export { default } from './sybmolicatePlugin.js';
2
1
  export * from './Symbolicator.js';
2
+ export { default } from './sybmolicatePlugin.js';
3
3
  export * from './types.js';
@@ -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
  }
@@ -17,7 +17,7 @@ export declare abstract class WebSocketServer<T extends WebSocket = WebSocket> i
17
17
  protected paths: string[];
18
18
  protected clients: Map<string, T>;
19
19
  protected nextClientId: number;
20
- private timer;
20
+ private heartbeatTimer;
21
21
  /**
22
22
  * Create a new instance of the WebSocketServer.
23
23
  * Any logging information, will be passed through standard `fastify.log` API.
@@ -15,7 +15,6 @@ export class WebSocketServer {
15
15
  */
16
16
  constructor(fastify, options) {
17
17
  this.nextClientId = 0;
18
- this.timer = null;
19
18
  this.fastify = fastify;
20
19
  this.name = options.name;
21
20
  this.paths = Array.isArray(options.path) ? options.path : [options.path];
@@ -23,7 +22,7 @@ export class WebSocketServer {
23
22
  this.server.on('connection', this.onConnection.bind(this));
24
23
  this.clients = new Map();
25
24
  // setup heartbeat timer
26
- this.timer = setInterval(() => {
25
+ this.heartbeatTimer = setInterval(() => {
27
26
  this.clients.forEach((socket) => {
28
27
  if (!socket.isAlive) {
29
28
  socket.terminate();
@@ -34,6 +33,9 @@ export class WebSocketServer {
34
33
  }
35
34
  });
36
35
  }, 30000);
36
+ this.fastify.addHook('onClose', async () => {
37
+ clearInterval(this.heartbeatTimer);
38
+ });
37
39
  }
38
40
  shouldUpgrade(pathname) {
39
41
  return this.paths.includes(pathname);
@@ -1,3 +1,3 @@
1
- export { default } from './wssPlugin.js';
2
- export * from './WebSocketServer.js';
3
1
  export * from './types.js';
2
+ export * from './WebSocketServer.js';
3
+ export { default } from './wssPlugin.js';
@@ -1,3 +1,3 @@
1
- export { default } from './wssPlugin.js';
2
- export * from './WebSocketServer.js';
3
1
  export * from './types.js';
2
+ export * from './WebSocketServer.js';
3
+ export { default } from './wssPlugin.js';
@@ -271,7 +271,7 @@ export class WebSocketMessageServer extends WebSocketServer {
271
271
  this.upgradeRequests[clientId] = request;
272
272
  socket.addEventListener('message', (event) => {
273
273
  const message = this.parseMessage(event.data.toString(),
274
- // @ts-ignore
274
+ // @ts-expect-error
275
275
  event.binary);
276
276
  if (!message) {
277
277
  this.fastify.log.error({
@@ -1,13 +1,13 @@
1
1
  import type { FastifyInstance } from 'fastify';
2
2
  import type { WebSocketServer } from 'ws';
3
3
  import type { Server } from '../../types.js';
4
- import { WebSocketRouter } from './WebSocketRouter.js';
5
- import { WebSocketServerAdapter } from './WebSocketServerAdapter.js';
6
4
  import { WebSocketApiServer } from './servers/WebSocketApiServer.js';
7
5
  import { WebSocketDevClientServer } from './servers/WebSocketDevClientServer.js';
8
6
  import { WebSocketEventsServer } from './servers/WebSocketEventsServer.js';
9
7
  import { WebSocketHMRServer } from './servers/WebSocketHMRServer.js';
10
8
  import { WebSocketMessageServer } from './servers/WebSocketMessageServer.js';
9
+ import { WebSocketRouter } from './WebSocketRouter.js';
10
+ import { WebSocketServerAdapter } from './WebSocketServerAdapter.js';
11
11
  declare module 'fastify' {
12
12
  interface FastifyInstance {
13
13
  wss: {
@@ -1,11 +1,11 @@
1
1
  import fastifyPlugin from 'fastify-plugin';
2
- import { WebSocketRouter } from './WebSocketRouter.js';
3
- import { WebSocketServerAdapter } from './WebSocketServerAdapter.js';
4
2
  import { WebSocketApiServer } from './servers/WebSocketApiServer.js';
5
3
  import { WebSocketDevClientServer } from './servers/WebSocketDevClientServer.js';
6
4
  import { WebSocketEventsServer } from './servers/WebSocketEventsServer.js';
7
5
  import { WebSocketHMRServer } from './servers/WebSocketHMRServer.js';
8
6
  import { WebSocketMessageServer } from './servers/WebSocketMessageServer.js';
7
+ import { WebSocketRouter } from './WebSocketRouter.js';
8
+ import { WebSocketServerAdapter } from './WebSocketServerAdapter.js';
9
9
  /**
10
10
  * Defined in @react-native/dev-middleware
11
11
  * Reference: https://github.com/facebook/react-native/blob/main/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js
package/dist/types.d.ts CHANGED
@@ -13,8 +13,7 @@ type MiddlewareObject<RequestInternal extends Http.IncomingMessage = Http.Incomi
13
13
  middleware: MiddlewareHandler<RequestInternal, ResponseInternal>;
14
14
  };
15
15
  export type Middleware<RequestInternal extends Http.IncomingMessage = Http.IncomingMessage, ResponseInternal extends Http.ServerResponse = Http.ServerResponse> = MiddlewareObject<RequestInternal, ResponseInternal> | MiddlewareHandler<RequestInternal, ResponseInternal>;
16
- export type { CompilerDelegate };
17
- export type { CodeFrame, InputStackFrame, ReactNativeStackFrame, StackFrame, SymbolicatorDelegate, SymbolicatorResults, };
16
+ export type { CodeFrame, CompilerDelegate, InputStackFrame, ReactNativeStackFrame, StackFrame, SymbolicatorDelegate, SymbolicatorResults, };
18
17
  interface ProxyConfig extends ProxyOptions {
19
18
  path?: ProxyOptions['pathFilter'];
20
19
  context?: ProxyOptions['pathFilter'];
@@ -128,6 +127,12 @@ export declare namespace Server {
128
127
  * @returns The resolved project path.
129
128
  */
130
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>;
131
136
  }
132
137
  /**
133
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.3.0",
5
+ "version": "5.4.0-canary-20260831210904",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
@@ -53,7 +53,7 @@
53
53
  "@types/babel__code-frame": "^7.0.6",
54
54
  "@types/node": "^20.19.31",
55
55
  "@types/ws": "^8.18.0",
56
- "typescript": "^5.9.3",
56
+ "typescript": "^7.0.2",
57
57
  "vitest": "^4.1.0"
58
58
  },
59
59
  "scripts": {