@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
package/src/ppr.ts
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
|
|
3
|
+
// Partial Prerendering (Next.js-compatible "mixed" model).
|
|
4
|
+
//
|
|
5
|
+
// At build time a PPR route is rendered in a "prerender" scope where there is
|
|
6
|
+
// no request. Any access to request data (cookies()/headers()/...) throws a
|
|
7
|
+
// PostponeError instead of a hard failure. The renderer catches that error at
|
|
8
|
+
// the nearest <Suspense> boundary, keeps the fallback in the static shell, and
|
|
9
|
+
// records the boundary as a dynamic "hole". At request time the shell is served
|
|
10
|
+
// immediately and only the recorded holes are re-rendered with the real request
|
|
11
|
+
// and streamed in.
|
|
12
|
+
//
|
|
13
|
+
// Stage C (cacheComponents) generalizes this: request APIs become HANGING
|
|
14
|
+
// PROMISES during prerender rather than throwing synchronously at the call
|
|
15
|
+
// site. The postpone fires at the first `await` of the hanging promise inside a
|
|
16
|
+
// Suspense subtree (dynamic-at-await-site), matching Next's semantic where
|
|
17
|
+
// passing `cookies()` as an un-awaited promise to a child does NOT make the
|
|
18
|
+
// parent dynamic. Awaiting it does.
|
|
19
|
+
|
|
20
|
+
// PostponeError + isPostpone live in a client-safe module (no node:async_hooks)
|
|
21
|
+
// so client control-flow code can import isPostpone without pulling this module
|
|
22
|
+
// (and its AsyncLocalStorage) into the browser bundle. Re-exported here so
|
|
23
|
+
// every server caller keeps importing them from ppr.
|
|
24
|
+
export { PostponeError, isPostpone } from './ppr-postpone';
|
|
25
|
+
import { isPostpone as isPostponeError, PostponeError } from './ppr-postpone';
|
|
26
|
+
|
|
27
|
+
// The prerender scope carries an AbortController so a hanging promise whose
|
|
28
|
+
// settlement microtask has not yet run stays pending once the prerender has
|
|
29
|
+
// produced its shell (see hangingPromise's `scope.signal.aborted` guard).
|
|
30
|
+
interface PrerenderScope {
|
|
31
|
+
/** Fires when the prerender pass has produced its shell. */
|
|
32
|
+
readonly signal: AbortSignal;
|
|
33
|
+
abort: () => void;
|
|
34
|
+
/**
|
|
35
|
+
* A RUNTIME-PREFETCH prerender (`unstable_instant` full prefetch): the render runs against a real sampled
|
|
36
|
+
* request, so request APIs RESOLVE from it instead of hanging - only connection()-gated content stays
|
|
37
|
+
* dynamic, its promise hanging so the boundary records a hole. Nothing this request-sampled render
|
|
38
|
+
* produces may be persisted into build artifacts.
|
|
39
|
+
*/
|
|
40
|
+
readonly runtimePrefetch?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Runtime-prefetch prerenders only: a runtime API (cookies/headers/params/searchParams, or a `use cache`
|
|
43
|
+
* read) has been AWAITED somewhere in this render. From that point on, synchronous platform IO aborts its
|
|
44
|
+
* boundary - see runWithRuntimePrefetchSyncIoAbort.
|
|
45
|
+
*/
|
|
46
|
+
awaitedRuntimeApi?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* A prerender whose output becomes a PREFETCH shell (the body segment the client's segment cache stores),
|
|
49
|
+
* NOT the document served for a direct request. Short-lived `use cache` scopes are omitted from it exactly
|
|
50
|
+
* as they are from a runtime prefetch - the client's stale window for the prefetch would otherwise be
|
|
51
|
+
* governed by a cache that expires before it.
|
|
52
|
+
*/
|
|
53
|
+
readonly prefetchShell?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* A BUILD prerender running under Next's task-boundary semantics (see
|
|
56
|
+
* awaitAtTaskBoundary). Only set for a cacheComponents build shell render.
|
|
57
|
+
*/
|
|
58
|
+
readonly taskBoundary?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Anchored on globalThis (like requestStorage in request/context.ts) so a
|
|
62
|
+
// compiled compat copy of this module shares the same prerender-scope ALS —
|
|
63
|
+
// unanchored, a second module instance splits prerender detection.
|
|
64
|
+
const PRERENDER_STORAGE = Symbol.for('pnext.prerenderStorage');
|
|
65
|
+
const prerenderStorage = ((globalThis as Record<PropertyKey, unknown>)[PRERENDER_STORAGE] ??=
|
|
66
|
+
new AsyncLocalStorage<PrerenderScope>()) as AsyncLocalStorage<PrerenderScope>;
|
|
67
|
+
|
|
68
|
+
interface PrerenderDynamicTracker {
|
|
69
|
+
dynamic: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const prerenderDynamicTrackerStorage = new AsyncLocalStorage<PrerenderDynamicTracker>();
|
|
73
|
+
|
|
74
|
+
export async function trackPrerenderDynamic<T>(callback: () => Promise<T>): Promise<{
|
|
75
|
+
value: T;
|
|
76
|
+
dynamic: boolean;
|
|
77
|
+
}> {
|
|
78
|
+
const tracker: PrerenderDynamicTracker = { dynamic: false };
|
|
79
|
+
const value = await prerenderDynamicTrackerStorage.run(tracker, callback);
|
|
80
|
+
return { value, dynamic: tracker.dynamic };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function markPrerenderDynamic(): void {
|
|
84
|
+
const tracker = prerenderDynamicTrackerStorage.getStore();
|
|
85
|
+
if (tracker) tracker.dynamic = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Build-shell dynamic-source tracking.
|
|
90
|
+
//
|
|
91
|
+
// A cacheComponents route whose only dynamic access is `params` inside a
|
|
92
|
+
// <Suspense> boundary produces a hole during the partial-shell prerender, yet
|
|
93
|
+
// must NOT be served as a postponed fallback shell: params are part of the URL
|
|
94
|
+
// and always available at request time, so the route is served with a blocking
|
|
95
|
+
// dynamic render (Next's fallback-shells "without IO should not postpone").
|
|
96
|
+
// A hole caused by cached IO (`use cache`/cached fetch) or by request data
|
|
97
|
+
// other than params (cookies()/headers()/connection()/searchParams) is a
|
|
98
|
+
// genuine partial-shell postpone and keeps the fallback-shell path.
|
|
99
|
+
//
|
|
100
|
+
// Anchored on globalThis because the built compat-server bundle loads its own
|
|
101
|
+
// copy of this module (same reason revalidate.ts / cache-fill state anchor
|
|
102
|
+
// there): the cache runtime recording a fill lives in a different module copy
|
|
103
|
+
// from the build driver that begins/reads the window.
|
|
104
|
+
const SHELL_SOURCES = Symbol.for('pnext.shellDynamicSources');
|
|
105
|
+
interface ShellSourceState {
|
|
106
|
+
active: boolean;
|
|
107
|
+
cacheIO: boolean;
|
|
108
|
+
nonParamsRequest: boolean;
|
|
109
|
+
}
|
|
110
|
+
function shellSourceState(): ShellSourceState {
|
|
111
|
+
const root = globalThis as Record<PropertyKey, unknown>;
|
|
112
|
+
return (root[SHELL_SOURCES] ??= {
|
|
113
|
+
active: false,
|
|
114
|
+
cacheIO: false,
|
|
115
|
+
nonParamsRequest: false,
|
|
116
|
+
}) as ShellSourceState;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Arm shell-source tracking for one partial-shell prerender (resets counters). */
|
|
120
|
+
export function beginShellSourceTracking(): void {
|
|
121
|
+
const state = shellSourceState();
|
|
122
|
+
state.active = true;
|
|
123
|
+
state.cacheIO = false;
|
|
124
|
+
state.nonParamsRequest = false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Read + disarm the dynamic sources observed during the last shell prerender. */
|
|
128
|
+
export function endShellSourceTracking(): { cacheIO: boolean; nonParamsRequest: boolean } {
|
|
129
|
+
const state = shellSourceState();
|
|
130
|
+
const result = { cacheIO: state.cacheIO, nonParamsRequest: state.nonParamsRequest };
|
|
131
|
+
state.active = false;
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Record a request-API postpone by name (params is URL-derived, everything else is real request IO). */
|
|
136
|
+
function recordShellRequestApi(api: string): void {
|
|
137
|
+
const state = shellSourceState();
|
|
138
|
+
if (state.active && api !== 'params') state.nonParamsRequest = true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Record that cached IO (a `use cache`/fetch/unstable_cache fill) ran during the shell. */
|
|
142
|
+
function recordShellCacheIO(): void {
|
|
143
|
+
const state = shellSourceState();
|
|
144
|
+
if (state.active) state.cacheIO = true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Render `callback` in prerender mode so request APIs postpone instead of throwing. */
|
|
148
|
+
export function runInPrerender<T>(
|
|
149
|
+
callback: () => T,
|
|
150
|
+
options: { runtimePrefetch?: boolean; prefetchShell?: boolean; taskBoundary?: boolean } = {},
|
|
151
|
+
): T {
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
activePrerenderControllers().add(controller);
|
|
154
|
+
const scope: PrerenderScope = {
|
|
155
|
+
signal: controller.signal,
|
|
156
|
+
abort: () => controller.abort(),
|
|
157
|
+
...(options.runtimePrefetch ? { runtimePrefetch: true, awaitedRuntimeApi: false } : {}),
|
|
158
|
+
...(options.prefetchShell ? { prefetchShell: true } : {}),
|
|
159
|
+
...(options.taskBoundary ? { taskBoundary: true } : {}),
|
|
160
|
+
};
|
|
161
|
+
return prerenderStorage.run(scope, callback);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The build process force-aborts every prerender AbortController created since
|
|
165
|
+
// the last sweep as a belt-and-braces backstop: completePrerender already aborts
|
|
166
|
+
// each scope on its own exit path, but a scope whose callback wired a timer to
|
|
167
|
+
// React.cacheSignal() (a slow cache component polling `setTimeout(..., { signal:
|
|
168
|
+
// React.cacheSignal() })`) and then escaped in a way that skipped its abort would
|
|
169
|
+
// keep the event loop alive and hang the whole build. Anchored on globalThis for
|
|
170
|
+
// the usual dual-module-copy reason (build driver vs built compat-server bundle).
|
|
171
|
+
const ACTIVE_PRERENDER_CONTROLLERS = Symbol.for('pnext.activePrerenderControllers');
|
|
172
|
+
function activePrerenderControllers(): Set<AbortController> {
|
|
173
|
+
const root = globalThis as Record<PropertyKey, unknown>;
|
|
174
|
+
return (root[ACTIVE_PRERENDER_CONTROLLERS] ??= new Set<AbortController>()) as Set<AbortController>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Force-abort (and forget) every prerender AbortController created since the last
|
|
179
|
+
* call. Call once per built route after its shell render returns so no route's
|
|
180
|
+
* stray cacheSignal-bound timer can outlive its render and hang the build.
|
|
181
|
+
*/
|
|
182
|
+
export function abortActivePrerenderScopes(): void {
|
|
183
|
+
const controllers = activePrerenderControllers();
|
|
184
|
+
for (const controller of controllers) controller.abort();
|
|
185
|
+
controllers.clear();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function isPrerendering() {
|
|
189
|
+
return prerenderStorage.getStore() !== undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** True inside a runtime-prefetch prerender (see PrerenderScope.runtimePrefetch). */
|
|
193
|
+
export function isRuntimePrefetchPrerender(): boolean {
|
|
194
|
+
return prerenderStorage.getStore()?.runtimePrefetch === true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** True inside a prefetch-shell prerender (see PrerenderScope.prefetchShell). */
|
|
198
|
+
export function isPrefetchShellPrerender(): boolean {
|
|
199
|
+
return prerenderStorage.getStore()?.prefetchShell === true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The active prerender scope (undefined outside a prerender). */
|
|
203
|
+
export function currentPrerenderScope(): PrerenderScope | undefined {
|
|
204
|
+
return prerenderStorage.getStore();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Complete the active prerender. Called after a shell has been produced: aborts
|
|
209
|
+
* the scope so any hanging promise whose settlement microtask has not yet run
|
|
210
|
+
* stays pending (see hangingPromise's `scope.signal.aborted` guard) rather than
|
|
211
|
+
* rejecting post-shell. Idempotent.
|
|
212
|
+
*/
|
|
213
|
+
export function completePrerender(scope: PrerenderScope | undefined): void {
|
|
214
|
+
if (!scope) return;
|
|
215
|
+
scope.abort();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Task-boundary prerender (cacheComponents build parity).
|
|
219
|
+
//
|
|
220
|
+
// Next ends a build prerender at the first TASK boundary: work that settles while the microtask queue
|
|
221
|
+
// drains lands in the static shell, while anything needing a fresh macrotask (setTimeout, setImmediate,
|
|
222
|
+
// real I/O) does NOT - the surrounding <Suspense> keeps its fallback and the content is filled in at request
|
|
223
|
+
// time. Upstream that falls out of React's `prerender` being started in one task and aborted in the next.
|
|
224
|
+
//
|
|
225
|
+
// pnext resolves the server tree itself rather than through React's prerender, so the boundary is modelled
|
|
226
|
+
// per server-component invocation: the component's promise races a deadline that fires once the microtask
|
|
227
|
+
// queue has drained. A microtask-settled component always wins the race; a task-settled one loses and
|
|
228
|
+
// postpones.
|
|
229
|
+
//
|
|
230
|
+
// The drain is counted in microtask turns rather than armed with `setImmediate`, because `setImmediate` is
|
|
231
|
+
// only ordered against the CHECK phase of the loop: a component's already-expired `setTimeout` sitting in
|
|
232
|
+
// the TIMERS phase beats it roughly half the time whenever a loop turn runs long, which baked Suspense
|
|
233
|
+
// content into the static shell instead of postponing. Microtasks cannot be preempted by any timer or I/O
|
|
234
|
+
// callback, so the verdict is the same on a quiet machine and under a saturated CPU.
|
|
235
|
+
//
|
|
236
|
+
// Arming the deadline per invocation, rather than once for the whole render, is what keeps a SIBLING static
|
|
237
|
+
// subtree renderable after another subtree hit the boundary - the parallel-routes case, where the `@slot`
|
|
238
|
+
// becomes a hole but `children` must still bake into the shell.
|
|
239
|
+
|
|
240
|
+
const taskPostponeSymbol = Symbol.for('pnext.taskPostpone');
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A postpone raised by the task boundary rather than by a request API. Escaping
|
|
244
|
+
* every <Suspense> boundary means the route cannot produce a static shell at
|
|
245
|
+
* all, which Next reports as a blocking-prerender build error.
|
|
246
|
+
*/
|
|
247
|
+
export function isTaskPostpone(error: unknown): boolean {
|
|
248
|
+
return isPostponeError(error) && taskPostponeSymbol in error;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function taskPostponeError(): PostponeError {
|
|
252
|
+
const error = new PostponeError('task');
|
|
253
|
+
Object.defineProperty(error, taskPostponeSymbol, { value: true });
|
|
254
|
+
return error;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** True inside a build prerender running under task-boundary semantics. */
|
|
258
|
+
export function isTaskBoundaryPrerender(): boolean {
|
|
259
|
+
return prerenderStorage.getStore()?.taskBoundary === true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const taskBoundaryArmedStorage = new AsyncLocalStorage<true>();
|
|
263
|
+
const taskBoundarySuspendedStorage = new AsyncLocalStorage<true>();
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Render `callback` with the task boundary ARMED. The renderer arms it only while resolving a <Suspense>
|
|
267
|
+
* subtree, because that is the only place a task postpone can be safely absorbed - the boundary records a
|
|
268
|
+
* hole and keeps its fallback. Task work ABOVE every boundary is left unbounded so it still bakes into the
|
|
269
|
+
* shell, matching Next, where such work escaping the root is a separate blocking-prerender concern.
|
|
270
|
+
*/
|
|
271
|
+
export function runWithTaskBoundaryArmed<T>(callback: () => T): T {
|
|
272
|
+
return taskBoundaryArmedStorage.run(true, callback);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Render `callback` with the task boundary suspended. Used for a `'use cache'` component's subtree: upstream
|
|
277
|
+
* the whole subtree renders inside the cache fill, so its (possibly task-settled) IO belongs in the static
|
|
278
|
+
* shell just like the fill's own - see the module-level `'use cache'` page shape, where the cached component
|
|
279
|
+
* returns immediately and its children do the IO.
|
|
280
|
+
*/
|
|
281
|
+
export function runWithTaskBoundarySuspended<T>(callback: () => T): T {
|
|
282
|
+
return taskBoundarySuspendedStorage.run(true, callback);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// compat/cache/use-cache tags both shapes of "renders inside a cache scope" with globally-registered
|
|
286
|
+
// symbols, so reading them keeps core free of a compat import: the `'use cache'` wrapper itself, and the
|
|
287
|
+
// render-once memo wrapper it puts on every server component inside a produced (cached) tree.
|
|
288
|
+
const useCacheWrapperSymbol = Symbol.for('pnext.compat.useCacheWrapper');
|
|
289
|
+
const useCacheMemoWrappedSymbol = Symbol.for('pnext.compat.useCacheMemoWrapped');
|
|
290
|
+
|
|
291
|
+
/** Whether `component` renders inside a compat `'use cache'` scope. */
|
|
292
|
+
export function isCachedComponent(component: unknown): boolean {
|
|
293
|
+
if (typeof component !== 'function') return false;
|
|
294
|
+
const tagged = component as unknown as Record<symbol, unknown>;
|
|
295
|
+
return tagged[useCacheWrapperSymbol] === true || tagged[useCacheMemoWrappedSymbol] === true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Bound a server-component invocation by the prerender's task boundary. Outside
|
|
300
|
+
* a task-boundary prerender (request renders, non-cacheComponents builds) the
|
|
301
|
+
* value is returned untouched, so this is a no-op on every serving path.
|
|
302
|
+
*/
|
|
303
|
+
export function awaitAtTaskBoundary<T>(value: T): T {
|
|
304
|
+
if (!isTaskBoundaryPrerender()) return value;
|
|
305
|
+
// Only armed inside a <Suspense> subtree (see runWithTaskBoundaryArmed), and
|
|
306
|
+
// never inside a `'use cache'` subtree (runWithTaskBoundarySuspended).
|
|
307
|
+
if (taskBoundaryArmedStorage.getStore() !== true) return value;
|
|
308
|
+
if (taskBoundarySuspendedStorage.getStore() === true) return value;
|
|
309
|
+
if (typeof (value as { then?: unknown } | null)?.then !== 'function') return value;
|
|
310
|
+
const deadline = taskDeadline();
|
|
311
|
+
// Cancel on settle so a component that wins the race does not leave its
|
|
312
|
+
// deadline burning through the rest of its microtask budget.
|
|
313
|
+
return Promise.race([value as PromiseLike<unknown>, deadline.promise]).finally(
|
|
314
|
+
deadline.cancel,
|
|
315
|
+
) as T;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Cache fills are the one kind of task-queue work a build prerender must WAIT for rather than cut off.
|
|
319
|
+
// Upstream this falls out of Next's two-pass model: a warmup render fills every cache,
|
|
320
|
+
// `cacheSignal().cacheReady()` waits for them, and only then does the real prerender run with its
|
|
321
|
+
// task-boundary abort, so by then every cached read settles from memory in a microtask. pnext renders once,
|
|
322
|
+
// so instead the deadline yields while a fill is still in flight and re-arms when the last one settles.
|
|
323
|
+
// Net effect matches: cached IO lands in the shell, uncached task work does not.
|
|
324
|
+
//
|
|
325
|
+
// A fill that never settles must not hang the build (the E236 fill-timeout guard covers 'use cache', but
|
|
326
|
+
// nothing bounds an unstable_cache producer), so the deadline only ever defers to fills a bounded number of
|
|
327
|
+
// times before firing anyway.
|
|
328
|
+
const MAX_CACHE_FILL_DEFERRALS = 50;
|
|
329
|
+
|
|
330
|
+
interface CacheFillState {
|
|
331
|
+
pending: number;
|
|
332
|
+
waiters: (() => void)[];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// The build process and the built compat-server bundle load their own copies of
|
|
336
|
+
// this module (same reason revalidate.ts / build-prerender-errors.ts anchor
|
|
337
|
+
// their state), and the cache runtime registering a fill lives in the other
|
|
338
|
+
// copy from the renderer arming the deadline. Anchor the counter on globalThis
|
|
339
|
+
// so both copies share one window.
|
|
340
|
+
const CACHE_FILL_STATE = Symbol.for('pnext.prerenderCacheFills');
|
|
341
|
+
|
|
342
|
+
function cacheFillState(): CacheFillState {
|
|
343
|
+
const root = globalThis as Record<PropertyKey, unknown>;
|
|
344
|
+
return (root[CACHE_FILL_STATE] ??= { pending: 0, waiters: [] }) as CacheFillState;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Register an in-flight cache fill so a prerender's task boundary waits for it.
|
|
349
|
+
* Cheap enough to run unconditionally (a counter plus one `.then`), which keeps
|
|
350
|
+
* it correct even where the caller's module copy cannot see the prerender ALS.
|
|
351
|
+
* Returns `fill` for chaining.
|
|
352
|
+
*/
|
|
353
|
+
export function trackPrerenderCacheFill<T>(fill: Promise<T>): Promise<T> {
|
|
354
|
+
recordShellCacheIO();
|
|
355
|
+
const state = cacheFillState();
|
|
356
|
+
state.pending += 1;
|
|
357
|
+
const settled = () => {
|
|
358
|
+
state.pending -= 1;
|
|
359
|
+
if (state.pending > 0) return;
|
|
360
|
+
const waiters = state.waiters;
|
|
361
|
+
state.waiters = [];
|
|
362
|
+
for (const waiter of waiters) waiter();
|
|
363
|
+
};
|
|
364
|
+
fill.then(settled, settled);
|
|
365
|
+
return fill;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// How many microtask turns count as "the queue drained". Generous enough that no
|
|
369
|
+
// realistic chain of already-resolved awaits (in this component or in the
|
|
370
|
+
// siblings interleaving with it) runs out of budget, and still free: the whole
|
|
371
|
+
// drain happens inside one event-loop turn, and it is cancelled the moment the
|
|
372
|
+
// component it guards settles.
|
|
373
|
+
const MICROTASK_DRAIN_TURNS = 1000;
|
|
374
|
+
|
|
375
|
+
function taskDeadline(): { promise: Promise<never>; cancel: () => void } {
|
|
376
|
+
let cancelled = false;
|
|
377
|
+
const promise = new Promise<never>((_resolve, reject) => {
|
|
378
|
+
let deferrals = 0;
|
|
379
|
+
const drain = (remaining: number) => {
|
|
380
|
+
if (cancelled) return;
|
|
381
|
+
if (remaining === 0) {
|
|
382
|
+
const state = cacheFillState();
|
|
383
|
+
if (state.pending > 0 && deferrals < MAX_CACHE_FILL_DEFERRALS) {
|
|
384
|
+
// Cache fills are the one kind of task work the boundary waits for:
|
|
385
|
+
// yield until the last one settles, then drain again.
|
|
386
|
+
deferrals += 1;
|
|
387
|
+
state.waiters.push(() => drain(MICROTASK_DRAIN_TURNS));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
reject(taskPostponeError());
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
// Yield through BOTH pre-macrotask queues each turn: the nextTick queue
|
|
394
|
+
// ahead of us runs first (a component awaiting `process.nextTick` must
|
|
395
|
+
// still land in the shell), then a microtask hop. Neither queue can be
|
|
396
|
+
// preempted by a timer or an I/O callback, which is what makes the verdict
|
|
397
|
+
// load-independent.
|
|
398
|
+
const next = () => void Promise.resolve().then(() => drain(remaining - 1));
|
|
399
|
+
if (typeof process?.nextTick === 'function') process.nextTick(next);
|
|
400
|
+
else next();
|
|
401
|
+
};
|
|
402
|
+
drain(MICROTASK_DRAIN_TURNS);
|
|
403
|
+
});
|
|
404
|
+
// The losing copy of a raced deadline must never reach the unhandled-rejection
|
|
405
|
+
// channel (the race attaches its own handler to the winning one).
|
|
406
|
+
promise.catch(() => undefined);
|
|
407
|
+
return {
|
|
408
|
+
promise,
|
|
409
|
+
cancel: () => {
|
|
410
|
+
cancelled = true;
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* A promise that never resolves during prerender. Its settlement depends on how the render observes it,
|
|
417
|
+
* matching Next's two cacheComponents contracts:
|
|
418
|
+
* - AWAITED (a `.then` with a fulfillment handler - the dynamic-at-await-site signal): rejects with
|
|
419
|
+
* PostponeError so the await throws and the nearest <Suspense> records a hole.
|
|
420
|
+
* - Only observed for rejection, or passed around and never awaited - the "stash the promise and observe
|
|
421
|
+
* its rejection later" pattern: rejects instead with the "During prerendering, <api> rejects when the
|
|
422
|
+
* prerender is complete" message so a subscribed rejection handler observes it.
|
|
423
|
+
* Either way the internal `.catch` below keeps an un-subscribed copy off the unhandled-rejection channel.
|
|
424
|
+
*/
|
|
425
|
+
/**
|
|
426
|
+
* The Error a hanging request-API promise rejects with once the prerender completes. Next names the API in
|
|
427
|
+
* backticks and its Node runtime prints the error as `Error: <message>`. Bun's console inspector would
|
|
428
|
+
* otherwise render an Error as lowercase `error:` plus a code frame, so a Node-style `util.inspect` custom
|
|
429
|
+
* hook returns the canonical form - the request-apis e2e suite substring-matches exactly that.
|
|
430
|
+
*/
|
|
431
|
+
function prerenderCompleteError(api: string): Error {
|
|
432
|
+
const message = `During prerendering, \`${api}\` rejects when the prerender is complete.`;
|
|
433
|
+
const error = new Error(message);
|
|
434
|
+
Object.defineProperty(error, Symbol.for('nodejs.util.inspect.custom'), {
|
|
435
|
+
value: () => {
|
|
436
|
+
const stackTail = (error.stack ?? '').split('\n').slice(1).join('\n');
|
|
437
|
+
return stackTail ? `Error: ${message}\n${stackTail}` : `Error: ${message}`;
|
|
438
|
+
},
|
|
439
|
+
enumerable: false,
|
|
440
|
+
configurable: true,
|
|
441
|
+
});
|
|
442
|
+
return error;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Tag read off a hanging promise's Proxy. A consumer that only wants to KNOW a promise is a hanging
|
|
447
|
+
* request-API promise, rather than consume its value, must be able to ask without marking the prerender
|
|
448
|
+
* dynamic - so the tag arm sits ahead of the then/catch/finally arms in the `get` trap.
|
|
449
|
+
*/
|
|
450
|
+
const HANGING_PROMISE = Symbol.for('pnext.hangingPromise');
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* True for the promise `hangingPromise` returns. Used by the island-props
|
|
454
|
+
* serializer: awaiting a fallback-shell `params`/`searchParams` promise on the
|
|
455
|
+
* way to a client component would postpone OUTSIDE every <Suspense> and destroy
|
|
456
|
+
* the whole partial shell, when the island's props are not the consumer that
|
|
457
|
+
* should postpone anything.
|
|
458
|
+
*/
|
|
459
|
+
export function isHangingPromise(value: unknown): boolean {
|
|
460
|
+
return (
|
|
461
|
+
typeof value === 'object' &&
|
|
462
|
+
value !== null &&
|
|
463
|
+
(value as Record<symbol, unknown>)[HANGING_PROMISE] === true
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function hangingPromise<T = never>(api: string): Promise<T> {
|
|
468
|
+
const scope = prerenderStorage.getStore();
|
|
469
|
+
// Outside a prerender scope this should not be called; return a caught
|
|
470
|
+
// rejection defensively so a stray await surfaces rather than hangs forever.
|
|
471
|
+
if (!scope) {
|
|
472
|
+
const stray = Promise.reject<T>(new PostponeError(api));
|
|
473
|
+
stray.catch(() => undefined);
|
|
474
|
+
return stray;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// The promise settles exactly once, via whichever observation happens first: a consumer awaiting it (a
|
|
478
|
+
// `.then` with a fulfillment handler) rejects with PostponeError so the await throws and the boundary
|
|
479
|
+
// records a hole; the prerender completing (scope abort) rejects with the "rejects when the prerender is
|
|
480
|
+
// complete" Error so stashed-promise subscribers observe it. It must stay PENDING until one of those
|
|
481
|
+
// happens - settling on a timer or microtask races late awaiters into the wrong rejection.
|
|
482
|
+
let settle: ((error: Error) => void) | undefined;
|
|
483
|
+
const promise = new Promise<T>((_resolve, reject) => {
|
|
484
|
+
settle = (error: Error) => reject(error);
|
|
485
|
+
});
|
|
486
|
+
const rejectAwaited = () => settle?.(new PostponeError(api));
|
|
487
|
+
if (scope.signal.aborted) {
|
|
488
|
+
settle?.(prerenderCompleteError(api));
|
|
489
|
+
} else {
|
|
490
|
+
scope.signal.addEventListener('abort', () => settle?.(prerenderCompleteError(api)), {
|
|
491
|
+
once: true,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
// Swallow the rejection for the un-subscribed copy so a hanging promise nobody
|
|
495
|
+
// observed never surfaces as an unhandled rejection; an actual awaiter/handler
|
|
496
|
+
// still observes the throw (it subscribes to the same promise).
|
|
497
|
+
promise.catch(() => undefined);
|
|
498
|
+
return new Proxy(promise, {
|
|
499
|
+
get(target, property) {
|
|
500
|
+
// Before every observation arm: reading the tag must not mark dynamic.
|
|
501
|
+
if (property === HANGING_PROMISE) return true;
|
|
502
|
+
if (property === 'then') {
|
|
503
|
+
return (...args: Parameters<typeof target.then>) => {
|
|
504
|
+
// A fulfillment handler means the value is being consumed (an await or
|
|
505
|
+
// value-taking `.then`): mark the boundary dynamic and postpone.
|
|
506
|
+
if (typeof args[0] === 'function') rejectAwaited();
|
|
507
|
+
markPrerenderDynamic();
|
|
508
|
+
recordShellRequestApi(api);
|
|
509
|
+
return target.then(...args);
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (property === 'catch') {
|
|
513
|
+
return (...args: Parameters<typeof target.catch>) => {
|
|
514
|
+
markPrerenderDynamic();
|
|
515
|
+
recordShellRequestApi(api);
|
|
516
|
+
return target.catch(...args);
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
if (property === 'finally') {
|
|
520
|
+
return (...args: Parameters<typeof target.finally>) => {
|
|
521
|
+
markPrerenderDynamic();
|
|
522
|
+
recordShellRequestApi(api);
|
|
523
|
+
return target.finally(...args);
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
const value: unknown = Reflect.get(target, property, target);
|
|
527
|
+
return value;
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
// Global cacheComponents flag (Stage C).
|
|
534
|
+
//
|
|
535
|
+
// Next 16 gates PPR-by-default on `nextConfig.cacheComponents: true` rather
|
|
536
|
+
// than the per-route `experimental_ppr` opt-in. Core owns the flag storage
|
|
537
|
+
// (a boolean cell); compat sets it from getNextConfig() during registration.
|
|
538
|
+
// The renderer/routing/build read it to decide whether every route is a PPR
|
|
539
|
+
// candidate and whether request APIs hang.
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
|
|
542
|
+
let cacheComponentsEnabled = false;
|
|
543
|
+
|
|
544
|
+
export function setCacheComponents(enabled: boolean): void {
|
|
545
|
+
cacheComponentsEnabled = enabled;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function cacheComponents(): boolean {
|
|
549
|
+
return cacheComponentsEnabled;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// Prerender determinism (Stage C-5).
|
|
554
|
+
//
|
|
555
|
+
// During a build prerender, incidental Math.random()/Date.now()/new Date() must
|
|
556
|
+
// not vary or fail the build. We patch them to deterministic values for the
|
|
557
|
+
// duration of a build prerender pass and restore afterwards. Build-only and
|
|
558
|
+
// single-route-at-a-time, so global patching is safe; at request time the real
|
|
559
|
+
// implementations run (random varies across requests). Applied only under
|
|
560
|
+
// cacheComponents so the legacy path is byte-identical.
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
|
|
563
|
+
// Real wall-clock that survives the determinism patch below. Cache bookkeeping
|
|
564
|
+
// (use-cache storedAt) must never be stamped with the frozen build clock — a
|
|
565
|
+
// `storedAt: 0` entry reads as perpetually expired at runtime.
|
|
566
|
+
const REAL_NOW = Symbol.for('pnext.realNow');
|
|
567
|
+
export function realNow(): number {
|
|
568
|
+
const stored = (globalThis as Record<PropertyKey, unknown>)[REAL_NOW];
|
|
569
|
+
return typeof stored === 'function' ? (stored as () => number)() : Date.now();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// A `use cache` body is allowed to be non-deterministic - its output is what gets cached, and Next's
|
|
573
|
+
// work-unit store inside a cache scope is `cache`, not `prerender-runtime`, so
|
|
574
|
+
// abortOnSynchronousPlatformIOAccess never applies there. The scope ALS is read off its globalThis symbol
|
|
575
|
+
// rather than imported: ppr.ts is core and must not depend on compat/cache.
|
|
576
|
+
const USE_CACHE_SCOPE_STORAGE = Symbol.for('pnext.compat.useCacheScopeStorage');
|
|
577
|
+
|
|
578
|
+
function insideUseCacheScope(): boolean {
|
|
579
|
+
const storage = (globalThis as Record<PropertyKey, unknown>)[USE_CACHE_SCOPE_STORAGE] as
|
|
580
|
+
| { getStore(): unknown }
|
|
581
|
+
| undefined;
|
|
582
|
+
return storage?.getStore() !== undefined;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export async function runWithPrerenderDeterminism<T>(fn: () => Promise<T>): Promise<T> {
|
|
586
|
+
if (!cacheComponentsEnabled) return fn();
|
|
587
|
+
const deterministicTime = 1;
|
|
588
|
+
const realRandom = Math.random;
|
|
589
|
+
const RealDate = Date;
|
|
590
|
+
(globalThis as Record<PropertyKey, unknown>)[REAL_NOW] = RealDate.now.bind(RealDate);
|
|
591
|
+
let counter = 0;
|
|
592
|
+
Math.random = () => {
|
|
593
|
+
counter = (counter * 1103515245 + 12345) & 0x7fffffff;
|
|
594
|
+
return (counter % 1000000) / 1000000;
|
|
595
|
+
};
|
|
596
|
+
const PatchedDate = function (this: unknown, ...args: unknown[]) {
|
|
597
|
+
if (!new.target) return new RealDate(deterministicTime).toString();
|
|
598
|
+
if (args.length === 0) return new RealDate(deterministicTime);
|
|
599
|
+
return Reflect.construct(RealDate, args) as Date;
|
|
600
|
+
} as unknown as DateConstructor;
|
|
601
|
+
Object.setPrototypeOf(PatchedDate, RealDate);
|
|
602
|
+
Object.defineProperty(PatchedDate, 'prototype', { value: RealDate.prototype });
|
|
603
|
+
PatchedDate.now = () => deterministicTime;
|
|
604
|
+
// eslint-disable-next-line no-global-assign
|
|
605
|
+
Date = PatchedDate;
|
|
606
|
+
try {
|
|
607
|
+
return await fn();
|
|
608
|
+
} finally {
|
|
609
|
+
Math.random = realRandom;
|
|
610
|
+
// eslint-disable-next-line no-global-assign
|
|
611
|
+
Date = RealDate;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Sync-IO abort during a runtime-prefetch prerender.
|
|
616
|
+
//
|
|
617
|
+
// A runtime prefetch renders against a REAL sampled request, so cookies()/headers()/params/`use cache`
|
|
618
|
+
// resolve - which means sync IO hidden BEHIND one of them (the classic `await cookies(); return
|
|
619
|
+
// <div>{Date.now()}</div>`) is only reachable at runtime, never at build time where validation would catch
|
|
620
|
+
// it. Next handles it with abortOnSynchronousPlatformIOAccess: once a runtime API has resolved, the next
|
|
621
|
+
// synchronous platform IO call aborts the boundary, silently, because the value is simply left for the
|
|
622
|
+
// navigation to render.
|
|
623
|
+
//
|
|
624
|
+
// pnext mirrors that by shimming the platform globals for the duration of the runtime-prefetch render and
|
|
625
|
+
// throwing a postpone (exactly what connection() does) when the scope's `awaitedRuntimeApi` flag is set.
|
|
626
|
+
// The shims are gated on the prerender ALS, so a concurrent request render on the same server sees the real
|
|
627
|
+
// implementations.
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Mark a runtime API's promise so AWAITING it arms the sync-IO abort for the rest of the runtime-prefetch
|
|
631
|
+
* render. Outside a runtime-prefetch prerender the promise is returned untouched.
|
|
632
|
+
*
|
|
633
|
+
* Await-precise rather than creation-precise: merely creating (or passing around) `cookies()` must not arm
|
|
634
|
+
* the abort - the same dynamic-at-await-site rule hangingPromise implements, and via the same Proxy trick,
|
|
635
|
+
* since `await` on a native promise bypasses a monkey-patched `then`.
|
|
636
|
+
*/
|
|
637
|
+
export function markRuntimeApiOnAwait<T>(promise: Promise<T>): Promise<T> {
|
|
638
|
+
const scope = prerenderStorage.getStore();
|
|
639
|
+
if (scope?.runtimePrefetch !== true) return promise;
|
|
640
|
+
return new Proxy(promise, {
|
|
641
|
+
get(target, property) {
|
|
642
|
+
if (property === 'then') {
|
|
643
|
+
return (...args: Parameters<typeof target.then>) => {
|
|
644
|
+
// A fulfillment handler means the value is being consumed (an await or
|
|
645
|
+
// a value-taking `.then`) — the point Next considers the runtime API
|
|
646
|
+
// resolved into the render.
|
|
647
|
+
if (typeof args[0] === 'function') scope.awaitedRuntimeApi = true;
|
|
648
|
+
return target.then(...args);
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
return Reflect.get(target, property, target) as unknown;
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Throw the boundary-aborting postpone when sync IO happens post-runtime-API. */
|
|
657
|
+
function abortOnSynchronousPlatformIOAccess(): void {
|
|
658
|
+
const scope = prerenderStorage.getStore();
|
|
659
|
+
if (scope?.runtimePrefetch !== true || scope.awaitedRuntimeApi !== true) return;
|
|
660
|
+
// Sync IO inside a `use cache` body is legal — see insideUseCacheScope above.
|
|
661
|
+
if (insideUseCacheScope()) return;
|
|
662
|
+
throw new PostponeError('sync IO');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Install `value` as an own property of `owner`, returning a restore thunk. */
|
|
666
|
+
function patchMethod(owner: object, key: string, value: unknown): () => void {
|
|
667
|
+
const hadOwn = Object.prototype.hasOwnProperty.call(owner, key);
|
|
668
|
+
const descriptor = Object.getOwnPropertyDescriptor(owner, key);
|
|
669
|
+
try {
|
|
670
|
+
Object.defineProperty(owner, key, {
|
|
671
|
+
value,
|
|
672
|
+
configurable: true,
|
|
673
|
+
writable: true,
|
|
674
|
+
enumerable: descriptor?.enumerable ?? false,
|
|
675
|
+
});
|
|
676
|
+
} catch {
|
|
677
|
+
return () => undefined;
|
|
678
|
+
}
|
|
679
|
+
return () => {
|
|
680
|
+
try {
|
|
681
|
+
if (hadOwn && descriptor) Object.defineProperty(owner, key, descriptor);
|
|
682
|
+
else delete (owner as Record<string, unknown>)[key];
|
|
683
|
+
} catch {
|
|
684
|
+
/* nothing sensible to do if the global refuses to be restored */
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Two runtime prefetches can be in flight at once, so installation is
|
|
690
|
+
// refcounted: the shims go in on 0→1 and come out on 1→0. Anchored on
|
|
691
|
+
// globalThis for the usual dual-module-copy reason.
|
|
692
|
+
const SYNC_IO_SHIMS = Symbol.for('pnext.runtimePrefetchSyncIoShims');
|
|
693
|
+
interface SyncIoShimState {
|
|
694
|
+
depth: number;
|
|
695
|
+
restore: (() => void)[];
|
|
696
|
+
}
|
|
697
|
+
function syncIoShimState(): SyncIoShimState {
|
|
698
|
+
const root = globalThis as Record<PropertyKey, unknown>;
|
|
699
|
+
return (root[SYNC_IO_SHIMS] ??= { depth: 0, restore: [] }) as SyncIoShimState;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function installSyncIoShims(): void {
|
|
703
|
+
const state = syncIoShimState();
|
|
704
|
+
if (++state.depth > 1) return;
|
|
705
|
+
const root = globalThis as Record<PropertyKey, unknown>;
|
|
706
|
+
const RealDate = Date;
|
|
707
|
+
const realRandom = Math.random;
|
|
708
|
+
// realNow() must keep reporting the true wall clock while Date.now is shimmed
|
|
709
|
+
// — cache bookkeeping (storedAt/expiry) reads it during the render.
|
|
710
|
+
const previousRealNow = root[REAL_NOW];
|
|
711
|
+
root[REAL_NOW] = RealDate.now.bind(RealDate);
|
|
712
|
+
const PatchedDate = function (this: unknown, ...args: unknown[]) {
|
|
713
|
+
if (!new.target) {
|
|
714
|
+
abortOnSynchronousPlatformIOAccess();
|
|
715
|
+
return new RealDate().toString();
|
|
716
|
+
}
|
|
717
|
+
if (args.length === 0) {
|
|
718
|
+
abortOnSynchronousPlatformIOAccess();
|
|
719
|
+
return new RealDate();
|
|
720
|
+
}
|
|
721
|
+
return Reflect.construct(RealDate, args) as Date;
|
|
722
|
+
} as unknown as DateConstructor;
|
|
723
|
+
Object.setPrototypeOf(PatchedDate, RealDate);
|
|
724
|
+
Object.defineProperty(PatchedDate, 'prototype', { value: RealDate.prototype });
|
|
725
|
+
PatchedDate.now = () => {
|
|
726
|
+
abortOnSynchronousPlatformIOAccess();
|
|
727
|
+
return RealDate.now();
|
|
728
|
+
};
|
|
729
|
+
// eslint-disable-next-line no-global-assign
|
|
730
|
+
Date = PatchedDate;
|
|
731
|
+
Math.random = () => {
|
|
732
|
+
abortOnSynchronousPlatformIOAccess();
|
|
733
|
+
return realRandom();
|
|
734
|
+
};
|
|
735
|
+
state.restore.push(() => {
|
|
736
|
+
// eslint-disable-next-line no-global-assign
|
|
737
|
+
Date = RealDate;
|
|
738
|
+
Math.random = realRandom;
|
|
739
|
+
root[REAL_NOW] = previousRealNow;
|
|
740
|
+
});
|
|
741
|
+
const perf = globalThis.performance as Performance | undefined;
|
|
742
|
+
if (perf) {
|
|
743
|
+
const realPerfNow = perf.now.bind(perf);
|
|
744
|
+
state.restore.push(
|
|
745
|
+
patchMethod(perf, 'now', () => {
|
|
746
|
+
abortOnSynchronousPlatformIOAccess();
|
|
747
|
+
return realPerfNow();
|
|
748
|
+
}),
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
const webCrypto = globalThis.crypto as Crypto | undefined;
|
|
752
|
+
if (typeof webCrypto?.getRandomValues === 'function') {
|
|
753
|
+
const realGetRandomValues = webCrypto.getRandomValues.bind(webCrypto);
|
|
754
|
+
state.restore.push(
|
|
755
|
+
patchMethod(webCrypto, 'getRandomValues', (array: ArrayBufferView) => {
|
|
756
|
+
abortOnSynchronousPlatformIOAccess();
|
|
757
|
+
return realGetRandomValues(array as never);
|
|
758
|
+
}),
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function restoreSyncIoShims(): void {
|
|
764
|
+
const state = syncIoShimState();
|
|
765
|
+
if (--state.depth > 0) return;
|
|
766
|
+
state.depth = 0;
|
|
767
|
+
const restore = state.restore;
|
|
768
|
+
state.restore = [];
|
|
769
|
+
for (const undo of restore) undo();
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Run a runtime-prefetch render with the sync-IO shims installed. Sync IO that
|
|
774
|
+
* happens once a runtime API has been awaited (see markRuntimeApiOnAwait)
|
|
775
|
+
* postpones its nearest <Suspense> boundary instead of returning a value.
|
|
776
|
+
*/
|
|
777
|
+
export async function runWithRuntimePrefetchSyncIoAbort<T>(fn: () => Promise<T>): Promise<T> {
|
|
778
|
+
installSyncIoShims();
|
|
779
|
+
try {
|
|
780
|
+
return await fn();
|
|
781
|
+
} finally {
|
|
782
|
+
restoreSyncIoShims();
|
|
783
|
+
}
|
|
784
|
+
}
|