UNPKG

next

Version:

The React Framework

532 lines (531 loc) • 28.8 kB
import { InvariantError } from '../shared/lib/invariant-error'; import { createPromiseWithResolvers } from '../shared/lib/promise-with-resolvers'; import { EnsureStaticLevel } from './app-render/segment-config/ensure-static'; import { RenderStage } from './app-render/staged-rendering'; import { workAsyncStorage } from './app-render/work-async-storage.external'; import { workUnitAsyncStorage } from './app-render/work-unit-async-storage.external'; import { getServerReact, getClientReact } from './runtime-reacts.external'; export function isHangingPromiseRejectionError(err) { if (typeof err !== 'object' || err === null || !('digest' in err)) { return false; } return err.digest === HANGING_PROMISE_REJECTION; } const HANGING_PROMISE_REJECTION = 'HANGING_PROMISE_REJECTION'; class HangingPromiseRejectionError extends Error { constructor(route, expression){ super(`During prerendering, ${expression} rejects when the prerender is complete. Typically these errors are handled by React but if you move ${expression} to a different context by using \`setTimeout\`, \`after\`, or similar functions you may observe this error and you should handle it in that context. This occurred at route "${route}".`), this.route = route, this.expression = expression, this.digest = HANGING_PROMISE_REJECTION; } } const CLIENT_HOOK_DYNAMIC = 'CLIENT_HOOK_DYNAMIC'; export class ClientHookDynamicError extends Error { constructor(route, expression){ super(`Route "${route}": Next.js encountered URL data \`${expression}\` in a Client Component outside of \`<Suspense>\`.\n\n` + `This blocks prerendering because the value is only available at runtime.\n\n` + `Ways to fix this:\n` + ` - [stream] Wrap the component in \`<Suspense fallback={...}>\` so the hook value streams in after prerendering\n` + ` - [block] Set \`export const instant = false\` to allow a blocking route\n\n` + `Learn more: https://nextjs.org/docs/messages/blocking-prerender-client-hook`), this.digest = CLIENT_HOOK_DYNAMIC; } } export function isClientHookDynamicError(err) { if (typeof err !== 'object' || err === null || !('digest' in err)) { return false; } return err.digest === CLIENT_HOOK_DYNAMIC; } const abortListenersBySignal = new WeakMap(); /** * Constructs a promise that never resolves, standing in for *dynamic* data: * data that is only available during a real dynamic request and hangs in * every kind of prerender — `io()`, `connection()`, uncached `fetch()`. * * This is primarily useful for cacheComponents where we use promise * resolution timing to determine which parts of a render can be included in a * prerender. * * Records nothing on the prerender store: the promise's holes are only ever * filled by a real dynamic request, so a runtime prefetch response would have * the same holes as the static one. If the data source would resolve during a * runtime prerender, use `makeRuntimeHangingPromise` instead. */ export function makeDynamicHangingPromise(signal, route, expression) { return makeHangingPromiseWithError(signal, new HangingPromiseRejectionError(route, expression)); } export function makeUntrackedHangingPromise(signal, route, expression) { return makeHangingPromiseWithError(signal, new HangingPromiseRejectionError(route, expression)); } /** * Constructs a promise that never resolves, standing in for session data * (which a runtime shell can access). * Examples: cookies, headers * * Awaiting one of these during a static prerender records on the prerender * store that a runtime shell would produce more content than the static * static shell, which the segment prefetch encoding uses * to tell the client whether a runtime request could be skipped. * * When unsure whether data is dynamic or runtime, prefer this method — the * cost of over-recording is a redundant runtime prefetch request; the cost of * under-recording is a permanently missing one. * * For fallback-param data — data a concrete (ISR-upgraded) prerender would * resolve — use `makeFallbackParamsHangingPromise` instead, so the access * is recorded with the right effect on the static-prefetch hint. */ export function makeSessionDataHangingPromise(signal, route, expression, workUnitStore) { const promise = makeHangingPromiseWithError(signal, new HangingPromiseRejectionError(route, expression)); return trackPromiseUsed(promise, trackSessionDataAccessed.bind(null, workUnitStore, expression)); } /** * Constructs a promise that never resolves, standing in for URL data, * which can be accessed in a runtime prefetch (but not a runtime shell). * Examples: fallback params, searchParams * * Awaiting one of these during a static prerender records on the prerender * store that a runtime prefetch would produce more content than the static * response, which the segment prefetch encoding uses * to tell the client whether a runtime prefetch request could be skipped. * * When unsure whether data is dynamic or runtime, prefer this method — the * cost of over-recording is a redundant runtime prefetch request; the cost of * under-recording is a permanently missing one. * * `workUnitStore` may be null ONLY when the caller tracks the access itself. * Such a caller MUST call `trackURLDataAccessed` from every path that * observes the promise (e.g. the proxy traps for `then`/`status`), * against the work unit store active at access time. * * For fallback-param data — data a concrete (ISR-upgraded) prerender would * resolve — use `makeFallbackParamsHangingPromise` instead, so the access * is recorded with the right effect on the static-prefetch hint. */ export function makeURLDataHangingPromise(signal, route, expression, workUnitStore) { const promise = makeHangingPromiseWithError(signal, new HangingPromiseRejectionError(route, expression)); if (workUnitStore === null) { return promise; } return trackPromiseUsed(promise, trackURLDataAccessed.bind(null, workUnitStore, expression)); } /** * Creates a promise that stands in for a result that will either be session data * or URL data, but we don't know which. */ export function makeUnknownRuntimeDataHangingPromise(signal, route, expression, workUnitStore) { // We don't know if this is session or URL data, i.e. if it should affect the shell // or only the prefetch. Track it conservatively as affecting both. return makeSessionDataHangingPromise(signal, route, expression, workUnitStore); } /** * Constructs a promise that never resolves, standing in for data that is only * accessible in the prefetch, but not in the shell, e.g. `prefetch()`. * This usage does *not* indicate that a runtime request is needed, * only that the data is not available in a shell. * * @internal */ export function makePrefetchHangingPromise(signal, route, expression) { return makeUntrackedHangingPromise(signal, route, expression); } /** * Variant of `makeRuntimeHangingPromise` for *fallback-param* data: fallback * route params and values derived solely from them (`params`, `rootParams`, * `pathname` during a fallback prerender). Like every runtime data access, * awaiting it records the access on the prerender store's response-level flag, * but its effect on the build-time static-prefetch hint differs — on a * fallback-upgradeable route the access is transient (a concrete prerender * resolves it), so it leaves the hint intact. See `trackFallbackParamsAccessed`. * * As with `makeRuntimeHangingPromise`, `workUnitStore` may be null ONLY when * the caller tracks the access itself by calling `trackFallbackParamsAccessed` * from every path that observes the promise. */ export function makeFallbackParamsHangingPromise(signal, route, expression, workUnitStore) { const promise = makeHangingPromiseWithError(signal, new HangingPromiseRejectionError(route, expression)); if (workUnitStore === null) { return promise; } return trackPromiseUsed(promise, trackFallbackParamsAccessed.bind(null, workUnitStore, expression)); } export function createPrerenderDataTracking() { return { runtimeDataAccessed: createPromiseWithResolvers(), shouldAttemptStaticShell: true, shouldAttemptStaticPrefetch: true }; } export function finishPrerenderDataTracking(prerenderDataTracking) { // If a runtime data access already resolved this promise, this is a no-op. prerenderDataTracking.runtimeDataAccessed.resolve(false); } /** * Records on a static prerender store that the render accessed a data source * which would have resolved in a runtime shell (or runtime prefetch). * No-op for all other store types. * * Prefer `makeRuntimeHangingPromise`. Use this function only when implementing * similar tracking and that one is not enough. */ function trackSessionDataAccessed(workUnitStore, expression) { trackRuntimeDataAccessed(workUnitStore, 1, expression); } /** * Records on a static prerender store that the render accessed a data source * which would have resolved in a runtime prefetch (but NOT in a runtime shell) * No-op for all other store types. * * Prefer `makeRuntimeHangingPromise`. Use this function only when implementing * similar tracking and that one is not enough. * * For fallback-param data, use `trackFallbackParamsAccessed` instead. When * unsure, this is the conservative choice. */ export function trackURLDataAccessed(workUnitStore, expression) { trackRuntimeDataAccessed(workUnitStore, 2, expression); } /** * Fallback-param variant of `trackRuntimeDataAccessed`, for accesses of * fallback route params and values derived solely from them. It records the * response-level flag all the same, but only clears the build-time * static-prefetch hint when the route is not fallback-upgradeable — on an * upgradeable route the access is transient, since ISR later produces a * concrete prerender that resolves it. */ export function trackFallbackParamsAccessed(workUnitStore, expression) { trackRuntimeDataAccessed(workUnitStore, 3, expression); } function trackRuntimeDataAccessed(workUnitStore, dataKind, expression) { switch(workUnitStore.type){ case 'prerender': { const { prerenderDataTracking, stagedRendering } = workUnitStore; if (!prerenderDataTracking || !stagedRendering) { return; } const { currentStage } = stagedRendering; if (currentStage === RenderStage.Before) { console.error(new InvariantError('Unexpected trackRuntimeDataAccessed in the Before stage.')); return; } if (currentStage >= RenderStage.NavigationStatic) { // Ignore any accesses that happen after `navigation()` resolves. // The purpose of this tracking is to judge whether a runtime prefetch // would give us a more complete result than a static one. // But `navigation()` wouldn't have resolved in a runtime prefetch, // so e.g. `await navigation(); await cookies()` wouldn't have more content // in those, and we shouldn't count it. return; } const ensureStaticLevel = workUnitStore.ensureStaticLevel ?? EnsureStaticLevel.None; // NOTE: In general, we keep hints in sync with `needsRuntimeRequest`, but they // don't have to always match. The client re-uses hints for the entire route, // while `needsRuntimeRequest` can vary across individual prerendered param values // (e.g. if cookies are accessed depending on a param value). // Hints can also become outdated if a route only starts using runtime data // after a revalidation, so we have to expect this and be resilient to it. // Also see the upgradeable fallback params case below, which deliberately // puts them out of sync. switch(dataKind){ case 1: { // Potentially deopt both the shell and the prefetch, // because if the shell accessed runtime data, so does the prefetch. // However, if we're already past the shell stage, the shell is not affected. // (which makes e.g. `await prefetch(); await cookies()` only affect the prefetch) let firstAffectedStage = null; if (currentStage <= RenderStage.ShellStatic && // Only track if we're not forcing the shell to be static. ensureStaticLevel < EnsureStaticLevel.Shell) { prerenderDataTracking.shouldAttemptStaticShell = false; firstAffectedStage ??= RenderStage.ShellStatic; logRuntimeDeopt == null ? void 0 : logRuntimeDeopt(expression, 'shell'); } if (currentStage <= RenderStage.PrefetchStatic && // Only track if we're not forcing the prefetch to be static. ensureStaticLevel < EnsureStaticLevel.Prefetch) { prerenderDataTracking.shouldAttemptStaticPrefetch = false; // NOTE: if the shell is affected, don't override it. firstAffectedStage ??= RenderStage.PrefetchStatic; logRuntimeDeopt == null ? void 0 : logRuntimeDeopt(expression, 'prefetch'); } if (firstAffectedStage !== null) { markRuntimeDataAccessWhenStageReached(prerenderDataTracking, stagedRendering, firstAffectedStage); } break; } case 3: { if (workUnitStore.isFallbackUpgradeable) { // An fallback-param access is transient when the route is // fallback-upgradeable (i.e. ISR later produces the concrete prerender a // static prefetch would hit) so it does not indicate the need for a runtime // request and thus does not affect the static hints. // // If runtime prefetches are not disallowed by `ensureStatic`, then we still // set `runtimeDataAccessed` (while keeping the hints static). This means that // if the concrete prerender isn't ready yet and we served the fallback, then // the client knows it can use a *runtime* prefetch for speculative links -- // a runtime prefetch can provide the same content (or more) as the concrete // prerender would. if (ensureStaticLevel < EnsureStaticLevel.Prefetch) { markRuntimeDataAccessWhenStageReached(prerenderDataTracking, stagedRendering, // `params` are URL data, so they only affect the prefetch RenderStage.PrefetchStatic); logRuntimeUpgradeableFallback == null ? void 0 : logRuntimeUpgradeableFallback(); } break; } // not an upgradeable fallback param access, so we treat it as URL data. // intentional fallthrough } case 2: { // Only deopt the prefetch, not the shell, which cannot access URL data anyway. if (currentStage <= RenderStage.PrefetchStatic && ensureStaticLevel < EnsureStaticLevel.Prefetch) { prerenderDataTracking.shouldAttemptStaticPrefetch = false; logRuntimeDeopt == null ? void 0 : logRuntimeDeopt(expression, 'prefetch'); markRuntimeDataAccessWhenStageReached(prerenderDataTracking, stagedRendering, RenderStage.PrefetchStatic); } break; } } break; } case 'prerender-client': case 'prerender-legacy': case 'prerender-runtime': case 'validation-client': case 'request': case 'cache': case 'private-cache': case 'unstable-cache': case 'build-time-generator': break; default: workUnitStore; } } /** * Tracks a runtime data access on the `runtimeDataAccessed` promise, * but with a delay until `targetStage`. * * When we encounter a URL data access like `await params`, we need to mark the * *prefetch* as needing runtime data, but the *shell* should remain unaffected * (because it cannot access params anyway). * * However, the shell and the prefetch share one `runtimeDataAccessed` promise, * and it needs to be accurately rewindable by the client * In other words, we need to make sure that it reads as `false` when rewound * to a shell, but as `true` in the final response (the prefetch). * * This means that if the `await params` is encountered during the shell stage, we * cannot resolve `runtimeDataAccessed` immediately. * Instead, we delay the resolution until the PrefetchStatic stage, so the promise * will remain unresolved when rewound to the shell stage (which reads as `false`). */ function markRuntimeDataAccessWhenStageReached(prerenderDataTracking, stageController, targetStage) { const { runtimeDataAccessed } = prerenderDataTracking; // NOTE: If we're already in or past the target stage, we can avoid allocating a closure, // because `onStage` would've executed the callback immediately anyway. if (stageController.currentStage >= targetStage) { runtimeDataAccessed.resolve(true); } else { stageController.onStage(targetStage, runtimeDataAccessed.resolve.bind(null, true)); } } const logRuntimeDeopt = process.env.NEXT_PRIVATE_DEBUG_RUNTIME_DATA ? (expression, kind)=>{ const { route } = workAsyncStorage.getStore(); console.log(`Route '${route}': deopting to a runtime ${kind} because it used ${expression}`); } : undefined; const logRuntimeUpgradeableFallback = process.env.NEXT_PRIVATE_DEBUG_RUNTIME_DATA ? ()=>{ const { route } = workAsyncStorage.getStore(); console.log(`Route '${route}': marking upgradeable fallback as runtime prefetchable`); } : undefined; /** * Signals that we cannot recover both a runtime shell and a static (PPR) shell * from the same render. Use this whenever the stage of a promise varies on * `RequestStore.needsRuntimeShell`. * */ export function trackIncompatibleShellContent(workUnitStore, reason) { const { stagedRendering } = workUnitStore; if (!stagedRendering) { return; } // TODO(app-shells): optimize this to only consider stages that are relevant for validation. // We should only track incompatible content when it can affect them. // For now, we simply exclude everything that happens in the dynamic stage. // (Note that we also need to account for cache misses that move things to a // different stage -- those should also preemptively set `hasIncompatibleShellContent` // because there's a chance that a render with warm caches would set it) const { currentStage } = stagedRendering; if (currentStage === RenderStage.Dynamic || currentStage === RenderStage.Abandoned) { return; } if (process.env.NEXT_PRIVATE_DEBUG_VALIDATION) { const workStore = workAsyncStorage.getStore(); console.log(`Route ${workStore.route}: Incompatible shell content: ${reason}`); } workUnitStore.hasIncompatibleShellContent = true; } export function makeClientHookHangingPromise(signal, error) { return makeHangingPromiseWithError(signal, error); } function makeHangingPromiseWithError(signal, error) { if (signal.aborted) { return Promise.reject(error); } else { const hangingPromise = new Promise((_, reject)=>{ const boundRejection = reject.bind(null, error); let currentListeners = abortListenersBySignal.get(signal); if (currentListeners) { currentListeners.push(boundRejection); } else { const listeners = [ boundRejection ]; abortListenersBySignal.set(signal, listeners); signal.addEventListener('abort', ()=>{ for(let i = 0; i < listeners.length; i++){ listeners[i](); } }, { once: true }); } }); // We are fine if no one actually awaits this promise. We shouldn't consider this an unhandled rejection so // we attach a noop catch handler here to suppress this warning. If you actually await somewhere or construct // your own promise out of it you'll need to ensure you handle the error when it rejects. hangingPromise.catch(ignoreReject); return hangingPromise; } } function ignoreReject() {} /** * Creates a promise that will be triggered when another promise resolves. * It will not emit unhandled rejections, which is important if the trigger * is a promise that might itself get rejected (e.g. when a prerender/render * are aborted due to sync IO) */ export function makePromiseFromTrigger(trigger, value) { const promise = trigger.then(()=>value); promise.catch(ignoreReject); return promise; } export function makeDevtoolsIOAwarePromise(underlying, requestStore, stage) { if (requestStore.stagedRendering) { // We resolve each stage in a timeout, so React DevTools will pick this up as IO. return requestStore.stagedRendering.delayUntilStage(stage, undefined, underlying); } // in React DevTools if we resolve in a setTimeout we will observe // the promise resolution as something that can suspend a boundary or root. return new Promise((resolve)=>{ // Must use setTimeout to be considered IO React DevTools. setImmediate will not work. setTimeout(()=>{ resolve(underlying); }, 0); }); } /** * Invokes `onUse` whenever `then()/catch()/finally()` are called on the promise * or when the promise is awaited. */ export function trackPromiseUsed(promise, onUse) { // We can instrument `.then()/.catch()/.finally()` in one go by using a Promise subclass // that implements a custom `.then()`, because `catch` and `finally` delegate to it. // // Alternative implementation ideas that were tried and rejected: // // 1. Patching the methods directly via `promise.then = (..args) => { ... }`: // doesn't work, because Node does not call the monkeypatched methods for native `await`: // > Native Promise [...]: The promise is directly used and awaited natively, without calling `then()`. // > https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#description // // 2. Wrapping in a proxy that returns a custom `then/catch/finally`: // breaks async stacks in React's IO tracking (stack becomes `Promise.then`) return TrackedPromise.from(promise, onUse); } class TrackedPromise extends Promise { #onUse; // We don't need derived promises to also be a TrackedPromise. // We only care about the first level of `.then()`. static get [Symbol.species]() { return Promise; } static from(promise, onUse) { // Whenever the promise we're tracking resolves/rejects, we should follow. const tracked = new TrackedPromise(promise.then.bind(promise)); tracked.#onUse = onUse; // Hanging promises catch rejections when created. Tracked promises are generally derived // from promises that may hang & reject, so we need to do the same. // However, we have to bypass the tracking we do in `TrackedPromise.then`. // (we're using `then` directly, because `catch` ends up delegating `TrackedPromise.then`) Promise.prototype.then.call(tracked, undefined, ignoreReject); return tracked; } then(onFulfilled, onRejected) { const onUse = this.#onUse; if (onUse) { try { onUse(); } catch (err) { // We don't want to break the method even if our tracking errored. console.error(err); } } return Promise.prototype.then.call(this, onFulfilled, onRejected); } constructor(...args){ super(...args), this.#onUse = null; } } export const RENDER_STAGES_BY_DATA_KIND = { sessionData: RenderStage.ShellRuntime, /** * Statically-prerenderable URL data, like static `params`. * It may need to be pushed to a runtime stage in a runtime prerender, * but it's semantically distinct from `runtimeUrlData` like `searchParams`, * which is always excluded from static prerenders. * */ staticUrlData: { /** * From that app's point of view, `prefetch()` is semantically the same as * `staticUrlData`, because it does not resolve in a shell but resolves in * a prefetch (even a static one). * We handle it separately to provide a specialized error in validation renders. */ prefetchApi: { static: RenderStage.PrefetchStatic_prefetchApi, runtime: RenderStage.PrefetchRuntime_prefetchApi }, static: RenderStage.PrefetchStatic, runtime: RenderStage.PrefetchRuntime }, /** * URL data that is never statically-prerenderable, like static `searchParams`. * */ runtimeUrlData: RenderStage.PrefetchRuntime }; export function applyOwnerStack(error) { if (process.env.NODE_ENV !== 'production') { var _getClientReact_captureOwnerStack, _getClientReact, _getServerReact_captureOwnerStack, _getServerReact; let ownerStack; const workUnitStore = workUnitAsyncStorage.getStore(); // captureOwnerStack() returns the owner stack for the current React // rendering context. Inside a cache scope this only includes the inner // component tree. The outer owner stack (captured before entering the // cache boundary in use-cache-wrapper.ts) is stored on the cache store. // We concatenate both to get the full component tree. const innerOwnerStack = ((_getClientReact = getClientReact()) == null ? void 0 : (_getClientReact_captureOwnerStack = _getClientReact.captureOwnerStack) == null ? void 0 : _getClientReact_captureOwnerStack.call(_getClientReact)) ?? ((_getServerReact = getServerReact()) == null ? void 0 : (_getServerReact_captureOwnerStack = _getServerReact.captureOwnerStack) == null ? void 0 : _getServerReact_captureOwnerStack.call(_getServerReact)); switch(workUnitStore == null ? void 0 : workUnitStore.type){ case 'cache': case 'private-cache': ownerStack = (innerOwnerStack || '') + (workUnitStore.outerOwnerStack || '') || undefined; break; case 'unstable-cache': case 'request': case 'prerender': case 'prerender-legacy': case 'prerender-runtime': case 'prerender-client': case 'validation-client': case 'build-time-generator': case undefined: ownerStack = innerOwnerStack; break; default: workUnitStore; } if (ownerStack) { let stack = ownerStack; if (error.stack) { const frames = []; for (const frame of error.stack.split('\n').slice(1)){ if (frame.includes('react_stack_bottom_frame')) { break; } frames.push(frame); } stack = '\n' + frames.join('\n') + stack; } error.stack = error.name + ': ' + error.message + stack; } } return error; } //# sourceMappingURL=dynamic-rendering-utils.js.map