next
Version:
The React Framework
265 lines (264 loc) • 12.1 kB
JavaScript
import '../require-hook';
import '../node-environment';
import { isAbsolute, relative } from 'path';
import { readFileSync, realpathSync } from 'fs';
import { fileURLToPath } from 'url';
import { installBindings } from '../../build/swc/install-bindings';
import { installCodeFrameSupport } from '../lib/install-code-frame';
import { loadClientReferenceManifestForPage, loadComponents } from '../load-components';
import { setHttpClientAndAgentOptions } from '../setup-http-agent-env';
import { serializeValidationErrorsToFlight } from '../app-render/dev-validation-error-delivery';
import { formatValidationEvent } from '../app-render/dev-validation-events';
import { getServerActionsManifest, setManifestsSingleton } from '../app-render/manifests-singleton';
import { setBundlerFindSourceMapImplementation } from '../patch-error-inspect';
/**
* Resolves a chunk's source map by reading the `.map` file the bundler emitted
* next to it, for chunks inside `distDir`.
*
* The main thread answers the same question through the Turbopack project
* handle, which cannot cross a thread boundary. Reading from disk is
* project-free and, more importantly, does not depend on the chunk having been
* evaluated in this thread: Node.js caches source maps per isolate, and the
* worker never renders server components, so it holds maps only for the chunks
* `loadComponents` pulled in. Frames arriving in the transported payload can
* point at any chunk the main render touched.
*
* This only helps Turbopack, which writes a `.map` beside every chunk. Webpack
* keeps its dev source maps in the compiler rather than on disk, so its frames
* from chunks this thread never evaluated stay unresolved, and its module URLs
* (`webpack-internal://…`) are declined below for the same reason. Frames from
* dependencies that are not bundled never reach this point, because
* `filterStackFrameDEV` drops `node_modules` and `node:` frames; bundled
* dependencies appear as chunks inside `distDir` like any other code.
*/ function createDiskSourceMapLookup(distDir) {
// The frames carry resolved paths, so compare against the resolved `distDir`
// to keep the containment check meaningful when the project sits behind a
// symlink.
let canonicalDistDir = distDir;
try {
canonicalDistDir = realpathSync(distDir);
} catch {}
const payloads = new Map();
return function findSourceMapPayloadOnDisk(sourceURL) {
let chunkPath = sourceURL;
if (chunkPath.startsWith('file://')) {
try {
chunkPath = fileURLToPath(chunkPath);
} catch {
return undefined;
}
}
if (!isAbsolute(chunkPath)) {
// Not an emitted chunk, e.g. `webpack-internal://` or `<anonymous>`.
return undefined;
}
const cached = payloads.get(chunkPath);
if (cached !== undefined || payloads.has(chunkPath)) {
return cached;
}
let payload;
const relativePath = relative(canonicalDistDir, chunkPath);
// Only chunks emitted into `distDir` have a source map to point at, and
// this keeps the lookup from reading arbitrary paths off disk.
if (!relativePath.startsWith('..') && !isAbsolute(relativePath)) {
try {
payload = JSON.parse(readFileSync(chunkPath + '.map', 'utf8'));
} catch {
payload = undefined;
}
}
payloads.set(chunkPath, payload);
return payload;
};
}
// Match the main dev server (`next-dev-server.ts`), which raises this so the
// server captures deeper stacks. React's owner-stack capture during the
// validation prerenders depends on it, so without it the worker's errors lose
// their owner-stack source attribution.
try {
Error.stackTraceLimit = 50;
} catch {}
// The lifecycle markers E2E tests read from the CLI. Emitted on the worker's
// stdout (piped to the parent) so they interleave with the parent's captured
// output the same way the in-process `runWithDevValidationLogging` markers do.
// Gated on the same test env that path checks.
const isTestLoggingEnabled = !!(process.env.__NEXT_TEST_MODE && process.env.NEXT_TEST_LOG_VALIDATION);
/**
* Adapts the pool's supersede flag into an `AbortSignal` the validation passes
* check at their depth/yield boundaries. The pool shares an `Int32Array`-backed
* `SharedArrayBuffer` whose first slot the main thread flips to non-zero (with
* `Atomics.store` + `Atomics.notify`) when a newer navigation supersedes this
* one. We wait for that notification with `Atomics.waitAsync`, which is
* event-driven rather than polled. A validation that finishes without being
* superseded calls `cleanup()`, which wakes our own still-pending wait so it
* leaves no waiter (and no retained buffer) behind.
*/ function createSupersedeSignal(abortBuffer) {
const controller = new AbortController();
const flag = new Int32Array(abortBuffer);
if (Atomics.load(flag, 0) !== 0) {
controller.abort();
return {
signal: controller.signal,
cleanup: ()=>{}
};
}
let settled = false;
const wait = Atomics.waitAsync(flag, 0, 0);
if (wait.async) {
wait.value.then(()=>{
if (settled) {
return;
}
settled = true;
// Woken either by a real supersede or by `cleanup()`; only the former
// leaves the flag set.
if (Atomics.load(flag, 0) !== 0) {
controller.abort();
}
});
} else if (Atomics.load(flag, 0) !== 0) {
// The flag flipped between the load above and the wait.
controller.abort();
}
return {
signal: controller.signal,
cleanup: ()=>{
if (!settled) {
Atomics.notify(flag, 0);
}
}
};
}
/**
* Waits out the test-only validation delay, resolving early if the render is
* superseded. Mirrors the delay in `runWithDevValidationLogging` so scheduler
* tests observe the same in-flight window on the worker path.
*/ async function applyTestValidationDelay(signal) {
const delayMs = Number(process.env.NEXT_TEST_DEV_VALIDATION_DELAY_MS);
if (!Number.isFinite(delayMs) || delayMs <= 0 || signal.aborted) {
return;
}
await new Promise((resolve)=>{
const finishDelay = ()=>{
clearTimeout(timeout);
signal.removeEventListener('abort', finishDelay);
resolve();
};
const timeout = setTimeout(finishDelay, delayMs);
signal.addEventListener('abort', finishDelay, {
once: true
});
});
}
/**
* Registers the client reference manifests of the pages that supplied client
* references to the render being validated, beyond the validated route's own
* manifest. This thread has its own manifests singleton, which `loadComponents`
* seeds with only the validated route, so without these the dev-only cross-page
* lookup in `createProxiedClientReferenceManifest` has no other manifest to
* search and decoding the transported payload fails. Usually a no-op, since the
* main thread only records a page when React's I/O tracking actually carried a
* reference across pages.
*/ async function registerAdditionalClientReferenceManifests(distDir, pages) {
if (pages.length === 0) {
return;
}
// Set by `loadComponents`. One server actions manifest covers the whole app,
// so the pages registered here share the validated route's.
const serverActionsManifest = getServerActionsManifest();
await Promise.all(pages.map(async (page)=>{
const clientReferenceManifest = await loadClientReferenceManifestForPage(distDir, page);
if (clientReferenceManifest) {
setManifestsSingleton({
page,
clientReferenceManifest,
serverActionsManifest
});
}
}));
}
/**
* Runs the dev instant/static-shell validation passes off the main thread.
* Reloads the route's compiled module, then delegates the whole validation to
* that module via `ComponentMod.routeModule.runValidationInDev`, so every
* render (flight re-encodes and client prerenders) runs inside the app-page
* bundle's single React instance. Logs any returned errors to the worker's
* stderr with source-mapped code frames, then encodes them as RSC Flight bytes
* for the main thread to forward to the dev overlay. Returns `null` when
* validation was superseded or produced no errors.
*/ export async function runDevValidation(message, abortBuffer) {
// Load the native SWC bindings and wire the code-frame renderer so the errors
// logged below render with a source-mapped code frame, matching the
// in-process dev output (the E2E tests snapshot the CLI text between the
// validation markers). The `build/swc` graph these pull in is bundled as a
// runtime external (see `next-runtime.webpack-config.js`), so it resolves
// from the installed `next/dist` tree rather than being compiled into this
// worker bundle, the same way the unbundled build worker loads it.
await installBindings();
installCodeFrameSupport();
setBundlerFindSourceMapImplementation(createDiskSourceMapLookup(message.distDir));
setHttpClientAndAgentOptions({
httpAgentOptions: message.nextConfigSerializable.httpAgentOptions
});
// Populates the manifests singleton for the route via `setManifestsSingleton`
// inside `loadComponents`, exactly as a real request does. The pool tears the
// worker down on HMR / route recompile so the next validation reloads from a
// clean require cache.
const { ComponentMod } = await loadComponents({
distDir: message.distDir,
page: message.page,
isAppPath: true,
isDev: true,
sriEnabled: false,
needsManifestsForLegacyReasons: true
});
await registerAdditionalClientReferenceManifests(message.distDir, message.additionalClientReferenceManifestPages);
const { signal, cleanup } = createSupersedeSignal(abortBuffer);
if (isTestLoggingEnabled) {
console.log(formatValidationEvent({
type: 'validation_start',
requestId: message.requestId,
url: message.request.urlPathname + message.request.urlSearch,
responseFinished: message.responseFinished
}));
}
try {
if (isTestLoggingEnabled) {
await applyTestValidationDelay(signal);
}
if (signal.aborted) {
return null;
}
// Crossing into the app-page bundle: the entire validation runs there, so
// the client prerenders use the same React the user's client components
// resolve through `ComponentMod`.
const validationErrors = await ComponentMod.routeModule.runValidationInDev(ComponentMod, message, signal);
if (validationErrors === undefined || signal.aborted) {
return null;
}
const errors = [];
for (const validationError of validationErrors){
// Log to the worker's stderr; `node-environment` +
// `installCodeFrameSupport` render the source-mapped stack and code frame
// there, matching the in-process CLI output.
console.error(validationError);
if (validationError instanceof Error) {
errors.push(validationError);
}
}
if (errors.length === 0) {
return null;
}
return await serializeValidationErrorsToFlight(ComponentMod, errors);
} finally{
cleanup();
if (isTestLoggingEnabled) {
console.log(formatValidationEvent({
type: signal.aborted ? 'validation_aborted' : 'validation_end',
requestId: message.requestId,
url: message.request.urlPathname + message.request.urlSearch
}));
}
}
}
//# sourceMappingURL=dev-validation-worker.js.map