mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Run dev validation in process when using Webpack (#96219)
Node.js caches source maps per isolate, so the dev validation worker only has maps for the chunk files loaded on its own thread. For chunks it never loads, such as a module behind a dynamic `import()`, it reads the `.map` that Turbopack writes next to the chunk. Webpack keeps its dev source maps in the compiler instead, so the worker has nothing to read and reports those frames with the position they have in the compiled output rather than in the source. This stops using the worker when the bundler is Webpack, so validation runs in process again, where those frames resolve, and `experimental.devValidationWorker` now has no effect there. Dev performance under Webpack goes back to what it was before validation moved to a worker, which is the better trade: the worker exists to keep the event loop responsive during rapid navigation, and that is not worth losing the source location on validation errors.
This commit is contained in:
@@ -1193,6 +1193,10 @@ export interface ExperimentalConfig {
|
||||
* (when `instant` is configured). Enabled by default; set to `false` to run
|
||||
* validation in-process, as an escape hatch to isolate a problem or fall back
|
||||
* if the worker misbehaves.
|
||||
*
|
||||
* Has no effect with Webpack, where validation always runs in process. The
|
||||
* worker's thread cannot reach Webpack's dev source maps, so validation
|
||||
* errors would be reported without a source location.
|
||||
*/
|
||||
devValidationWorker?: boolean
|
||||
|
||||
|
||||
@@ -232,7 +232,17 @@ export default class DevServer extends Server {
|
||||
// navigation. Gated by `experimental.devValidationWorker`. The worker is
|
||||
// spawned lazily on the first navigation that validates, so this install is
|
||||
// free when a project doesn't use Cache Components.
|
||||
if (this.nextConfig.experimental.devValidationWorker !== false) {
|
||||
//
|
||||
// Turbopack only, because the worker's thread has source maps just for the
|
||||
// chunks it loaded itself, and resolves the rest by reading the `.map`
|
||||
// Turbopack writes next to each chunk. Webpack keeps its dev source maps in
|
||||
// the compiler, which the worker's thread cannot reach, so validation
|
||||
// errors would be reported without a source location. Running validation on
|
||||
// the main thread costs dev performance but keeps those frames intact.
|
||||
if (
|
||||
process.env.TURBOPACK &&
|
||||
this.nextConfig.experimental.devValidationWorker !== false
|
||||
) {
|
||||
installDevValidationWorker({
|
||||
distDir: this.distDir,
|
||||
buildId: this.buildId,
|
||||
|
||||
+38
-62
@@ -1,18 +1,22 @@
|
||||
import { nextTestSetup } from 'e2e-utils'
|
||||
import { getDevCliValidationOutput } from 'e2e-utils/instant-validation'
|
||||
|
||||
// The dev validation worker symbolicates the frames it logs on its own thread,
|
||||
// where Node.js caches source maps only for the chunk files that thread loaded.
|
||||
// The worker never renders server components, and the built entry reaches
|
||||
// segment modules through lazy getters, so a module the bundler splits into a
|
||||
// chunk of its own is never loaded there and has no map in that cache. The
|
||||
// worker resolves those by reading the `.map` the bundler emitted next to the
|
||||
// chunk, which only exists for Turbopack. Each route below owns its copy of the
|
||||
// module, because a module shared between routes lands in whichever chunk the
|
||||
// first compiled route put it in, which would make these snapshots depend on
|
||||
// the order the routes run in.
|
||||
// Validation errors are logged with source-mapped frames, and the routes below
|
||||
// cover the two cases that differ in where that source map comes from. Node.js
|
||||
// caches source maps per isolate and per chunk file, so a validation pass
|
||||
// running on a worker thread only has maps for the chunks that thread loaded.
|
||||
// It currently never renders server components, and the built entry reaches
|
||||
// segment modules through lazy getters, so it loads whatever `loadComponents`
|
||||
// pulls in and nothing else: code in the route's own chunk resolves, a module
|
||||
// the bundler splits into a chunk of its own does not, and the worker reads the
|
||||
// `.map` emitted next to that chunk instead. Only Turbopack emits one, which is
|
||||
// why validation runs in process under Webpack, so the two bundlers arrive at
|
||||
// the same frames by different means. Each route owns its copy of the module,
|
||||
// because a module shared between routes lands in whichever chunk the first
|
||||
// compiled route put it in, which would make these snapshots depend on the
|
||||
// order the routes run in.
|
||||
describe('dev-validation-worker-source-maps', () => {
|
||||
const { next, isTurbopack } = nextTestSetup({
|
||||
const { next } = nextTestSetup({
|
||||
files: __dirname,
|
||||
env: { NEXT_TEST_LOG_VALIDATION: '1' },
|
||||
})
|
||||
@@ -20,8 +24,8 @@ describe('dev-validation-worker-source-maps', () => {
|
||||
it('symbolicates a cookie read in a statically imported module', async () => {
|
||||
const browser = await next.browser('/static')
|
||||
|
||||
// The module stays in the route's own chunk, which the worker loads, so its
|
||||
// frames resolve the same way they do for in-process validation.
|
||||
// The module stays in the route's own chunk, so its map is in the cache of
|
||||
// whichever thread validates the route.
|
||||
expect(
|
||||
await getDevCliValidationOutput(await browser.url(), () => next.cliOutput)
|
||||
).toMatchInlineSnapshot(`
|
||||
@@ -53,57 +57,29 @@ describe('dev-validation-worker-source-maps', () => {
|
||||
() => next.cliOutput
|
||||
)
|
||||
|
||||
// The module lands in a chunk of its own, which the worker never loads,
|
||||
// so it has no map for it in the worker thread's cache.
|
||||
if (isTurbopack) {
|
||||
// Reading the `.map` next to the chunk resolves it anyway.
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
"Error: Route "/dynamic": Next.js encountered runtime data during prerendering.
|
||||
// A module in a chunk of its own is the case a worker thread cannot resolve
|
||||
// from Node.js' cache: under Turbopack the worker falls back to the emitted
|
||||
// `.map`, and under Webpack validation runs in process, where the module
|
||||
// was evaluated and its map is cached.
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
"Error: Route "/dynamic": Next.js encountered runtime data during prerendering.
|
||||
|
||||
\`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` accessed outside of \`<Suspense>\` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.
|
||||
\`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` accessed outside of \`<Suspense>\` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.
|
||||
|
||||
Ways to fix this:
|
||||
- [stream] Provide a placeholder with \`<Suspense fallback={...}>\` around the data access
|
||||
- [block] Set \`export const instant = false\` to allow a blocking route
|
||||
Ways to fix this:
|
||||
- [stream] Provide a placeholder with \`<Suspense fallback={...}>\` around the data access
|
||||
- [block] Set \`export const instant = false\` to allow a blocking route
|
||||
|
||||
Learn more: https://nextjs.org/docs/messages/blocking-prerender-runtime
|
||||
at readCookie (app/dynamic/read-cookie.ts:4:30)
|
||||
at DynamicPage (app/dynamic/page.tsx:4:31)
|
||||
2 |
|
||||
3 | export async function readCookie(): Promise<string> {
|
||||
> 4 | const store = await cookies()
|
||||
| ^
|
||||
5 | return store.get('probe')?.value ?? 'none'
|
||||
6 | }
|
||||
7 |"
|
||||
`)
|
||||
} else {
|
||||
// Webpack keeps the module path but reports the position it has in the
|
||||
// compiled output. It holds its dev source maps in the compiler rather
|
||||
// than writing them next to the chunks, so unlike for Turbopack there is
|
||||
// nothing on disk for the worker to read instead.
|
||||
//
|
||||
// TODO(veil): Turn `experimental.devValidationWorker` off for Webpack
|
||||
// until the worker can resolve these frames.
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
"Error: Route "/dynamic": Next.js encountered runtime data during prerendering.
|
||||
|
||||
\`cookies()\`, \`headers()\`, \`params\`, or \`searchParams\` accessed outside of \`<Suspense>\` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.
|
||||
|
||||
Ways to fix this:
|
||||
- [stream] Provide a placeholder with \`<Suspense fallback={...}>\` around the data access
|
||||
- [block] Set \`export const instant = false\` to allow a blocking route
|
||||
|
||||
Learn more: https://nextjs.org/docs/messages/blocking-prerender-runtime
|
||||
at readCookie (webpack-internal:///(rsc)/./app/dynamic/read-cookie.ts:8:78)
|
||||
at DynamicPage (app/dynamic/page.tsx:4:31)
|
||||
2 | const { readCookie } = await import('./read-cookie')
|
||||
3 |
|
||||
> 4 | return <p id="value">{await readCookie()}</p>
|
||||
| ^
|
||||
5 | }
|
||||
6 |"
|
||||
`)
|
||||
}
|
||||
Learn more: https://nextjs.org/docs/messages/blocking-prerender-runtime
|
||||
at readCookie (app/dynamic/read-cookie.ts:4:30)
|
||||
at DynamicPage (app/dynamic/page.tsx:4:31)
|
||||
2 |
|
||||
3 | export async function readCookie(): Promise<string> {
|
||||
> 4 | const store = await cookies()
|
||||
| ^
|
||||
5 | return store.get('probe')?.value ?? 'none'
|
||||
6 | }
|
||||
7 |"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user