diff --git a/.changeset/webpack-symbolicate-absolute-paths.md b/.changeset/webpack-symbolicate-absolute-paths.md new file mode 100644 index 000000000..2d0d15d27 --- /dev/null +++ b/.changeset/webpack-symbolicate-absolute-paths.md @@ -0,0 +1,5 @@ +--- +"@callstack/repack": patch +--- + +Fix the webpack compiler double-joining absolute paths in `getSource`. The dev server resolves symbolicated stack frames to absolute paths before asking the compiler for their source, but the webpack compiler joined them onto the project root a second time, so the lookup failed. This only affected the fallback used when a frame's source is not embedded in the source map, for example with `nosources-*` devtools, and now matches the Rspack compiler. 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'); + }); +});