@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,86 @@
|
|
|
1
|
+
// Per-link navigation pending state for `useLinkStatus()` (next/link). A single link is "pending" at a
|
|
2
|
+
// time: the last one whose click started an in-flight soft navigation. It clears on completion, and on
|
|
3
|
+
// any navigation started elsewhere - all of which broadcast a location change.
|
|
4
|
+
import { createContext } from 'preact/compat';
|
|
5
|
+
import { softNavigate } from '../../api/router';
|
|
6
|
+
import { locationListeners, onNavigationStart } from '../../api/router/events';
|
|
7
|
+
import { patchHistory } from '../../api/client-navigation';
|
|
8
|
+
|
|
9
|
+
export interface LinkStatus {
|
|
10
|
+
pending: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const LinkStatusContext = createContext<LinkStatus>({ pending: false });
|
|
14
|
+
|
|
15
|
+
type Listener = () => void;
|
|
16
|
+
|
|
17
|
+
let pendingToken: symbol | null = null;
|
|
18
|
+
// Set right before navigateWithStatus starts its own soft-nav, so the resulting
|
|
19
|
+
// synchronous nav-start is recognized as the link's own, not a supersession.
|
|
20
|
+
let ownNavStart: symbol | null = null;
|
|
21
|
+
const listeners = new Set<Listener>();
|
|
22
|
+
let wired = false;
|
|
23
|
+
|
|
24
|
+
function notify() {
|
|
25
|
+
for (const listener of [...listeners]) listener();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function setPending(token: symbol | null) {
|
|
29
|
+
if (pendingToken === token) return;
|
|
30
|
+
pendingToken = token;
|
|
31
|
+
notify();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A different navigation starting (router.push, refresh, another link)
|
|
35
|
+
// supersedes the pending link immediately, even while its own nav is still
|
|
36
|
+
// in flight — its synchronous start isn't the pending link's own.
|
|
37
|
+
function onNavStart() {
|
|
38
|
+
if (ownNavStart !== null) {
|
|
39
|
+
ownNavStart = null;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
setPending(null);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A location change ends the pending state: for the pending link's own nav this
|
|
46
|
+
// only fires at completion (its pushState happens after the fetch resolves);
|
|
47
|
+
// for shallow pushState / browser back it fires immediately. Both should clear.
|
|
48
|
+
function onLocationChange() {
|
|
49
|
+
setPending(null);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function wire() {
|
|
53
|
+
if (wired || (!process.browser && typeof window === 'undefined')) return;
|
|
54
|
+
wired = true;
|
|
55
|
+
// Raw window.history.pushState/replaceState (shallow routing) must clear the
|
|
56
|
+
// pending link; the patch broadcasts them through locationListeners.
|
|
57
|
+
patchHistory();
|
|
58
|
+
onNavigationStart(onNavStart);
|
|
59
|
+
locationListeners().add(onLocationChange);
|
|
60
|
+
window.addEventListener('popstate', onLocationChange);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function subscribeLinkStatus(_token: symbol, listener: Listener) {
|
|
64
|
+
wire();
|
|
65
|
+
listeners.add(listener);
|
|
66
|
+
return () => listeners.delete(listener);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function linkPending(token: symbol) {
|
|
70
|
+
return pendingToken === token;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Start a soft navigation owned by this link: mark it pending, clear on
|
|
74
|
+
// completion. A newer click flips the token, so a stale completion won't clear
|
|
75
|
+
// the newer link's pending state.
|
|
76
|
+
export function navigateWithStatus(
|
|
77
|
+
token: symbol,
|
|
78
|
+
href: string,
|
|
79
|
+
options: { replace?: boolean; scroll?: boolean },
|
|
80
|
+
) {
|
|
81
|
+
ownNavStart = token;
|
|
82
|
+
setPending(token);
|
|
83
|
+
void softNavigate(href, options).finally(() => {
|
|
84
|
+
if (pendingToken === token) setPending(null);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// DEFERRED TIER of the navigation compat layer (see ./nav-compat). Every policy
|
|
2
|
+
// here is read by the router runtime and by nothing else, so it loads with the
|
|
3
|
+
// runtime chunk — the installers finish before the facade hands the runtime out,
|
|
4
|
+
// so the first fetch already sees Next's windows and caches.
|
|
5
|
+
import { setSoftNavBoundary } from '../../api/router';
|
|
6
|
+
import { setNavigationScrollAction } from '../../api/router/events';
|
|
7
|
+
import {
|
|
8
|
+
setLoadingShellPredictionPolicy,
|
|
9
|
+
setPrefetchStaleTimePolicy,
|
|
10
|
+
setRevalidationPrefetchDelay,
|
|
11
|
+
setSegmentCachePolicy,
|
|
12
|
+
setShellStaleTimePolicy,
|
|
13
|
+
setStylesheetReconciler,
|
|
14
|
+
} from '../../api/router/policies';
|
|
15
|
+
import { installSegmentCachePolicy } from './segment-cache-policy';
|
|
16
|
+
// `output: 'export'` document fetching. Imported for its module-scope
|
|
17
|
+
// registration, which has to beat the router's first prefetches — this module
|
|
18
|
+
// resolving before the runtime is handed out is exactly that guarantee.
|
|
19
|
+
import '../export/client';
|
|
20
|
+
import { reconcileNextStylesheets } from './css-order';
|
|
21
|
+
import { staleTimeForEntryMs, staticStaleTimeMs } from './prefetch-cache';
|
|
22
|
+
import { isOutsideBasePath } from './base-path';
|
|
23
|
+
import { predictNextLoadingShell } from './optimistic-routing';
|
|
24
|
+
import { applyNextNavigationScroll } from './navigation-scroll';
|
|
25
|
+
import { installRouteAnnouncer } from './route-announcer';
|
|
26
|
+
|
|
27
|
+
export function installNavPolicies() {
|
|
28
|
+
// Route announcer: mount the empty `<next-route-announcer>` and update it on
|
|
29
|
+
// every soft-nav commit (a11y — announce client route changes to AT). Next
|
|
30
|
+
// mounts it at hydration; it announces nothing until the first navigation, and
|
|
31
|
+
// this tier lands on idle, long before one can happen.
|
|
32
|
+
installRouteAnnouncer();
|
|
33
|
+
// Prefetch staleTime policy: static 300s / dynamic 0, with the
|
|
34
|
+
// experimental.staleTimes override injected as window.__PNEXT_STALE_TIMES__,
|
|
35
|
+
// and a response `x-nextjs-stale-time` header overriding both when present.
|
|
36
|
+
setPrefetchStaleTimePolicy(staleTimeForEntryMs);
|
|
37
|
+
setRevalidationPrefetchDelay(300);
|
|
38
|
+
// Loading-shell window: a route's cached loading boundary (and the prefetch
|
|
39
|
+
// entry carrying it) reuses for the STATIC window even when its dynamic data
|
|
40
|
+
// is immediately stale — Next's split staleness model.
|
|
41
|
+
setShellStaleTimePolicy(staticStaleTimeMs);
|
|
42
|
+
setLoadingShellPredictionPolicy(predictNextLoadingShell);
|
|
43
|
+
// Per-segment (vary-params) client cache — Segment-M2 deliverable 3. Entries
|
|
44
|
+
// are keyed on the params a segment provably reads, so one entry serves every
|
|
45
|
+
// param value it does not depend on.
|
|
46
|
+
setSegmentCachePolicy(installSegmentCachePolicy());
|
|
47
|
+
setNavigationScrollAction(applyNextNavigationScroll);
|
|
48
|
+
setStylesheetReconciler(reconcileNextStylesheets);
|
|
49
|
+
// basePath boundary: a same-origin URL outside the configured basePath must
|
|
50
|
+
// hard-navigate (a `basePath:false` target), not soft-swap.
|
|
51
|
+
setSoftNavBoundary(isOutsideBasePath);
|
|
52
|
+
// NOTE: the standalone segment-prefetch wire runtime (segment-prefetch.ts) is
|
|
53
|
+
// deliberately NOT installed: its intent-time tree/body fetches fed caches
|
|
54
|
+
// nothing consumed and broke Next's request-count contracts (a hover inside a
|
|
55
|
+
// "no-requests" scope must not fetch). The router's own prefetch cache +
|
|
56
|
+
// loading-shell cache cover the segment semantics on the HTML-swap model.
|
|
57
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Client-side navigation compat, installed once per compat entry.
|
|
2
|
+
//
|
|
3
|
+
// FIRST PAINT holds only what a click can hit before the navigation runtime lands: Next's
|
|
4
|
+
// javascript:-URL block message (a static Link click reports it with no runtime at all) and
|
|
5
|
+
// `window.next.router`, which app code can call at any time. Everything else here is a POLICY the
|
|
6
|
+
// runtime alone reads, so it rides the runtime's own chunk via registerDeferredInstall and never costs
|
|
7
|
+
// a page that never navigates.
|
|
8
|
+
import {
|
|
9
|
+
prefetchRoute,
|
|
10
|
+
registerDeferredInstall,
|
|
11
|
+
setBlockedJavascriptUrlReporter,
|
|
12
|
+
softNavigate,
|
|
13
|
+
} from '../../api/router';
|
|
14
|
+
|
|
15
|
+
const BLOCKED_JS_URL_MESSAGE =
|
|
16
|
+
'Next.js has blocked a javascript: URL as a security precaution.';
|
|
17
|
+
|
|
18
|
+
export function installNavCompat() {
|
|
19
|
+
if (!process.browser && typeof window === 'undefined') return;
|
|
20
|
+
setBlockedJavascriptUrlReporter(() => console.error(BLOCKED_JS_URL_MESSAGE));
|
|
21
|
+
const windowWithReporter = window as typeof window & { __PNEXT_JS_URL_REPORTER__?: boolean };
|
|
22
|
+
if (!windowWithReporter.__PNEXT_JS_URL_REPORTER__) {
|
|
23
|
+
windowWithReporter.__PNEXT_JS_URL_REPORTER__ = true;
|
|
24
|
+
window.addEventListener('pnext:blocked-javascript-url', () => {
|
|
25
|
+
console.error(BLOCKED_JS_URL_MESSAGE);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
installNextRouterGlobal();
|
|
29
|
+
registerDeferredInstall(() => import('./nav-compat-runtime').then(m => m.installNavPolicies()));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// `window.next.router` — Next's own global handle. Pure-core apps have no such
|
|
33
|
+
// namespace, so it installs from compat rather than from the router hub.
|
|
34
|
+
function installNextRouterGlobal() {
|
|
35
|
+
window.next ??= {};
|
|
36
|
+
window.next.router = {
|
|
37
|
+
...window.next.router,
|
|
38
|
+
push: href => softNavigate(String(href)),
|
|
39
|
+
replace: href => softNavigate(String(href), { replace: true }),
|
|
40
|
+
prefetch: href => prefetchRoute(String(href), { strict: true }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { NavigationScrollAction, NavigationScrollOptions } from '../../api/router';
|
|
2
|
+
|
|
3
|
+
const RECT_PROPERTIES = ['bottom', 'height', 'left', 'right', 'top', 'width', 'x', 'y'] as const;
|
|
4
|
+
|
|
5
|
+
export type NextScrollPlan = 'hash' | 'preserve' | 'restore' | 'top' | 'top-then-target';
|
|
6
|
+
|
|
7
|
+
export function nextScrollPlan(
|
|
8
|
+
url: URL,
|
|
9
|
+
options: NavigationScrollOptions,
|
|
10
|
+
targetInViewport: boolean | undefined,
|
|
11
|
+
): NextScrollPlan {
|
|
12
|
+
if (options.pop) return 'restore';
|
|
13
|
+
if (options.scroll === false) return 'preserve';
|
|
14
|
+
if (url.hash) return 'hash';
|
|
15
|
+
if (targetInViewport === undefined) return 'top';
|
|
16
|
+
return targetInViewport ? 'preserve' : 'top-then-target';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function firstScrollCandidate<T>(
|
|
20
|
+
candidates: Iterable<T>,
|
|
21
|
+
shouldSkip: (candidate: T) => boolean,
|
|
22
|
+
): T | undefined {
|
|
23
|
+
for (const candidate of candidates) if (!shouldSkip(candidate)) return candidate;
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function deepestSingleChild<T>(node: T | undefined, onlyChild: (node: T) => T | undefined) {
|
|
28
|
+
while (node) {
|
|
29
|
+
const child = onlyChild(node);
|
|
30
|
+
if (!child) return node;
|
|
31
|
+
node = child;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function topOnlyScrollPosition(scrollX: number): [number, number] {
|
|
37
|
+
return [scrollX, 0];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const applyNextNavigationScroll: NavigationScrollAction = (url, options) => {
|
|
41
|
+
const target = scrollFocusTarget();
|
|
42
|
+
const plan = nextScrollPlan(
|
|
43
|
+
url,
|
|
44
|
+
options,
|
|
45
|
+
target ? targetTopInViewport(target, document.documentElement.clientHeight) : undefined,
|
|
46
|
+
);
|
|
47
|
+
if (plan === 'restore') {
|
|
48
|
+
const state = history.state as { __pnextScroll?: [number, number] } | null;
|
|
49
|
+
const [x, y] = state?.__pnextScroll ?? [0, 0];
|
|
50
|
+
window.scrollTo(x, y);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// React hoists a rendered <style precedence>/<link rel=stylesheet>/metadata element out of the segment
|
|
54
|
+
// and into <head>. Next resolves its scroll target with findDOMNode() on the changed segment, so when
|
|
55
|
+
// the segment's FIRST host element is one of those the walk starts inside <head>, skips every
|
|
56
|
+
// zero-rect sibling there and runs out - the navigation scrolls nowhere and focuses nothing. pnext
|
|
57
|
+
// renders those elements in place, so recognize the leading hoisted element and reproduce the same
|
|
58
|
+
// no-op. Placed after `restore` so history traversal still restores its saved position.
|
|
59
|
+
if (segmentLeadsWithHoistedElement()) return;
|
|
60
|
+
if (plan === 'hash') {
|
|
61
|
+
scrollToHash(url.hash);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (plan === 'preserve') {
|
|
65
|
+
if (target && options.scroll !== false) focusSegment(target);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (plan === 'top') {
|
|
69
|
+
window.scrollTo(...topOnlyScrollPosition(window.scrollX));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!target) return;
|
|
73
|
+
window.scrollTo(...topOnlyScrollPosition(window.scrollX));
|
|
74
|
+
if (!targetTopInViewport(target, document.documentElement.clientHeight)) target.scrollIntoView();
|
|
75
|
+
focusSegment(target);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function shouldSkipScrollElement(element: HTMLElement) {
|
|
79
|
+
const position = getComputedStyle(element).position;
|
|
80
|
+
if (position === 'sticky' || position === 'fixed') return true;
|
|
81
|
+
const rect = element.getBoundingClientRect();
|
|
82
|
+
return RECT_PROPERTIES.every(item => rect[item] === 0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function targetTopInViewport(element: HTMLElement, viewportHeight: number) {
|
|
86
|
+
const rects = element.getClientRects();
|
|
87
|
+
if (rects.length === 0) return false;
|
|
88
|
+
let top = Number.POSITIVE_INFINITY;
|
|
89
|
+
for (const rect of rects) top = Math.min(top, rect.top);
|
|
90
|
+
return top >= 0 && top <= viewportHeight;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function scrollFocusTarget(): HTMLElement | null {
|
|
94
|
+
const segment = changedSegmentRoot();
|
|
95
|
+
if (!segment) return null;
|
|
96
|
+
// Next resolves the target with findDOMNode() on the changed segment: the
|
|
97
|
+
// FIRST host element the segment rendered, never a descendant of it. The page
|
|
98
|
+
// container IS that segment, so its first element child is the target as-is.
|
|
99
|
+
// The fallback roots below wrap the segment in pnext's own chrome, so there
|
|
100
|
+
// the single-child chain is walked down to the content that segment rendered.
|
|
101
|
+
const first =
|
|
102
|
+
segment.id === PAGE_CONTAINER_ID
|
|
103
|
+
? (segment.firstElementChild ?? undefined)
|
|
104
|
+
: deepestSingleChild(segment.firstElementChild ?? undefined, node =>
|
|
105
|
+
node.nextElementSibling === null ? (node.firstElementChild ?? undefined) : undefined,
|
|
106
|
+
);
|
|
107
|
+
const nodes: HTMLElement[] = [];
|
|
108
|
+
for (let node = first; node; node = node.nextElementSibling ?? undefined) {
|
|
109
|
+
if (node instanceof HTMLElement) nodes.push(node);
|
|
110
|
+
}
|
|
111
|
+
return firstScrollCandidate(nodes, shouldSkipScrollElement) ?? null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The renderer's page-slot wrapper — the segment a navigation replaced. */
|
|
115
|
+
const PAGE_CONTAINER_ID = 'pnext-page';
|
|
116
|
+
|
|
117
|
+
// Elements React relocates into <head> when a component renders them.
|
|
118
|
+
const HOISTED_TAGS = new Set(['style', 'link', 'title', 'meta', 'base']);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Whether the changed segment's first rendered element is one React would have hoisted into <head> -
|
|
122
|
+
* the state in which Next's scroll walk never reaches the page content (see the call site).
|
|
123
|
+
*/
|
|
124
|
+
function segmentLeadsWithHoistedElement(): boolean {
|
|
125
|
+
const segment = changedSegmentRoot();
|
|
126
|
+
const first = segment?.firstElementChild;
|
|
127
|
+
return first !== null && first !== undefined && HOISTED_TAGS.has(first.localName);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function changedSegmentRoot(): HTMLElement | null {
|
|
131
|
+
const page = document.getElementById(PAGE_CONTAINER_ID);
|
|
132
|
+
if (page instanceof HTMLElement) return page;
|
|
133
|
+
const root = document.querySelector('[data-pnext-root]');
|
|
134
|
+
return root instanceof HTMLElement ? root : document.body;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function scrollToHash(hash: string) {
|
|
138
|
+
const id = decodeURIComponent(hash.slice(1));
|
|
139
|
+
const target = id === 'top' ? document.body : (document.getElementById(id) ?? namedTarget(id));
|
|
140
|
+
target?.scrollIntoView();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function namedTarget(name: string): HTMLElement | null {
|
|
144
|
+
const named = document.getElementsByName(name)[0];
|
|
145
|
+
return named instanceof HTMLElement ? named : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function focusSegment(target: HTMLElement) {
|
|
149
|
+
try {
|
|
150
|
+
target.focus({ preventScroll: true });
|
|
151
|
+
} catch {
|
|
152
|
+
// A swap can detach the target before focus runs.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Optimistic routing (Next's route-prediction trie) - COMPAT client policy.
|
|
2
|
+
//
|
|
3
|
+
// After a route pattern is learned from one URL (its prefetched/visited shell carries
|
|
4
|
+
// `window.__PNEXT_ROUTE__`), a navigation to a DIFFERENT URL matching the same pattern paints that
|
|
5
|
+
// cached loading shell instantly with the predicted params - no tree prefetch needed.
|
|
6
|
+
//
|
|
7
|
+
// Two guards keep predictions honest, both mirroring Next:
|
|
8
|
+
// - `staticChildren`: literal siblings of the dynamic level. A target whose segment names a static
|
|
9
|
+
// sibling must resolve on the server, since the sibling route wins over the dynamic pattern, so
|
|
10
|
+
// prediction bails.
|
|
11
|
+
// - rewrite detection: a shell whose OWN stored pathname does not match its route pattern was
|
|
12
|
+
// produced by a rewrite. Such a response must never seed predictions - the rewrite may map a
|
|
13
|
+
// sibling URL somewhere else entirely, or vary on search params.
|
|
14
|
+
|
|
15
|
+
import type { RouteParamValue } from '../../types';
|
|
16
|
+
|
|
17
|
+
interface RouteState {
|
|
18
|
+
// Bracket-free colon form: `:id` for a dynamic segment, `:slug*` for a
|
|
19
|
+
// catch-all; `catchAllOptional` distinguishes `[[...slug]]` (matches zero
|
|
20
|
+
// segments) from `[...slug]` (requires at least one).
|
|
21
|
+
route: string;
|
|
22
|
+
params: Record<string, RouteParamValue>;
|
|
23
|
+
catchAllOptional?: boolean;
|
|
24
|
+
staticChildren?: string[];
|
|
25
|
+
staticChildrenBySegment?: Record<string, string[]>;
|
|
26
|
+
prefetchKind?: 'shell' | 'eager';
|
|
27
|
+
/** `prefetch = 'allow-runtime'`: the shell is request-sampled PER URL. */
|
|
28
|
+
runtimePrefetch?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface PredictedShell {
|
|
32
|
+
html: string;
|
|
33
|
+
route: RouteState;
|
|
34
|
+
prefetch?: 'shell' | 'eager';
|
|
35
|
+
/**
|
|
36
|
+
* True when the predicted shell belongs to an `allow-runtime` route. Its content is derived from the
|
|
37
|
+
* sampled request's params, so a cached shell of ONE param value says nothing about another - the
|
|
38
|
+
* router must still prefetch every unseen URL of the route (a `partial` shell, being static, is shared).
|
|
39
|
+
*
|
|
40
|
+
* ...UNLESS `experimental.appShells` is on: there the runtime prefetch of a non-eager route renders
|
|
41
|
+
* with its `params` HANGING, so the cached shell IS the route's shared App Shell and says the same
|
|
42
|
+
* thing for every param. Revealing another link of the route then has nothing to fetch.
|
|
43
|
+
*/
|
|
44
|
+
runtimePrefetch?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** `experimental.appShells`, stamped into the document by register-render. */
|
|
48
|
+
function appShellsEnabled(): boolean {
|
|
49
|
+
if (!process.browser && typeof window === 'undefined') return false;
|
|
50
|
+
return (window as { __PNEXT_APP_SHELLS__?: boolean }).__PNEXT_APP_SHELLS__ === true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function predictNextLoadingShell(
|
|
54
|
+
url: URL,
|
|
55
|
+
shells: ReadonlyMap<string, string>,
|
|
56
|
+
): PredictedShell | undefined {
|
|
57
|
+
// Most recently stored shells first (Map preserves insertion order).
|
|
58
|
+
for (const [pathname, html] of [...shells.entries()].reverse()) {
|
|
59
|
+
const state = routeStateFromShell(html);
|
|
60
|
+
if (!state) continue;
|
|
61
|
+
// A shell's own URL must fit its route pattern before it can teach the
|
|
62
|
+
// route trie. This catches rewrites, including a dynamic response served
|
|
63
|
+
// for a known static sibling.
|
|
64
|
+
if (!matchRoute(state, pathname)) continue;
|
|
65
|
+
const params = matchRoute(state, url.pathname);
|
|
66
|
+
if (params) {
|
|
67
|
+
return {
|
|
68
|
+
html: reifyShellParams(html, state.params, params),
|
|
69
|
+
route: {
|
|
70
|
+
route: state.route,
|
|
71
|
+
params,
|
|
72
|
+
...(state.catchAllOptional ? { catchAllOptional: true } : {}),
|
|
73
|
+
},
|
|
74
|
+
...(state.prefetchKind ? { prefetch: state.prefetchKind } : {}),
|
|
75
|
+
...(state.runtimePrefetch && !appShellsEnabled() ? { runtimePrefetch: true } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function routeStateFromShell(html: string): RouteState | undefined {
|
|
83
|
+
const raw = /window\.__PNEXT_ROUTE__=(\{.*?\});<\/script>/.exec(html)?.[1];
|
|
84
|
+
if (!raw) return undefined;
|
|
85
|
+
try {
|
|
86
|
+
const state = JSON.parse(raw) as RouteState;
|
|
87
|
+
return typeof state.route === 'string' && state.params && typeof state.params === 'object'
|
|
88
|
+
? state
|
|
89
|
+
: undefined;
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function matchRoute(state: RouteState, pathname: string): Record<string, RouteParamValue> | undefined {
|
|
96
|
+
const pattern = state.route.split('/').filter(Boolean);
|
|
97
|
+
const target = pathname.split('/').filter(Boolean).map(decodeSegment);
|
|
98
|
+
const params: Record<string, RouteParamValue> = {};
|
|
99
|
+
let cursor = 0;
|
|
100
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
101
|
+
const segment = pattern[index]!;
|
|
102
|
+
// Colon form: `:slug*` is a catch-all, `:id` a single dynamic segment.
|
|
103
|
+
const catchAll = /^:([\w$]+)\*$/.exec(segment);
|
|
104
|
+
const dynamic = catchAll ? null : /^:([\w$]+)$/.exec(segment);
|
|
105
|
+
const siblingKey = String(index);
|
|
106
|
+
const siblings = state.staticChildrenBySegment?.[siblingKey] ??
|
|
107
|
+
(index === pattern.length - 1 ? state.staticChildren : undefined);
|
|
108
|
+
// A dynamic trie node is usable only after the server has said its static
|
|
109
|
+
// siblings are complete. This is the production-only null-vs-empty rule.
|
|
110
|
+
if ((dynamic || catchAll) && siblings === undefined) return undefined;
|
|
111
|
+
if (siblings?.includes(target[cursor] ?? '')) return undefined;
|
|
112
|
+
if (catchAll) {
|
|
113
|
+
const name = catchAll[1]!;
|
|
114
|
+
if (state.catchAllOptional) {
|
|
115
|
+
// `[[...slug]]`: matches the base path plus any nested tail.
|
|
116
|
+
const rest = target.slice(cursor);
|
|
117
|
+
if (rest.length > 0) params[name] = rest;
|
|
118
|
+
return params;
|
|
119
|
+
}
|
|
120
|
+
// `[...slug]`: a catch-all learned from a parallel slot must not predict a
|
|
121
|
+
// sibling URL for the primary route. Only reuse it when the source shell
|
|
122
|
+
// itself was rendered through that catch-all, and require ≥1 segment.
|
|
123
|
+
if (!Array.isArray(state.params[name])) return undefined;
|
|
124
|
+
const rest = target.slice(cursor);
|
|
125
|
+
if (rest.length === 0) return undefined;
|
|
126
|
+
params[name] = rest;
|
|
127
|
+
return params;
|
|
128
|
+
}
|
|
129
|
+
const value = target[cursor++];
|
|
130
|
+
if (value === undefined) return undefined;
|
|
131
|
+
if (dynamic) params[dynamic[1]!] = value;
|
|
132
|
+
else if (segment !== value) return undefined;
|
|
133
|
+
}
|
|
134
|
+
return cursor === target.length ? params : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function reifyShellParams(
|
|
138
|
+
html: string,
|
|
139
|
+
previous: Record<string, RouteParamValue>,
|
|
140
|
+
params: Record<string, RouteParamValue>,
|
|
141
|
+
): string {
|
|
142
|
+
if (typeof DOMParser === 'undefined') return html;
|
|
143
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
144
|
+
materializeClientMarkers(doc);
|
|
145
|
+
for (const root of doc.querySelectorAll<HTMLElement>('[data-pnext-params]')) {
|
|
146
|
+
const raw = root.getAttribute('data-pnext-params');
|
|
147
|
+
if (!raw) continue;
|
|
148
|
+
try {
|
|
149
|
+
const scoped = JSON.parse(raw) as Record<string, RouteParamValue>;
|
|
150
|
+
if (sameParams(scoped, previous)) root.setAttribute('data-pnext-params', JSON.stringify(params));
|
|
151
|
+
} catch {
|
|
152
|
+
// A malformed island scope cannot safely be reified.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return `<!doctype html>${doc.documentElement.outerHTML}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function materializeClientMarkers(doc: Document): void {
|
|
159
|
+
const comments: Comment[] = [];
|
|
160
|
+
const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_COMMENT);
|
|
161
|
+
while (walker.nextNode()) comments.push(walker.currentNode as Comment);
|
|
162
|
+
for (const start of comments.reverse()) {
|
|
163
|
+
const match = /^(pnext-client|pnext-page):([^>]*)$/.exec(start.data);
|
|
164
|
+
const kind = match?.[1];
|
|
165
|
+
const encoded = match?.[2];
|
|
166
|
+
if (!kind || !encoded || !start.parentNode) continue;
|
|
167
|
+
const container = doc.createElement('div');
|
|
168
|
+
const tag = kind === 'pnext-page' ? 'div' : 'pnext-client';
|
|
169
|
+
container.innerHTML = `<${tag} ${encoded}></${tag}>`;
|
|
170
|
+
const island = container.firstElementChild;
|
|
171
|
+
if (!island) continue;
|
|
172
|
+
let node = start.nextSibling;
|
|
173
|
+
while (node && !(node.nodeType === Node.COMMENT_NODE && node.nodeValue === `/${kind}`)) {
|
|
174
|
+
const next = node.nextSibling;
|
|
175
|
+
island.append(node);
|
|
176
|
+
node = next;
|
|
177
|
+
}
|
|
178
|
+
if (!node) continue;
|
|
179
|
+
start.replaceWith(island);
|
|
180
|
+
node.remove();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function sameParams(
|
|
185
|
+
left: Record<string, RouteParamValue>,
|
|
186
|
+
right: Record<string, RouteParamValue>,
|
|
187
|
+
): boolean {
|
|
188
|
+
const leftKeys = Object.keys(left);
|
|
189
|
+
const rightKeys = Object.keys(right);
|
|
190
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
191
|
+
return leftKeys.every(key => {
|
|
192
|
+
const a = left[key];
|
|
193
|
+
const b = right[key];
|
|
194
|
+
return Array.isArray(a)
|
|
195
|
+
? Array.isArray(b) && a.length === b.length && a.every((value, index) => value === b[index])
|
|
196
|
+
: a === b;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function decodeSegment(segment: string) {
|
|
201
|
+
try {
|
|
202
|
+
return decodeURIComponent(segment);
|
|
203
|
+
} catch {
|
|
204
|
+
return segment;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Client prefetch-cache policy (COMPAT - ships to the browser).
|
|
2
|
+
//
|
|
3
|
+
// Core's router owns a single HTML-swap prefetch cache with a flat TTL. Next's semantics split that
|
|
4
|
+
// window into a static staleTime and a dynamic staleTime, overridable through
|
|
5
|
+
// `experimental.staleTimes.{static,dynamic}`, and it seeds the cache from visited pages, uses low fetch
|
|
6
|
+
// priority for viewport prefetches, and skips prefetching entirely for bot user agents.
|
|
7
|
+
//
|
|
8
|
+
// The exact static/dynamic distinction Next draws is a property of the RSC segment payload. On the
|
|
9
|
+
// HTML-swap model the router can still learn a route's staleness from the `x-nextjs-stale-time`
|
|
10
|
+
// response header; when present it wins, otherwise the entry falls back to the configured dynamic
|
|
11
|
+
// window. This module is the single source of the windows plus the config override, read by the core
|
|
12
|
+
// router through a registered hook so core carries no next.config dependency.
|
|
13
|
+
//
|
|
14
|
+
// Injected into the client at build time as `window.__PNEXT_STALE_TIMES__`; undefined means Next defaults.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/** Next's built-in prefetch staleTimes (milliseconds). */
|
|
18
|
+
// Since Next 15 the DYNAMIC default is 0: dynamic data is never reused across
|
|
19
|
+
// navigations unless `experimental.staleTimes.dynamic` opts in. (The loading
|
|
20
|
+
// shell of a dynamic route still reuses for the STATIC window.)
|
|
21
|
+
export const DEFAULT_DYNAMIC_STALE_TIME_MS = 0;
|
|
22
|
+
export const DEFAULT_STATIC_STALE_TIME_MS = 300_000;
|
|
23
|
+
/** Next's runtime-prefetch stale threshold (RUNTIME_PREFETCH_DYNAMIC_STALE). */
|
|
24
|
+
const RUNTIME_DOCUMENT_STALE_TIME_MS = 30_000;
|
|
25
|
+
|
|
26
|
+
/** Shape injected by the server (seconds, mirroring next.config). */
|
|
27
|
+
interface StaleTimesConfig {
|
|
28
|
+
/** experimental.staleTimes.dynamic (seconds). */
|
|
29
|
+
dynamic?: number;
|
|
30
|
+
/** experimental.staleTimes.static (seconds). */
|
|
31
|
+
static?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare global {
|
|
35
|
+
interface Window {
|
|
36
|
+
__PNEXT_STALE_TIMES__?: StaleTimesConfig;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function configuredStaleTimes(): StaleTimesConfig {
|
|
41
|
+
if (!process.browser && typeof window === 'undefined') return {};
|
|
42
|
+
return window.__PNEXT_STALE_TIMES__ ?? {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function windowFromSeconds(value: number | undefined, fallbackMs: number): number {
|
|
46
|
+
// A configured 0 means "always fresh" (never reuse) — honour it exactly.
|
|
47
|
+
return typeof value === 'number' && Number.isFinite(value) ? value * 1000 : fallbackMs;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The dynamic prefetch window in ms (config override or 30s default). */
|
|
51
|
+
export function dynamicStaleTimeMs(): number {
|
|
52
|
+
return windowFromSeconds(configuredStaleTimes().dynamic, DEFAULT_DYNAMIC_STALE_TIME_MS);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The static prefetch window in ms (config override or 300s default). */
|
|
56
|
+
export function staticStaleTimeMs(): number {
|
|
57
|
+
return windowFromSeconds(configuredStaleTimes().static, DEFAULT_STATIC_STALE_TIME_MS);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The reuse window for a prefetched/visited entry. `prefetchFull` marks a `prefetch={true}` full-page
|
|
62
|
+
* prefetch, which Next reuses for the FULL static window even when the route itself is dynamic - it
|
|
63
|
+
* wins over the header. A route that reported an explicit `x-nextjs-stale-time` uses that value;
|
|
64
|
+
* otherwise the entry is treated as dynamic.
|
|
65
|
+
*
|
|
66
|
+
* `static`/`runtime` classify a per-segment entry (the cached static stage a navigation paints before
|
|
67
|
+
* its dynamic stage lands). A STATIC payload reports its own staleness and that value is authoritative:
|
|
68
|
+
* widening it to the configured static window keeps painting an expired static stage. A RUNTIME payload
|
|
69
|
+
* is request-sampled data, never static content: it reuses for the window its own short-lived cache
|
|
70
|
+
* reported, or the dynamic window when the response named none - never the static one.
|
|
71
|
+
*
|
|
72
|
+
* The header value is used RAW here, not through `getStaleTimeMs`: this cache backs the classic
|
|
73
|
+
* staleTimes semantics, where a configured `staleTimes.dynamic: 0` must mean "never reuse". The clamp
|
|
74
|
+
* applies only to the per-segment cache.
|
|
75
|
+
*/
|
|
76
|
+
export function staleTimeForEntryMs(options: {
|
|
77
|
+
headerStaleTimeSeconds?: number;
|
|
78
|
+
prefetchFull?: boolean;
|
|
79
|
+
static?: boolean;
|
|
80
|
+
runtime?: boolean;
|
|
81
|
+
runtimeDocument?: boolean;
|
|
82
|
+
}): number {
|
|
83
|
+
// A NAVIGATION document of an `allow-runtime` route: its header reports the route's PUBLIC cache
|
|
84
|
+
// window (the private/runtime caches resolve after the headers flushed), but the content is
|
|
85
|
+
// request-sampled. Next files no such entry from a navigation at all - the runtime-prefetch stream is
|
|
86
|
+
// what seeds its segment cache - so anything recorded from the document must not outlive the
|
|
87
|
+
// runtime-prefetch threshold: shorter-lived private caches are hung out of runtime samples entirely.
|
|
88
|
+
if (options.runtimeDocument) {
|
|
89
|
+
const headerMs =
|
|
90
|
+
typeof options.headerStaleTimeSeconds === 'number'
|
|
91
|
+
? options.headerStaleTimeSeconds * 1000
|
|
92
|
+
: Number.POSITIVE_INFINITY;
|
|
93
|
+
return Math.min(headerMs, RUNTIME_DOCUMENT_STALE_TIME_MS);
|
|
94
|
+
}
|
|
95
|
+
if ((options.static || options.runtime) && typeof options.headerStaleTimeSeconds === 'number') {
|
|
96
|
+
return options.headerStaleTimeSeconds * 1000;
|
|
97
|
+
}
|
|
98
|
+
if (options.runtime) return dynamicStaleTimeMs();
|
|
99
|
+
if (options.prefetchFull) {
|
|
100
|
+
return Math.max(
|
|
101
|
+
staticStaleTimeMs(),
|
|
102
|
+
typeof options.headerStaleTimeSeconds === 'number'
|
|
103
|
+
? options.headerStaleTimeSeconds * 1000
|
|
104
|
+
: 0,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (typeof options.headerStaleTimeSeconds === 'number') {
|
|
108
|
+
return options.headerStaleTimeSeconds * 1000;
|
|
109
|
+
}
|
|
110
|
+
return dynamicStaleTimeMs();
|
|
111
|
+
}
|