From 7c93e95aa3d43cd3f53e56c3af8a5c0d1a2e153c Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Mon, 12 May 2025 21:05:26 +0200 Subject: [PATCH 1/2] [build-sourcemaps] Sourcemap errors during prerender if `experimental.enablePrerenderSourceMaps` is enabled Plan is to enable it internally and track build times for a week. I don't expect any noticeable difference. The plan is to make it the default. You can already op-in today by including `--enable-source-maps` in `NODE_OPTIONS` during `next build`. --- .changeset/swift-socks-find.md | 5 + packages/next/src/build/index.ts | 1 + packages/next/src/lib/worker.ts | 6 + packages/next/src/server/config-schema.ts | 1 + packages/next/src/server/config-shared.ts | 7 ++ .../fixtures/default/next.config.js | 1 + .../fixtures/edge/next.config.js | 2 + .../server-source-maps-edge.test.ts | 9 +- .../server-source-maps.test.ts | 117 +++++++++++++++--- 9 files changed, 128 insertions(+), 21 deletions(-) create mode 100644 .changeset/swift-socks-find.md diff --git a/.changeset/swift-socks-find.md b/.changeset/swift-socks-find.md new file mode 100644 index 000000000000..babf8d42cd19 --- /dev/null +++ b/.changeset/swift-socks-find.md @@ -0,0 +1,5 @@ +--- +'next': patch +--- + +Sourcemap errors during prerender if `experimental.enablePrerenderSourceMaps` is enabled diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 614e512d78a5..e428f3fbedaf 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -758,6 +758,7 @@ export function createStaticWorker( progress?.clear() }, debuggerPortOffset, + enableSourceMaps: config.experimental.enablePrerenderSourceMaps, // remove --max-old-space-size flag as it can cause memory issues. isolatedMemory: true, enableWorkerThreads: config.experimental.workerThreads, diff --git a/packages/next/src/lib/worker.ts b/packages/next/src/lib/worker.ts index 75bd73eb94b5..c86784fb951c 100644 --- a/packages/next/src/lib/worker.ts +++ b/packages/next/src/lib/worker.ts @@ -43,6 +43,7 @@ export class Worker { * `-1` if not inspectable */ debuggerPortOffset: number + enableSourceMaps?: boolean /** * True if `--max-old-space-size` should not be forwarded to the worker. */ @@ -57,6 +58,7 @@ export class Worker { } ) { let { + enableSourceMaps, timeout, onRestart, logger = console, @@ -91,6 +93,10 @@ export class Worker { } } + if (enableSourceMaps) { + nodeOptions['enable-source-maps'] = true + } + if (isolatedMemory) { delete nodeOptions['max-old-space-size'] delete nodeOptions['max_old_space_size'] diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 5722af8fe92e..f3cd899017d0 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -464,6 +464,7 @@ export const configSchema: zod.ZodType = z.lazy(() => optimizeServerReact: z.boolean().optional(), clientTraceMetadata: z.array(z.string()).optional(), serverMinification: z.boolean().optional(), + enablePrerenderSourceMaps: z.boolean().optional(), serverSourceMaps: z.boolean().optional(), useWasmBinary: z.boolean().optional(), useLightningcss: z.boolean().optional(), diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 0b5314a1e346..17aaeb27eb85 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -559,6 +559,12 @@ export interface ExperimentalConfig { */ serverMinification?: boolean + /** + * Enables source maps while generating static pages. + * Helps with errors during the prerender phase in `next build`. + */ + enablePrerenderSourceMaps?: boolean + /** * Enables source maps generation for the server production bundle. */ @@ -1309,6 +1315,7 @@ export const defaultConfig = { appNavFailHandling: false, prerenderEarlyExit: true, serverMinification: true, + enablePrerenderSourceMaps: false, serverSourceMaps: false, linkNoTouchStart: false, caseSensitiveRoutes: false, diff --git a/test/e2e/app-dir/server-source-maps/fixtures/default/next.config.js b/test/e2e/app-dir/server-source-maps/fixtures/default/next.config.js index 1e411141b940..4c40830b5701 100644 --- a/test/e2e/app-dir/server-source-maps/fixtures/default/next.config.js +++ b/test/e2e/app-dir/server-source-maps/fixtures/default/next.config.js @@ -6,6 +6,7 @@ const nextConfig = { cpus: 1, ppr: true, dynamicIO: true, + enablePrerenderSourceMaps: true, serverSourceMaps: true, }, serverExternalPackages: ['external-pkg'], diff --git a/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js b/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js index 70e8c4e01e19..9e036b0292ef 100644 --- a/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js +++ b/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js @@ -3,6 +3,8 @@ */ const nextConfig = { experimental: { + cpus: 1, + enablePrerenderSourceMaps: true, serverSourceMaps: true, }, } diff --git a/test/e2e/app-dir/server-source-maps/server-source-maps-edge.test.ts b/test/e2e/app-dir/server-source-maps/server-source-maps-edge.test.ts index f8ec2192a75e..61ec4cc12223 100644 --- a/test/e2e/app-dir/server-source-maps/server-source-maps-edge.test.ts +++ b/test/e2e/app-dir/server-source-maps/server-source-maps-edge.test.ts @@ -42,7 +42,8 @@ describe('app-dir - server source maps edge runtime', () => { '\n}' ) } else { - // TODO: Test `next build` with `--enable-source-maps`. + // Edge runtime pages are not prerendered during `next build`. + // `next start` is not sourcemapped on purpose. } }) @@ -71,7 +72,8 @@ describe('app-dir - server source maps edge runtime', () => { ) expect(cliOutput).toMatch(/digest: '\d+'/) } else { - // TODO: Test `next build` with `--enable-source-maps`. + // Edge runtime pages are not prerendered during `next build`. + // `next start` is not sourcemapped on purpose. } }) @@ -99,7 +101,8 @@ describe('app-dir - server source maps edge runtime', () => { ) expect(cliOutput).toMatch(/digest: '\d+'/) } else { - // TODO: Test `next build` with `--enable-source-maps`. + // Edge runtime pages are not prerendered during `next build`. + // `next start` is not sourcemapped on purpose. } }) }) diff --git a/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts b/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts index a3950aa3833d..56e85eaa03a8 100644 --- a/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts +++ b/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts @@ -25,10 +25,10 @@ describe('app-dir - server source maps', () => { if (skipped) return it('logged errors have a sourcemapped stack with a codeframe', async () => { - const outputIndex = next.cliOutput.length - await next.render('/rsc-error-log') - if (isNextDev) { + const outputIndex = next.cliOutput.length + await next.render('/rsc-error-log') + await retry(() => { expect(next.cliOutput.slice(outputIndex)).toContain( 'Error: rsc-error-log' @@ -48,15 +48,36 @@ describe('app-dir - server source maps', () => { '\n' ) } else { - // TODO: Test `next build` with `--enable-source-maps`. + if (isTurbopack) { + // TODO(veil): Sourcemap names + // TODO(veil): relative paths + expect(normalizeCliOutput(next.cliOutput)).toContain( + '(turbopack:///[project]/app/rsc-error-log/page.js:4:16)' + ) + expect(normalizeCliOutput(next.cliOutput)).toContain( + '' + + "\n> 4 | const error = new Error('rsc-error-log')" + + '\n | ^' + ) + } else { + expect(normalizeCliOutput(next.cliOutput)).toContain( + 'webpack:///app/rsc-error-log/page.js:5:16' + ) + expect(normalizeCliOutput(next.cliOutput)).toContain( + '' + + '\n> 5 | console.error(error)' + + '\n | ^' + + '\n' + ) + } } }) it('logged errors have a sourcemapped `cause`', async () => { - const outputIndex = next.cliOutput.length - await next.render('/rsc-error-log-cause') - if (isNextDev) { + const outputIndex = next.cliOutput.length + await next.render('/rsc-error-log-cause') + await retry(() => { expect(next.cliOutput.slice(outputIndex)).toContain( 'Error: rsc-error-log-cause' @@ -84,15 +105,36 @@ describe('app-dir - server source maps', () => { '\n' ) } else { - // TODO: Test `next build` with `--enable-source-maps`. + if (isTurbopack) { + // TODO(veil): Sourcemap names + // TODO(veil): relative paths + expect(normalizeCliOutput(next.cliOutput)).toContain( + '(turbopack:///[project]/app/rsc-error-log-cause/page.js:2:16)' + ) + expect(normalizeCliOutput(next.cliOutput)).toContain( + '(turbopack:///[project]/app/rsc-error-log-cause/page.js:7:16)' + ) + expect(normalizeCliOutput(next.cliOutput)).toContain( + '' + + "\n> 2 | const error = new Error('rsc-error-log-cause', { cause })" + + '\n | ^' + ) + expect(normalizeCliOutput(next.cliOutput)).toContain( + '' + + "\n > 7 | const error = new Error('Boom')" + + '\n | ^' + ) + } else { + // TODO(veil): line/column numbers are flaky in Webpack + } } }) it('stack frames are ignore-listed in ssr', async () => { - const outputIndex = next.cliOutput.length - const browser = await next.browser('/ssr-error-log-ignore-listed') - if (isNextDev) { + const outputIndex = next.cliOutput.length + const browser = await next.browser('/ssr-error-log-ignore-listed') + await retry(() => { expect(next.cliOutput.slice(outputIndex)).toContain( 'Error: ssr-error-log-ignore-listed' @@ -189,7 +231,19 @@ describe('app-dir - server source maps', () => { `) } } else { - // TODO: Test `next build` with `--enable-source-maps`. + if (isTurbopack) { + // TODO(veil): Sourcemapping line off + // TODO(veil): Sourcemap names + // TODO(veil): relative paths + expect(normalizeCliOutput(next.cliOutput)).toContain( + '(turbopack:///[project]/app/ssr-error-log-ignore-listed/page.js:24:2)' + ) + expect(normalizeCliOutput(next.cliOutput)).toContain( + '\n> 24 | })\n | ^' + ) + } else { + // TODO(veil): line/column numbers are flaky in Webpack + } } }) @@ -242,15 +296,28 @@ describe('app-dir - server source maps', () => { '\n' ) } else { - // TODO: Test `next build` with `--enable-source-maps`. + if (isTurbopack) { + // TODO(veil): Sourcemap names + // TODO(veil): relative paths + expect(normalizeCliOutput(next.cliOutput)).toContain( + 'at (turbopack:///[project]/app/rsc-error-log-ignore-listed/page.js:8:16)' + ) + expect(normalizeCliOutput(next.cliOutput)).toContain( + '' + + "\n> 8 | const error = new Error('rsc-error-log-ignore-listed')" + + '\n | ^' + ) + } else { + // TODO(veil): line/column numbers are flaky in Webpack + } } }) it('thrown SSR errors', async () => { - const outputIndex = next.cliOutput.length - const browser = await next.browser('/ssr-throw') - if (isNextDev) { + const outputIndex = next.cliOutput.length + const browser = await next.browser('/ssr-throw') + await retry(() => { expect(next.cliOutput.slice(outputIndex)).toContain('Error: ssr-throw') }) @@ -287,7 +354,7 @@ describe('app-dir - server source maps', () => { } `) } else { - // TODO: Test `next build` with `--enable-source-maps`. + // SSR errors are not logged because React retries them during hydration. } }) @@ -356,7 +423,21 @@ describe('app-dir - server source maps', () => { ).toEqual(3) } } else { - // TODO: test `next start` with `--enable-source-maps` + if (isTurbopack) { + // Expect the invalid sourcemap warning only once per render. + // Dynamic I/O renders two times. + expect( + normalizeCliOutput(next.cliOutput).split('Invalid source map.') + .length - 1 + ).toEqual(2) + } else { + // Webpack is silent about invalid sourcemaps for next build. + + expect( + normalizeCliOutput(next.cliOutput).split('Invalid source map.') + .length - 1 + ).toEqual(0) + } } }) }) From 3d3b509ee47f51577944d14723e0612dd2e17abb Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Wed, 14 May 2025 12:16:27 +0200 Subject: [PATCH 2/2] CI-only test adjustments :( --- .../server-source-maps.test.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts b/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts index 56e85eaa03a8..96dfabfa29d5 100644 --- a/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts +++ b/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts @@ -60,15 +60,7 @@ describe('app-dir - server source maps', () => { '\n | ^' ) } else { - expect(normalizeCliOutput(next.cliOutput)).toContain( - 'webpack:///app/rsc-error-log/page.js:5:16' - ) - expect(normalizeCliOutput(next.cliOutput)).toContain( - '' + - '\n> 5 | console.error(error)' + - '\n | ^' + - '\n' - ) + // TODO(veil): line/column numbers are flaky in Webpack } } }) @@ -425,14 +417,16 @@ describe('app-dir - server source maps', () => { } else { if (isTurbopack) { // Expect the invalid sourcemap warning only once per render. - // Dynamic I/O renders two times. expect( normalizeCliOutput(next.cliOutput).split('Invalid source map.') .length - 1 - ).toEqual(2) + ).toEqual( + // >= 20 + // behavior in Node.js 20+ is intended + process.versions.node.startsWith('18') ? 0 : 2 + ) } else { // Webpack is silent about invalid sourcemaps for next build. - expect( normalizeCliOutput(next.cliOutput).split('Invalid source map.') .length - 1