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'); + }); +});