@wular/pnext 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -0
- package/bin/pnext +67 -0
- package/config/lint/base.js +48 -0
- package/config/ts/base.json +26 -0
- package/config/ts/react.json +13 -0
- package/package.json +70 -0
- package/reference/compat.md +63 -0
- package/reference/config.md +120 -0
- package/reference/css.md +58 -0
- package/reference/dev.md +69 -0
- package/reference/env.md +40 -0
- package/reference/metadata.md +86 -0
- package/reference/navigation.md +149 -0
- package/reference/overview.md +35 -0
- package/reference/performance.md +97 -0
- package/reference/rendering.md +127 -0
- package/reference/routing.md +167 -0
- package/reference/typegen.md +64 -0
- package/src/api/cache.ts +80 -0
- package/src/api/client-cache.ts +9 -0
- package/src/api/client-navigation.ts +279 -0
- package/src/api/dynamic.tsx +102 -0
- package/src/api/link.tsx +119 -0
- package/src/api/navigation.ts +198 -0
- package/src/api/router/events.ts +53 -0
- package/src/api/router/history.ts +70 -0
- package/src/api/router/hub.ts +194 -0
- package/src/api/router/policies.ts +107 -0
- package/src/api/router/runtime.ts +5238 -0
- package/src/api/router/types.ts +299 -0
- package/src/api/router.ts +167 -0
- package/src/api/server.ts +323 -0
- package/src/api/suspense.ts +16 -0
- package/src/cache/context.ts +61 -0
- package/src/cli/adapters/vercel-warm.ts +375 -0
- package/src/cli/adapters/vercel.ts +1310 -0
- package/src/cli/analyze-print.ts +181 -0
- package/src/cli/analyze.ts +328 -0
- package/src/cli/boot-trace.ts +29 -0
- package/src/cli/build.ts +3114 -0
- package/src/cli/dev.ts +276 -0
- package/src/cli/index.ts +196 -0
- package/src/cli/named-bin.ts +119 -0
- package/src/cli/serve-ui.ts +160 -0
- package/src/cli/start.ts +1425 -0
- package/src/client/build.ts +2136 -0
- package/src/client/chunk-fold.ts +526 -0
- package/src/client/entry.ts +1525 -0
- package/src/client/paths.ts +22 -0
- package/src/client/prebuilt.ts +621 -0
- package/src/client/profile.ts +75 -0
- package/src/client/react-compiler.ts +94 -0
- package/src/client/reference-stub.ts +145 -0
- package/src/client/reference.ts +47 -0
- package/src/compat/actions/action-client.ts +531 -0
- package/src/compat/actions/action-dispatch.ts +676 -0
- package/src/compat/actions/action-router.ts +40 -0
- package/src/compat/actions/action-shared.ts +95 -0
- package/src/compat/actions/client-plugin.ts +135 -0
- package/src/compat/actions/client-stub.ts +65 -0
- package/src/compat/actions/config.ts +164 -0
- package/src/compat/actions/detect.ts +208 -0
- package/src/compat/actions/discovery.ts +244 -0
- package/src/compat/actions/early-submit.ts +40 -0
- package/src/compat/actions/endpoint.ts +602 -0
- package/src/compat/actions/flight.ts +52 -0
- package/src/compat/actions/form-state.ts +73 -0
- package/src/compat/actions/hoist.ts +485 -0
- package/src/compat/actions/ids.ts +39 -0
- package/src/compat/actions/index.ts +41 -0
- package/src/compat/actions/instances.ts +144 -0
- package/src/compat/actions/origin.ts +109 -0
- package/src/compat/actions/protocol.ts +125 -0
- package/src/compat/actions/registry.ts +74 -0
- package/src/compat/actions/rewrite.ts +282 -0
- package/src/compat/actions/serve.ts +414 -0
- package/src/compat/actions/server-tag.ts +21 -0
- package/src/compat/actions/unrecognized-error.ts +30 -0
- package/src/compat/adapter/build-complete.ts +257 -0
- package/src/compat/bundler/bun-externals.ts +53 -0
- package/src/compat/bundler/cjs-exports.ts +542 -0
- package/src/compat/bundler/config.ts +363 -0
- package/src/compat/bundler/externals.ts +34 -0
- package/src/compat/bundler/import-meta-url.ts +60 -0
- package/src/compat/bundler/modularize-imports.ts +119 -0
- package/src/compat/bundler/new-url-asset.ts +87 -0
- package/src/compat/bundler/optimize-package-imports.ts +273 -0
- package/src/compat/bundler/polyfill.ts +88 -0
- package/src/compat/bundler/react-compiler.ts +61 -0
- package/src/compat/bundler/react-profiler.tsx +25 -0
- package/src/compat/bundler/relay-transform.ts +116 -0
- package/src/compat/bundler/require-context.ts +281 -0
- package/src/compat/bundler/resolve-extensions.ts +75 -0
- package/src/compat/bundler/source-cache.ts +61 -0
- package/src/compat/bundler/static-imports.ts +25 -0
- package/src/compat/bundler/symlink-imports.ts +119 -0
- package/src/compat/bundler/tsconfig-paths.ts +50 -0
- package/src/compat/bundler/wasm.ts +153 -0
- package/src/compat/bundler/webpack-loaders.ts +685 -0
- package/src/compat/bundler/worker.ts +278 -0
- package/src/compat/cache/build-flags.ts +81 -0
- package/src/compat/cache/build-prerender-errors.ts +163 -0
- package/src/compat/cache/custom-handler.ts +159 -0
- package/src/compat/cache/fetch-patch.ts +745 -0
- package/src/compat/cache/handler.ts +100 -0
- package/src/compat/cache/modern-handler.ts +275 -0
- package/src/compat/cache/resume-data-cache.ts +143 -0
- package/src/compat/cache/revalidate.ts +759 -0
- package/src/compat/cache/runtime-error.ts +124 -0
- package/src/compat/cache/use-cache-transform.ts +961 -0
- package/src/compat/cache/use-cache.ts +1695 -0
- package/src/compat/cache-control.ts +269 -0
- package/src/compat/client/base-path.ts +64 -0
- package/src/compat/client/css-order.ts +36 -0
- package/src/compat/client/errors/control-flow.ts +92 -0
- package/src/compat/client/errors/error-boundary.ts +222 -0
- package/src/compat/client/errors/global-error.ts +238 -0
- package/src/compat/client/errors/install.ts +217 -0
- package/src/compat/client/errors/lazy.ts +53 -0
- package/src/compat/client/errors/primitive-throw.ts +126 -0
- package/src/compat/client/errors/soft-refresh.ts +14 -0
- package/src/compat/client/link-status.ts +86 -0
- package/src/compat/client/nav-compat-runtime.ts +57 -0
- package/src/compat/client/nav-compat.ts +42 -0
- package/src/compat/client/navigation-scroll.ts +154 -0
- package/src/compat/client/optimistic-routing.ts +206 -0
- package/src/compat/client/prefetch-cache.ts +111 -0
- package/src/compat/client/route-announcer.ts +72 -0
- package/src/compat/client/segment-cache-policy.ts +159 -0
- package/src/compat/client/segment-cache.ts +1077 -0
- package/src/compat/client/segment-prefetch.ts +375 -0
- package/src/compat/client/trailing-slash.ts +24 -0
- package/src/compat/css/chunking.ts +254 -0
- package/src/compat/css/inline-css.ts +73 -0
- package/src/compat/css/lightningcss.ts +90 -0
- package/src/compat/css/modules.ts +373 -0
- package/src/compat/css/nonce.ts +30 -0
- package/src/compat/css/sass-plugin.ts +65 -0
- package/src/compat/css/sass.ts +392 -0
- package/src/compat/css/styled-jsx-runtime.ts +80 -0
- package/src/compat/css/styled-jsx.ts +49 -0
- package/src/compat/edge-runtime.ts +71 -0
- package/src/compat/export/client.ts +112 -0
- package/src/compat/export/index.ts +272 -0
- package/src/compat/export/standalone.ts +207 -0
- package/src/compat/image-optimizer/cache.ts +119 -0
- package/src/compat/image-optimizer/detect.ts +143 -0
- package/src/compat/image-optimizer/index.ts +601 -0
- package/src/compat/image-optimizer/source.ts +243 -0
- package/src/compat/index.ts +458 -0
- package/src/compat/lifecycle/after-scope.ts +86 -0
- package/src/compat/lifecycle/after.ts +173 -0
- package/src/compat/lifecycle/error-funnel.ts +306 -0
- package/src/compat/lifecycle/error-serialize.ts +87 -0
- package/src/compat/lifecycle/error-ui.ts +167 -0
- package/src/compat/lifecycle/instrumentation-client.ts +138 -0
- package/src/compat/lifecycle/instrumentation.ts +277 -0
- package/src/compat/lifecycle/node-console.ts +19 -0
- package/src/compat/lifecycle/testmode.ts +263 -0
- package/src/compat/mdx/compile.ts +219 -0
- package/src/compat/mdx/next-mdx-stub.ts +46 -0
- package/src/compat/mdx/plugin.ts +37 -0
- package/src/compat/metadata-route-artifacts.ts +458 -0
- package/src/compat/metadata.ts +295 -0
- package/src/compat/middleware/manifest.ts +210 -0
- package/src/compat/misc/action-return.ts +173 -0
- package/src/compat/next/cache.ts +211 -0
- package/src/compat/next/canonical-url.ts +35 -0
- package/src/compat/next/client-cache.ts +57 -0
- package/src/compat/next/client-navigation.ts +313 -0
- package/src/compat/next/client-only.ts +3 -0
- package/src/compat/next/client-script.tsx +215 -0
- package/src/compat/next/client-server.ts +39 -0
- package/src/compat/next/config-loader.ts +569 -0
- package/src/compat/next/config.ts +29 -0
- package/src/compat/next/constants.cjs +6 -0
- package/src/compat/next/constants.ts +6 -0
- package/src/compat/next/custom-server.ts +236 -0
- package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
- package/src/compat/next/dynamic.tsx +46 -0
- package/src/compat/next/error.tsx +148 -0
- package/src/compat/next/font/cache.ts +171 -0
- package/src/compat/next/font/google.ts +2 -0
- package/src/compat/next/font/index.ts +8 -0
- package/src/compat/next/font/local.ts +5 -0
- package/src/compat/next/font/runtime-client.ts +71 -0
- package/src/compat/next/font/runtime.ts +974 -0
- package/src/compat/next/font/shared.ts +281 -0
- package/src/compat/next/form.tsx +156 -0
- package/src/compat/next/head.tsx +10 -0
- package/src/compat/next/headers.ts +247 -0
- package/src/compat/next/image/config.ts +196 -0
- package/src/compat/next/image/optimizer.ts +96 -0
- package/src/compat/next/image/patterns.ts +103 -0
- package/src/compat/next/image/shared.ts +141 -0
- package/src/compat/next/image/static-metadata.ts +283 -0
- package/src/compat/next/image/validate.ts +269 -0
- package/src/compat/next/image-client.tsx +215 -0
- package/src/compat/next/image-props.ts +575 -0
- package/src/compat/next/image-usage.ts +102 -0
- package/src/compat/next/image.tsx +56 -0
- package/src/compat/next/index.ts +1 -0
- package/src/compat/next/legacy-image.tsx +97 -0
- package/src/compat/next/link-usage.ts +29 -0
- package/src/compat/next/link-validation-transform.ts +200 -0
- package/src/compat/next/link.tsx +466 -0
- package/src/compat/next/navigation.cjs +21 -0
- package/src/compat/next/navigation.ts +188 -0
- package/src/compat/next/offline.ts +51 -0
- package/src/compat/next/og.ts +324 -0
- package/src/compat/next/optimistic-route-state.ts +188 -0
- package/src/compat/next/preferred-region.ts +39 -0
- package/src/compat/next/redirects.ts +131 -0
- package/src/compat/next/resource-hints.ts +136 -0
- package/src/compat/next/rewrites.ts +350 -0
- package/src/compat/next/root-params.ts +142 -0
- package/src/compat/next/router.cjs +49 -0
- package/src/compat/next/router.ts +143 -0
- package/src/compat/next/script.tsx +355 -0
- package/src/compat/next/server-only.ts +3 -0
- package/src/compat/next/server.ts +28 -0
- package/src/compat/next/svgr.ts +58 -0
- package/src/compat/next/telemetry.ts +77 -0
- package/src/compat/next/user-agent.ts +100 -0
- package/src/compat/next/web-vitals.ts +56 -0
- package/src/compat/otel/api.ts +95 -0
- package/src/compat/otel/client-trace-metadata.ts +71 -0
- package/src/compat/otel/fetch-span.ts +77 -0
- package/src/compat/otel/tracer.ts +944 -0
- package/src/compat/pages/client-plugin.ts +108 -0
- package/src/compat/pages/index.ts +527 -0
- package/src/compat/pages/router-state.ts +94 -0
- package/src/compat/ppr/io.ts +38 -0
- package/src/compat/ppr/missing-root-params.ts +105 -0
- package/src/compat/ppr/root-params-scan.ts +164 -0
- package/src/compat/ppr/root-params-transform.ts +75 -0
- package/src/compat/ppr/root-params.ts +129 -0
- package/src/compat/ppr/segment-config-incompat.ts +34 -0
- package/src/compat/protocol.ts +202 -0
- package/src/compat/react/client.ts +59 -0
- package/src/compat/react/compiler-runtime.ts +60 -0
- package/src/compat/react/dom-client.ts +115 -0
- package/src/compat/react/dom-react-server.ts +20 -0
- package/src/compat/react/dom-server.ts +40 -0
- package/src/compat/react/dom.ts +154 -0
- package/src/compat/react/preact.ts +522 -0
- package/src/compat/react/react-server.ts +84 -0
- package/src/compat/react/router-shim.ts +26 -0
- package/src/compat/react/server-component-use.ts +48 -0
- package/src/compat/react/server-inserted-html.ts +87 -0
- package/src/compat/react/server.ts +156 -0
- package/src/compat/react/view-transition.ts +60 -0
- package/src/compat/register/actions.ts +875 -0
- package/src/compat/register/boot.ts +141 -0
- package/src/compat/register/build-tier.ts +11 -0
- package/src/compat/register/build.ts +182 -0
- package/src/compat/register/bundler.ts +587 -0
- package/src/compat/register/cache.ts +85 -0
- package/src/compat/register/client-errors.ts +18 -0
- package/src/compat/register/config.ts +17 -0
- package/src/compat/register/css-extras.ts +120 -0
- package/src/compat/register/edge-runtime.ts +6 -0
- package/src/compat/register/errors.ts +46 -0
- package/src/compat/register/export.ts +23 -0
- package/src/compat/register/font.ts +36 -0
- package/src/compat/register/hooks.ts +34 -0
- package/src/compat/register/image.ts +133 -0
- package/src/compat/register/index.ts +111 -0
- package/src/compat/register/instrumentation-client.ts +35 -0
- package/src/compat/register/lifecycle.ts +86 -0
- package/src/compat/register/mdx.ts +48 -0
- package/src/compat/register/middleware.ts +36 -0
- package/src/compat/register/misc.ts +44 -0
- package/src/compat/register/otel.ts +288 -0
- package/src/compat/register/pages-api.ts +473 -0
- package/src/compat/register/ppr.ts +56 -0
- package/src/compat/register/protocol.ts +57 -0
- package/src/compat/register/proxy.ts +127 -0
- package/src/compat/register/render.ts +268 -0
- package/src/compat/register/routing.ts +410 -0
- package/src/compat/register/segment.ts +1903 -0
- package/src/compat/register/static-image.ts +21 -0
- package/src/compat/register/typed-routes.ts +35 -0
- package/src/compat/register/usecache.ts +131 -0
- package/src/compat/register/validation.ts +56 -0
- package/src/compat/segment/loading-boundary.ts +113 -0
- package/src/compat/segment/page-slot.ts +200 -0
- package/src/compat/segment/tree.ts +481 -0
- package/src/compat/segment/vary-key.ts +102 -0
- package/src/compat/segment/vary-params.ts +551 -0
- package/src/compat/static-params.ts +33 -0
- package/src/compat/tsconfig-defaults.ts +301 -0
- package/src/compat/typecheck/index.ts +1481 -0
- package/src/compat/typecheck/worker.ts +26 -0
- package/src/compat/typed-routes/index.ts +92 -0
- package/src/compat/typed-routes/manifest.ts +356 -0
- package/src/compat/typed-routes/typegen.ts +566 -0
- package/src/compat/validation/errors.ts +159 -0
- package/src/compat/validation/index.ts +1770 -0
- package/src/compat/validation/prerender-diagnostics.ts +1508 -0
- package/src/compat-bootstrap.ts +67 -0
- package/src/config.ts +218 -0
- package/src/css/build.ts +697 -0
- package/src/css/index.ts +2 -0
- package/src/css/postcss.ts +236 -0
- package/src/css/worker.ts +34 -0
- package/src/dev/client-actions.ts +35 -0
- package/src/dev/client-chunk-store.ts +92 -0
- package/src/dev/client-key-cache.ts +178 -0
- package/src/dev/global-css-cache.ts +212 -0
- package/src/dev/imports.ts +2430 -0
- package/src/dev/module-cache.ts +721 -0
- package/src/dev/module-generations.ts +38 -0
- package/src/dev/module-transform.ts +188 -0
- package/src/dev/node-module-bundle-cache.ts +63 -0
- package/src/dev/restart-cache.ts +10 -0
- package/src/dev/route-bundle-key-cache.ts +154 -0
- package/src/dev/route-facts-cache.ts +223 -0
- package/src/dev/server.ts +1710 -0
- package/src/dynamic/source.ts +307 -0
- package/src/dynamic/tree-shake.ts +262 -0
- package/src/env.ts +92 -0
- package/src/extensions.ts +1898 -0
- package/src/index.ts +34 -0
- package/src/internal.ts +43 -0
- package/src/islands/boundary-error.ts +8 -0
- package/src/islands/static-children.ts +37 -0
- package/src/islands/static-slots.ts +106 -0
- package/src/ppr-postpone.ts +24 -0
- package/src/ppr.ts +784 -0
- package/src/proxy.ts +752 -0
- package/src/render/hooks.ts +384 -0
- package/src/render/index.ts +1 -0
- package/src/render/island-context.ts +47 -0
- package/src/render/metadata.ts +857 -0
- package/src/render/renderer.ts +7391 -0
- package/src/render/resource-hints.ts +44 -0
- package/src/render/slots.tsx +679 -0
- package/src/request/context.ts +396 -0
- package/src/resolve/engine.ts +219 -0
- package/src/resolve/imports.ts +1104 -0
- package/src/resolve/scan-facts.ts +474 -0
- package/src/resolve/source-text.ts +86 -0
- package/src/routing/forwarded.ts +41 -0
- package/src/routing/handler.ts +271 -0
- package/src/routing/href.ts +203 -0
- package/src/routing/metadata.ts +1018 -0
- package/src/routing/request-runtime.ts +43 -0
- package/src/routing/routes.ts +2560 -0
- package/src/routing/slots.ts +432 -0
- package/src/runtime/server.ts +3453 -0
- package/src/runtime/vendor.ts +1160 -0
- package/src/style-modules.d.ts +9 -0
- package/src/typegen.ts +151 -0
- package/src/types.ts +725 -0
- package/src/utils/ansi.ts +9 -0
- package/src/utils/content-type.ts +31 -0
- package/src/utils/decode.ts +7 -0
- package/src/utils/dev-profile.ts +31 -0
- package/src/utils/error-log.ts +29 -0
- package/src/utils/fs-cache.ts +31 -0
- package/src/utils/fs.ts +119 -0
- package/src/utils/html.ts +46 -0
- package/src/utils/serialize.ts +378 -0
- package/src/utils/source.ts +35 -0
- package/src/utils/verbose.ts +39 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,1903 @@
|
|
|
1
|
+
// Segment-prefetch responder registration (COMPAT - may import core freely). Registers:
|
|
2
|
+
// - a request interceptor that answers `/_tree` segment-prefetch requests with the minimal
|
|
3
|
+
// RootTreePrefetch payload, Content-Type text/x-component, x-nextjs-stale-time and
|
|
4
|
+
// cache-control private,no-store; the router Vary is merged by the protocol finalizer.
|
|
5
|
+
// - a response finalizer that stamps x-nextjs-stale-time on ordinary prefetch responses that did
|
|
6
|
+
// not already carry one from `use cache`, so the client segment cache learns the reuse window.
|
|
7
|
+
//
|
|
8
|
+
// The interceptor short-circuits BEFORE route matching / static lookup: a segment prefetch never
|
|
9
|
+
// renders a page, it only announces the route tree + staleness. It runs first among compat
|
|
10
|
+
// interceptors so an action POST never mistakes a `/_tree` GET.
|
|
11
|
+
//
|
|
12
|
+
// `_rsc` cache-buster handling: an RSC/prefetch request carries a `?_rsc=<hash>` param the CDN keys
|
|
13
|
+
// on. The interceptor accepts any value and never treats it as an app search param.
|
|
14
|
+
//
|
|
15
|
+
// No-op when compat is off. The `/_tree` sentinel only fires when the client segment-cache runtime
|
|
16
|
+
// sends it; the HTML-swap navigation path is untouched.
|
|
17
|
+
|
|
18
|
+
import type { ResolvedConfig } from '../../config';
|
|
19
|
+
import type { RouteManifestEntry } from '../../types';
|
|
20
|
+
import { nextCompatEnabled } from '../../compat';
|
|
21
|
+
import {
|
|
22
|
+
getRenderExtensions,
|
|
23
|
+
onExtensionHostReset,
|
|
24
|
+
getRequestExtensions,
|
|
25
|
+
registerRequestInterceptors,
|
|
26
|
+
registerResponseFinalizers,
|
|
27
|
+
type RequestInterceptor,
|
|
28
|
+
type ResponseFinalizer,
|
|
29
|
+
type ResponseFinalizerContext,
|
|
30
|
+
withRouteRuntime,
|
|
31
|
+
} from '../../extensions';
|
|
32
|
+
import { getRequestRuntime } from '../../routing/request-runtime';
|
|
33
|
+
import { takeCacheLifeStash } from '../cache/use-cache';
|
|
34
|
+
import { matchInterception, parseNavState, selectRouteForRequest } from '../../routing/routes';
|
|
35
|
+
import {
|
|
36
|
+
currentPprShellHtml,
|
|
37
|
+
pprShellPath,
|
|
38
|
+
renderPageResponse,
|
|
39
|
+
renderPartialShell,
|
|
40
|
+
renderRuntimePrefetchDocument,
|
|
41
|
+
schedulePprShellUpgrade,
|
|
42
|
+
withRequestRouteParams,
|
|
43
|
+
LATE_METADATA_MARKER,
|
|
44
|
+
} from '../../render/renderer';
|
|
45
|
+
import {
|
|
46
|
+
NEXT_ROUTER_PREFETCH_HEADER,
|
|
47
|
+
NEXT_ROUTER_SEGMENT_PREFETCH_HEADER,
|
|
48
|
+
RSC_HEADER,
|
|
49
|
+
RSC_CONTENT_TYPE_HEADER,
|
|
50
|
+
} from '../next/dist/client/components/app-router-headers';
|
|
51
|
+
import {
|
|
52
|
+
bodySegmentFile,
|
|
53
|
+
buildRootTreePrefetch,
|
|
54
|
+
DEFAULT_DYNAMIC_STALE_TIME_SECONDS,
|
|
55
|
+
DEFAULT_STATIC_STALE_TIME_SECONDS,
|
|
56
|
+
segmentMetaFile,
|
|
57
|
+
segmentBodyResponse,
|
|
58
|
+
ROUTE_SEGMENT_PATH,
|
|
59
|
+
TREE_SEGMENT_PATH,
|
|
60
|
+
treeSegmentFile,
|
|
61
|
+
treePrefetchResponse,
|
|
62
|
+
type SegmentAppShell,
|
|
63
|
+
type SegmentMeta,
|
|
64
|
+
type SegmentVaryInfo,
|
|
65
|
+
} from '../segment/tree';
|
|
66
|
+
import {
|
|
67
|
+
responseVaryParamsFromWire,
|
|
68
|
+
varyNamesFor,
|
|
69
|
+
withVaryParamsTracking,
|
|
70
|
+
type ResponseVaryParams,
|
|
71
|
+
} from '../segment/vary-params';
|
|
72
|
+
import { pageHasInPageSuspense } from '../segment/loading-boundary';
|
|
73
|
+
import { pageSlotFrame, stripPageSlotContent } from '../segment/page-slot';
|
|
74
|
+
import { withoutInlineCss } from '../css/inline-css';
|
|
75
|
+
import { getNextConfig } from '../next/config-loader';
|
|
76
|
+
import { CACHE_BUSTING_REDIRECT_HEADER } from '../protocol';
|
|
77
|
+
import { registerRevalidationInvalidator } from '../cache/revalidate';
|
|
78
|
+
import { staticSiblingNames } from '../next/optimistic-route-state';
|
|
79
|
+
import {
|
|
80
|
+
loadCompatRewrites,
|
|
81
|
+
resolveCompatRewrite,
|
|
82
|
+
type CompatRewrite,
|
|
83
|
+
} from '../next/rewrites';
|
|
84
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
85
|
+
import { join } from 'node:path';
|
|
86
|
+
import { writeFile } from 'node:fs/promises';
|
|
87
|
+
|
|
88
|
+
/** experimental.staleTimes.{static,dynamic} from next.config (seconds). */
|
|
89
|
+
function configuredStaleTimes(): { static?: number; dynamic?: number } {
|
|
90
|
+
const experimental = getNextConfig().experimental;
|
|
91
|
+
if (!experimental || typeof experimental !== 'object') return {};
|
|
92
|
+
const staleTimes = (experimental as { staleTimes?: unknown }).staleTimes;
|
|
93
|
+
if (!staleTimes || typeof staleTimes !== 'object') return {};
|
|
94
|
+
const value = staleTimes as { static?: unknown; dynamic?: unknown };
|
|
95
|
+
return {
|
|
96
|
+
...(typeof value.static === 'number' ? { static: value.static } : {}),
|
|
97
|
+
...(typeof value.dynamic === 'number' ? { dynamic: value.dynamic } : {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Partial Prefetching: a FULL (`<Link prefetch={true}>`) prefetch is downgraded to a partial/PPR
|
|
103
|
+
* prefetch - the static shell only, never the dynamic data - when the app opts in globally
|
|
104
|
+
* (`partialPrefetching`) or the target route opts in per segment (`export const prefetch =
|
|
105
|
+
* 'partial'`, resolved through the route's whole segment chain so a nested leaf's opt-in reaches
|
|
106
|
+
* the route the scheduler asks about). `'unstable_eager'` is the opposite policy and never downgrades.
|
|
107
|
+
*/
|
|
108
|
+
function routePartialPrefetch(route: RouteManifestEntry): boolean {
|
|
109
|
+
// ...EXCEPT under App Shells, where `prefetch = 'partial'` means "rely on the shared shell for the
|
|
110
|
+
// DEFAULT prefetch". An explicit `prefetch={true}` link opts back into per-link prefetching there, so
|
|
111
|
+
// its full prefetch must carry this param's own content.
|
|
112
|
+
if (route.segmentConfig?.prefetch === 'partial') return !appShellsEnabled();
|
|
113
|
+
return (getNextConfig() as { partialPrefetching?: unknown }).partialPrefetching === true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** `experimental.appShells` from next.config. */
|
|
117
|
+
function appShellsEnabled(): boolean {
|
|
118
|
+
const experimental = getNextConfig().experimental;
|
|
119
|
+
if (!experimental || typeof experimental !== 'object') return false;
|
|
120
|
+
return (experimental as { appShells?: unknown }).appShells === true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* App Shells: the body prefetch of a runtime-prefetch route serves the SHARED (param-independent)
|
|
125
|
+
* shell, so the render must leave `params` HANGING - the params-dependent subtree stays behind its
|
|
126
|
+
* <Suspense> fallback and the client caches the response at the fallback vary path, reusable for
|
|
127
|
+
* every param. Request data (cookies/headers) still resolves: it is not URL-derived.
|
|
128
|
+
*
|
|
129
|
+
* Strictly opt-in. WITHOUT `appShells` a runtime prefetch is per-URL and its params MUST resolve.
|
|
130
|
+
* An `unstable_eager` segment is excluded even under App Shells: eager routes keep their per-link
|
|
131
|
+
* Speculative prefetch, which carries that param's own content.
|
|
132
|
+
*/
|
|
133
|
+
function appShellFallbackParams(route: RouteManifestEntry): boolean {
|
|
134
|
+
if (!appShellsEnabled()) return false;
|
|
135
|
+
return route.segmentConfig?.prefetch !== 'unstable_eager';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function validateRscRequestHeaders(): boolean {
|
|
139
|
+
const experimental = getNextConfig().experimental;
|
|
140
|
+
if (!experimental || typeof experimental !== 'object') return false;
|
|
141
|
+
return (experimental as { validateRSCRequestHeaders?: unknown }).validateRSCRequestHeaders === true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function deploymentId(): string {
|
|
145
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
146
|
+
return process.env.NEXT_DEPLOYMENT_ID || process.env.NEXT_PUBLIC_BUILD_ID || 'pnext';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Effective staleTime (seconds) for a route of the given disposition. */
|
|
150
|
+
function staleTimeSecondsFor(isStatic: boolean): number {
|
|
151
|
+
const configured = configuredStaleTimes();
|
|
152
|
+
if (isStatic) return configured.static ?? DEFAULT_STATIC_STALE_TIME_SECONDS;
|
|
153
|
+
return configured.dynamic ?? DEFAULT_DYNAMIC_STALE_TIME_SECONDS;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The segment-prefetch header value, or null when this isn't a segment prefetch. */
|
|
157
|
+
function segmentPrefetchTarget(request: Request): string | null {
|
|
158
|
+
if (request.headers.get(RSC_HEADER) !== '1') return null;
|
|
159
|
+
return request.headers.get(NEXT_ROUTER_SEGMENT_PREFETCH_HEADER);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let segmentRevalidationVersion = 0;
|
|
163
|
+
const segmentArtifactVersions = new Map<string, number>();
|
|
164
|
+
|
|
165
|
+
// NOTE: there is deliberately NO server-side "the client's `_rsc` predates the last revalidation,
|
|
166
|
+
// bounce it to a fresh CDN key" redirect. Next never issues one: `_rsc` is derived from the
|
|
167
|
+
// request's negotiation headers, and freshness after a revalidation comes from the server
|
|
168
|
+
// re-rendering the stale artifact, not from rotating the URL. A version bounce here would leak
|
|
169
|
+
// server state ACROSS browser sessions, so every page load after a revalidation replays a hash the
|
|
170
|
+
// server has already seen at an older version and its first prefetch answers 302 instead of the
|
|
171
|
+
// payload - which router-act cannot intercept.
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A cache-busting redirect to a per-variant `_rsc` URL, byte-for-byte Next's wire form (307 + a
|
|
175
|
+
* PATH-RELATIVE Location):
|
|
176
|
+
*
|
|
177
|
+
* - 307, not 302. A prefetch that lands on a 302 is invisible to the e2e router-act helper, which
|
|
178
|
+
* only follows 307/308.
|
|
179
|
+
* - Relative Location. An absolute URL is built from the URL the ORIGIN server sees, which behind a
|
|
180
|
+
* proxy/CDN is its internal port - following it would bypass the CDN the client is talking to.
|
|
181
|
+
*
|
|
182
|
+
* It is CACHEABLE (no `cache-control: no-store`): the Location already folds the request's
|
|
183
|
+
* negotiation headers into its `_rsc` token, so a Vary-ignoring CDN that replays the cached redirect
|
|
184
|
+
* still sends each variant to its own URL. Marking it no-store instead broke a reference CDN, which
|
|
185
|
+
* drops the redirect status+Location for non-cacheable responses.
|
|
186
|
+
*/
|
|
187
|
+
function cacheBustingRedirect(location: string): Response {
|
|
188
|
+
const target = new URL(location);
|
|
189
|
+
return new Response(null, {
|
|
190
|
+
status: 307,
|
|
191
|
+
headers: {
|
|
192
|
+
location: `${target.pathname}${target.search}`,
|
|
193
|
+
// Protocol marker: this 307 is a CDN key rotation, not an app redirect()
|
|
194
|
+
// — the finalizer must not fold it into Next's 200+Location RSC form
|
|
195
|
+
// (see normalizeRscRedirectStatus, which strips this header).
|
|
196
|
+
[CACHE_BUSTING_REDIRECT_HEADER]: '1',
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const segmentPrefetchInterceptor: RequestInterceptor = async (request, ctx) => {
|
|
202
|
+
const config = ctx.config;
|
|
203
|
+
if (!nextCompatEnabled(config)) return undefined;
|
|
204
|
+
const segment = segmentPrefetchTarget(request);
|
|
205
|
+
const isRscRequest = request.headers.get(RSC_HEADER) === '1';
|
|
206
|
+
if (segment === null && !isRscRequest) return undefined;
|
|
207
|
+
const method = request.method.toUpperCase();
|
|
208
|
+
if (method !== 'GET' && method !== 'HEAD') return undefined;
|
|
209
|
+
if (segment === null && request.headers.has('x-pnext-soft-nav')) return undefined;
|
|
210
|
+
|
|
211
|
+
const runtime = getRequestRuntime();
|
|
212
|
+
if (!runtime) return undefined;
|
|
213
|
+
|
|
214
|
+
const url = new URL(request.url);
|
|
215
|
+
if (
|
|
216
|
+
segment !== null &&
|
|
217
|
+
validateRscRequestHeaders() &&
|
|
218
|
+
!url.searchParams.has('_rsc')
|
|
219
|
+
) {
|
|
220
|
+
// Redirect a header-only (no `_rsc`) prefetch to its per-variant cache-
|
|
221
|
+
// busting URL. The Location folds this request's negotiation headers into
|
|
222
|
+
// the `_rsc` token so distinct variants resolve to distinct URLs.
|
|
223
|
+
return cacheBustingRedirect(withRscQuery(url, request.headers));
|
|
224
|
+
}
|
|
225
|
+
// next.config `rewrites` must apply on the RSC/segment serving path too. This
|
|
226
|
+
// interceptor short-circuits BEFORE the register-actions rewrite interceptor
|
|
227
|
+
// runs, so config rewrites (and their x-nextjs-rewritten-path/query headers)
|
|
228
|
+
// would never fire on RSC requests without resolving them here. When a rewrite
|
|
229
|
+
// matches we serve the destination route and stamp the rewritten headers
|
|
230
|
+
// directly (interceptor responses bypass the protocol finalizer). When it does
|
|
231
|
+
// NOT match we fall through unchanged, and the register-actions interceptor
|
|
232
|
+
// handles the full-render path as before.
|
|
233
|
+
const rewrite = await resolveSegmentRewrite(config, request, url);
|
|
234
|
+
const matchPathname = rewrite?.pathname ?? url.pathname;
|
|
235
|
+
const rewriteHeaders = rewrite?.headers;
|
|
236
|
+
|
|
237
|
+
// The `_rsc` cache-buster is a CDN key, not an app param: it never reaches
|
|
238
|
+
// route matching.
|
|
239
|
+
const selection = selectRouteForRequest(runtime.routes, matchPathname, undefined);
|
|
240
|
+
|
|
241
|
+
// A pnext-router prefetch carries the origin's nav state. When an
|
|
242
|
+
// interception entry targets this URL from that origin, the segment/RSC
|
|
243
|
+
// fast path must NOT serve: it renders without nav context, so the cached
|
|
244
|
+
// document would lack the intercepted slot content and the navigation that
|
|
245
|
+
// reuses it would commit the un-intercepted page (modal suites regressed
|
|
246
|
+
// exactly this way). Fall through to the host-render path instead.
|
|
247
|
+
const prefetchNav = parseNavState(request);
|
|
248
|
+
if (
|
|
249
|
+
prefetchNav &&
|
|
250
|
+
((prefetchNav.children &&
|
|
251
|
+
matchInterception(runtime.routes, matchPathname, prefetchNav.children)) ||
|
|
252
|
+
(selection?.route.kind === 'page' &&
|
|
253
|
+
(selection.route.slotDirs?.length || selection.route.synthetic)))
|
|
254
|
+
) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
const isStatic = selection?.route.kind === 'page' && selection.route.mode === 'static';
|
|
258
|
+
|
|
259
|
+
if (segment === TREE_SEGMENT_PATH && selection?.route.kind === 'page' && selection.route.ppr) {
|
|
260
|
+
schedulePprShellUpgrade({ config, route: selection.route, url: new URL(request.url) });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (segment === null) {
|
|
264
|
+
if (selection?.route.kind !== 'page') return undefined;
|
|
265
|
+
const meta = readSegmentMeta(config.outPath, selection.route.id);
|
|
266
|
+
// A PREFETCH (`next-router-prefetch: 1`) is answered with the cheap route-metadata payload - it only
|
|
267
|
+
// has to teach the client the route's identity and staleness window. A plain `rsc: 1` fetch is the
|
|
268
|
+
// full-route flight request Next answers with the route's rendered content, so render the page for
|
|
269
|
+
// those the same way the PPR branch does.
|
|
270
|
+
const isPrefetchRequest = request.headers.get(NEXT_ROUTER_PREFETCH_HEADER) === '1';
|
|
271
|
+
if (selection.route.ppr || !isPrefetchRequest) {
|
|
272
|
+
const response = await getRenderExtensions().collectRenderMeta(
|
|
273
|
+
() =>
|
|
274
|
+
// A flight body carries stylesheet REFERENCES, never inlined CSS
|
|
275
|
+
// (`experimental.inlineCss` is a document-only optimization) — see
|
|
276
|
+
// compat/css/inline-css.ts.
|
|
277
|
+
withoutInlineCss(() =>
|
|
278
|
+
withRouteRuntime(selection.route.segmentConfig?.runtime, () =>
|
|
279
|
+
renderPageResponse({
|
|
280
|
+
config,
|
|
281
|
+
route: selection.route,
|
|
282
|
+
params: selection.params,
|
|
283
|
+
url,
|
|
284
|
+
request,
|
|
285
|
+
}),
|
|
286
|
+
),
|
|
287
|
+
),
|
|
288
|
+
{
|
|
289
|
+
fetchCache: selection.route.segmentConfig?.fetchCache,
|
|
290
|
+
route: matchPathname,
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
// pnext's full-route RSC body is the rendered document (HTML-swap
|
|
294
|
+
// protocol), but a flight response must never LOOK like a document: the
|
|
295
|
+
// root-param fallback suites assert the body of an `RSC: 1` fetch does
|
|
296
|
+
// not contain `<!DOCTYPE html>` (while resume-data-cache asserts it DOES
|
|
297
|
+
// carry the page's content). Strip the doctype prefix and append the
|
|
298
|
+
// route's flight segment marker (Next's payload names the leaf segment
|
|
299
|
+
// `__PAGE__`, which the inline-css suite sanity-checks for); this path
|
|
300
|
+
// never serves the pnext client router (x-pnext-soft-nav bails out above).
|
|
301
|
+
const rscBody = stripDocumentDoctype(
|
|
302
|
+
response.value,
|
|
303
|
+
flightSegmentMarker(selection.route.id, isStatic),
|
|
304
|
+
);
|
|
305
|
+
rscBody.headers.set('content-type', RSC_CONTENT_TYPE_HEADER);
|
|
306
|
+
rscBody.headers.set('x-nextjs-stale-time', String(effectiveStaleTime(meta, isStatic)));
|
|
307
|
+
rscBody.headers.set('x-nextjs-deployment-id', deploymentId());
|
|
308
|
+
rscBody.headers.set('cache-control', 'private, no-store');
|
|
309
|
+
return withRewriteHeaders(rscBody, rewriteHeaders);
|
|
310
|
+
}
|
|
311
|
+
return withRewriteHeaders(
|
|
312
|
+
fullRouteRscResponse(
|
|
313
|
+
selection.route.id,
|
|
314
|
+
isStatic,
|
|
315
|
+
effectiveStaleTime(meta, isStatic),
|
|
316
|
+
routeStaticChildren(runtime.routes, selection.route),
|
|
317
|
+
),
|
|
318
|
+
rewriteHeaders,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Beyond `/_tree`: serve a framed segment payload. A segment request must
|
|
323
|
+
// never fall through to the document/static-file path, which would pair
|
|
324
|
+
// text/x-component headers with a full HTML document.
|
|
325
|
+
if (segment !== TREE_SEGMENT_PATH) {
|
|
326
|
+
if (selection?.route.kind !== 'page') {
|
|
327
|
+
return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
|
|
328
|
+
}
|
|
329
|
+
const routeId = selection.route.id;
|
|
330
|
+
const file = bodySegmentFile(config.outPath, routeId);
|
|
331
|
+
const meta = readSegmentMeta(config.outPath, routeId);
|
|
332
|
+
// `/_layout`: w9-segment-split. The client holds a cached PAGE frame for
|
|
333
|
+
// this URL (its vary set does not contain the param that changed) but its
|
|
334
|
+
// LAYOUT frame missed, so it asks for the layout alone. Render the route and
|
|
335
|
+
// cut the page's markup out of the document: the response carries the layout
|
|
336
|
+
// chain only, keyed on the LAYOUT vary set, and the client splices its
|
|
337
|
+
// cached page frame back in (compat/segment/page-slot.ts).
|
|
338
|
+
if (segment === LAYOUT_SEGMENT_PATH) {
|
|
339
|
+
const rendered = await renderRuntimeSegment({
|
|
340
|
+
config,
|
|
341
|
+
selection,
|
|
342
|
+
pathname: matchPathname,
|
|
343
|
+
url,
|
|
344
|
+
request,
|
|
345
|
+
// NOT truncated at the loading boundary. A truncated render serves the route's baked fallback
|
|
346
|
+
// shell, whose params HANG - the layout never reads them, so the response would publish an
|
|
347
|
+
// EMPTY vary set and the client would share one layout frame across every param value. The
|
|
348
|
+
// page's markup is cut out of the finished document instead, which costs a full render but
|
|
349
|
+
// keeps the layout's vary set honest.
|
|
350
|
+
nav: prefetchNav,
|
|
351
|
+
});
|
|
352
|
+
if (!rendered) return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
|
|
353
|
+
return withRewriteHeaders(
|
|
354
|
+
segmentBodyResponse(
|
|
355
|
+
stripPageSlotContent(rendered.body),
|
|
356
|
+
effectiveStaleTime(meta, true),
|
|
357
|
+
// A layout frame is never the whole page: the navigation still needs
|
|
358
|
+
// the page frame (from cache, or from its own dynamic request).
|
|
359
|
+
true,
|
|
360
|
+
isStatic,
|
|
361
|
+
deploymentId(),
|
|
362
|
+
segment,
|
|
363
|
+
// An EMPTY tracked set on a route that HAS params means the render read none of them -
|
|
364
|
+
// either true (a param-independent layout) or an artifact of the document coming back from
|
|
365
|
+
// a prerender cache. The two are indistinguishable here and the failure modes are not
|
|
366
|
+
// symmetric: over-varying costs a cache hit, under-varying serves one param's layout for
|
|
367
|
+
// another. So publish NO vary set and let the client key the frame on its exact URL.
|
|
368
|
+
varyTrusted(rendered.vary, selection, 'layout')
|
|
369
|
+
? segmentVaryInfo(rendered.vary, 'layout', selection)
|
|
370
|
+
: undefined,
|
|
371
|
+
),
|
|
372
|
+
rewriteHeaders,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
// `/_page`: the inverse frame of `/_layout`. The client already renders this route's layout chain
|
|
376
|
+
// - it is navigating WITHIN it, or holds a valid cached layout frame - so it asks for the page
|
|
377
|
+
// slot alone. The response carries the slot markers plus the page's own markup and NOTHING above
|
|
378
|
+
// them. Rendered like `/_layout`: NOT truncated at the loading boundary, so the tracked vary set
|
|
379
|
+
// is the page's honest param access.
|
|
380
|
+
if (segment === PAGE_SEGMENT_PATH) {
|
|
381
|
+
const rendered = await renderRuntimeSegment({
|
|
382
|
+
config,
|
|
383
|
+
selection,
|
|
384
|
+
pathname: matchPathname,
|
|
385
|
+
url,
|
|
386
|
+
request,
|
|
387
|
+
nav: prefetchNav,
|
|
388
|
+
});
|
|
389
|
+
if (!rendered) return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
|
|
390
|
+
const frame = pageSlotFrame(rendered.body);
|
|
391
|
+
// No page slot (a document whose renderer dropped the markers, a global
|
|
392
|
+
// error page): the frame cannot be proven to line up, so answer a MISS and
|
|
393
|
+
// let the client hard-fall-back to the whole document.
|
|
394
|
+
if (frame === null) return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
|
|
395
|
+
return withRewriteHeaders(
|
|
396
|
+
segmentBodyResponse(
|
|
397
|
+
frame,
|
|
398
|
+
// Rendered for the NAVIGATION: a dynamic route's frame carries
|
|
399
|
+
// request data, so it reuses for the dynamic window only.
|
|
400
|
+
effectiveStaleTime(meta, isStatic),
|
|
401
|
+
// A page frame is never the whole document: whatever consumes it still
|
|
402
|
+
// owns the layout chain around it.
|
|
403
|
+
true,
|
|
404
|
+
isStatic,
|
|
405
|
+
deploymentId(),
|
|
406
|
+
segment,
|
|
407
|
+
varyTrusted(rendered.vary, selection, 'page')
|
|
408
|
+
? segmentVaryInfo(rendered.vary, 'page', selection)
|
|
409
|
+
: undefined,
|
|
410
|
+
),
|
|
411
|
+
rewriteHeaders,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
// App Shells, second request: the client asks for the route's SHARED shell explicitly. Unlike the
|
|
415
|
+
// runtime-prefetch branch below - which only fires for `allow-runtime`/`unstable_instant` routes -
|
|
416
|
+
// this serves a FULLY STATIC route's param-independent shell too: `params` hang, so the
|
|
417
|
+
// params-dependent subtree stays behind its fallback and the response's empty vary set files it at
|
|
418
|
+
// the fallback vary path. Strictly opt-in: without `experimental.appShells` the marker is ignored.
|
|
419
|
+
if (
|
|
420
|
+
(segment === ROUTE_SEGMENT_PATH || segment === '/_index') &&
|
|
421
|
+
appShellsEnabled() &&
|
|
422
|
+
request.headers.get(APP_SHELL_PREFETCH_HEADER) === '1'
|
|
423
|
+
) {
|
|
424
|
+
const shell = await renderInstantPrefetchShell({
|
|
425
|
+
config,
|
|
426
|
+
selection,
|
|
427
|
+
pathname: matchPathname,
|
|
428
|
+
url,
|
|
429
|
+
request,
|
|
430
|
+
fallbackParams: true,
|
|
431
|
+
});
|
|
432
|
+
if (!shell) return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
|
|
433
|
+
return withRewriteHeaders(
|
|
434
|
+
segmentBodyResponse(
|
|
435
|
+
shell.html,
|
|
436
|
+
effectiveStaleTime(meta, true),
|
|
437
|
+
true,
|
|
438
|
+
// A shared shell is a pure function of the ROUTE (its params hung),
|
|
439
|
+
// so the client may serve it for any URL of that route — which its
|
|
440
|
+
// reuse policy allows only for static payloads.
|
|
441
|
+
isStatic || (selection.route.hasStaticParams === true && selection.route.usesRequest !== true),
|
|
442
|
+
deploymentId(),
|
|
443
|
+
segment,
|
|
444
|
+
segmentVaryInfo(shell.vary, 'body', selection),
|
|
445
|
+
),
|
|
446
|
+
rewriteHeaders,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
// A FULL prefetch of a Partial-Prefetching route is served exactly like a
|
|
450
|
+
// default (partial) one — static shell, dynamic boundaries left as holes —
|
|
451
|
+
// and marked so the requesting client still treats it as a shell (the
|
|
452
|
+
// navigation fetches the dynamic continuation).
|
|
453
|
+
const partialPrefetchDowngrade =
|
|
454
|
+
request.headers.get(NEXT_ROUTER_PREFETCH_HEADER) !== '1' &&
|
|
455
|
+
routePartialPrefetch(selection.route);
|
|
456
|
+
const partialPrefetch =
|
|
457
|
+
request.headers.get(NEXT_ROUTER_PREFETCH_HEADER) === '1' || partialPrefetchDowngrade;
|
|
458
|
+
const runtimePrefetch = routeUsesRuntimePrefetch(selection.route);
|
|
459
|
+
// Partial Prefetching under App Shells: a route declaring `export const prefetch = 'partial'`
|
|
460
|
+
// relies on its SHARED app shell, so the per-link Speculative prefetch must not additionally fetch
|
|
461
|
+
// that param's own content. Answering the per-link default prefetch with the shared shell (params
|
|
462
|
+
// hang, empty vary set) collapses two requests into the one that carries the shell, and the empty
|
|
463
|
+
// vary set files the entry at the fallback vary path so revealing another param needs nothing from
|
|
464
|
+
// the server.
|
|
465
|
+
//
|
|
466
|
+
// Strictly scoped: `experimental.appShells` plus the segment's OWN `prefetch = 'partial'` opt-in.
|
|
467
|
+
// The global `partialPrefetching` config and `unstable_eager` are deliberately excluded.
|
|
468
|
+
if (
|
|
469
|
+
(segment === ROUTE_SEGMENT_PATH || segment === '/_index') &&
|
|
470
|
+
request.headers.get(NEXT_ROUTER_PREFETCH_HEADER) === '1' &&
|
|
471
|
+
appShellsEnabled() &&
|
|
472
|
+
selection.route.segmentConfig?.prefetch === 'partial'
|
|
473
|
+
) {
|
|
474
|
+
const shell = await renderInstantPrefetchShell({
|
|
475
|
+
config,
|
|
476
|
+
selection,
|
|
477
|
+
pathname: matchPathname,
|
|
478
|
+
url,
|
|
479
|
+
request,
|
|
480
|
+
fallbackParams: true,
|
|
481
|
+
});
|
|
482
|
+
if (shell) {
|
|
483
|
+
return withRewriteHeaders(
|
|
484
|
+
segmentBodyResponse(
|
|
485
|
+
shell.html,
|
|
486
|
+
effectiveStaleTime(meta, true),
|
|
487
|
+
// A shell, not the whole page: the navigation still fetches the
|
|
488
|
+
// param-specific continuation behind the boundary.
|
|
489
|
+
true,
|
|
490
|
+
// A pure function of the ROUTE (its params hung), so the client may
|
|
491
|
+
// paint it for any URL of that route — which its reuse policy allows
|
|
492
|
+
// only for payloads marked static.
|
|
493
|
+
true,
|
|
494
|
+
deploymentId(),
|
|
495
|
+
segment,
|
|
496
|
+
segmentVaryInfo(shell.vary, 'body', selection),
|
|
497
|
+
),
|
|
498
|
+
rewriteHeaders,
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// A route that opts into runtime prefetching (`unstable_instant` or `export const prefetch =
|
|
503
|
+
// 'allow-runtime'`) renders its BODY prefetch as a RUNTIME-PREFETCH prerender: the response
|
|
504
|
+
// samples request data while connection()-gated content is omitted, and nothing from this
|
|
505
|
+
// request-sampled render is persisted. The client caches it as a shell, so the navigation still
|
|
506
|
+
// fetches the dynamic continuation.
|
|
507
|
+
//
|
|
508
|
+
// The selection is the ROUTE's opt-in, NOT the prefetch header: the client ALWAYS sends
|
|
509
|
+
// `next-router-prefetch: 1`, so gating on the header would never let the shell render. It stays
|
|
510
|
+
// scoped to the body segment and yields to an explicit partial-prefetch (static) downgrade.
|
|
511
|
+
if (
|
|
512
|
+
// The client sends '/_index' as the body-segment key (router.ts
|
|
513
|
+
// SEGMENT_PREFETCH_HEADER); ROUTE_SEGMENT_PATH ('/') covers direct/legacy
|
|
514
|
+
// callers. Both mean the whole-route body.
|
|
515
|
+
(segment === ROUTE_SEGMENT_PATH || segment === '/_index') &&
|
|
516
|
+
!partialPrefetchDowngrade &&
|
|
517
|
+
routeRuntimePrefetchRender(selection.route)
|
|
518
|
+
) {
|
|
519
|
+
const fallbackParams = appShellFallbackParams(selection.route);
|
|
520
|
+
const instant = await renderInstantPrefetchShell({
|
|
521
|
+
config,
|
|
522
|
+
selection,
|
|
523
|
+
pathname: matchPathname,
|
|
524
|
+
url,
|
|
525
|
+
request,
|
|
526
|
+
fallbackParams,
|
|
527
|
+
});
|
|
528
|
+
if (instant) {
|
|
529
|
+
// A `pprMetadata` route's <title> is DYNAMIC (generateMetadata reads a param) while this shared
|
|
530
|
+
// shell body legitimately varies on nothing: leaving the title inside it would under-vary the body
|
|
531
|
+
// on its own head and serve one slug's title for another. Outline it here too - the client
|
|
532
|
+
// re-fetches `/_head` per URL - exactly as the baked/full prefetch path below does.
|
|
533
|
+
let instantHtml = instant.html;
|
|
534
|
+
let instantHeadOutlined = false;
|
|
535
|
+
if (selection.route.pprMetadata === true) {
|
|
536
|
+
const outlined = outlineDocumentTitle(instantHtml);
|
|
537
|
+
if (outlined) {
|
|
538
|
+
instantHtml = outlined;
|
|
539
|
+
instantHeadOutlined = true;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// This response owns its window (computed just below from what the
|
|
543
|
+
// sampled render really resolved). Drop the request-level `use cache`
|
|
544
|
+
// aggregate so the header finalizer does not overwrite it with the
|
|
545
|
+
// route's longer public-cache window.
|
|
546
|
+
takeCacheLifeStash();
|
|
547
|
+
const response = segmentBodyResponse(
|
|
548
|
+
instantHtml,
|
|
549
|
+
// A request-sampled payload reuses for the SHORTEST window it sampled,
|
|
550
|
+
// never the route's baked (public-cache) one: the private caches whose
|
|
551
|
+
// values ride in this response go stale first.
|
|
552
|
+
instant.staleSeconds !== undefined
|
|
553
|
+
? Math.min(instant.staleSeconds, effectiveStaleTime(meta, true))
|
|
554
|
+
: effectiveStaleTime(meta, true),
|
|
555
|
+
true,
|
|
556
|
+
isStatic,
|
|
557
|
+
deploymentId(),
|
|
558
|
+
segment,
|
|
559
|
+
varyTrusted(
|
|
560
|
+
instant.vary,
|
|
561
|
+
selection,
|
|
562
|
+
'body',
|
|
563
|
+
fallbackParams,
|
|
564
|
+
// This render always happened LIVE, so an explicit `allow-runtime`
|
|
565
|
+
// route's empty set is the truth: nothing was read before the
|
|
566
|
+
// render postponed, and every param shares one loading shell.
|
|
567
|
+
routeExplicitRuntimePrefetch(selection.route),
|
|
568
|
+
)
|
|
569
|
+
? segmentVaryInfo(instant.vary, 'body', selection)
|
|
570
|
+
: undefined,
|
|
571
|
+
);
|
|
572
|
+
// Dedicated marker for the client: ONLY a runtime-prefetch (instant)
|
|
573
|
+
// shell downgrades a full prefetch to shell semantics. The generic
|
|
574
|
+
// postponed flag also rides ordinary PPR full renders, which stream
|
|
575
|
+
// their resumed content and MUST keep committing without a refetch
|
|
576
|
+
// (app-client-cache full-prefetch contract).
|
|
577
|
+
response.headers.set('x-pnext-runtime-prefetch', '1');
|
|
578
|
+
if (instantHeadOutlined) {
|
|
579
|
+
response.headers.set(
|
|
580
|
+
'x-pnext-head-outlined',
|
|
581
|
+
headFetchedFirst(selection) ? 'first' : '1',
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
// A COMPLETE runtime prefetch (the render never postponed - the page reads only cookies/params and
|
|
585
|
+
// does no uncached IO) lets the client commit the following navigation network-free. The response
|
|
586
|
+
// itself stays `postponed` on the wire: it is request-sampled, so it must never become
|
|
587
|
+
// CDN-cacheable nor commit through the whole-document caches.
|
|
588
|
+
if (!instant.postponed) response.headers.set('x-pnext-runtime-complete', '1');
|
|
589
|
+
return withRewriteHeaders(response, rewriteHeaders);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
let body =
|
|
593
|
+
partialPrefetch && (meta?.postponed !== true || !runtimePrefetch)
|
|
594
|
+
? await readBakedSegment({ config, selection, pathname: matchPathname, file })
|
|
595
|
+
: undefined;
|
|
596
|
+
const baked = body !== undefined;
|
|
597
|
+
// A default (partial) prefetch of a dynamic page with no baked segment must NOT fall through to a
|
|
598
|
+
// live render: Next answers such prefetches from static data only, and a hanging dynamic render
|
|
599
|
+
// would pin the prefetch connection open forever. Routes that opt into runtime prefetching keep
|
|
600
|
+
// the live render. Exception: a pure params-derived route (generateStaticParams, no request data)
|
|
601
|
+
// IS static content and its live render cannot hang, so it keeps the full render - it can lack a
|
|
602
|
+
// baked segment when its fallback shell failed to bake.
|
|
603
|
+
const paramsOnlyStatic =
|
|
604
|
+
(selection.route.hasStaticParams === true && selection.route.usesRequest !== true) ||
|
|
605
|
+
clientStaticPrefetchable(selection.route);
|
|
606
|
+
// A route with a LOADING boundary renders a truncated loading shell on a default prefetch (Next's
|
|
607
|
+
// "show layout eagerly with loading one level down") - the render stops at the boundary, so it never
|
|
608
|
+
// hangs. Only a dynamic route with NO loading/Suspense boundary would pin the connection open, so the
|
|
609
|
+
// miss short-circuit applies just to those.
|
|
610
|
+
const hasLoadingShell = routeHasLoadingBoundary(selection.route);
|
|
611
|
+
// A route the build already prerendered for THIS param set (generateStaticParams)
|
|
612
|
+
// has a known-good static shell: the prefetch renders that shell truncated at
|
|
613
|
+
// its Suspense boundary (dynamic content stays a hole), so it cannot hang even
|
|
614
|
+
// though the route reads request data (connection()) inside the boundary.
|
|
615
|
+
const paramPrerendered = routeParamPrerendered(selection.route, selection.params);
|
|
616
|
+
// A params-derived route normally serves its complete content on a default prefetch - but only
|
|
617
|
+
// when the build actually knows that content. When THIS param set was never enumerated by
|
|
618
|
+
// generateStaticParams and the route has a `loading` boundary, its page is dynamic for this URL
|
|
619
|
+
// and the default prefetch stops at the loading shell. Truncating there also keeps the published
|
|
620
|
+
// vary set to what the shell really read, so every itemId of a category shares one entry.
|
|
621
|
+
const truncateAtLoading = routeHasLoadingFile(selection.route) && !paramPrerendered;
|
|
622
|
+
// A fully STATIC route with no baked segment (no cacheComponents build)
|
|
623
|
+
// also live-renders: its content is static data Next serves complete on a
|
|
624
|
+
// default prefetch, and the render cannot hang.
|
|
625
|
+
if (
|
|
626
|
+
partialPrefetch &&
|
|
627
|
+
!runtimePrefetch &&
|
|
628
|
+
body === undefined &&
|
|
629
|
+
!paramsOnlyStatic &&
|
|
630
|
+
!paramPrerendered &&
|
|
631
|
+
!isStatic &&
|
|
632
|
+
!hasLoadingShell
|
|
633
|
+
) {
|
|
634
|
+
return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
|
|
635
|
+
}
|
|
636
|
+
// Segment-M2 deliverable 4: exactly the routes the miss short-circuit above
|
|
637
|
+
// used to swallow, now kept alive by an IN-PAGE <Suspense> boundary. Next
|
|
638
|
+
// answers their default prefetch with the route's SHARED fallback shell:
|
|
639
|
+
// params hang, so the params-dependent child stays behind its Suspense
|
|
640
|
+
// fallback and the (empty) vary set files ONE entry the client reuses for
|
|
641
|
+
// every param value. A concrete-params render would record the child's param
|
|
642
|
+
// access instead and key an identical fallback per URL. Same conditions as
|
|
643
|
+
// the short-circuit, so no route that already served content changes shape;
|
|
644
|
+
// a shell that fails to render falls through to the live truncated render.
|
|
645
|
+
if (
|
|
646
|
+
(segment === ROUTE_SEGMENT_PATH || segment === '/_index') &&
|
|
647
|
+
partialPrefetch &&
|
|
648
|
+
!runtimePrefetch &&
|
|
649
|
+
body === undefined &&
|
|
650
|
+
!paramsOnlyStatic &&
|
|
651
|
+
!paramPrerendered &&
|
|
652
|
+
!isStatic &&
|
|
653
|
+
!routeHasLoadingFile(selection.route)
|
|
654
|
+
) {
|
|
655
|
+
const shell = await renderInstantPrefetchShell({
|
|
656
|
+
config,
|
|
657
|
+
selection,
|
|
658
|
+
pathname: matchPathname,
|
|
659
|
+
url,
|
|
660
|
+
request,
|
|
661
|
+
fallbackParams: true,
|
|
662
|
+
});
|
|
663
|
+
if (shell) {
|
|
664
|
+
return withRewriteHeaders(
|
|
665
|
+
segmentBodyResponse(
|
|
666
|
+
shell.html,
|
|
667
|
+
effectiveStaleTime(meta, true),
|
|
668
|
+
// A shell, not the whole page: the navigation still fetches the
|
|
669
|
+
// dynamic continuation behind the boundary.
|
|
670
|
+
true,
|
|
671
|
+
// A pure function of the ROUTE (its params hung), so the client may
|
|
672
|
+
// paint it for any URL of that route — which its reuse policy allows
|
|
673
|
+
// only for payloads marked static.
|
|
674
|
+
true,
|
|
675
|
+
deploymentId(),
|
|
676
|
+
segment,
|
|
677
|
+
segmentVaryInfo(shell.vary, 'body', selection),
|
|
678
|
+
),
|
|
679
|
+
rewriteHeaders,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
// `/_head`: the outlined dynamic head (metadata) of a full prefetch. The
|
|
684
|
+
// client fetches it as a follow-up to an /_index response marked
|
|
685
|
+
// x-pnext-head-outlined, so the title arrives in its own response AFTER
|
|
686
|
+
// the page content (Next fetches metadata separately from segment data,
|
|
687
|
+
// and the suites assert that response order).
|
|
688
|
+
if (segment === HEAD_SEGMENT_PATH) {
|
|
689
|
+
const rendered = await renderRuntimeSegment({
|
|
690
|
+
config,
|
|
691
|
+
selection,
|
|
692
|
+
pathname: matchPathname,
|
|
693
|
+
url,
|
|
694
|
+
request,
|
|
695
|
+
nav: prefetchNav,
|
|
696
|
+
});
|
|
697
|
+
const title = rendered ? extractDocumentTitle(rendered.body) : null;
|
|
698
|
+
if (title === null) return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
|
|
699
|
+
return withRewriteHeaders(
|
|
700
|
+
segmentBodyResponse(
|
|
701
|
+
title,
|
|
702
|
+
effectiveStaleTime(meta, false),
|
|
703
|
+
false,
|
|
704
|
+
false,
|
|
705
|
+
deploymentId(),
|
|
706
|
+
segment,
|
|
707
|
+
// The head segment publishes the HEAD vary set — generateMetadata's
|
|
708
|
+
// own param access, tracked separately from the page body's.
|
|
709
|
+
segmentVaryInfo(rendered!.vary, 'head', selection),
|
|
710
|
+
),
|
|
711
|
+
rewriteHeaders,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
let postponed: boolean;
|
|
715
|
+
// Segment-M2: the vary set of the live render, when one happened. A BAKED
|
|
716
|
+
// segment was produced at build time outside any tracking scope, so it stays
|
|
717
|
+
// undefined and the client keys that entry on the exact URL.
|
|
718
|
+
let bodyVary: ResponseVaryParams | undefined;
|
|
719
|
+
// The set came off the BAKED `route.segment.meta`, i.e. the BUILD already applied its own trust rule
|
|
720
|
+
// before persisting it - and only the build has the evidence (was the render tracked? did its params
|
|
721
|
+
// hang?). Re-testing it below with the REQUEST-render rule would discard a fallback shell's honest
|
|
722
|
+
// empty set. Presence in the meta IS the trust.
|
|
723
|
+
let bakedVaryFromMeta = false;
|
|
724
|
+
// True when this response is a render TRUNCATED at the route's `loading`
|
|
725
|
+
// boundary (see `truncateAtLoading`): the shell above the boundary, with the
|
|
726
|
+
// dynamic page left as a hole.
|
|
727
|
+
let truncatedShell = false;
|
|
728
|
+
/** The live render's own `use cache` window, when one rendered. */
|
|
729
|
+
let renderStaleSeconds: number | undefined;
|
|
730
|
+
if (body === undefined) {
|
|
731
|
+
// A route that only IMPLIED runtime prefetching (it reads searchParams, so a baked segment must
|
|
732
|
+
// not alias across search values) still follows the default-prefetch contract: the render is
|
|
733
|
+
// truncated at its loading/Suspense shell, never a full dynamic render. Only an explicit
|
|
734
|
+
// `allow-runtime` serves runtime data on a partial prefetch, and a pure params-derived route
|
|
735
|
+
// serves its complete static content.
|
|
736
|
+
truncatedShell =
|
|
737
|
+
partialPrefetch && !routeExplicitRuntimePrefetch(selection.route) && truncateAtLoading;
|
|
738
|
+
const rendered = await renderRuntimeSegment({
|
|
739
|
+
config,
|
|
740
|
+
selection,
|
|
741
|
+
pathname: matchPathname,
|
|
742
|
+
url,
|
|
743
|
+
request,
|
|
744
|
+
keepPrefetchHeader:
|
|
745
|
+
partialPrefetch &&
|
|
746
|
+
!routeExplicitRuntimePrefetch(selection.route) &&
|
|
747
|
+
(!paramsOnlyStatic || truncateAtLoading),
|
|
748
|
+
nav: prefetchNav,
|
|
749
|
+
});
|
|
750
|
+
if (!rendered) return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
|
|
751
|
+
body = rendered.body;
|
|
752
|
+
bodyVary = rendered.vary;
|
|
753
|
+
renderStaleSeconds = rendered.staleSeconds;
|
|
754
|
+
// A truncated (loading-shell) prefetch is often answered from the route's baked PPR SUB-SHELL, so
|
|
755
|
+
// the request tracked no param access even though the bytes are a known function of the sub-shell's
|
|
756
|
+
// own params. Publish those instead of "unknown" - otherwise every itemId of one category re-fetches
|
|
757
|
+
// a byte-identical shell.
|
|
758
|
+
if (
|
|
759
|
+
truncateAtLoading &&
|
|
760
|
+
bodyVary.params.length === 0 &&
|
|
761
|
+
!bodyVary.search &&
|
|
762
|
+
Object.keys(selection.params).length > 0
|
|
763
|
+
) {
|
|
764
|
+
bodyVary = subShellVary(selection.route, selection.params) ?? bodyVary;
|
|
765
|
+
}
|
|
766
|
+
// The same artifact, one level up: a render answered from this URL's own
|
|
767
|
+
// BUILD prerender executes no user code, so it tracks nothing even though
|
|
768
|
+
// the build render recorded exactly what each segment read. Publish the
|
|
769
|
+
// persisted sets (`route.prerenderVary`) rather than "unknown".
|
|
770
|
+
if (bodyVary.params.length === 0 && !bodyVary.search) {
|
|
771
|
+
bodyVary = prerenderVary(selection.route, matchPathname) ?? bodyVary;
|
|
772
|
+
}
|
|
773
|
+
postponed = rendered.postponed || (partialPrefetch && meta?.postponed === true);
|
|
774
|
+
// unstable_dynamicOnHover resume: the client already holds this route's
|
|
775
|
+
// static shell (x-pnext-resume-shell) and a full hover prefetch must not
|
|
776
|
+
// re-send static content it has cached. Strip the document down to its
|
|
777
|
+
// streamed dynamic chunks; the client merges them into its shell.
|
|
778
|
+
if (!partialPrefetch && request.headers.get('x-pnext-resume-shell') === '1') {
|
|
779
|
+
const resume = resumeOnlyDocument(body);
|
|
780
|
+
if (resume) {
|
|
781
|
+
const response = segmentBodyResponse(
|
|
782
|
+
resume,
|
|
783
|
+
effectiveStaleTime(meta, false),
|
|
784
|
+
false,
|
|
785
|
+
false,
|
|
786
|
+
deploymentId(),
|
|
787
|
+
segment,
|
|
788
|
+
varyTrusted(rendered.vary, selection, 'body')
|
|
789
|
+
? segmentVaryInfo(rendered.vary, 'body', selection)
|
|
790
|
+
: undefined,
|
|
791
|
+
);
|
|
792
|
+
response.headers.set('x-pnext-resume-only', '1');
|
|
793
|
+
return withRewriteHeaders(response, rewriteHeaders);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
} else {
|
|
797
|
+
postponed = meta?.postponed === true;
|
|
798
|
+
// Segment-M2: a BAKED body carries the vary set its BUILD render tracked
|
|
799
|
+
// (persisted into `route.segment.meta`). Without it every prerendered
|
|
800
|
+
// route keys on its exact URL and re-fetches the shared shell for every
|
|
801
|
+
// param value. Absent (an older/untracked artifact) stays "unknown".
|
|
802
|
+
bodyVary = meta?.vary ? responseVaryParamsFromWire(meta) : undefined;
|
|
803
|
+
bakedVaryFromMeta = bodyVary !== undefined;
|
|
804
|
+
}
|
|
805
|
+
// A FULL prefetch of a route with DYNAMIC metadata: outline the <title> out of this response -
|
|
806
|
+
// the client fetches it via `/_head` as a separate, LATER response and merges it back before
|
|
807
|
+
// caching (Next serves the head separately, and the suites assert that order). A PARTIAL prefetch
|
|
808
|
+
// outlines too: its body is just as shared across params. A baked route-level shell was rendered
|
|
809
|
+
// with the params hanging and carries NO <title> at all, but the head is dynamic all the same, so
|
|
810
|
+
// mark it outlined and let the client fetch `/_head`.
|
|
811
|
+
let headOutlined = false;
|
|
812
|
+
if (selection.route.pprMetadata === true) {
|
|
813
|
+
const outlined = outlineDocumentTitle(body);
|
|
814
|
+
if (outlined) {
|
|
815
|
+
body = outlined;
|
|
816
|
+
headOutlined = true;
|
|
817
|
+
} else if (!/<title>/i.test(body)) {
|
|
818
|
+
headOutlined = true;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const completePrerender = baked && !postponed;
|
|
822
|
+
// A postponed (shell-only) prefetch response carries only static data, so its shell reuses for the
|
|
823
|
+
// STATIC window regardless of the route's mode. Likewise a DEFAULT prefetch of a route that never
|
|
824
|
+
// reads request data: its content is a pure function of the URL, so even a complete live render reuses
|
|
825
|
+
// for the static window.
|
|
826
|
+
const routeStaleTime = completePrerender
|
|
827
|
+
? meta && meta.staleTime > 0
|
|
828
|
+
? meta.staleTime
|
|
829
|
+
: staleTimeSecondsFor(true)
|
|
830
|
+
: effectiveStaleTime(
|
|
831
|
+
meta,
|
|
832
|
+
postponed ||
|
|
833
|
+
isStatic ||
|
|
834
|
+
(partialPrefetch && (!selection.route.usesRequest || paramsOnlyStatic)),
|
|
835
|
+
);
|
|
836
|
+
// A RUNTIME-PREFETCH answer (`prefetch = 'allow-runtime'` downgrading a full
|
|
837
|
+
// prefetch) carries request-sampled data, so it goes stale with the SHORTEST
|
|
838
|
+
// cache the render resolved — typically a `use cache: private` window — not
|
|
839
|
+
// with the route's baked public-cache window.
|
|
840
|
+
const staleTime =
|
|
841
|
+
partialPrefetchDowngrade && renderStaleSeconds !== undefined
|
|
842
|
+
? Math.min(renderStaleSeconds, routeStaleTime)
|
|
843
|
+
: routeStaleTime;
|
|
844
|
+
const bodyResponse = segmentBodyResponse(
|
|
845
|
+
body,
|
|
846
|
+
staleTime,
|
|
847
|
+
postponed,
|
|
848
|
+
// A pure params-derived live render is a prerender: its bytes are a
|
|
849
|
+
// function of the URL alone. Marking it lets the client alias it as
|
|
850
|
+
// reusable full data (no speculative re-fetch of the same URL).
|
|
851
|
+
//
|
|
852
|
+
// A TRUNCATED loading shell of such a route qualifies too: everything it
|
|
853
|
+
// contains sits above the boundary and reads nothing but params, so the
|
|
854
|
+
// client may share it across every URL its vary set covers instead of
|
|
855
|
+
// re-fetching a byte-identical shell per param value.
|
|
856
|
+
baked || isStatic || ((!postponed || truncatedShell) && paramsOnlyStatic),
|
|
857
|
+
deploymentId(),
|
|
858
|
+
segment,
|
|
859
|
+
bodyVary &&
|
|
860
|
+
(bakedVaryFromMeta ||
|
|
861
|
+
varyTrusted(
|
|
862
|
+
bodyVary,
|
|
863
|
+
selection,
|
|
864
|
+
'body',
|
|
865
|
+
false,
|
|
866
|
+
// Live render only (`!baked`): a baked body tracked nothing, so its empty set is the
|
|
867
|
+
// artifact this guard exists for. `allow-runtime` ONLY, not every "explicit"
|
|
868
|
+
// runtime-prefetch route: an `unstable_eager` route RESOLVES its params in this render, so
|
|
869
|
+
// an empty set there is the untracked artifact, not the truth - trusting it published one
|
|
870
|
+
// shared entry for the whole route and elided every later param's prefetch.
|
|
871
|
+
selection.route.segmentConfig?.prefetch === 'allow-runtime' && !baked,
|
|
872
|
+
))
|
|
873
|
+
? segmentVaryInfo(bodyVary, 'body', selection)
|
|
874
|
+
: undefined,
|
|
875
|
+
// App Shells: a COMPLETE per-URL prerender carries the route's shared
|
|
876
|
+
// shell alongside it, so a navigation to another param of the route has
|
|
877
|
+
// an instant shell to paint without a second request.
|
|
878
|
+
completePrerender || (!postponed && paramsOnlyStatic)
|
|
879
|
+
? routeAppShell(config, selection)
|
|
880
|
+
: undefined,
|
|
881
|
+
);
|
|
882
|
+
if (headOutlined) {
|
|
883
|
+
bodyResponse.headers.set(
|
|
884
|
+
'x-pnext-head-outlined',
|
|
885
|
+
headFetchedFirst(selection) ? 'first' : '1',
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
// The client asked for a FULL prefetch; this answer is a shell. Same marker
|
|
889
|
+
// the runtime-prefetch (unstable_instant) downgrade uses — the generic
|
|
890
|
+
// postponed flags never downgrade a full prefetch (ordinary PPR full
|
|
891
|
+
// renders carry them while streaming complete resumed content).
|
|
892
|
+
if (partialPrefetchDowngrade) bodyResponse.headers.set('x-pnext-runtime-prefetch', '1');
|
|
893
|
+
return withRewriteHeaders(bodyResponse, rewriteHeaders);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// `/_tree`: the RootTreePrefetch. Unmatched/non-page targets get an empty
|
|
897
|
+
// (dynamic) tree so the client falls back to a full nav fetch (never 404s).
|
|
898
|
+
const routeId = selection?.route.id;
|
|
899
|
+
const bakedTree = routeId ? readSegmentTree(config.outPath, routeId) : undefined;
|
|
900
|
+
if (routeId && bakedTree) {
|
|
901
|
+
// The route tree's SHAPE is static in Next: it reuses for the static window
|
|
902
|
+
// regardless of whether the route itself renders dynamically. markUrlStaticFresh
|
|
903
|
+
// (router.ts) only records a URL's static freshness when x-nextjs-stale-time > 0,
|
|
904
|
+
// so a dynamic route's tree (dynamic window = 0) would never warm the elision
|
|
905
|
+
// map and every re-revealed link would refetch the tree (breaking the
|
|
906
|
+
// encoded-slash-params back-navigation contract). Stamp the STATIC window on the
|
|
907
|
+
// tree even for dynamic routes; the segment body branch keeps its own windows.
|
|
908
|
+
const staleTime = isStatic
|
|
909
|
+
? effectiveStaleTime(readSegmentMeta(config.outPath, routeId), true)
|
|
910
|
+
: staleTimeSecondsFor(true);
|
|
911
|
+
return withRewriteHeaders(
|
|
912
|
+
new Response(
|
|
913
|
+
treeWithStaleTime(bakedTree, staleTime, url.searchParams.has('_rsc')),
|
|
914
|
+
{
|
|
915
|
+
status: 200,
|
|
916
|
+
headers: {
|
|
917
|
+
'content-type': RSC_CONTENT_TYPE_HEADER,
|
|
918
|
+
'x-nextjs-postponed': '2',
|
|
919
|
+
'x-nextjs-stale-time': String(staleTime),
|
|
920
|
+
...(isStatic ? { 'x-nextjs-prerender': '1' } : {}),
|
|
921
|
+
'x-nextjs-deployment-id': deploymentId(),
|
|
922
|
+
// A static route's baked tree is CDN-cacheable like Next's
|
|
923
|
+
// prerendered payloads; the per-variant `_rsc` cache-buster keys
|
|
924
|
+
// it correctly on Vary-ignoring CDNs.
|
|
925
|
+
'cache-control': isStatic
|
|
926
|
+
? 's-maxage=31536000, stale-while-revalidate'
|
|
927
|
+
: 'private, no-store',
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
),
|
|
931
|
+
rewriteHeaders,
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
const payload = buildRootTreePrefetch({
|
|
935
|
+
pathname: matchPathname,
|
|
936
|
+
isStatic,
|
|
937
|
+
staleTimeSeconds: staleTimeSecondsFor(isStatic),
|
|
938
|
+
routeId: selection?.route.id,
|
|
939
|
+
runtimePrefetch:
|
|
940
|
+
(selection?.route.segmentConfig as { prefetch?: unknown } | undefined)?.prefetch ===
|
|
941
|
+
'allow-runtime',
|
|
942
|
+
postponed: Boolean(selection?.route.pprHoles?.length),
|
|
943
|
+
// Segment-M2: route identity + this URL's params so the client can key
|
|
944
|
+
// shared segment entries before it has fetched a body for this URL.
|
|
945
|
+
...(selection?.route.kind === 'page'
|
|
946
|
+
? { routePattern: selection.route.route, params: wireParams(selection.params) }
|
|
947
|
+
: {}),
|
|
948
|
+
...(selection?.route.pprMetadata === true
|
|
949
|
+
? { headOutlined: true, ...(headFetchedFirst(selection) ? { headFirst: true } : {}) }
|
|
950
|
+
: {}),
|
|
951
|
+
});
|
|
952
|
+
return withRewriteHeaders(
|
|
953
|
+
withDeploymentId(
|
|
954
|
+
treePrefetchResponse(payload, { format: url.searchParams.has('_rsc') ? 'flight' : 'json' }),
|
|
955
|
+
),
|
|
956
|
+
rewriteHeaders,
|
|
957
|
+
);
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* The vary payload stamped onto a segment response - the segment's own vary names plus the route identity
|
|
962
|
+
* and concrete params the client needs to build the shared cache key for OTHER URLs of the same route.
|
|
963
|
+
*/
|
|
964
|
+
function segmentVaryInfo(
|
|
965
|
+
vary: ResponseVaryParams,
|
|
966
|
+
kind: 'body' | 'head' | 'layout' | 'page',
|
|
967
|
+
selection: NonNullable<ReturnType<typeof selectRouteForRequest>>,
|
|
968
|
+
): SegmentVaryInfo {
|
|
969
|
+
return {
|
|
970
|
+
vary: varyNamesFor(vary, kind),
|
|
971
|
+
// w9-segment-split: a BODY response carries the layout chain and the page in
|
|
972
|
+
// one document, so its own `vary` stays their union (it is only byte-correct
|
|
973
|
+
// for a URL matching both). The split sets ride alongside it so the client
|
|
974
|
+
// can file the two frames separately and re-fetch just the one that varied.
|
|
975
|
+
// Only when the render actually TRACKED something: a document answered from
|
|
976
|
+
// a prerender cache reads no params, and an empty set would file the page
|
|
977
|
+
// frame as shareable across every param value (see varyTrusted).
|
|
978
|
+
...(kind === 'body' && (vary.params.length > 0 || vary.search)
|
|
979
|
+
? {
|
|
980
|
+
layoutVary: varyNamesFor(vary, 'layout'),
|
|
981
|
+
pageVary: varyNamesFor(vary, 'page'),
|
|
982
|
+
}
|
|
983
|
+
: {}),
|
|
984
|
+
route: selection.route.route,
|
|
985
|
+
params: wireParams(selection.params),
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* True when a segment's tracked vary set may be published as-is: the render recorded at least one
|
|
991
|
+
* access, or the route has no params for it to record. An untrusted set is published as UNKNOWN,
|
|
992
|
+
* which keys the entry on its exact URL instead of sharing it across param values.
|
|
993
|
+
*
|
|
994
|
+
* An EMPTY tracked set on a route that HAS params is only ever the truth for a `fallbackParams`
|
|
995
|
+
* render, where the params genuinely hung. For a CONCRETE-param render the same empty set is more
|
|
996
|
+
* often an artifact, and the failure modes are not symmetric: over-varying costs a cache hit,
|
|
997
|
+
* under-varying serves one param's content for another.
|
|
998
|
+
*
|
|
999
|
+
* `runtimePrefetch` is the second escape hatch: an EXPLICIT `allow-runtime` render that happened
|
|
1000
|
+
* LIVE tracked its param access for real, so an empty set there means the params were never read
|
|
1001
|
+
* before the render postponed. Never pass it for a BAKED body, produced outside any tracking scope.
|
|
1002
|
+
*/
|
|
1003
|
+
function varyTrusted(
|
|
1004
|
+
vary: ResponseVaryParams,
|
|
1005
|
+
selection: NonNullable<ReturnType<typeof selectRouteForRequest>>,
|
|
1006
|
+
kind: 'body' | 'layout' | 'page',
|
|
1007
|
+
fallbackParams = false,
|
|
1008
|
+
runtimePrefetch = false,
|
|
1009
|
+
): boolean {
|
|
1010
|
+
if (kind === 'layout') {
|
|
1011
|
+
if (vary.layout.length > 0 || vary.layoutSearch) return true;
|
|
1012
|
+
} else if (kind === 'page') {
|
|
1013
|
+
// The mirror of the layout arm: a PAGE frame is trustworthy on its OWN tracked access. An empty page
|
|
1014
|
+
// set is common and honest - a page that reads no params while its layout reads them all - but on a
|
|
1015
|
+
// params-bearing route it is indistinguishable from an untracked render, so it falls through to the
|
|
1016
|
+
// same "route has no params" test the body arm ends on.
|
|
1017
|
+
if (vary.page.length > 0 || vary.pageSearch) return true;
|
|
1018
|
+
} else {
|
|
1019
|
+
if (vary.params.length > 0 || vary.search) return true;
|
|
1020
|
+
if (fallbackParams) return true;
|
|
1021
|
+
if (runtimePrefetch) return true;
|
|
1022
|
+
}
|
|
1023
|
+
return Object.keys(selection.params).length === 0;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** Route params in the JSON-safe shape the client's vary keying reads. */
|
|
1027
|
+
function wireParams(
|
|
1028
|
+
params: Record<string, unknown>,
|
|
1029
|
+
): Record<string, string | string[]> {
|
|
1030
|
+
const wire: Record<string, string | string[]> = {};
|
|
1031
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1032
|
+
if (Array.isArray(value)) wire[key] = (value as unknown[]).map(paramText);
|
|
1033
|
+
else if (value !== undefined && value !== null) wire[key] = paramText(value);
|
|
1034
|
+
}
|
|
1035
|
+
return wire;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/** Route param values are strings (or string arrays); coerce defensively. */
|
|
1039
|
+
function paramText(value: unknown): string {
|
|
1040
|
+
return typeof value === 'string' ? value : JSON.stringify(value) ?? '';
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* True when the route's segment chain declares a `loading` convention (or its
|
|
1045
|
+
* page/layout source wraps content in <Suspense>). A default prefetch of such a
|
|
1046
|
+
* route renders its loading shell (truncated at the boundary) instead of a
|
|
1047
|
+
* hanging dynamic render, so the miss short-circuit must not fire for it.
|
|
1048
|
+
*/
|
|
1049
|
+
function routeHasLoadingBoundary(route: RouteManifestEntry): boolean {
|
|
1050
|
+
if (routeHasLoadingFile(route)) return true;
|
|
1051
|
+
// No `loading` file, but the PAGE itself wraps its params-dependent subtree in <Suspense>. That boundary
|
|
1052
|
+
// truncates the prefetch render exactly like a loading convention does, so the route serves the shared
|
|
1053
|
+
// fallback shell instead of a segment miss. Page file only - a layout's <Suspense> says nothing about
|
|
1054
|
+
// where the page's own render stops.
|
|
1055
|
+
return pageHasInPageSuspense(route.file);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* A `loading` convention file in the route's OWN app segment chain marks a guaranteed truncation point -
|
|
1060
|
+
* the render stops at it, never hanging. (sourceFiles also lists framework modules; anchor on the app
|
|
1061
|
+
* directory.)
|
|
1062
|
+
*/
|
|
1063
|
+
function routeHasLoadingFile(route: RouteManifestEntry): boolean {
|
|
1064
|
+
const appMarker = route.file.lastIndexOf('/app/');
|
|
1065
|
+
const appDir = appMarker === -1 ? undefined : route.file.slice(0, appMarker + '/app/'.length);
|
|
1066
|
+
if (appDir === undefined) return false;
|
|
1067
|
+
return (route.sourceFiles ?? [route.file]).some(
|
|
1068
|
+
file => file.startsWith(appDir) && /[\\/]loading\.[jt]sx?$/.test(file),
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/** True when the build prerendered this route for the given param set. */
|
|
1073
|
+
function routeParamPrerendered(
|
|
1074
|
+
route: RouteManifestEntry,
|
|
1075
|
+
params: Record<string, unknown>,
|
|
1076
|
+
): boolean {
|
|
1077
|
+
const sets = route.prerenderedParams;
|
|
1078
|
+
if (!sets?.length) return false;
|
|
1079
|
+
return sets.some(set => {
|
|
1080
|
+
const keys = Object.keys(set);
|
|
1081
|
+
return (
|
|
1082
|
+
keys.length === Object.keys(params).length &&
|
|
1083
|
+
keys.every(key => String(set[key]) === String(params[key]))
|
|
1084
|
+
);
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* The vary set of a prefetch answered from a PPR SUB-SHELL - the build baked that shell for a param
|
|
1090
|
+
* SUBSET, so a request-time render tracks nothing while its bytes are a pure function of exactly
|
|
1091
|
+
* those params. The layout chain consumed them; the page contributed only its `loading` fallback, so
|
|
1092
|
+
* the PAGE frame stays shareable across every value of the remaining params.
|
|
1093
|
+
*/
|
|
1094
|
+
/**
|
|
1095
|
+
* The route's shared APP SHELL - the build's params-hanging render. Carried alongside a per-URL
|
|
1096
|
+
* prerender so the client can file it at the route's fallback vary path. Undefined for a param-free
|
|
1097
|
+
* route, or when the build baked no such shell.
|
|
1098
|
+
*/
|
|
1099
|
+
function routeAppShell(
|
|
1100
|
+
config: ResolvedConfig,
|
|
1101
|
+
selection: NonNullable<ReturnType<typeof selectRouteForRequest>>,
|
|
1102
|
+
): SegmentAppShell | undefined {
|
|
1103
|
+
// Strictly opt-in, like the client's second (param-stripped) shell prefetch:
|
|
1104
|
+
// without the flag the extra bytes would ride on every prerender response.
|
|
1105
|
+
if (!appShellsEnabled()) return undefined;
|
|
1106
|
+
if (!selection.route.route.includes(':')) return undefined;
|
|
1107
|
+
const file = join(config.outPath, 'ppr', `${selection.route.id}.html`);
|
|
1108
|
+
if (!existsSync(file)) return undefined;
|
|
1109
|
+
try {
|
|
1110
|
+
return { html: readFileSync(file, 'utf8'), route: selection.route.route };
|
|
1111
|
+
} catch {
|
|
1112
|
+
return undefined;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/** The vary sets the BUILD render of this exact prerendered URL tracked. */
|
|
1117
|
+
function prerenderVary(
|
|
1118
|
+
route: RouteManifestEntry,
|
|
1119
|
+
pathname: string,
|
|
1120
|
+
): ResponseVaryParams | undefined {
|
|
1121
|
+
const wire = route.prerenderVary?.[pathname || '/'];
|
|
1122
|
+
return wire ? responseVaryParamsFromWire(wire) : undefined;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function subShellVary(
|
|
1126
|
+
route: RouteManifestEntry,
|
|
1127
|
+
params: Record<string, unknown>,
|
|
1128
|
+
): ResponseVaryParams | undefined {
|
|
1129
|
+
const shell = route.pprSubShells?.find(candidate =>
|
|
1130
|
+
Object.entries(candidate.concreteParams).every(
|
|
1131
|
+
([name, value]) => String(value) === String(params[name]),
|
|
1132
|
+
),
|
|
1133
|
+
);
|
|
1134
|
+
const names = Object.keys(shell?.concreteParams ?? {}).sort();
|
|
1135
|
+
if (!names.length) return undefined;
|
|
1136
|
+
return responseVaryParamsFromWire({ vary: names, layoutVary: names, pageVary: [] });
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
async function readBakedSegment({
|
|
1140
|
+
config,
|
|
1141
|
+
selection,
|
|
1142
|
+
pathname,
|
|
1143
|
+
file,
|
|
1144
|
+
}: {
|
|
1145
|
+
config: ResolvedConfig;
|
|
1146
|
+
selection: NonNullable<ReturnType<typeof selectRouteForRequest>>;
|
|
1147
|
+
pathname: string;
|
|
1148
|
+
file: string;
|
|
1149
|
+
}): Promise<string | undefined> {
|
|
1150
|
+
if (!existsSync(file)) return undefined;
|
|
1151
|
+
await regenerateStaleSegment({ config, selection, pathname, file });
|
|
1152
|
+
try {
|
|
1153
|
+
// A baked FALLBACK-shell segment was rendered with every param hanging, so
|
|
1154
|
+
// its `__PNEXT_ROUTE__` carries `params: {}`. Stamp this request's params
|
|
1155
|
+
// in, exactly as the document path does — the client paints this body as
|
|
1156
|
+
// the loading shell and reads its route state for useParams()/prediction.
|
|
1157
|
+
return withRequestRouteParams(readFileSync(file, 'utf8'), selection.params);
|
|
1158
|
+
} catch {
|
|
1159
|
+
return undefined;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/** True when the route EXPLICITLY opts into runtime prefetching via segment config. */
|
|
1164
|
+
function routeExplicitRuntimePrefetch(route: RouteManifestEntry): boolean {
|
|
1165
|
+
const prefetch = (route.segmentConfig as { prefetch?: unknown } | undefined)?.prefetch;
|
|
1166
|
+
return prefetch === 'allow-runtime' || prefetch === 'unstable_eager';
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* True when the route opts into rendering its body prefetch as a runtime-
|
|
1171
|
+
* prefetch prerender: `unstable_instant = true` (page or layout chain) or
|
|
1172
|
+
* `export const prefetch = 'allow-runtime'`. A leaf-most `unstable_instant =
|
|
1173
|
+
* false` opts back out even when a layout (or `allow-runtime`) opts in.
|
|
1174
|
+
*/
|
|
1175
|
+
function routeRuntimePrefetchRender(route: RouteManifestEntry): boolean {
|
|
1176
|
+
const config = route.segmentConfig;
|
|
1177
|
+
if (config?.unstableInstant === false) return false;
|
|
1178
|
+
return config?.unstableInstant === true || config?.prefetch === 'allow-runtime';
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function routeUsesRuntimePrefetch(route: RouteManifestEntry): boolean {
|
|
1182
|
+
if (routeExplicitRuntimePrefetch(route)) return true;
|
|
1183
|
+
if (clientStaticPrefetchable(route)) return false;
|
|
1184
|
+
try {
|
|
1185
|
+
return /\b(?:searchParams|useSearchParams)\b/.test(readFileSync(route.file, 'utf8'));
|
|
1186
|
+
} catch {
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* A CLIENT page's params/searchParams access resolves client-side from the live URL, so its server
|
|
1193
|
+
* render is a pure function of the exact URL and is servable complete on a default prefetch. Only
|
|
1194
|
+
* true when no server segment in the chain reads real request data.
|
|
1195
|
+
*/
|
|
1196
|
+
function clientStaticPrefetchable(route: RouteManifestEntry): boolean {
|
|
1197
|
+
if (route.client !== true) return false;
|
|
1198
|
+
// Only the route's own app segments matter (sourceFiles also lists framework
|
|
1199
|
+
// modules, whose text mentions request APIs in comments/implementations).
|
|
1200
|
+
const appDir = route.file.slice(0, route.file.lastIndexOf('/app/') + '/app/'.length);
|
|
1201
|
+
for (const file of route.sourceFiles ?? []) {
|
|
1202
|
+
if (file === route.file) continue;
|
|
1203
|
+
if (!appDir || !file.startsWith(appDir)) continue;
|
|
1204
|
+
try {
|
|
1205
|
+
if (/\b(?:cookies|headers|connection|draftMode)\s*\(/.test(readFileSync(file, 'utf8'))) {
|
|
1206
|
+
return false;
|
|
1207
|
+
}
|
|
1208
|
+
} catch {
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return true;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Render the runtime-prefetch (unstable_instant) shell for a full segment prefetch: request data sampled,
|
|
1217
|
+
* connection() content omitted. Best-effort - null falls back to the ordinary full render.
|
|
1218
|
+
*/
|
|
1219
|
+
async function renderInstantPrefetchShell({
|
|
1220
|
+
config,
|
|
1221
|
+
selection,
|
|
1222
|
+
pathname,
|
|
1223
|
+
url,
|
|
1224
|
+
request,
|
|
1225
|
+
fallbackParams = false,
|
|
1226
|
+
}: {
|
|
1227
|
+
config: ResolvedConfig;
|
|
1228
|
+
selection: NonNullable<ReturnType<typeof selectRouteForRequest>>;
|
|
1229
|
+
pathname: string;
|
|
1230
|
+
url: URL;
|
|
1231
|
+
request: Request;
|
|
1232
|
+
/** App Shells: render the SHARED shell (params hang, request data resolves). */
|
|
1233
|
+
fallbackParams?: boolean;
|
|
1234
|
+
}): Promise<{
|
|
1235
|
+
html: string;
|
|
1236
|
+
postponed: boolean;
|
|
1237
|
+
vary: ResponseVaryParams;
|
|
1238
|
+
/** Shortest `use cache` stale window this render actually sampled. */
|
|
1239
|
+
staleSeconds?: number;
|
|
1240
|
+
} | null> {
|
|
1241
|
+
if (selection.route.kind !== 'page') return null;
|
|
1242
|
+
const renderUrl = new URL(url);
|
|
1243
|
+
renderUrl.pathname = pathname;
|
|
1244
|
+
renderUrl.searchParams.delete('_rsc');
|
|
1245
|
+
const headers = new Headers(request.headers);
|
|
1246
|
+
headers.delete(NEXT_ROUTER_PREFETCH_HEADER);
|
|
1247
|
+
headers.delete(NEXT_ROUTER_SEGMENT_PREFETCH_HEADER);
|
|
1248
|
+
// Segment-M2 deliverable 1: the render runs inside a vary-params tracking
|
|
1249
|
+
// scope so the response can publish which params it actually read.
|
|
1250
|
+
let staleSeconds: number | undefined;
|
|
1251
|
+
const tracked = await withVaryParamsTracking(() =>
|
|
1252
|
+
getRenderExtensions().collectRenderMeta(
|
|
1253
|
+
async () => {
|
|
1254
|
+
const value = await withRouteRuntime(selection.route.segmentConfig?.runtime, () =>
|
|
1255
|
+
renderRuntimePrefetchDocument({
|
|
1256
|
+
config,
|
|
1257
|
+
route: selection.route,
|
|
1258
|
+
params: selection.params,
|
|
1259
|
+
url: renderUrl,
|
|
1260
|
+
request: new Request(renderUrl, { headers }),
|
|
1261
|
+
...(fallbackParams ? { fallbackParams: true } : {}),
|
|
1262
|
+
}),
|
|
1263
|
+
);
|
|
1264
|
+
// Read inside the collection scope: the shortest window any cache this
|
|
1265
|
+
// render SAMPLED reported (a `use cache: private` one included), which
|
|
1266
|
+
// is how long the request-sampled payload may be reused.
|
|
1267
|
+
staleSeconds = getRenderExtensions().currentCacheStaleSeconds();
|
|
1268
|
+
return value;
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
fetchCache: selection.route.segmentConfig?.fetchCache,
|
|
1272
|
+
route: pathname,
|
|
1273
|
+
},
|
|
1274
|
+
),
|
|
1275
|
+
);
|
|
1276
|
+
const rendered = tracked.value.value;
|
|
1277
|
+
return rendered
|
|
1278
|
+
? {
|
|
1279
|
+
...rendered,
|
|
1280
|
+
vary: tracked.vary,
|
|
1281
|
+
...(staleSeconds !== undefined ? { staleSeconds } : {}),
|
|
1282
|
+
}
|
|
1283
|
+
: null;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
async function renderRuntimeSegment({
|
|
1287
|
+
config,
|
|
1288
|
+
selection,
|
|
1289
|
+
pathname,
|
|
1290
|
+
url,
|
|
1291
|
+
request,
|
|
1292
|
+
keepPrefetchHeader = false,
|
|
1293
|
+
nav,
|
|
1294
|
+
}: {
|
|
1295
|
+
config: ResolvedConfig;
|
|
1296
|
+
selection: NonNullable<ReturnType<typeof selectRouteForRequest>>;
|
|
1297
|
+
pathname: string;
|
|
1298
|
+
url: URL;
|
|
1299
|
+
request: Request;
|
|
1300
|
+
/** Keep next-router-prefetch so the render truncates at its static shell. */
|
|
1301
|
+
keepPrefetchHeader?: boolean;
|
|
1302
|
+
/**
|
|
1303
|
+
* The client's echoed nav state. Threading it makes the render apply the
|
|
1304
|
+
* same shared-layout skip as a navigation render (Next dedupes shared
|
|
1305
|
+
* layouts on prefetches via next-url too), so a prefetch never re-executes
|
|
1306
|
+
* a layout the origin page already renders.
|
|
1307
|
+
*/
|
|
1308
|
+
nav?: ReturnType<typeof parseNavState>;
|
|
1309
|
+
}): Promise<
|
|
1310
|
+
| {
|
|
1311
|
+
body: string;
|
|
1312
|
+
postponed: boolean;
|
|
1313
|
+
vary: ResponseVaryParams;
|
|
1314
|
+
/** Shortest `use cache` stale window this render actually resolved. */
|
|
1315
|
+
staleSeconds?: number;
|
|
1316
|
+
}
|
|
1317
|
+
| undefined
|
|
1318
|
+
> {
|
|
1319
|
+
if (selection.route.kind !== 'page') return undefined;
|
|
1320
|
+
const renderUrl = new URL(url);
|
|
1321
|
+
renderUrl.pathname = pathname;
|
|
1322
|
+
renderUrl.searchParams.delete('_rsc');
|
|
1323
|
+
const headers = new Headers(request.headers);
|
|
1324
|
+
if (!keepPrefetchHeader) headers.delete(NEXT_ROUTER_PREFETCH_HEADER);
|
|
1325
|
+
headers.delete(NEXT_ROUTER_SEGMENT_PREFETCH_HEADER);
|
|
1326
|
+
// The segment header is stripped so the render takes the ordinary document
|
|
1327
|
+
// path, but the renderer still has to know this body is a PREFETCH payload
|
|
1328
|
+
// and not a navigation: a prefetch that retains a shared layout must render
|
|
1329
|
+
// the delta (skip marker) rather than resume the prebuilt shell, which
|
|
1330
|
+
// carries the origin page's layout markup back to a client that has it.
|
|
1331
|
+
headers.set(SEGMENT_RENDER_HEADER, '1');
|
|
1332
|
+
const renderRequest = new Request(renderUrl, { headers });
|
|
1333
|
+
// Segment-M2 deliverable 1: track param access for the whole render, INCLUDING
|
|
1334
|
+
// the body stream — the tracking scope closes only once the body is read.
|
|
1335
|
+
const tracked = await withVaryParamsTracking(async () => {
|
|
1336
|
+
const rendered = await getRenderExtensions().collectRenderMeta(
|
|
1337
|
+
() =>
|
|
1338
|
+
withRouteRuntime(selection.route.segmentConfig?.runtime, () =>
|
|
1339
|
+
renderPageResponse({
|
|
1340
|
+
config,
|
|
1341
|
+
route: selection.route,
|
|
1342
|
+
params: selection.params,
|
|
1343
|
+
url: renderUrl,
|
|
1344
|
+
request: renderRequest,
|
|
1345
|
+
...(nav ? { nav: { soft: true, state: nav, childrenPath: pathname } } : {}),
|
|
1346
|
+
}),
|
|
1347
|
+
),
|
|
1348
|
+
{
|
|
1349
|
+
fetchCache: selection.route.segmentConfig?.fetchCache,
|
|
1350
|
+
route: pathname,
|
|
1351
|
+
},
|
|
1352
|
+
);
|
|
1353
|
+
if (!rendered.value.ok) return undefined;
|
|
1354
|
+
// The render's OWN window (the `use cache` finalizer stamped the shortest
|
|
1355
|
+
// life it actually resolved, private caches included) — authoritative for a
|
|
1356
|
+
// request-sampled payload, whose baked route meta only knows the public
|
|
1357
|
+
// caches' longer window.
|
|
1358
|
+
const header = Number(rendered.value.headers.get('x-nextjs-stale-time'));
|
|
1359
|
+
return {
|
|
1360
|
+
body: await rendered.value.text(),
|
|
1361
|
+
postponed: rendered.value.headers.get('x-nextjs-postponed') === '1',
|
|
1362
|
+
...(Number.isFinite(header) && header > 0 ? { staleSeconds: header } : {}),
|
|
1363
|
+
};
|
|
1364
|
+
});
|
|
1365
|
+
return tracked.value ? { ...tracked.value, vary: tracked.vary } : undefined;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/** Segment request key for the outlined dynamic head (metadata) of a route. */
|
|
1369
|
+
const HEAD_SEGMENT_PATH = '/_head';
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* The segment request key for a route's LAYOUT frame - the document with the page's markup cut out.
|
|
1373
|
+
* Requested when the client's page frame for a URL is a hit but its layout frame is not.
|
|
1374
|
+
*/
|
|
1375
|
+
const LAYOUT_SEGMENT_PATH = '/_layout';
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* The segment request key for a route's PAGE frame - the page slot alone, with the layout chain cut away.
|
|
1379
|
+
* The inverse of `/_layout`, requested by a navigation that already owns the destination's layout, or
|
|
1380
|
+
* holds a valid cached layout frame for it.
|
|
1381
|
+
*/
|
|
1382
|
+
const PAGE_SEGMENT_PATH = '/_page';
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* App Shells: the client's marker for the SECOND, param-stripped prefetch that
|
|
1386
|
+
* primes a route's shared shell (compat/client/segment-prefetch.ts). Only
|
|
1387
|
+
* meaningful under `experimental.appShells`.
|
|
1388
|
+
*/
|
|
1389
|
+
const APP_SHELL_PREFETCH_HEADER = 'x-pnext-app-shell';
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Marks the render behind a segment-prefetch response, after the segment header
|
|
1393
|
+
* itself has been stripped. The renderer keys its shared-layout delta on this
|
|
1394
|
+
* (see fullSegmentPrefetchSkipsSharedLayout).
|
|
1395
|
+
*/
|
|
1396
|
+
const SEGMENT_RENDER_HEADER = 'x-pnext-segment-render';
|
|
1397
|
+
|
|
1398
|
+
const DOCUMENT_TITLE_PATTERN = /<title(?:\s[^>]*)?>[\s\S]*?<\/title>/;
|
|
1399
|
+
|
|
1400
|
+
/** The document's `<title>…</title>` markup, or null when it has none. */
|
|
1401
|
+
function extractDocumentTitle(html: string): string | null {
|
|
1402
|
+
return DOCUMENT_TITLE_PATTERN.exec(html)?.[0] ?? null;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* True when this route's outlined head must be fetched BEFORE its body. A route with DYNAMIC PARAMS
|
|
1407
|
+
* shares one body across every param value while its metadata varies per URL, so the head is its own
|
|
1408
|
+
* leading segment and is answered first. A PARAMLESS route has nothing to share - its head is
|
|
1409
|
+
* outlined out of the one body response and arrives after it.
|
|
1410
|
+
*/
|
|
1411
|
+
function headFetchedFirst(selection: { route: RouteManifestEntry }): boolean {
|
|
1412
|
+
return selection.route.pprMetadata === true && /[:*]/.test(selection.route.route);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/** The document with its `<title>` removed, or null when it has none. */
|
|
1416
|
+
function outlineDocumentTitle(html: string): string | null {
|
|
1417
|
+
const match = DOCUMENT_TITLE_PATTERN.exec(html);
|
|
1418
|
+
if (!match) return null;
|
|
1419
|
+
return html.slice(0, match.index) + html.slice(match.index + match[0].length);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Cut a streamed document down to its dynamic continuation (the hidden stream chunk divs plus everything
|
|
1424
|
+
* after them), wrapped as a minimal parseable document. Null when the document has no streamed chunks -
|
|
1425
|
+
* nothing dynamic to resume, so the caller serves the full document.
|
|
1426
|
+
*/
|
|
1427
|
+
function resumeOnlyDocument(html: string): string | null {
|
|
1428
|
+
const cut = html.indexOf('<div hidden data-pnext-stream');
|
|
1429
|
+
if (cut === -1) return null;
|
|
1430
|
+
const end = html.lastIndexOf('</body>');
|
|
1431
|
+
const chunks = end > cut ? html.slice(cut, end) : html.slice(cut);
|
|
1432
|
+
return `<!DOCTYPE html><html><body>${chunks}</body></html>`;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function segmentMissResponse(): Response {
|
|
1436
|
+
return new Response(null, {
|
|
1437
|
+
status: 204,
|
|
1438
|
+
headers: {
|
|
1439
|
+
'content-type': RSC_CONTENT_TYPE_HEADER,
|
|
1440
|
+
'x-nextjs-postponed': '2',
|
|
1441
|
+
'cache-control': 'private, no-store',
|
|
1442
|
+
},
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/** x-nextjs-rewritten-path/query values for a matched config rewrite. */
|
|
1447
|
+
interface RewriteHeaderInfo {
|
|
1448
|
+
path?: string;
|
|
1449
|
+
query?: string;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/** Modification time of `file`, or undefined when it does not exist. */
|
|
1453
|
+
function fileMtimeMs(file: string): number | undefined {
|
|
1454
|
+
try {
|
|
1455
|
+
return statSync(file).mtimeMs;
|
|
1456
|
+
} catch {
|
|
1457
|
+
return undefined;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
async function regenerateStaleSegment({
|
|
1462
|
+
config,
|
|
1463
|
+
selection,
|
|
1464
|
+
pathname,
|
|
1465
|
+
file,
|
|
1466
|
+
}: {
|
|
1467
|
+
config: ResolvedConfig;
|
|
1468
|
+
selection: NonNullable<ReturnType<typeof selectRouteForRequest>>;
|
|
1469
|
+
pathname: string;
|
|
1470
|
+
file: string;
|
|
1471
|
+
}): Promise<void> {
|
|
1472
|
+
let mtimeMs: number;
|
|
1473
|
+
try {
|
|
1474
|
+
mtimeMs = statSync(file).mtimeMs;
|
|
1475
|
+
} catch {
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
const servedVersion = segmentArtifactVersions.get(file) ?? 0;
|
|
1479
|
+
const url = new URL(`http://pnext.local${pathname}`);
|
|
1480
|
+
// A PPR route's shell is the SAME render this segment body is cut from, so a
|
|
1481
|
+
// shell that was rewritten after this artifact makes the artifact stale even
|
|
1482
|
+
// when no revalidation is outstanding: without it the document serves the new
|
|
1483
|
+
// regeneration's data while the prefetch keeps replaying the old one's.
|
|
1484
|
+
const shellMtimeMs =
|
|
1485
|
+
selection.route.ppr === true
|
|
1486
|
+
? fileMtimeMs(pprShellPath(config.outPath, selection.route.id))
|
|
1487
|
+
: undefined;
|
|
1488
|
+
const stale =
|
|
1489
|
+
servedVersion < segmentRevalidationVersion ||
|
|
1490
|
+
getRequestExtensions().staticStaleness(pathname, mtimeMs, []) ||
|
|
1491
|
+
(shellMtimeMs !== undefined && shellMtimeMs > mtimeMs);
|
|
1492
|
+
if (!stale) return;
|
|
1493
|
+
|
|
1494
|
+
// Adopt the shell this route currently serves (regenerating it only when it
|
|
1495
|
+
// is itself stale, through the renderer's single-flight) rather than running
|
|
1496
|
+
// a second, independent prerender whose cache reads would resolve to their
|
|
1497
|
+
// own values.
|
|
1498
|
+
if (shellMtimeMs !== undefined) {
|
|
1499
|
+
const shell = await currentPprShellHtml({
|
|
1500
|
+
config,
|
|
1501
|
+
route: selection.route,
|
|
1502
|
+
params: selection.params,
|
|
1503
|
+
url,
|
|
1504
|
+
});
|
|
1505
|
+
if (shell !== null) {
|
|
1506
|
+
await writeFile(file, shell);
|
|
1507
|
+
segmentArtifactVersions.set(file, segmentRevalidationVersion);
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const rendered = await getRenderExtensions().collectRenderMeta(
|
|
1513
|
+
() =>
|
|
1514
|
+
withRouteRuntime(selection.route.segmentConfig?.runtime, () =>
|
|
1515
|
+
renderPartialShell({
|
|
1516
|
+
config,
|
|
1517
|
+
route: selection.route,
|
|
1518
|
+
params: selection.params,
|
|
1519
|
+
url,
|
|
1520
|
+
runtimeRegen: true,
|
|
1521
|
+
}),
|
|
1522
|
+
),
|
|
1523
|
+
{
|
|
1524
|
+
fetchCache: selection.route.segmentConfig?.fetchCache,
|
|
1525
|
+
refreshFetches: true,
|
|
1526
|
+
blockingStaleFetches: true,
|
|
1527
|
+
route: pathname,
|
|
1528
|
+
prerender: true,
|
|
1529
|
+
},
|
|
1530
|
+
);
|
|
1531
|
+
if (!rendered.value) return;
|
|
1532
|
+
await writeFile(file, rendered.value.shell);
|
|
1533
|
+
segmentArtifactVersions.set(file, segmentRevalidationVersion);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const segmentRewriteCache = new Map<string, Promise<CompatRewrite[]>>();
|
|
1537
|
+
|
|
1538
|
+
function loadSegmentRewrites(config: ResolvedConfig): Promise<CompatRewrite[]> {
|
|
1539
|
+
let cached = segmentRewriteCache.get(config.root);
|
|
1540
|
+
if (!cached) {
|
|
1541
|
+
cached = loadCompatRewrites(config.root);
|
|
1542
|
+
segmentRewriteCache.set(config.root, cached);
|
|
1543
|
+
}
|
|
1544
|
+
return cached;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function parseCookieHeader(header: string | null): Record<string, string> {
|
|
1548
|
+
if (!header) return {};
|
|
1549
|
+
const cookies: Record<string, string> = {};
|
|
1550
|
+
for (const part of header.split(';')) {
|
|
1551
|
+
const index = part.indexOf('=');
|
|
1552
|
+
if (index === -1) continue;
|
|
1553
|
+
const key = part.slice(0, index).trim();
|
|
1554
|
+
if (key) cookies[key] = decodeURIComponent(part.slice(index + 1).trim());
|
|
1555
|
+
}
|
|
1556
|
+
return cookies;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/**
|
|
1560
|
+
* Resolve next.config `rewrites` for an RSC/segment request. Returns the
|
|
1561
|
+
* destination pathname (used for route matching) plus the rewritten-path/query
|
|
1562
|
+
* header values Next emits on RSC responses. The `_rsc` cache-buster is a CDN
|
|
1563
|
+
* key, not an app param, so it is excluded from the query matched/appended by
|
|
1564
|
+
* the rewrite (otherwise it would leak into x-nextjs-rewritten-query).
|
|
1565
|
+
*/
|
|
1566
|
+
async function resolveSegmentRewrite(
|
|
1567
|
+
config: ResolvedConfig,
|
|
1568
|
+
request: Request,
|
|
1569
|
+
url: URL,
|
|
1570
|
+
): Promise<{ pathname: string; headers: RewriteHeaderInfo } | undefined> {
|
|
1571
|
+
const rewrites = await loadSegmentRewrites(config);
|
|
1572
|
+
if (rewrites.length === 0) return undefined;
|
|
1573
|
+
const query = new URLSearchParams(url.searchParams);
|
|
1574
|
+
query.delete('_rsc');
|
|
1575
|
+
const result = resolveCompatRewrite(rewrites, url.pathname, {
|
|
1576
|
+
host: request.headers.get('host') ?? '',
|
|
1577
|
+
headers: request.headers,
|
|
1578
|
+
cookies: parseCookieHeader(request.headers.get('cookie')),
|
|
1579
|
+
query,
|
|
1580
|
+
});
|
|
1581
|
+
if (!result) return undefined;
|
|
1582
|
+
const resolvedQuery = result.search.toString();
|
|
1583
|
+
return {
|
|
1584
|
+
pathname: result.pathname,
|
|
1585
|
+
headers: {
|
|
1586
|
+
// Path header only when the pathname actually changed; query header only
|
|
1587
|
+
// when the destination carries a query (mirrors recordRewrite in the
|
|
1588
|
+
// full-render path).
|
|
1589
|
+
...(result.pathname !== url.pathname ? { path: result.pathname } : {}),
|
|
1590
|
+
...(resolvedQuery ? { query: resolvedQuery } : {}),
|
|
1591
|
+
},
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/**
|
|
1596
|
+
* Stamp x-nextjs-rewritten-path/query on a segment/RSC response. Interceptor
|
|
1597
|
+
* responses short-circuit before the protocol finalizer runs, so the compat
|
|
1598
|
+
* rewrite headers must be set here directly. No-op when no rewrite matched.
|
|
1599
|
+
*/
|
|
1600
|
+
function withRewriteHeaders(response: Response, info: RewriteHeaderInfo | undefined): Response {
|
|
1601
|
+
// Interceptor responses bypass the normal response-finalizer pipeline, so
|
|
1602
|
+
// they must merge the router's CDN vary contract themselves.
|
|
1603
|
+
mergeRouterVary(response.headers);
|
|
1604
|
+
if (info?.path !== undefined) response.headers.set('x-nextjs-rewritten-path', info.path);
|
|
1605
|
+
if (info?.query !== undefined) response.headers.set('x-nextjs-rewritten-query', info.query);
|
|
1606
|
+
return response;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const ROUTER_VARY = [
|
|
1610
|
+
'rsc',
|
|
1611
|
+
'next-router-state-tree',
|
|
1612
|
+
'next-router-prefetch',
|
|
1613
|
+
'next-router-segment-prefetch',
|
|
1614
|
+
];
|
|
1615
|
+
|
|
1616
|
+
function mergeRouterVary(headers: Headers): void {
|
|
1617
|
+
const existing = headers.get('vary');
|
|
1618
|
+
const values = existing ? existing.split(',').map(value => value.trim()).filter(Boolean) : [];
|
|
1619
|
+
const present = new Set(values.map(value => value.toLowerCase()));
|
|
1620
|
+
for (const value of ROUTER_VARY) {
|
|
1621
|
+
if (!present.has(value)) {
|
|
1622
|
+
present.add(value);
|
|
1623
|
+
values.push(value);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
headers.set('vary', values.join(', '));
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// Stamp x-nextjs-stale-time on router responses that a `use cache` route did
|
|
1630
|
+
// not already stamp, so both prefetches and navigation-seeded entries learn
|
|
1631
|
+
// their reuse window from the route mode.
|
|
1632
|
+
const APP_ROUTE_KINDS = new Set<ResponseFinalizerContext['routeKind']>([
|
|
1633
|
+
'html',
|
|
1634
|
+
'data',
|
|
1635
|
+
'route-handler',
|
|
1636
|
+
]);
|
|
1637
|
+
|
|
1638
|
+
const prefetchStaleTimeFinalizer: ResponseFinalizer = ctx => {
|
|
1639
|
+
if (!APP_ROUTE_KINDS.has(ctx.routeKind)) return;
|
|
1640
|
+
if (ctx.request.headers.get(RSC_HEADER) !== '1') return;
|
|
1641
|
+
if (!ctx.request.headers.has('x-pnext-soft-nav')) return;
|
|
1642
|
+
if (ctx.headers.has('x-nextjs-stale-time')) return;
|
|
1643
|
+
const prefetchRequest = ctx.request.headers.get(NEXT_ROUTER_PREFETCH_HEADER) === '1';
|
|
1644
|
+
const postponedResponse = ctx.headers.get('x-nextjs-postponed') === '1';
|
|
1645
|
+
// routeMode: a static/isr prerender reuses for the static window, a dynamic render for the dynamic
|
|
1646
|
+
// window, an unknown mode conservatively for the dynamic one. A shell-only (postponed) PREFETCH
|
|
1647
|
+
// response carries only static data, so its shell reuses for the static window regardless.
|
|
1648
|
+
//
|
|
1649
|
+
// A URL whose params the BUILD PRERENDERED is static even though `routeModeOf`
|
|
1650
|
+
// reports the route `dynamic` (it ignores `prerenderedParams`, and an
|
|
1651
|
+
// interception/parallel-slot render is request-time by nature). Corrected here
|
|
1652
|
+
// rather than in `routeModeOf`: routeMode also drives cache-control and the
|
|
1653
|
+
// ISR promotion, which must keep seeing the route-level mode.
|
|
1654
|
+
const runtime = getRequestRuntime();
|
|
1655
|
+
const selection = runtime
|
|
1656
|
+
? selectRouteForRequest(runtime.routes, ctx.request.url.pathname, undefined)
|
|
1657
|
+
: null;
|
|
1658
|
+
const isStatic =
|
|
1659
|
+
ctx.routeMode === 'static' ||
|
|
1660
|
+
ctx.routeMode === 'isr' ||
|
|
1661
|
+
(prefetchRequest && postponedResponse) ||
|
|
1662
|
+
(selection !== null && routeParamPrerendered(selection.route, selection.params ?? {}));
|
|
1663
|
+
// A NAVIGATION response of a postponed (PPR) route streams the resumed
|
|
1664
|
+
// dynamic data: it is a dynamic document and must reuse for the DYNAMIC
|
|
1665
|
+
// window (staleTimes.dynamic gates how long the client re-commits it), even
|
|
1666
|
+
// though the route's built segment meta carries the shell's static window.
|
|
1667
|
+
if (!isStatic && postponedResponse) {
|
|
1668
|
+
ctx.headers.set('x-nextjs-stale-time', String(staleTimeSecondsFor(false)));
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
// A statically-served prerender never re-runs its `use cache` scopes, so the
|
|
1672
|
+
// use-cache finalizer can't stamp the route's own cacheLife window here. The
|
|
1673
|
+
// build persisted it into the route's segment meta — prefer that over the
|
|
1674
|
+
// mode default so a page's cacheLife({stale}) drives client expiry.
|
|
1675
|
+
if (runtime && selection?.route.kind === 'page') {
|
|
1676
|
+
const meta = readSegmentMeta(runtime.config.outPath, selection.route.id);
|
|
1677
|
+
ctx.headers.set('x-nextjs-stale-time', String(effectiveStaleTime(meta, isStatic)));
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
ctx.headers.set('x-nextjs-stale-time', String(staleTimeSecondsFor(isStatic)));
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1683
|
+
// Wire-compat content type: Next answers router fetches (rsc: 1) with
|
|
1684
|
+
// `text/x-component`, and suites assert exactly that on soft-nav responses.
|
|
1685
|
+
// Only the pnext HTML-swap router's own fetches are flipped (x-pnext-soft-nav
|
|
1686
|
+
// marks them); plain document loads keep text/html.
|
|
1687
|
+
const rscContentTypeFinalizer: ResponseFinalizer = ctx => {
|
|
1688
|
+
if (ctx.routeKind !== 'html') return;
|
|
1689
|
+
if (ctx.request.headers.get(RSC_HEADER) !== '1') return;
|
|
1690
|
+
if (!ctx.request.headers.has('x-pnext-soft-nav')) return;
|
|
1691
|
+
ctx.headers.set('content-type', RSC_CONTENT_TYPE_HEADER);
|
|
1692
|
+
};
|
|
1693
|
+
|
|
1694
|
+
const rscDeploymentFinalizer: ResponseFinalizer = ctx => {
|
|
1695
|
+
if (!APP_ROUTE_KINDS.has(ctx.routeKind)) return;
|
|
1696
|
+
if (ctx.request.headers.get(RSC_HEADER) !== '1') return;
|
|
1697
|
+
if (!ctx.headers.has('x-nextjs-deployment-id')) {
|
|
1698
|
+
ctx.headers.set('x-nextjs-deployment-id', deploymentId());
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
function readSegmentMeta(outPath: string, routeId: string): SegmentMeta | undefined {
|
|
1703
|
+
const file = segmentMetaFile(outPath, routeId);
|
|
1704
|
+
if (!existsSync(file)) return undefined;
|
|
1705
|
+
try {
|
|
1706
|
+
return JSON.parse(readFileSync(file, 'utf8')) as SegmentMeta;
|
|
1707
|
+
} catch {
|
|
1708
|
+
return undefined;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function effectiveStaleTime(meta: SegmentMeta | undefined, isStatic: boolean): number {
|
|
1713
|
+
const configured = staleTimeSecondsFor(isStatic);
|
|
1714
|
+
if (!meta) return configured;
|
|
1715
|
+
const builtDefault = isStatic ? DEFAULT_STATIC_STALE_TIME_SECONDS : DEFAULT_DYNAMIC_STALE_TIME_SECONDS;
|
|
1716
|
+
if (meta.staleTime === builtDefault) return configured;
|
|
1717
|
+
// A STATIC-data response (a postponed/truncated shell) of a route whose meta was built with the DYNAMIC
|
|
1718
|
+
// default must reuse for the static window - the meta's 0 describes the route's dynamic resume, not its
|
|
1719
|
+
// static shell. Pinning the shell always-stale made every re-revealed link refetch the route tree.
|
|
1720
|
+
if (isStatic && meta.staleTime === DEFAULT_DYNAMIC_STALE_TIME_SECONDS) return configured;
|
|
1721
|
+
return meta.staleTime;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function readSegmentTree(outPath: string, routeId: string): string | undefined {
|
|
1725
|
+
const file = treeSegmentFile(outPath, routeId);
|
|
1726
|
+
if (!existsSync(file)) return undefined;
|
|
1727
|
+
try {
|
|
1728
|
+
return readFileSync(file, 'utf8');
|
|
1729
|
+
} catch {
|
|
1730
|
+
return undefined;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function treeWithStaleTime(tree: string, staleTime: number, flight: boolean): string {
|
|
1735
|
+
const json = tree.startsWith('0:') ? tree.slice(2) : tree;
|
|
1736
|
+
try {
|
|
1737
|
+
const payload = JSON.parse(json) as { staleTime?: unknown };
|
|
1738
|
+
const body = JSON.stringify(
|
|
1739
|
+
payload.staleTime === staleTime ? payload : { ...payload, staleTime },
|
|
1740
|
+
);
|
|
1741
|
+
return flight ? `0:${body}` : body;
|
|
1742
|
+
} catch {
|
|
1743
|
+
return tree;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
function cacheBustingToken(url: URL, headers?: Headers): string {
|
|
1748
|
+
let hash = 5381;
|
|
1749
|
+
// Like Next's setCacheBustingSearchParam, the token derives from the router
|
|
1750
|
+
// negotiation HEADERS too: the `/_tree` and `/_index` variants of one URL
|
|
1751
|
+
// must never share a token, or a Vary-ignoring CDN would replay one for the
|
|
1752
|
+
// other after a redirect strips the client's own cache-buster.
|
|
1753
|
+
const variant = headers
|
|
1754
|
+
? `|${headers.get(NEXT_ROUTER_PREFETCH_HEADER) ?? ''}|${headers.get(NEXT_ROUTER_SEGMENT_PREFETCH_HEADER) ?? ''}|${headers.get('next-url') ?? ''}`
|
|
1755
|
+
: '';
|
|
1756
|
+
const input = `${url.pathname}?${url.searchParams.toString()}${variant}`;
|
|
1757
|
+
for (let index = 0; index < input.length; index++) {
|
|
1758
|
+
hash = ((hash << 5) + hash) ^ input.charCodeAt(index);
|
|
1759
|
+
}
|
|
1760
|
+
return (hash >>> 0).toString(36);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
function withRscQuery(url: URL, headers?: Headers): string {
|
|
1764
|
+
const next = new URL(url);
|
|
1765
|
+
next.searchParams.delete('_rsc');
|
|
1766
|
+
next.searchParams.set('_rsc', cacheBustingToken(next, headers));
|
|
1767
|
+
return next.href;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// The public URL pattern of a route in the client's bracket form, split into
|
|
1771
|
+
// segments — the same shape the optimistic-routing trie keys on.
|
|
1772
|
+
function routePublicPattern(route: Pick<RouteManifestEntry, 'route'>): string[] {
|
|
1773
|
+
return route.route
|
|
1774
|
+
.replace(/:([a-zA-Z0-9_]+)\*/g, '[...$1]')
|
|
1775
|
+
.replace(/:([a-zA-Z0-9_]+)/g, '[$1]')
|
|
1776
|
+
.split('/')
|
|
1777
|
+
.filter(Boolean);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
/**
|
|
1781
|
+
* Static siblings of the route's deepest dynamic segment, for the RSC payload.
|
|
1782
|
+
* Next's flight data carries the sibling names of a dynamic trie level so the
|
|
1783
|
+
* client never predicts over a static route; the static-siblings suite asserts
|
|
1784
|
+
* the names appear in the server response.
|
|
1785
|
+
*/
|
|
1786
|
+
function routeStaticChildren(
|
|
1787
|
+
routes: readonly RouteManifestEntry[],
|
|
1788
|
+
route: RouteManifestEntry,
|
|
1789
|
+
): string[] {
|
|
1790
|
+
return staticSiblingNames(routes, routePublicPattern(route));
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* The trailing flight marker appended to a full-route RSC body: the leaf
|
|
1795
|
+
* segment name Next's flight payload carries (`__PAGE__`) plus the route
|
|
1796
|
+
* identity, in the same shape `fullRouteRscResponse` sends for a prefetch. An
|
|
1797
|
+
* HTML comment so it stays inert for anything that parses the body as markup.
|
|
1798
|
+
*/
|
|
1799
|
+
function flightSegmentMarker(routeId: string, isStatic: boolean): string {
|
|
1800
|
+
return `\n<!--pnext-flight:${JSON.stringify({ route: routeId, isStatic, segment: '__PAGE__' })}-->`;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* Remove a leading `<!DOCTYPE html>` from a streamed document body (first
|
|
1805
|
+
* chunk(s) only; the rest of the stream passes through untouched) and append
|
|
1806
|
+
* `tail` once the stream ends. Used on the full-route RSC path so an `RSC: 1`
|
|
1807
|
+
* response is never mistakable for a document while keeping the rendered
|
|
1808
|
+
* content byte-identical otherwise.
|
|
1809
|
+
*/
|
|
1810
|
+
function stripDocumentDoctype(response: Response, tail = ''): Response {
|
|
1811
|
+
if (!response.body) return response;
|
|
1812
|
+
const prefix = '<!DOCTYPE html>';
|
|
1813
|
+
const decoder = new TextDecoder();
|
|
1814
|
+
const encoder = new TextEncoder();
|
|
1815
|
+
let buffered = '';
|
|
1816
|
+
let resolved = false;
|
|
1817
|
+
let tailEmitted = false;
|
|
1818
|
+
const emit = (controller: TransformStreamDefaultController<Uint8Array>, text: string) => {
|
|
1819
|
+
if (text) controller.enqueue(encoder.encode(text));
|
|
1820
|
+
};
|
|
1821
|
+
const transformed = response.body.pipeThrough(
|
|
1822
|
+
new TransformStream<Uint8Array, Uint8Array>({
|
|
1823
|
+
transform(chunk, controller) {
|
|
1824
|
+
if (resolved) {
|
|
1825
|
+
// The late-metadata section is not document bytes: the tail belongs
|
|
1826
|
+
// to the DOCUMENT, so it goes out ahead of the marker.
|
|
1827
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
1828
|
+
const late = text.indexOf(LATE_METADATA_MARKER);
|
|
1829
|
+
if (late !== -1 && !tailEmitted) {
|
|
1830
|
+
tailEmitted = true;
|
|
1831
|
+
emit(controller, text.slice(0, late));
|
|
1832
|
+
emit(controller, tail);
|
|
1833
|
+
emit(controller, text.slice(late));
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
emit(controller, text);
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
buffered += decoder.decode(chunk, { stream: true });
|
|
1840
|
+
if (buffered.length < prefix.length && prefix.startsWith(buffered)) return;
|
|
1841
|
+
resolved = true;
|
|
1842
|
+
emit(controller, buffered.startsWith(prefix) ? buffered.slice(prefix.length) : buffered);
|
|
1843
|
+
},
|
|
1844
|
+
flush(controller) {
|
|
1845
|
+
if (!resolved) emit(controller, buffered);
|
|
1846
|
+
if (!tailEmitted) emit(controller, tail);
|
|
1847
|
+
},
|
|
1848
|
+
}),
|
|
1849
|
+
);
|
|
1850
|
+
return new Response(transformed, {
|
|
1851
|
+
status: response.status,
|
|
1852
|
+
statusText: response.statusText,
|
|
1853
|
+
headers: response.headers,
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
function fullRouteRscResponse(
|
|
1858
|
+
routeId: string,
|
|
1859
|
+
isStatic: boolean,
|
|
1860
|
+
staleTime: number,
|
|
1861
|
+
staticChildren: string[] = [],
|
|
1862
|
+
): Response {
|
|
1863
|
+
const payload = {
|
|
1864
|
+
route: routeId,
|
|
1865
|
+
isStatic,
|
|
1866
|
+
segment: '__PAGE__',
|
|
1867
|
+
...(staticChildren.length > 0 ? { staticChildren } : {}),
|
|
1868
|
+
};
|
|
1869
|
+
return new Response(`0:${JSON.stringify(payload)}`, {
|
|
1870
|
+
status: 200,
|
|
1871
|
+
headers: {
|
|
1872
|
+
'content-type': RSC_CONTENT_TYPE_HEADER,
|
|
1873
|
+
'x-nextjs-stale-time': String(staleTime),
|
|
1874
|
+
'x-nextjs-deployment-id': deploymentId(),
|
|
1875
|
+
'cache-control': 'private, no-store',
|
|
1876
|
+
},
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
function withDeploymentId(response: Response): Response {
|
|
1881
|
+
response.headers.set('x-nextjs-deployment-id', deploymentId());
|
|
1882
|
+
return response;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
let registered = false;
|
|
1886
|
+
// Host-scoped guard: a fresh host must be registered into again.
|
|
1887
|
+
onExtensionHostReset(() => (registered = false));
|
|
1888
|
+
|
|
1889
|
+
export function registerSegmentExtensions(config: ResolvedConfig): void {
|
|
1890
|
+
if (registered) return;
|
|
1891
|
+
if (!nextCompatEnabled(config)) return;
|
|
1892
|
+
registered = true;
|
|
1893
|
+
registerRevalidationInvalidator(() => {
|
|
1894
|
+
segmentRevalidationVersion += 1;
|
|
1895
|
+
});
|
|
1896
|
+
// Register the responder FIRST so `/_tree` is handled before action dispatch.
|
|
1897
|
+
registerRequestInterceptors(segmentPrefetchInterceptor);
|
|
1898
|
+
registerResponseFinalizers(
|
|
1899
|
+
prefetchStaleTimeFinalizer,
|
|
1900
|
+
rscContentTypeFinalizer,
|
|
1901
|
+
rscDeploymentFinalizer,
|
|
1902
|
+
);
|
|
1903
|
+
}
|