From 6ae4f6535761017b2f24de0ae9f40f221c714343 Mon Sep 17 00:00:00 2001 From: Bao Nguyen Date: Mon, 24 Aug 2026 17:38:47 +0700 Subject: [PATCH] fix: resolve absolute stack frame paths in the webpack compiler The dev server resolves symbolicated stack frames to absolute paths before handing them to the compiler. The Rspack compiler guards this with path.isAbsolute, but the webpack one joined them onto the project root a second time, so the file was never found and the redbox lost its code frame. Apply the same guard to the webpack compiler. --- .../webpack-symbolicate-absolute-paths.md | 5 +++ .../repack/src/commands/webpack/Compiler.ts | 4 +- .../webpack/__tests__/Compiler.test.ts | 42 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 .changeset/webpack-symbolicate-absolute-paths.md diff --git a/.changeset/webpack-symbolicate-absolute-paths.md b/.changeset/webpack-symbolicate-absolute-paths.md new file mode 100644 index 000000000..4ac623244 --- /dev/null +++ b/.changeset/webpack-symbolicate-absolute-paths.md @@ -0,0 +1,5 @@ +--- +"@callstack/repack": patch +--- + +Fix missing source code frames on the React Native redbox when using webpack. The dev server resolves symbolicated stack frames to absolute paths, but the webpack compiler joined them onto the project root a second time, so the file was never found and every frame was rendered without its source. diff --git a/packages/repack/src/commands/webpack/Compiler.ts b/packages/repack/src/commands/webpack/Compiler.ts index 76d27b086..5cede2b7e 100644 --- a/packages/repack/src/commands/webpack/Compiler.ts +++ b/packages/repack/src/commands/webpack/Compiler.ts @@ -276,7 +276,9 @@ export class Compiler implements CompilerInterface { } try { - const filePath = path.join(this.rootDir, filename); + const filePath = path.isAbsolute(filename) + ? filename + : path.join(this.rootDir, filename); const source = await fs.promises.readFile(filePath, 'utf8'); return source; } catch { diff --git a/packages/repack/src/commands/webpack/__tests__/Compiler.test.ts b/packages/repack/src/commands/webpack/__tests__/Compiler.test.ts index b89debb99..bf7327915 100644 --- a/packages/repack/src/commands/webpack/__tests__/Compiler.test.ts +++ b/packages/repack/src/commands/webpack/__tests__/Compiler.test.ts @@ -1,8 +1,11 @@ import type { EventEmitter } from 'node:events'; import { Worker } from 'node:worker_threads'; +import { fs, vol } from 'memfs'; import type { Reporter } from '../../../logging/types.js'; import { Compiler } from '../Compiler.js'; +jest.mock('node:fs', () => jest.requireActual('memfs').fs); + jest.mock('node:worker_threads', () => { const { EventEmitter } = jest.requireActual('node:events'); @@ -70,3 +73,42 @@ test('terminates active workers when closed', async () => { expect(worker.terminate).toHaveBeenCalledTimes(1); }); + +describe('getSource', () => { + const reporter: Reporter = { + process: jest.fn(), + flush: jest.fn(), + stop: jest.fn(), + }; + + const createCompiler = () => + new Compiler(['ios'], { host: '' }, reporter, '/project', '/react-native'); + + beforeEach(() => { + vol.reset(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('reads an absolute filename as-is', async () => { + vol.fromJSON({ '/outside/project/file.js': 'absolute source' }); + const readFile = jest.spyOn(fs.promises, 'readFile'); + + await expect( + createCompiler().getSource('/outside/project/file.js', 'ios') + ).resolves.toBe('absolute source'); + expect(readFile).toHaveBeenCalledWith('/outside/project/file.js', 'utf8'); + }); + + test('resolves a relative filename against the project root', async () => { + vol.fromJSON({ '/project/src/index.js': 'source under the project root' }); + const readFile = jest.spyOn(fs.promises, 'readFile'); + + await expect( + createCompiler().getSource('src/index.js', 'ios') + ).resolves.toBe('source under the project root'); + expect(readFile).toHaveBeenCalledWith('/project/src/index.js', 'utf8'); + }); +});