@wular/pnext 0.0.4 → 0.0.7
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 +22 -16
- package/bin/pnext +6 -1
- package/config/lint/base.js +7 -7
- package/config/ts/base.json +2 -4
- package/config/ts/react.json +2 -6
- package/package.json +23 -2
- package/reference/compat.md +87 -22
- package/reference/config.md +40 -93
- package/reference/css.md +42 -33
- package/reference/dev.md +26 -33
- package/reference/env.md +18 -20
- package/reference/getting-started.md +132 -0
- package/reference/metadata.md +31 -46
- package/reference/navigation.md +65 -105
- package/reference/performance.md +21 -142
- package/reference/rendering.md +78 -75
- package/reference/routing.md +97 -107
- package/reference/typegen.md +27 -29
- package/src/api/cache.ts +36 -37
- package/src/api/client-cache.ts +2 -2
- package/src/api/client-navigation.ts +121 -114
- package/src/api/dynamic.tsx +58 -55
- package/src/api/link.tsx +52 -53
- package/src/api/navigation.ts +59 -58
- package/src/api/server.ts +152 -144
- package/src/api/suspense.ts +4 -4
- package/src/cli/adapters/vercel-warm.ts +126 -121
- package/src/cli/adapters/vercel.ts +437 -443
- package/src/cli/analyze.ts +333 -144
- package/src/cli/{named-bin.ts → boot/named-bin.ts} +37 -37
- package/src/cli/{boot-trace.ts → boot/trace.ts} +10 -10
- package/src/cli/build.ts +976 -978
- package/src/cli/create.ts +72 -64
- package/src/cli/dev.ts +116 -116
- package/src/cli/index.ts +94 -85
- package/src/cli/migrate/package-json.ts +73 -71
- package/src/cli/migrate/report.ts +32 -33
- package/src/cli/migrate/{index.ts → run.ts} +48 -49
- package/src/cli/migrate/scan.ts +46 -46
- package/src/cli/migrate/spinner.ts +9 -9
- package/src/cli/migrate/tsconfig.ts +35 -35
- package/src/cli/{server-entry.ts → serve/entry.ts} +57 -57
- package/src/cli/{request-pipeline.ts → serve/pipeline.ts} +386 -394
- package/src/cli/{serve-ui.ts → serve/ui.ts} +47 -47
- package/src/cli/start.ts +65 -66
- package/src/{typegen.ts → cli/typegen.ts} +59 -59
- package/src/client/build.ts +802 -768
- package/src/client/chunk-fold.ts +245 -240
- package/src/client/{paths.ts → chunk-name.ts} +6 -6
- package/src/client/entry.ts +162 -147
- package/src/client/prebuilt.ts +231 -223
- package/src/client/profile.ts +29 -29
- package/src/client/react-compiler.ts +20 -15
- package/src/client/{compat-surface.ts → react-tier.ts} +64 -64
- package/src/client/reference-stub.ts +51 -51
- package/src/client/reference.ts +19 -19
- package/src/{api → client}/router/events.ts +32 -17
- package/src/{api → client}/router/history.ts +16 -16
- package/src/{api → client}/router/hub.ts +52 -53
- package/src/{api/router.ts → client/router/index.ts} +61 -60
- package/src/{api → client}/router/policies.ts +24 -24
- package/src/{api → client}/router/runtime.ts +2022 -1995
- package/src/{api → client}/router/types.ts +98 -98
- package/src/compat/actions/client-plugin.ts +42 -43
- package/src/compat/actions/client-stub.ts +14 -14
- package/src/compat/actions/{action-client.ts → client.ts} +194 -193
- package/src/compat/actions/config.ts +63 -64
- package/src/compat/actions/detect.ts +68 -68
- package/src/compat/actions/discovery.ts +105 -105
- package/src/compat/actions/{action-dispatch.ts → dispatch.ts} +277 -274
- package/src/compat/actions/early-submit.ts +1 -1
- package/src/compat/actions/endpoint.ts +198 -198
- package/src/compat/actions/extensions.ts +811 -0
- package/src/compat/actions/flight.ts +23 -23
- package/src/compat/actions/form-state.ts +34 -34
- package/src/compat/actions/hoist.ts +382 -236
- package/src/compat/actions/ids.ts +7 -12
- package/src/compat/actions/index.ts +8 -8
- package/src/compat/actions/instances.ts +46 -46
- package/src/compat/actions/origin.ts +47 -48
- package/src/compat/actions/protocol.ts +22 -22
- package/src/compat/actions/registry.ts +19 -19
- package/src/compat/{misc/action-return.ts → actions/return.ts} +60 -57
- package/src/compat/actions/rewrite.ts +125 -125
- package/src/compat/actions/{action-router.ts → router.ts} +10 -10
- package/src/compat/actions/serve.ts +132 -136
- package/src/compat/actions/server-tag.ts +4 -4
- package/src/compat/actions/{action-shared.ts → shared.ts} +34 -34
- package/src/compat/actions/unrecognized-error.ts +4 -4
- package/src/compat/{index.ts → aliases.ts} +112 -109
- package/src/compat/bundler/bun-externals.ts +18 -18
- package/src/compat/bundler/cjs-exports.ts +271 -223
- package/src/compat/bundler/config.ts +116 -111
- package/src/compat/bundler/externals.ts +12 -12
- package/src/compat/bundler/import-meta-url.ts +19 -19
- package/src/compat/bundler/modularize-imports.ts +36 -33
- package/src/compat/bundler/new-url-asset.ts +22 -24
- package/src/compat/bundler/optimize-package-imports.ts +111 -107
- package/src/compat/bundler/polyfill.ts +28 -28
- package/src/compat/bundler/react-compiler.ts +35 -29
- package/src/compat/bundler/react-profiler.tsx +11 -11
- package/src/compat/bundler/relay-transform.ts +48 -47
- package/src/compat/bundler/require-context.ts +119 -113
- package/src/compat/bundler/resolve-extensions.ts +19 -19
- package/src/compat/bundler/source-cache.ts +25 -25
- package/src/compat/bundler/static-imports.ts +7 -7
- package/src/compat/bundler/symlink-imports.ts +46 -46
- package/src/compat/bundler/tsconfig-paths.ts +13 -15
- package/src/compat/bundler/wasm.ts +58 -60
- package/src/compat/bundler/webpack-loaders.ts +254 -241
- package/src/compat/bundler/worker.ts +101 -104
- package/src/compat/cache/build-flags.ts +29 -29
- package/src/compat/cache/build-prerender-errors.ts +40 -44
- package/src/compat/cache/custom-handler.ts +60 -53
- package/src/compat/cache/fetch-patch.ts +240 -240
- package/src/compat/cache/handler.ts +27 -31
- package/src/compat/cache/modern-handler.ts +148 -126
- package/src/compat/cache/resume-data-cache.ts +47 -45
- package/src/compat/cache/revalidate.ts +233 -232
- package/src/compat/cache/runtime-error.ts +35 -35
- package/src/compat/cache/use-cache-transform.ts +442 -417
- package/src/compat/cache/use-cache.ts +650 -614
- package/src/compat/cache-control.ts +140 -142
- package/src/compat/client/base-path.ts +21 -20
- package/src/compat/client/css-order.ts +18 -18
- package/src/compat/client/errors/bare-boundary.ts +11 -11
- package/src/compat/client/errors/control-flow.ts +23 -23
- package/src/compat/client/errors/error-boundary.ts +61 -61
- package/src/compat/client/errors/global-error.ts +101 -91
- package/src/compat/client/errors/install.ts +71 -72
- package/src/compat/client/errors/lazy.ts +23 -23
- package/src/compat/client/errors/primitive-throw.ts +47 -45
- package/src/compat/client/errors/soft-refresh.ts +4 -4
- package/src/compat/client/link-status.ts +33 -33
- package/src/compat/client/{nav-compat-runtime.ts → nav-runtime.ts} +20 -20
- package/src/compat/client/{nav-compat.ts → nav.ts} +12 -13
- package/src/compat/client/navigation-scroll.ts +63 -63
- package/src/compat/client/optimistic-routing.ts +93 -88
- package/src/compat/client/prefetch-cache.ts +23 -24
- package/src/compat/client/route-announcer.ts +35 -35
- package/src/compat/client/segment-cache-policy.ts +20 -20
- package/src/compat/client/segment-cache.ts +309 -315
- package/src/compat/client/segment-prefetch.ts +127 -132
- package/src/compat/client/trailing-slash.ts +5 -4
- package/src/compat/css/chunking.ts +113 -116
- package/src/compat/css/inline-css.ts +21 -21
- package/src/compat/css/lightningcss.ts +37 -38
- package/src/compat/css/modules.ts +161 -175
- package/src/compat/css/nonce.ts +7 -7
- package/src/compat/css/sass-plugin.ts +18 -21
- package/src/compat/css/sass.ts +150 -152
- package/src/compat/css/styled-jsx-runtime.ts +27 -27
- package/src/compat/css/styled-jsx.ts +21 -21
- package/src/compat/edge-runtime.ts +27 -27
- package/src/compat/{adapter → export}/build-complete.ts +72 -75
- package/src/compat/export/client.ts +29 -31
- package/src/compat/export/{index.ts → emit.ts} +111 -110
- package/src/compat/export/standalone.ts +62 -54
- package/src/compat/image-optimizer/cache.ts +51 -49
- package/src/compat/image-optimizer/detect.ts +52 -52
- package/src/compat/image-optimizer/{index.ts → optimize.ts} +189 -194
- package/src/compat/image-optimizer/source.ts +77 -80
- package/src/compat/lifecycle/after-scope.ts +26 -26
- package/src/compat/lifecycle/after.ts +48 -45
- package/src/compat/lifecycle/error-funnel.ts +98 -102
- package/src/compat/lifecycle/error-serialize.ts +30 -26
- package/src/compat/lifecycle/error-ui.ts +67 -32
- package/src/compat/lifecycle/instrumentation-client.ts +36 -38
- package/src/compat/lifecycle/instrumentation.ts +85 -85
- package/src/compat/lifecycle/node-console.ts +11 -11
- package/src/compat/lifecycle/testmode.ts +160 -132
- package/src/compat/mdx/compile.ts +60 -57
- package/src/compat/mdx/plugin.ts +11 -11
- package/src/compat/mdx/{next-mdx-stub.ts → stub.ts} +5 -5
- package/src/compat/{metadata-route-artifacts.ts → metadata-artifacts.ts} +195 -191
- package/src/compat/metadata.ts +75 -81
- package/src/compat/next/cache.ts +68 -69
- package/src/compat/next/canonical-url.ts +9 -9
- package/src/compat/next/client-cache.ts +19 -19
- package/src/compat/next/client-navigation.ts +112 -116
- package/src/compat/next/client-only.ts +1 -1
- package/src/compat/next/client-script.tsx +99 -93
- package/src/compat/next/client-server.ts +10 -10
- package/src/compat/next/config-loader.ts +176 -173
- package/src/compat/next/config.ts +7 -7
- package/src/compat/next/constants.cjs +6 -6
- package/src/compat/next/constants.ts +6 -6
- package/src/compat/next/custom-server.ts +26 -24
- package/src/compat/next/dist/client/components/app-router-headers.ts +21 -21
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +3 -4
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -1
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -1
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -1
- package/src/compat/next/dynamic.tsx +21 -18
- package/src/compat/next/error.tsx +52 -52
- package/src/compat/next/font/cache.ts +74 -74
- package/src/compat/next/font/google.ts +2 -2
- package/src/compat/next/font/index.ts +1 -1
- package/src/compat/next/font/local.ts +3 -3
- package/src/compat/next/font/runtime-client.ts +17 -15
- package/src/compat/next/font/runtime.ts +443 -408
- package/src/compat/next/font/shared.ts +120 -108
- package/src/compat/next/form.tsx +63 -63
- package/src/compat/next/head.tsx +2 -2
- package/src/compat/next/headers.ts +103 -95
- package/src/compat/next/image/client.tsx +220 -0
- package/src/compat/next/image/config.ts +56 -58
- package/src/compat/next/image/optimizer.ts +40 -36
- package/src/compat/next/image/patterns.ts +37 -40
- package/src/compat/next/{image-props.ts → image/props.ts} +208 -202
- package/src/compat/next/image/shared.ts +65 -57
- package/src/compat/next/image/static-metadata.ts +98 -107
- package/src/compat/next/image/validate.ts +79 -88
- package/src/compat/next/image.tsx +19 -23
- package/src/compat/next/index.ts +1 -1
- package/src/compat/next/legacy-image.tsx +59 -60
- package/src/compat/next/{link-validation-transform.ts → link-transform.ts} +95 -96
- package/src/compat/next/link.tsx +158 -158
- package/src/compat/next/navigation.cjs +12 -3
- package/src/compat/next/navigation.ts +48 -50
- package/src/compat/next/offline.ts +27 -27
- package/src/compat/next/og.ts +121 -124
- package/src/compat/next/preferred-region.ts +13 -14
- package/src/compat/next/redirects.ts +58 -56
- package/src/compat/next/resource-hints.ts +73 -76
- package/src/compat/next/rewrites.ts +130 -133
- package/src/compat/next/root-params.ts +45 -45
- package/src/compat/next/{optimistic-route-state.ts → route-state.ts} +52 -52
- package/src/compat/next/router.cjs +4 -2
- package/src/compat/next/router.ts +58 -61
- package/src/compat/next/script.tsx +108 -108
- package/src/compat/next/server-only.ts +1 -1
- package/src/compat/next/server.ts +19 -19
- package/src/compat/next/svgr.ts +18 -17
- package/src/compat/next/telemetry.ts +24 -24
- package/src/compat/next/{image-usage.ts → usage.ts} +70 -39
- package/src/compat/next/user-agent.ts +53 -49
- package/src/compat/next/web-vitals.ts +22 -24
- package/src/compat/otel/api.ts +41 -41
- package/src/compat/otel/client-trace-metadata.ts +25 -27
- package/src/compat/otel/fetch-span.ts +29 -29
- package/src/compat/otel/tracer.ts +331 -331
- package/src/compat/pages/api.ts +456 -0
- package/src/compat/pages/client-plugin.ts +36 -36
- package/src/compat/pages/router-state.ts +34 -34
- package/src/compat/pages/{index.ts → router.ts} +130 -135
- package/src/compat/ppr/io.ts +12 -12
- package/src/compat/ppr/missing-root-params.ts +34 -38
- package/src/compat/ppr/root-params-scan.ts +66 -66
- package/src/compat/ppr/root-params-transform.ts +24 -26
- package/src/compat/ppr/root-params.ts +30 -30
- package/src/compat/ppr/segment-config-incompat.ts +6 -7
- package/src/compat/protocol.ts +71 -70
- package/src/compat/react/action-state.ts +47 -48
- package/src/compat/react/client-lite.ts +15 -15
- package/src/compat/react/client.ts +4 -4
- package/src/compat/react/compiler-runtime.ts +11 -11
- package/src/compat/react/dom-client.ts +44 -44
- package/src/compat/react/dom-react-server.ts +10 -16
- package/src/compat/react/dom-server.ts +10 -10
- package/src/compat/react/dom.ts +52 -52
- package/src/compat/react/hooks-extra.ts +34 -35
- package/src/compat/react/parity.ts +64 -61
- package/src/compat/react/preact.ts +81 -82
- package/src/compat/react/react-server.ts +28 -28
- package/src/compat/react/router-shim.ts +1 -1
- package/src/compat/react/server-component-use.ts +8 -8
- package/src/compat/react/server-inserted-html.ts +30 -31
- package/src/compat/react/server.ts +53 -55
- package/src/compat/react/use.ts +32 -32
- package/src/compat/react/view-transition.ts +20 -20
- package/src/compat/register/actions.ts +35 -824
- package/src/compat/register/boot.ts +41 -41
- package/src/compat/register/build-tier.ts +5 -5
- package/src/compat/register/build.ts +74 -70
- package/src/compat/register/bundler.ts +161 -156
- package/src/compat/register/cache.ts +20 -20
- package/src/compat/register/client-errors.ts +3 -3
- package/src/compat/register/config.ts +6 -6
- package/src/compat/register/css-extras.ts +30 -34
- package/src/compat/register/edge-runtime.ts +3 -3
- package/src/compat/register/errors.ts +10 -12
- package/src/compat/register/export.ts +16 -16
- package/src/compat/register/font.ts +11 -11
- package/src/compat/register/hooks.ts +2 -2
- package/src/compat/register/image.ts +40 -40
- package/src/compat/register/index.ts +59 -62
- package/src/compat/register/instrumentation-client.ts +10 -10
- package/src/compat/register/lifecycle.ts +25 -28
- package/src/compat/register/mdx.ts +10 -10
- package/src/compat/register/middleware.ts +226 -16
- package/src/compat/register/otel.ts +80 -88
- package/src/compat/register/pages-api.ts +10 -463
- package/src/compat/register/ppr.ts +16 -16
- package/src/compat/register/protocol.ts +17 -18
- package/src/compat/register/proxy.ts +49 -51
- package/src/compat/register/render.ts +79 -76
- package/src/compat/register/routing.ts +162 -159
- package/src/compat/register/segment.ts +24 -1897
- package/src/compat/register/static-image.ts +3 -3
- package/src/compat/register/{misc.ts → taint.ts} +9 -9
- package/src/compat/register/typed-routes.ts +14 -14
- package/src/compat/register/{usecache.ts → use-cache.ts} +39 -39
- package/src/compat/register/validation.ts +18 -18
- package/src/compat/segment/loading-boundary.ts +43 -45
- package/src/compat/segment/page-slot.ts +69 -69
- package/src/compat/segment/serve.ts +1884 -0
- package/src/compat/segment/tree.ts +113 -112
- package/src/compat/segment/vary-key.ts +38 -38
- package/src/compat/segment/vary-params.ts +138 -142
- package/src/compat/static-params.ts +14 -12
- package/src/compat/tsconfig-defaults.ts +87 -91
- package/src/compat/typecheck/{index.ts → check.ts} +234 -212
- package/src/compat/typecheck/worker.ts +15 -12
- package/src/compat/typed-routes/{index.ts → generate.ts} +36 -36
- package/src/compat/typed-routes/manifest.ts +174 -170
- package/src/compat/typed-routes/typegen.ts +127 -110
- package/src/compat/validation/errors.ts +16 -19
- package/src/compat/validation/prerender-diagnostics.ts +521 -504
- package/src/compat/validation/{index.ts → validate.ts} +647 -648
- package/src/compat-bootstrap.ts +16 -16
- package/src/config.ts +86 -69
- package/src/css/build.ts +230 -227
- package/src/css/postcss.ts +79 -80
- package/src/css/worker.ts +14 -15
- package/src/dev/client-actions.ts +10 -10
- package/src/dev/client-chunk-store.ts +27 -27
- package/src/dev/{client-key-cache.ts → restart/client-key.ts} +76 -76
- package/src/dev/{restart-cache.ts → restart/enabled.ts} +1 -1
- package/src/dev/{global-css-cache.ts → restart/global-css.ts} +91 -83
- package/src/dev/{node-module-bundle-cache.ts → restart/node-modules.ts} +24 -24
- package/src/dev/{route-bundle-key-cache.ts → restart/route-bundle-key.ts} +53 -53
- package/src/dev/{route-facts-cache.ts → restart/route-facts.ts} +82 -82
- package/src/dev/server.ts +804 -820
- package/src/env.ts +46 -43
- package/src/extensions.ts +491 -478
- package/src/index.ts +8 -8
- package/src/internal.ts +20 -23
- package/src/{islands → render}/boundary-error.ts +3 -3
- package/src/render/hooks.ts +71 -71
- package/src/render/island-context.ts +14 -14
- package/src/render/metadata.ts +310 -310
- package/src/{ppr-postpone.ts → render/postpone.ts} +5 -5
- package/src/{ppr.ts → render/ppr.ts} +244 -245
- package/src/render/renderer.ts +2087 -2082
- package/src/render/resource-hints.ts +16 -17
- package/src/render/slots.tsx +224 -235
- package/src/{islands → render}/static-children.ts +9 -12
- package/src/{islands → render}/static-slots.ts +37 -37
- package/src/{cache/context.ts → request/cache.ts} +20 -20
- package/src/request/context.ts +107 -107
- package/src/{dynamic/source.ts → resolve/dynamic.ts} +139 -139
- package/src/resolve/engine.ts +90 -77
- package/src/resolve/imports.ts +475 -463
- package/src/resolve/scan-facts.ts +318 -296
- package/src/resolve/source-text.ts +37 -37
- package/src/{dynamic → resolve}/tree-shake.ts +132 -128
- package/src/routing/forwarded.ts +19 -19
- package/src/routing/handler.ts +84 -91
- package/src/routing/href.ts +87 -70
- package/src/routing/{metadata.ts → metadata-files.ts} +403 -401
- package/src/{proxy.ts → routing/proxy.ts} +306 -312
- package/src/routing/{request-runtime.ts → request-environment.ts} +10 -10
- package/src/routing/routes.ts +827 -815
- package/src/routing/slots.ts +164 -160
- package/src/runtime/loader.ts +954 -0
- package/src/{dev → runtime}/module-cache.ts +309 -287
- package/src/{dev → runtime}/module-generations.ts +9 -9
- package/src/{dev → runtime}/module-transform.ts +81 -72
- package/src/{dev/imports.ts → runtime/modules.ts} +934 -847
- package/src/runtime/{server.ts → vendor-build.ts} +848 -1696
- package/src/runtime/vendor.ts +428 -406
- package/src/styles.d.ts +9 -0
- package/src/types.ts +324 -335
- package/src/utils/ansi.ts +5 -5
- package/src/utils/{source.ts → code.ts} +15 -12
- package/src/utils/content-type.ts +3 -3
- package/src/utils/decode.ts +2 -2
- package/src/utils/dev-profile.ts +13 -13
- package/src/utils/error-log.ts +6 -6
- package/src/utils/esbuild.ts +18 -18
- package/src/utils/fs-cache.ts +13 -13
- package/src/utils/fs.ts +57 -49
- package/src/utils/html.ts +20 -24
- package/src/utils/native-require.ts +8 -8
- package/src/utils/serialize.ts +139 -146
- package/src/utils/verbose.ts +18 -18
- package/reference/overview.md +0 -35
- package/src/cli/analyze-print.ts +0 -181
- package/src/compat/middleware/manifest.ts +0 -210
- package/src/compat/next/image-client.tsx +0 -215
- package/src/compat/next/link-usage.ts +0 -29
- package/src/css/index.ts +0 -2
- package/src/render/index.ts +0 -1
- package/src/style-modules.d.ts +0 -9
|
@@ -25,16 +25,16 @@ import {
|
|
|
25
25
|
segmentVaryCacheKey,
|
|
26
26
|
SEARCH_PARAMS_SENTINEL,
|
|
27
27
|
VARY_KEY_SEPARATOR,
|
|
28
|
-
} from '../segment/vary-key'
|
|
28
|
+
} from '../segment/vary-key'
|
|
29
29
|
import {
|
|
30
30
|
composeSegmentFrames,
|
|
31
31
|
pageSlotRange,
|
|
32
32
|
stripPageSlotContent,
|
|
33
33
|
stripStreamedContinuation,
|
|
34
|
-
} from '../segment/page-slot'
|
|
34
|
+
} from '../segment/page-slot'
|
|
35
35
|
|
|
36
36
|
/** LRU bound, matching the router's own segment caches. */
|
|
37
|
-
const SEGMENT_ENTRY_LIMIT = 64
|
|
37
|
+
const SEGMENT_ENTRY_LIMIT = 64
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* w9-segment-split: the two derived frames of a body response. The LAYOUT frame
|
|
@@ -44,96 +44,96 @@ const SEGMENT_ENTRY_LIMIT = 64;
|
|
|
44
44
|
* whose page did not re-fetches `/_layout` alone and composes it with the
|
|
45
45
|
* cached page frame.
|
|
46
46
|
*/
|
|
47
|
-
export const LAYOUT_FRAME_PATH = '/_layout'
|
|
48
|
-
export const PAGE_FRAME_PATH = '/_page'
|
|
47
|
+
export const LAYOUT_FRAME_PATH = '/_layout'
|
|
48
|
+
export const PAGE_FRAME_PATH = '/_page'
|
|
49
49
|
|
|
50
50
|
/** The body-segment request key the router sends (`/` is the legacy alias). */
|
|
51
|
-
export const BODY_SEGMENT_PATHS = ['/_index', '/', PAGE_FRAME_PATH] as const
|
|
51
|
+
export const BODY_SEGMENT_PATHS = ['/_index', '/', PAGE_FRAME_PATH] as const
|
|
52
52
|
|
|
53
53
|
export interface SegmentPayloadMeta {
|
|
54
54
|
/** Segment vary names (may include the `?` sentinel), or null when unknown. */
|
|
55
|
-
vary: string[] | null
|
|
55
|
+
vary: string[] | null
|
|
56
56
|
/**
|
|
57
57
|
* w9-segment-split: the LAYOUT frame's own vary set, when the server split
|
|
58
58
|
* the response. Null/absent means the response was not split and only the
|
|
59
59
|
* whole-document entry is filed.
|
|
60
60
|
*/
|
|
61
|
-
layoutVary?: string[] | null
|
|
61
|
+
layoutVary?: string[] | null
|
|
62
62
|
/** w9-segment-split: the PAGE frame's own vary set, when the server split. */
|
|
63
|
-
pageVary?: string[] | null
|
|
63
|
+
pageVary?: string[] | null
|
|
64
64
|
/** Matched route pattern in colon form, or null. */
|
|
65
|
-
route: string | null
|
|
65
|
+
route: string | null
|
|
66
66
|
/** Concrete params of the URL that produced the payload. */
|
|
67
|
-
params: Record<string, string | string[]
|
|
67
|
+
params: Record<string, string | string[]>
|
|
68
68
|
/**
|
|
69
69
|
* The route's shared APP SHELL, when the per-URL prerender carried one. Filed
|
|
70
70
|
* as its own param-independent entry so a navigation to another param of the
|
|
71
71
|
* route paints instantly.
|
|
72
72
|
*/
|
|
73
|
-
shell?: { html: string; route: string }
|
|
73
|
+
shell?: { html: string; route: string }
|
|
74
74
|
/**
|
|
75
75
|
* Literal siblings of each DYNAMIC level of the route, keyed by segment index
|
|
76
76
|
* (the server's `staticChildrenBySegment`). A pathname naming one of them is
|
|
77
77
|
* served by its own static route, so this payload must never key onto it.
|
|
78
78
|
*/
|
|
79
|
-
staticSiblings?: Record<string, string[]
|
|
79
|
+
staticSiblings?: Record<string, string[]>
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export interface SegmentRecordInput extends SegmentPayloadMeta {
|
|
83
83
|
/** Segment request path (`/_index`, `/_head`, …). */
|
|
84
|
-
segmentPath: string
|
|
84
|
+
segmentPath: string
|
|
85
85
|
/** Requested pathname (percent-encoding preserved). */
|
|
86
|
-
pathname: string
|
|
86
|
+
pathname: string
|
|
87
87
|
/** Requested query string (leading `?` optional). */
|
|
88
|
-
search: string
|
|
88
|
+
search: string
|
|
89
89
|
/** The payload the router would commit for this segment. */
|
|
90
|
-
html: string
|
|
90
|
+
html: string
|
|
91
91
|
/** Reuse window in milliseconds. */
|
|
92
|
-
staleTimeMs: number
|
|
92
|
+
staleTimeMs: number
|
|
93
93
|
/** True when the payload is a whole document, not a truncated shell. */
|
|
94
|
-
complete: boolean
|
|
94
|
+
complete: boolean
|
|
95
95
|
/** True when the bytes are a pure function of the URL (a prerender). */
|
|
96
|
-
static: boolean
|
|
96
|
+
static: boolean
|
|
97
97
|
/** True when the payload came from a runtime (request-sampled) prefetch. */
|
|
98
|
-
runtime: boolean
|
|
98
|
+
runtime: boolean
|
|
99
99
|
/** True when the route's catch-all is optional (`[[...slug]]`). */
|
|
100
|
-
catchAllOptional?: boolean
|
|
100
|
+
catchAllOptional?: boolean
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
interface SegmentEntry {
|
|
104
|
-
key: string
|
|
105
|
-
html: string
|
|
104
|
+
key: string
|
|
105
|
+
html: string
|
|
106
106
|
/** The URL this payload was fetched for, canonicalized (`_rsc` stripped). */
|
|
107
|
-
url: string
|
|
108
|
-
time: number
|
|
109
|
-
staleTimeMs: number
|
|
110
|
-
complete: boolean
|
|
111
|
-
static: boolean
|
|
112
|
-
runtime: boolean
|
|
107
|
+
url: string
|
|
108
|
+
time: number
|
|
109
|
+
staleTimeMs: number
|
|
110
|
+
complete: boolean
|
|
111
|
+
static: boolean
|
|
112
|
+
runtime: boolean
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
export interface SegmentHit {
|
|
116
|
-
html: string
|
|
116
|
+
html: string
|
|
117
117
|
/** True when this hit may commit a navigation with no network request. */
|
|
118
|
-
networkFree: boolean
|
|
118
|
+
networkFree: boolean
|
|
119
119
|
/**
|
|
120
120
|
* True when a PREFETCH of this URL has nothing left to fetch: the entry is fresh and its own vary set
|
|
121
121
|
* justifies serving this URL. Weaker than `networkFree` - a navigation may still need the dynamic
|
|
122
122
|
* remainder.
|
|
123
123
|
*/
|
|
124
|
-
prefetchSatisfied: boolean
|
|
125
|
-
runtime: boolean
|
|
124
|
+
prefetchSatisfied: boolean
|
|
125
|
+
runtime: boolean
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
interface LearnedRoute {
|
|
129
|
-
route: string
|
|
130
|
-
vary: string[]
|
|
131
|
-
catchAllOptional: boolean
|
|
129
|
+
route: string
|
|
130
|
+
vary: string[]
|
|
131
|
+
catchAllOptional: boolean
|
|
132
132
|
/** Literal siblings of the route's dynamic levels, by segment index. */
|
|
133
|
-
staticSiblings?: Record<string, string[]
|
|
133
|
+
staticSiblings?: Record<string, string[]>
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
const entries = new Map<string, SegmentEntry>()
|
|
136
|
+
const entries = new Map<string, SegmentEntry>()
|
|
137
137
|
/**
|
|
138
138
|
* Learned route vary sets, per segment path, keyed by route + vary set.
|
|
139
139
|
*
|
|
@@ -144,13 +144,13 @@ const entries = new Map<string, SegmentEntry>();
|
|
|
144
144
|
* overwriting each other; `candidateKeys` tries the most specific first.
|
|
145
145
|
* Insertion order = learn order.
|
|
146
146
|
*/
|
|
147
|
-
const learnedRoutes = new Map<string, Map<string, LearnedRoute>>()
|
|
147
|
+
const learnedRoutes = new Map<string, Map<string, LearnedRoute>>()
|
|
148
148
|
/** pathname -> the route pattern the SERVER resolved it to (sibling guard). */
|
|
149
|
-
const resolvedRoutes = new Map<string, string>()
|
|
149
|
+
const resolvedRoutes = new Map<string, string>()
|
|
150
150
|
|
|
151
151
|
/** Wall clock, overridable in tests (the e2e suites mock `Date`). */
|
|
152
152
|
function now(): number {
|
|
153
|
-
return Date.now()
|
|
153
|
+
return Date.now()
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// ---------------------------------------------------------------------------
|
|
@@ -158,21 +158,21 @@ function now(): number {
|
|
|
158
158
|
// ---------------------------------------------------------------------------
|
|
159
159
|
|
|
160
160
|
export function recordSegment(input: SegmentRecordInput): void {
|
|
161
|
-
recordOneSegment(input)
|
|
161
|
+
recordOneSegment(input)
|
|
162
162
|
// w9-segment-split: a body response the server split also files its two
|
|
163
163
|
// frames, each under its OWN vary set. Additive — the whole-document entry
|
|
164
164
|
// above is untouched, so nothing that hits today starts missing.
|
|
165
165
|
if (!BODY_SEGMENT_PATHS.includes(input.segmentPath as (typeof BODY_SEGMENT_PATHS)[number])) {
|
|
166
|
-
return
|
|
166
|
+
return
|
|
167
167
|
}
|
|
168
|
-
if (input.segmentPath === PAGE_FRAME_PATH) return
|
|
168
|
+
if (input.segmentPath === PAGE_FRAME_PATH) return
|
|
169
169
|
// The response's layout frame, filed under this URL's EXACT key. Independent of `layoutVary` - the
|
|
170
170
|
// server publishes that set only when it can prove what the layout read, but a frame keyed on the
|
|
171
171
|
// URL it was fetched for needs no such proof. BOTH layout-frame records are skipped when the payload
|
|
172
172
|
// carries no page slot: `stripPageSlotContent` would hand back the whole document, and a "layout
|
|
173
173
|
// frame" with no slot to splice into makes `needsPageFrameOnly` promise a composition that always
|
|
174
174
|
// fails - two serial round trips for one navigation.
|
|
175
|
-
const framable = pageSlotRange(input.html) !== null
|
|
175
|
+
const framable = pageSlotRange(input.html) !== null
|
|
176
176
|
if (framable) {
|
|
177
177
|
recordOneSegment({
|
|
178
178
|
...input,
|
|
@@ -184,7 +184,7 @@ export function recordSegment(input: SegmentRecordInput): void {
|
|
|
184
184
|
params: {},
|
|
185
185
|
vary: null,
|
|
186
186
|
complete: false,
|
|
187
|
-
})
|
|
187
|
+
})
|
|
188
188
|
}
|
|
189
189
|
if (framable && input.layoutVary) {
|
|
190
190
|
recordOneSegment({
|
|
@@ -195,10 +195,10 @@ export function recordSegment(input: SegmentRecordInput): void {
|
|
|
195
195
|
// A layout frame is never the whole page: it may paint, but a navigation
|
|
196
196
|
// still needs the page below it.
|
|
197
197
|
complete: false,
|
|
198
|
-
})
|
|
198
|
+
})
|
|
199
199
|
}
|
|
200
200
|
if (input.pageVary) {
|
|
201
|
-
recordOneSegment({ ...input, segmentPath: PAGE_FRAME_PATH, vary: input.pageVary })
|
|
201
|
+
recordOneSegment({ ...input, segmentPath: PAGE_FRAME_PATH, vary: input.pageVary })
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -215,23 +215,23 @@ export function recordSegment(input: SegmentRecordInput): void {
|
|
|
215
215
|
* and that shell IS shared across params by design.
|
|
216
216
|
*/
|
|
217
217
|
function isRewriteResponse(input: SegmentRecordInput): boolean {
|
|
218
|
-
if (!input.route) return false
|
|
218
|
+
if (!input.route) return false
|
|
219
219
|
// The catch-all tail is matched in its OPTIONAL form regardless of what the payload declared: a
|
|
220
220
|
// `[[...slug]]` index response does not always carry `catchAllOptional` on the wire, and reading
|
|
221
221
|
// its absence as "the URL is a segment short" would mis-file every optional-catchall index as a
|
|
222
222
|
// rewrite. The guard's bias is to accuse only what it can prove.
|
|
223
|
-
const derived = matchRoutePattern(input.route, input.pathname, true)
|
|
224
|
-
if (!derived) return true
|
|
223
|
+
const derived = matchRoutePattern(input.route, input.pathname, true)
|
|
224
|
+
if (!derived) return true
|
|
225
225
|
return Object.entries(input.params).some(([name, value]) => {
|
|
226
|
-
const own = derived[name]
|
|
227
|
-
if (own === undefined) return false
|
|
226
|
+
const own = derived[name]
|
|
227
|
+
if (own === undefined) return false
|
|
228
228
|
return Array.isArray(value) || Array.isArray(own)
|
|
229
229
|
? !Array.isArray(value) ||
|
|
230
230
|
!Array.isArray(own) ||
|
|
231
231
|
value.length !== own.length ||
|
|
232
232
|
value.some((part, index) => part !== own[index])
|
|
233
|
-
: value !== own
|
|
234
|
-
})
|
|
233
|
+
: value !== own
|
|
234
|
+
})
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
function recordOneSegment(rawInput: SegmentRecordInput): void {
|
|
@@ -239,48 +239,48 @@ function recordOneSegment(rawInput: SegmentRecordInput): void {
|
|
|
239
239
|
// available for the URL it was fetched for and is invisible to every other.
|
|
240
240
|
const input: SegmentRecordInput = isRewriteResponse(rawInput)
|
|
241
241
|
? { ...rawInput, route: null, vary: null }
|
|
242
|
-
: rawInput
|
|
242
|
+
: rawInput
|
|
243
243
|
if (rawInput.route) {
|
|
244
244
|
// LRU-bound like `entries`: one record per distinct pathname otherwise
|
|
245
245
|
// grows without limit (memory-pressure leak-slope test).
|
|
246
|
-
resolvedRoutes.delete(input.pathname)
|
|
246
|
+
resolvedRoutes.delete(input.pathname)
|
|
247
247
|
// The resolved-route record stays TRUE even for a rewrite: the server
|
|
248
248
|
// really did answer this pathname with that route, and the sibling guard
|
|
249
249
|
// in `candidateKeys` reads it to keep other routes off this URL.
|
|
250
|
-
resolvedRoutes.set(rawInput.pathname, rawInput.route)
|
|
250
|
+
resolvedRoutes.set(rawInput.pathname, rawInput.route)
|
|
251
251
|
while (resolvedRoutes.size > SEGMENT_ENTRY_LIMIT) {
|
|
252
|
-
const oldest = resolvedRoutes.keys().next().value
|
|
253
|
-
if (oldest === undefined) break
|
|
254
|
-
resolvedRoutes.delete(oldest)
|
|
252
|
+
const oldest = resolvedRoutes.keys().next().value
|
|
253
|
+
if (oldest === undefined) break
|
|
254
|
+
resolvedRoutes.delete(oldest)
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
const key = entryKey(input.segmentPath, input.route, input.params, input.search, input.vary, {
|
|
258
258
|
pathname: input.pathname,
|
|
259
|
-
})
|
|
259
|
+
})
|
|
260
260
|
if (input.route && input.vary !== null) {
|
|
261
|
-
let byRoute = learnedRoutes.get(input.segmentPath)
|
|
261
|
+
let byRoute = learnedRoutes.get(input.segmentPath)
|
|
262
262
|
if (!byRoute) {
|
|
263
|
-
byRoute = new Map<string, LearnedRoute>()
|
|
264
|
-
learnedRoutes.set(input.segmentPath, byRoute)
|
|
263
|
+
byRoute = new Map<string, LearnedRoute>()
|
|
264
|
+
learnedRoutes.set(input.segmentPath, byRoute)
|
|
265
265
|
}
|
|
266
|
-
const learnedKey = `${input.route}${VARY_KEY_SEPARATOR}${[...input.vary].sort().join(',')}
|
|
266
|
+
const learnedKey = `${input.route}${VARY_KEY_SEPARATOR}${[...input.vary].sort().join(',')}`
|
|
267
267
|
// The sibling set is a property of the ROUTE, not of one response: a payload
|
|
268
268
|
// that omits it (a framed segment truncated above the state script) must not
|
|
269
269
|
// un-learn what an earlier one published.
|
|
270
|
-
const staticSiblings = input.staticSiblings ?? byRoute.get(learnedKey)?.staticSiblings
|
|
270
|
+
const staticSiblings = input.staticSiblings ?? byRoute.get(learnedKey)?.staticSiblings
|
|
271
271
|
// Re-inserting moves the route to the end: most recently learned wins on
|
|
272
272
|
// lookup, mirroring the loading-shell prediction order.
|
|
273
|
-
byRoute.delete(learnedKey)
|
|
273
|
+
byRoute.delete(learnedKey)
|
|
274
274
|
byRoute.set(learnedKey, {
|
|
275
275
|
route: input.route,
|
|
276
276
|
vary: input.vary,
|
|
277
277
|
catchAllOptional: input.catchAllOptional === true,
|
|
278
278
|
...(staticSiblings ? { staticSiblings } : {}),
|
|
279
|
-
})
|
|
279
|
+
})
|
|
280
280
|
// LRU-bound like `entries`/`resolvedRoutes`: a route publishing a new vary
|
|
281
281
|
// set per prefetch (dynamic params in the vary set) would otherwise grow this
|
|
282
282
|
// map without limit (memory-pressure leak-slope test).
|
|
283
|
-
trimLearnedRoutes(byRoute)
|
|
283
|
+
trimLearnedRoutes(byRoute)
|
|
284
284
|
}
|
|
285
285
|
entries.set(key, {
|
|
286
286
|
key,
|
|
@@ -291,9 +291,9 @@ function recordOneSegment(rawInput: SegmentRecordInput): void {
|
|
|
291
291
|
complete: input.complete,
|
|
292
292
|
static: input.static,
|
|
293
293
|
runtime: input.runtime,
|
|
294
|
-
})
|
|
295
|
-
touch(key)
|
|
296
|
-
trim()
|
|
294
|
+
})
|
|
295
|
+
touch(key)
|
|
296
|
+
trim()
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
// ---------------------------------------------------------------------------
|
|
@@ -308,30 +308,30 @@ function recordOneSegment(rawInput: SegmentRecordInput): void {
|
|
|
308
308
|
* stage. Everything else is returned for painting only.
|
|
309
309
|
*/
|
|
310
310
|
export function takeSegment(options: {
|
|
311
|
-
pathname: string
|
|
312
|
-
search: string
|
|
313
|
-
segmentPath: string
|
|
311
|
+
pathname: string
|
|
312
|
+
search: string
|
|
313
|
+
segmentPath: string
|
|
314
314
|
}): SegmentHit | null {
|
|
315
|
-
const requestedUrl = options.pathname + normalizeSegmentSearch(options.search)
|
|
315
|
+
const requestedUrl = options.pathname + normalizeSegmentSearch(options.search)
|
|
316
316
|
const usable: {
|
|
317
|
-
candidate: { key: string; learnedKey?: string; params?: Record<string, string | string[]> }
|
|
318
|
-
entry: SegmentEntry
|
|
319
|
-
}[] = []
|
|
317
|
+
candidate: { key: string; learnedKey?: string; params?: Record<string, string | string[]> }
|
|
318
|
+
entry: SegmentEntry
|
|
319
|
+
}[] = []
|
|
320
320
|
for (const candidate of candidateKeys(options.segmentPath, options.pathname, options.search)) {
|
|
321
|
-
const entry = entries.get(candidate.key)
|
|
322
|
-
if (!entry) continue
|
|
321
|
+
const entry = entries.get(candidate.key)
|
|
322
|
+
if (!entry) continue
|
|
323
323
|
if (now() - entry.time >= entry.staleTimeMs) {
|
|
324
|
-
entries.delete(candidate.key)
|
|
325
|
-
continue
|
|
324
|
+
entries.delete(candidate.key)
|
|
325
|
+
continue
|
|
326
326
|
}
|
|
327
327
|
// A shared (different-URL) hit is sound for STATIC content - and for a request-sampled payload
|
|
328
328
|
// the SERVER's own vary set authorised: a non-exact candidate key exists only because a learned
|
|
329
329
|
// route published a vary set, and that set is the server's assertion of what the payload depends
|
|
330
330
|
// on. Everything else stays exact-URL: a request-sampled payload belongs to the URL it sampled.
|
|
331
|
-
const shared = entry.url !== requestedUrl
|
|
332
|
-
const varyAuthorised = candidate.learnedKey !== undefined
|
|
333
|
-
if (shared && !entry.static && !(entry.runtime && varyAuthorised)) continue
|
|
334
|
-
usable.push({ candidate, entry })
|
|
331
|
+
const shared = entry.url !== requestedUrl
|
|
332
|
+
const varyAuthorised = candidate.learnedKey !== undefined
|
|
333
|
+
if (shared && !entry.static && !(entry.runtime && varyAuthorised)) continue
|
|
334
|
+
usable.push({ candidate, entry })
|
|
335
335
|
}
|
|
336
336
|
// Candidate ORDER is specificity (exact URL first), but an INCOMPLETE first
|
|
337
337
|
// match yields to a RUNTIME prefetch of this exact URL: an `allow-runtime`
|
|
@@ -339,25 +339,26 @@ export function takeSegment(options: {
|
|
|
339
339
|
// its Suspense boundaries, while a document-derived entry for the same URL
|
|
340
340
|
// holds the fallbacks. A complete first match already covers everything a
|
|
341
341
|
// runtime sample could, so it keeps the established first-match order.
|
|
342
|
-
const first = usable[0]
|
|
342
|
+
const first = usable[0]
|
|
343
343
|
const chosen =
|
|
344
344
|
first && !first.entry.complete
|
|
345
345
|
? (usable.find(({ entry }) => entry.runtime && entry.url === requestedUrl) ?? first)
|
|
346
|
-
: first
|
|
346
|
+
: first
|
|
347
347
|
if (chosen) {
|
|
348
|
-
const { candidate, entry } = chosen
|
|
349
|
-
const key = candidate.key
|
|
350
|
-
const shared = entry.url !== requestedUrl
|
|
351
|
-
touch(key)
|
|
348
|
+
const { candidate, entry } = chosen
|
|
349
|
+
const key = candidate.key
|
|
350
|
+
const shared = entry.url !== requestedUrl
|
|
351
|
+
touch(key)
|
|
352
352
|
// A learned route that is still serving lookups is live: re-insert it so the
|
|
353
353
|
// LRU bound evicts the route patterns nobody navigates to.
|
|
354
|
-
if (candidate.learnedKey) touchLearnedRoute(options.segmentPath, candidate.learnedKey)
|
|
354
|
+
if (candidate.learnedKey) touchLearnedRoute(options.segmentPath, candidate.learnedKey)
|
|
355
355
|
return {
|
|
356
356
|
// A SHARED payload was rendered for a DIFFERENT URL of the same route, so the concrete params
|
|
357
357
|
// baked into its three param carriers belong to that URL. Re-stamp them for the URL being served -
|
|
358
358
|
// the client-side mirror of the server's `withRequestRouteParams`, and the same soundness rule:
|
|
359
359
|
// the vary set authorises sharing the CONTENT, never the param values.
|
|
360
|
-
html:
|
|
360
|
+
html:
|
|
361
|
+
shared && candidate.params ? restampSharedParams(entry.html, candidate.params) : entry.html,
|
|
361
362
|
// A COMPLETE + STATIC entry is network-free even when SHARED: a static prerender whose vary set
|
|
362
363
|
// does not contain the differing param is by construction byte-identical for this URL, so there
|
|
363
364
|
// is nothing left to fetch. Everything else stays exact-URL only: a shared SHELL (an App Shell
|
|
@@ -368,12 +369,11 @@ export function takeSegment(options: {
|
|
|
368
369
|
// An exact-URL hit always answers no. A SHARED hit answers no when the sharing was authorised -
|
|
369
370
|
// either the payload is static, or a published vary set let `candidateKeys` produce the shared
|
|
370
371
|
// match. The navigation that follows still fetches the dynamic remainder.
|
|
371
|
-
prefetchSatisfied:
|
|
372
|
-
!shared || (optimisticRoutingEnabled() && (entry.static || entry.runtime)),
|
|
372
|
+
prefetchSatisfied: !shared || (optimisticRoutingEnabled() && (entry.static || entry.runtime)),
|
|
373
373
|
runtime: entry.runtime,
|
|
374
|
-
}
|
|
374
|
+
}
|
|
375
375
|
}
|
|
376
|
-
return null
|
|
376
|
+
return null
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
/**
|
|
@@ -391,7 +391,7 @@ function optimisticRoutingEnabled(): boolean {
|
|
|
391
391
|
return (
|
|
392
392
|
(globalThis as { __PNEXT_NO_OPTIMISTIC_ROUTING__?: boolean })
|
|
393
393
|
.__PNEXT_NO_OPTIMISTIC_ROUTING__ !== true
|
|
394
|
-
)
|
|
394
|
+
)
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
/**
|
|
@@ -408,24 +408,24 @@ function optimisticRoutingEnabled(): boolean {
|
|
|
408
408
|
* and the App-Shell scheduler relies on re-fetching them.
|
|
409
409
|
*/
|
|
410
410
|
export function segmentPrefetchCovered(options: {
|
|
411
|
-
pathname: string
|
|
412
|
-
search: string
|
|
413
|
-
segmentPath: string
|
|
411
|
+
pathname: string
|
|
412
|
+
search: string
|
|
413
|
+
segmentPath: string
|
|
414
414
|
}): { html: string } | null {
|
|
415
|
-
const requestedUrl = options.pathname + normalizeSegmentSearch(options.search)
|
|
415
|
+
const requestedUrl = options.pathname + normalizeSegmentSearch(options.search)
|
|
416
416
|
for (const candidate of candidateKeys(options.segmentPath, options.pathname, options.search)) {
|
|
417
|
-
const entry = entries.get(candidate.key)
|
|
418
|
-
if (!entry) continue
|
|
419
|
-
if (now() - entry.time >= entry.staleTimeMs) continue
|
|
420
|
-
if (entry.runtime) continue
|
|
417
|
+
const entry = entries.get(candidate.key)
|
|
418
|
+
if (!entry) continue
|
|
419
|
+
if (now() - entry.time >= entry.staleTimeMs) continue
|
|
420
|
+
if (entry.runtime) continue
|
|
421
421
|
// An exact-URL hit is `takeSegment`'s business (it already dedupes there);
|
|
422
422
|
// this predicate exists for the SHARED case the vary set authorised.
|
|
423
|
-
if (entry.url === requestedUrl || candidate.learnedKey === undefined) continue
|
|
423
|
+
if (entry.url === requestedUrl || candidate.learnedKey === undefined) continue
|
|
424
424
|
return {
|
|
425
425
|
html: candidate.params ? restampSharedParams(entry.html, candidate.params) : entry.html,
|
|
426
|
-
}
|
|
426
|
+
}
|
|
427
427
|
}
|
|
428
|
-
return null
|
|
428
|
+
return null
|
|
429
429
|
}
|
|
430
430
|
|
|
431
431
|
// ---------------------------------------------------------------------------
|
|
@@ -439,14 +439,14 @@ export function segmentPrefetchCovered(options: {
|
|
|
439
439
|
* what a navigation PAINTS, and that is the half of the split that has not been
|
|
440
440
|
* validated end to end. `setSegmentSplitCommit(true)` opts in.
|
|
441
441
|
*/
|
|
442
|
-
let segmentSplitCommit = false
|
|
442
|
+
let segmentSplitCommit = false
|
|
443
443
|
|
|
444
444
|
export function setSegmentSplitCommit(enabled: boolean): void {
|
|
445
|
-
segmentSplitCommit = enabled
|
|
445
|
+
segmentSplitCommit = enabled
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
export function segmentSplitCommitEnabled(): boolean {
|
|
449
|
-
return segmentSplitCommit
|
|
449
|
+
return segmentSplitCommit
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
/**
|
|
@@ -456,23 +456,23 @@ export function segmentSplitCommitEnabled(): boolean {
|
|
|
456
456
|
* param) but hit the page frame, so only the layout has to come off the network.
|
|
457
457
|
*/
|
|
458
458
|
export function takeComposedSegment(options: {
|
|
459
|
-
pathname: string
|
|
460
|
-
search: string
|
|
459
|
+
pathname: string
|
|
460
|
+
search: string
|
|
461
461
|
}): SegmentHit | null {
|
|
462
|
-
if (!segmentSplitCommit) return null
|
|
463
|
-
const layout = takeSegment({ ...options, segmentPath: LAYOUT_FRAME_PATH })
|
|
464
|
-
if (!layout) return null
|
|
465
|
-
const page = takeSegment({ ...options, segmentPath: PAGE_FRAME_PATH })
|
|
466
|
-
if (!page) return null
|
|
467
|
-
const html = composeSegmentFrames(layout.html, page.html)
|
|
468
|
-
if (html === null) return null
|
|
462
|
+
if (!segmentSplitCommit) return null
|
|
463
|
+
const layout = takeSegment({ ...options, segmentPath: LAYOUT_FRAME_PATH })
|
|
464
|
+
if (!layout) return null
|
|
465
|
+
const page = takeSegment({ ...options, segmentPath: PAGE_FRAME_PATH })
|
|
466
|
+
if (!page) return null
|
|
467
|
+
const html = composeSegmentFrames(layout.html, page.html)
|
|
468
|
+
if (html === null) return null
|
|
469
469
|
return {
|
|
470
470
|
html,
|
|
471
471
|
// A composed document is only as committable as its weaker half.
|
|
472
472
|
networkFree: layout.networkFree && page.networkFree,
|
|
473
473
|
prefetchSatisfied: layout.prefetchSatisfied && page.prefetchSatisfied,
|
|
474
474
|
runtime: layout.runtime || page.runtime,
|
|
475
|
-
}
|
|
475
|
+
}
|
|
476
476
|
}
|
|
477
477
|
|
|
478
478
|
// ---------------------------------------------------------------------------
|
|
@@ -492,34 +492,34 @@ export function takeComposedSegment(options: {
|
|
|
492
492
|
* only when the caller says the destination is the current location.
|
|
493
493
|
*/
|
|
494
494
|
interface DocumentLayoutFrame {
|
|
495
|
-
html: string
|
|
496
|
-
time: number
|
|
497
|
-
staleTimeMs: number
|
|
495
|
+
html: string
|
|
496
|
+
time: number
|
|
497
|
+
staleTimeMs: number
|
|
498
498
|
}
|
|
499
|
-
const documentLayoutFrames = new Map<string, DocumentLayoutFrame>()
|
|
499
|
+
const documentLayoutFrames = new Map<string, DocumentLayoutFrame>()
|
|
500
500
|
/** Small on purpose: only the live URL's frame is ever read back. */
|
|
501
|
-
const DOCUMENT_LAYOUT_LIMIT = 8
|
|
501
|
+
const DOCUMENT_LAYOUT_LIMIT = 8
|
|
502
502
|
|
|
503
503
|
export function recordDocumentLayoutFrame(input: {
|
|
504
|
-
pathname: string
|
|
505
|
-
search: string
|
|
506
|
-
html: string
|
|
507
|
-
staleTimeMs: number
|
|
504
|
+
pathname: string
|
|
505
|
+
search: string
|
|
506
|
+
html: string
|
|
507
|
+
staleTimeMs: number
|
|
508
508
|
}): void {
|
|
509
509
|
// No page slot: `stripPageSlotContent` would return the whole document, and a
|
|
510
510
|
// composed document would then carry the page twice.
|
|
511
|
-
if (pageSlotRange(input.html) === null) return
|
|
512
|
-
const key = input.pathname + normalizeSegmentSearch(input.search)
|
|
513
|
-
documentLayoutFrames.delete(key)
|
|
511
|
+
if (pageSlotRange(input.html) === null) return
|
|
512
|
+
const key = input.pathname + normalizeSegmentSearch(input.search)
|
|
513
|
+
documentLayoutFrames.delete(key)
|
|
514
514
|
documentLayoutFrames.set(key, {
|
|
515
515
|
html: stripPageSlotContent(input.html),
|
|
516
516
|
time: now(),
|
|
517
517
|
staleTimeMs: input.staleTimeMs,
|
|
518
|
-
})
|
|
518
|
+
})
|
|
519
519
|
while (documentLayoutFrames.size > DOCUMENT_LAYOUT_LIMIT) {
|
|
520
|
-
const oldest = documentLayoutFrames.keys().next().value
|
|
521
|
-
if (oldest === undefined) break
|
|
522
|
-
documentLayoutFrames.delete(oldest)
|
|
520
|
+
const oldest = documentLayoutFrames.keys().next().value
|
|
521
|
+
if (oldest === undefined) break
|
|
522
|
+
documentLayoutFrames.delete(oldest)
|
|
523
523
|
}
|
|
524
524
|
}
|
|
525
525
|
|
|
@@ -529,24 +529,24 @@ export function recordDocumentLayoutFrame(input: {
|
|
|
529
529
|
* document's own frame.
|
|
530
530
|
*/
|
|
531
531
|
function cachedLayoutFrame(options: {
|
|
532
|
-
pathname: string
|
|
533
|
-
search: string
|
|
534
|
-
sameUrl: boolean
|
|
532
|
+
pathname: string
|
|
533
|
+
search: string
|
|
534
|
+
sameUrl: boolean
|
|
535
535
|
}): string | null {
|
|
536
536
|
const hit = takeSegment({
|
|
537
537
|
pathname: options.pathname,
|
|
538
538
|
search: options.search,
|
|
539
539
|
segmentPath: LAYOUT_FRAME_PATH,
|
|
540
|
-
})
|
|
540
|
+
})
|
|
541
541
|
// The continuation chunks are cut at CONSUMPTION time, so every source of a
|
|
542
542
|
// layout frame — the server's `/_layout` response, a frame derived from a
|
|
543
543
|
// prefetch, the live document's — is covered by one rule.
|
|
544
|
-
if (hit) return stripStreamedContinuation(hit.html)
|
|
545
|
-
if (!options.sameUrl) return null
|
|
546
|
-
const frame = documentLayoutFrames.get(options.pathname + normalizeSegmentSearch(options.search))
|
|
547
|
-
if (!frame) return null
|
|
548
|
-
if (now() - frame.time >= frame.staleTimeMs) return null
|
|
549
|
-
return stripStreamedContinuation(frame.html)
|
|
544
|
+
if (hit) return stripStreamedContinuation(hit.html)
|
|
545
|
+
if (!options.sameUrl) return null
|
|
546
|
+
const frame = documentLayoutFrames.get(options.pathname + normalizeSegmentSearch(options.search))
|
|
547
|
+
if (!frame) return null
|
|
548
|
+
if (now() - frame.time >= frame.staleTimeMs) return null
|
|
549
|
+
return stripStreamedContinuation(frame.html)
|
|
550
550
|
}
|
|
551
551
|
|
|
552
552
|
/**
|
|
@@ -556,11 +556,11 @@ function cachedLayoutFrame(options: {
|
|
|
556
556
|
* `composeCachedLayout`, and any failure there falls back to the whole-document fetch.
|
|
557
557
|
*/
|
|
558
558
|
export function needsPageFrameOnly(options: {
|
|
559
|
-
pathname: string
|
|
560
|
-
search: string
|
|
561
|
-
sameUrl: boolean
|
|
559
|
+
pathname: string
|
|
560
|
+
search: string
|
|
561
|
+
sameUrl: boolean
|
|
562
562
|
}): boolean {
|
|
563
|
-
return cachedLayoutFrame(options) !== null
|
|
563
|
+
return cachedLayoutFrame(options) !== null
|
|
564
564
|
}
|
|
565
565
|
|
|
566
566
|
/**
|
|
@@ -569,13 +569,13 @@ export function needsPageFrameOnly(options: {
|
|
|
569
569
|
* gate and the response) or the two cannot be proven to line up.
|
|
570
570
|
*/
|
|
571
571
|
export function composeCachedLayout(options: {
|
|
572
|
-
pathname: string
|
|
573
|
-
search: string
|
|
574
|
-
sameUrl: boolean
|
|
575
|
-
pageHtml: string
|
|
572
|
+
pathname: string
|
|
573
|
+
search: string
|
|
574
|
+
sameUrl: boolean
|
|
575
|
+
pageHtml: string
|
|
576
576
|
}): string | null {
|
|
577
|
-
const layout = cachedLayoutFrame(options)
|
|
578
|
-
return layout === null ? null : composeSegmentFrames(layout, options.pageHtml)
|
|
577
|
+
const layout = cachedLayoutFrame(options)
|
|
578
|
+
return layout === null ? null : composeSegmentFrames(layout, options.pageHtml)
|
|
579
579
|
}
|
|
580
580
|
|
|
581
581
|
/**
|
|
@@ -586,17 +586,17 @@ export function composeCachedLayout(options: {
|
|
|
586
586
|
* two cannot be proven to line up.
|
|
587
587
|
*/
|
|
588
588
|
export function composeFetchedLayoutFrame(options: {
|
|
589
|
-
pathname: string
|
|
590
|
-
search: string
|
|
591
|
-
layoutHtml: string
|
|
589
|
+
pathname: string
|
|
590
|
+
search: string
|
|
591
|
+
layoutHtml: string
|
|
592
592
|
}): string | null {
|
|
593
593
|
const page = takeSegment({
|
|
594
594
|
pathname: options.pathname,
|
|
595
595
|
search: options.search,
|
|
596
596
|
segmentPath: PAGE_FRAME_PATH,
|
|
597
|
-
})
|
|
598
|
-
if (!page) return null
|
|
599
|
-
return composeSegmentFrames(stripStreamedContinuation(options.layoutHtml), page.html)
|
|
597
|
+
})
|
|
598
|
+
if (!page) return null
|
|
599
|
+
return composeSegmentFrames(stripStreamedContinuation(options.layoutHtml), page.html)
|
|
600
600
|
}
|
|
601
601
|
|
|
602
602
|
/**
|
|
@@ -609,30 +609,24 @@ export function composeFetchedLayoutFrame(options: {
|
|
|
609
609
|
* params-dependent content sits behind a Suspense boundary and resolves in the dynamic stage), so once
|
|
610
610
|
* the page frame's vary set covers the URL the prefetch has nothing left to fetch.
|
|
611
611
|
*/
|
|
612
|
-
export function runtimePageFrameSatisfied(options: {
|
|
613
|
-
pathname
|
|
614
|
-
search: string;
|
|
615
|
-
}): boolean {
|
|
616
|
-
const requestedUrl = options.pathname + normalizeSegmentSearch(options.search);
|
|
612
|
+
export function runtimePageFrameSatisfied(options: { pathname: string; search: string }): boolean {
|
|
613
|
+
const requestedUrl = options.pathname + normalizeSegmentSearch(options.search)
|
|
617
614
|
for (const candidate of candidateKeys(PAGE_FRAME_PATH, options.pathname, options.search)) {
|
|
618
|
-
const entry = entries.get(candidate.key)
|
|
619
|
-
if (!entry?.runtime) continue
|
|
620
|
-
if (now() - entry.time >= entry.staleTimeMs) continue
|
|
615
|
+
const entry = entries.get(candidate.key)
|
|
616
|
+
if (!entry?.runtime) continue
|
|
617
|
+
if (now() - entry.time >= entry.staleTimeMs) continue
|
|
621
618
|
// SHARED coverage only: a URL the client fetched for itself keeps its own
|
|
622
619
|
// prefetch (a stale/incomplete entry of that URL must still refetch); this
|
|
623
620
|
// rule exists for the sibling param the vary set already covers.
|
|
624
|
-
if (entry.url === requestedUrl || candidate.learnedKey === undefined) continue
|
|
625
|
-
return true
|
|
621
|
+
if (entry.url === requestedUrl || candidate.learnedKey === undefined) continue
|
|
622
|
+
return true
|
|
626
623
|
}
|
|
627
|
-
return false
|
|
624
|
+
return false
|
|
628
625
|
}
|
|
629
626
|
|
|
630
|
-
export function needsLayoutFrameOnly(options: {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}): boolean {
|
|
634
|
-
if (takeSegment({ ...options, segmentPath: LAYOUT_FRAME_PATH })) return false;
|
|
635
|
-
return takeSegment({ ...options, segmentPath: PAGE_FRAME_PATH }) !== null;
|
|
627
|
+
export function needsLayoutFrameOnly(options: { pathname: string; search: string }): boolean {
|
|
628
|
+
if (takeSegment({ ...options, segmentPath: LAYOUT_FRAME_PATH })) return false
|
|
629
|
+
return takeSegment({ ...options, segmentPath: PAGE_FRAME_PATH }) !== null
|
|
636
630
|
}
|
|
637
631
|
|
|
638
632
|
/**
|
|
@@ -650,29 +644,29 @@ function candidateKeys(
|
|
|
650
644
|
): { key: string; learnedKey?: string; params?: Record<string, string | string[]> }[] {
|
|
651
645
|
const keys: { key: string; learnedKey?: string; params?: Record<string, string | string[]> }[] = [
|
|
652
646
|
{ key: entryKey(segmentPath, null, {}, search, null, { pathname }) },
|
|
653
|
-
]
|
|
654
|
-
const byRoute = learnedRoutes.get(segmentPath)
|
|
655
|
-
if (!byRoute) return keys
|
|
656
|
-
const ownRoute = resolvedRoutes.get(pathname)
|
|
647
|
+
]
|
|
648
|
+
const byRoute = learnedRoutes.get(segmentPath)
|
|
649
|
+
if (!byRoute) return keys
|
|
650
|
+
const ownRoute = resolvedRoutes.get(pathname)
|
|
657
651
|
const learned = [...byRoute.entries()]
|
|
658
652
|
.reverse()
|
|
659
653
|
.filter(([, candidate]) => ownRoute === undefined || candidate.route === ownRoute)
|
|
660
654
|
// Stable sort: within one specificity the most recently learned wins.
|
|
661
|
-
.sort(([, a], [, b]) => b.vary.length - a.vary.length)
|
|
655
|
+
.sort(([, a], [, b]) => b.vary.length - a.vary.length)
|
|
662
656
|
for (const [learnedKey, candidate] of learned) {
|
|
663
|
-
const params = matchRoutePattern(candidate.route, pathname, candidate.catchAllOptional)
|
|
664
|
-
if (!params) continue
|
|
657
|
+
const params = matchRoutePattern(candidate.route, pathname, candidate.catchAllOptional)
|
|
658
|
+
if (!params) continue
|
|
665
659
|
// The pattern matches, but the segment it matched is a literal the server
|
|
666
660
|
// published as a STATIC SIBLING of this dynamic level: that sibling's own
|
|
667
661
|
// route serves the URL, so the dynamic route's shared bytes are not its.
|
|
668
|
-
if (namesStaticSibling(pathname, candidate.staticSiblings)) continue
|
|
662
|
+
if (namesStaticSibling(pathname, candidate.staticSiblings)) continue
|
|
669
663
|
keys.push({
|
|
670
664
|
key: entryKey(segmentPath, candidate.route, params, search, candidate.vary, { pathname }),
|
|
671
665
|
learnedKey,
|
|
672
666
|
params,
|
|
673
|
-
})
|
|
667
|
+
})
|
|
674
668
|
}
|
|
675
|
-
return keys
|
|
669
|
+
return keys
|
|
676
670
|
}
|
|
677
671
|
|
|
678
672
|
function entryKey(
|
|
@@ -690,7 +684,7 @@ function entryKey(
|
|
|
690
684
|
search,
|
|
691
685
|
vary: route ? vary : null,
|
|
692
686
|
pathname: context.pathname,
|
|
693
|
-
})
|
|
687
|
+
})
|
|
694
688
|
}
|
|
695
689
|
|
|
696
690
|
// ---------------------------------------------------------------------------
|
|
@@ -707,25 +701,25 @@ export function matchRoutePattern(
|
|
|
707
701
|
pathname: string,
|
|
708
702
|
optionalCatchAll = false,
|
|
709
703
|
): Record<string, string | string[]> | null {
|
|
710
|
-
const patternParts = pattern.split('/').filter(Boolean)
|
|
711
|
-
const pathParts = pathname.split('/').filter(Boolean)
|
|
712
|
-
const params: Record<string, string | string[]> = {}
|
|
713
|
-
let cursor = 0
|
|
704
|
+
const patternParts = pattern.split('/').filter(Boolean)
|
|
705
|
+
const pathParts = pathname.split('/').filter(Boolean)
|
|
706
|
+
const params: Record<string, string | string[]> = {}
|
|
707
|
+
let cursor = 0
|
|
714
708
|
for (const part of patternParts) {
|
|
715
|
-
const catchAll = /^:([\w$]+)\*$/.exec(part)
|
|
709
|
+
const catchAll = /^:([\w$]+)\*$/.exec(part)
|
|
716
710
|
if (catchAll) {
|
|
717
|
-
const rest = pathParts.slice(cursor).map(decodeSegment)
|
|
718
|
-
if (rest.length === 0 && !optionalCatchAll) return null
|
|
719
|
-
if (rest.length > 0) params[catchAll[1]!] = rest
|
|
720
|
-
return params
|
|
711
|
+
const rest = pathParts.slice(cursor).map(decodeSegment)
|
|
712
|
+
if (rest.length === 0 && !optionalCatchAll) return null
|
|
713
|
+
if (rest.length > 0) params[catchAll[1]!] = rest
|
|
714
|
+
return params
|
|
721
715
|
}
|
|
722
|
-
const dynamic = /^:([\w$]+)$/.exec(part)
|
|
723
|
-
const value = pathParts[cursor++]
|
|
724
|
-
if (value === undefined) return null
|
|
725
|
-
if (dynamic) params[dynamic[1]!] = decodeSegment(value)
|
|
726
|
-
else if (part !== value) return null
|
|
716
|
+
const dynamic = /^:([\w$]+)$/.exec(part)
|
|
717
|
+
const value = pathParts[cursor++]
|
|
718
|
+
if (value === undefined) return null
|
|
719
|
+
if (dynamic) params[dynamic[1]!] = decodeSegment(value)
|
|
720
|
+
else if (part !== value) return null
|
|
727
721
|
}
|
|
728
|
-
return cursor === pathParts.length ? params : null
|
|
722
|
+
return cursor === pathParts.length ? params : null
|
|
729
723
|
}
|
|
730
724
|
|
|
731
725
|
// ---------------------------------------------------------------------------
|
|
@@ -733,8 +727,8 @@ export function matchRoutePattern(
|
|
|
733
727
|
// ---------------------------------------------------------------------------
|
|
734
728
|
|
|
735
729
|
/** The island prop placeholder the server fills per request (slots.tsx). */
|
|
736
|
-
const PROMISE_MARKER_KEY = '__pnextPromise'
|
|
737
|
-
const ISLAND_PARAMS_ATTRIBUTE = 'data-pnext-params'
|
|
730
|
+
const PROMISE_MARKER_KEY = '__pnextPromise'
|
|
731
|
+
const ISLAND_PARAMS_ATTRIBUTE = 'data-pnext-params'
|
|
738
732
|
|
|
739
733
|
/**
|
|
740
734
|
* Rewrite every param carrier in a payload rendered for a SIBLING URL of the
|
|
@@ -753,40 +747,40 @@ const ISLAND_PARAMS_ATTRIBUTE = 'data-pnext-params';
|
|
|
753
747
|
* content that merely happens to contain the same string is untouched.
|
|
754
748
|
*/
|
|
755
749
|
function restampSharedParams(html: string, params: Record<string, string | string[]>): string {
|
|
756
|
-
const match = /(window\.__PNEXT_ROUTE__=)(\{.*?\})(;?<\/script>)/s.exec(html)
|
|
757
|
-
if (!match?.[2]) return html
|
|
758
|
-
let state: Record<string, unknown
|
|
750
|
+
const match = /(window\.__PNEXT_ROUTE__=)(\{.*?\})(;?<\/script>)/s.exec(html)
|
|
751
|
+
if (!match?.[2]) return html
|
|
752
|
+
let state: Record<string, unknown>
|
|
759
753
|
try {
|
|
760
|
-
state = JSON.parse(match[2]) as Record<string, unknown
|
|
754
|
+
state = JSON.parse(match[2]) as Record<string, unknown>
|
|
761
755
|
} catch {
|
|
762
756
|
// A route script we cannot parse cannot be re-stamped safely.
|
|
763
|
-
return html
|
|
757
|
+
return html
|
|
764
758
|
}
|
|
765
|
-
const baked = (state.params ?? {}) as Record<string, string | string[]
|
|
766
|
-
const bakedJson = paramsJson(baked)
|
|
767
|
-
const targetJson = paramsJson(params)
|
|
768
|
-
if (bakedJson === targetJson) return html
|
|
759
|
+
const baked = (state.params ?? {}) as Record<string, string | string[]>
|
|
760
|
+
const bakedJson = paramsJson(baked)
|
|
761
|
+
const targetJson = paramsJson(params)
|
|
762
|
+
if (bakedJson === targetJson) return html
|
|
769
763
|
const rewritten =
|
|
770
764
|
html.slice(0, match.index) +
|
|
771
765
|
`${match[1]}${paramsJson({ ...state, params })}${match[3]}` +
|
|
772
|
-
html.slice(match.index + match[0].length)
|
|
766
|
+
html.slice(match.index + match[0].length)
|
|
773
767
|
const replacements: [string, string][] = [
|
|
774
768
|
[islandParamsAttribute(bakedJson), islandParamsAttribute(targetJson)],
|
|
775
769
|
[promiseMarkerJson(bakedJson), promiseMarkerJson(targetJson)],
|
|
776
|
-
]
|
|
770
|
+
]
|
|
777
771
|
return rewriteIslandMarkup(rewritten, text => {
|
|
778
|
-
let next = text
|
|
772
|
+
let next = text
|
|
779
773
|
for (const [from, to] of replacements) {
|
|
780
|
-
next = next.split(from).join(to).split(escapeAttribute(from)).join(escapeAttribute(to))
|
|
774
|
+
next = next.split(from).join(to).split(escapeAttribute(from)).join(escapeAttribute(to))
|
|
781
775
|
}
|
|
782
|
-
return next
|
|
783
|
-
})
|
|
776
|
+
return next
|
|
777
|
+
})
|
|
784
778
|
}
|
|
785
779
|
|
|
786
780
|
const islandParamsAttribute = (json: string) =>
|
|
787
|
-
`${ISLAND_PARAMS_ATTRIBUTE}="${json.replaceAll('"', '"')}"
|
|
788
|
-
const promiseMarkerJson = (json: string) => `{"${PROMISE_MARKER_KEY}":${json}}
|
|
789
|
-
const escapeAttribute = (json: string) => json.replaceAll('"', '"')
|
|
781
|
+
`${ISLAND_PARAMS_ATTRIBUTE}="${json.replaceAll('"', '"')}"`
|
|
782
|
+
const promiseMarkerJson = (json: string) => `{"${PROMISE_MARKER_KEY}":${json}}`
|
|
783
|
+
const escapeAttribute = (json: string) => json.replaceAll('"', '"')
|
|
790
784
|
|
|
791
785
|
/** `serializeProps`' output shape, mirrored so the strings compare byte-wise. */
|
|
792
786
|
const JSON_ESCAPES: Record<string, string> = {
|
|
@@ -795,10 +789,10 @@ const JSON_ESCAPES: Record<string, string> = {
|
|
|
795
789
|
'&': '\\u0026',
|
|
796
790
|
'\u2028': '\\u2028',
|
|
797
791
|
'\u2029': '\\u2029',
|
|
798
|
-
}
|
|
792
|
+
}
|
|
799
793
|
|
|
800
794
|
function paramsJson(value: unknown): string {
|
|
801
|
-
return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, char => JSON_ESCAPES[char] ?? char)
|
|
795
|
+
return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, char => JSON_ESCAPES[char] ?? char)
|
|
802
796
|
}
|
|
803
797
|
|
|
804
798
|
/**
|
|
@@ -810,21 +804,21 @@ function rewriteIslandMarkup(html: string, rewrite: (text: string) => string): s
|
|
|
810
804
|
return rewrite(html).replace(
|
|
811
805
|
/<!--pnext-(client|client-after|page):([^>]*)-->/g,
|
|
812
806
|
(marker, kind: string, encoded: string) => {
|
|
813
|
-
const decoded = encoded.replaceAll('<', '<').replaceAll('>', '>')
|
|
814
|
-
const next = rewrite(decoded)
|
|
807
|
+
const decoded = encoded.replaceAll('<', '<').replaceAll('>', '>')
|
|
808
|
+
const next = rewrite(decoded)
|
|
815
809
|
return next === decoded
|
|
816
810
|
? marker
|
|
817
|
-
: `<!--pnext-${kind}:${next.replaceAll('<', '<').replaceAll('>', '>')}
|
|
811
|
+
: `<!--pnext-${kind}:${next.replaceAll('<', '<').replaceAll('>', '>')}-->`
|
|
818
812
|
},
|
|
819
|
-
)
|
|
813
|
+
)
|
|
820
814
|
}
|
|
821
815
|
|
|
822
816
|
function decodeSegment(segment: string): string {
|
|
823
817
|
try {
|
|
824
|
-
return decodeURIComponent(segment)
|
|
818
|
+
return decodeURIComponent(segment)
|
|
825
819
|
} catch {
|
|
826
820
|
// A malformed escape is the route-cache identity as written; keep it.
|
|
827
|
-
return segment
|
|
821
|
+
return segment
|
|
828
822
|
}
|
|
829
823
|
}
|
|
830
824
|
|
|
@@ -840,57 +834,57 @@ function decodeSegment(segment: string): string {
|
|
|
840
834
|
export function evictPageSegments(): void {
|
|
841
835
|
for (const key of [...entries.keys()]) {
|
|
842
836
|
if (BODY_SEGMENT_PATHS.some(path => key.startsWith(path + VARY_KEY_SEPARATOR))) {
|
|
843
|
-
entries.delete(key)
|
|
837
|
+
entries.delete(key)
|
|
844
838
|
}
|
|
845
839
|
}
|
|
846
840
|
// The vary sets learned for those page segments describe payloads that are
|
|
847
841
|
// gone; keeping them would leak one map per route across refreshes.
|
|
848
|
-
for (const path of BODY_SEGMENT_PATHS) learnedRoutes.delete(path)
|
|
842
|
+
for (const path of BODY_SEGMENT_PATHS) learnedRoutes.delete(path)
|
|
849
843
|
}
|
|
850
844
|
|
|
851
845
|
/** Drop every entry (a revalidation invalidates the whole segment cache). */
|
|
852
846
|
export function clearSegmentCache(): void {
|
|
853
|
-
entries.clear()
|
|
854
|
-
learnedRoutes.clear()
|
|
855
|
-
resolvedRoutes.clear()
|
|
847
|
+
entries.clear()
|
|
848
|
+
learnedRoutes.clear()
|
|
849
|
+
resolvedRoutes.clear()
|
|
856
850
|
// A revalidation invalidates the layouts too: a `/_page`-only navigation
|
|
857
851
|
// after one would keep painting the pre-revalidation layout chain.
|
|
858
|
-
documentLayoutFrames.clear()
|
|
852
|
+
documentLayoutFrames.clear()
|
|
859
853
|
}
|
|
860
854
|
|
|
861
855
|
/** Test seam: the number of live entries. */
|
|
862
856
|
export function segmentEntryCount(): number {
|
|
863
|
-
return entries.size
|
|
857
|
+
return entries.size
|
|
864
858
|
}
|
|
865
859
|
|
|
866
860
|
function touch(key: string): void {
|
|
867
|
-
const entry = entries.get(key)
|
|
868
|
-
if (!entry) return
|
|
869
|
-
entries.delete(key)
|
|
870
|
-
entries.set(key, entry)
|
|
861
|
+
const entry = entries.get(key)
|
|
862
|
+
if (!entry) return
|
|
863
|
+
entries.delete(key)
|
|
864
|
+
entries.set(key, entry)
|
|
871
865
|
}
|
|
872
866
|
|
|
873
867
|
function trim(): void {
|
|
874
868
|
while (entries.size > SEGMENT_ENTRY_LIMIT) {
|
|
875
|
-
const oldest = entries.keys().next().value
|
|
876
|
-
if (oldest === undefined) return
|
|
877
|
-
entries.delete(oldest)
|
|
869
|
+
const oldest = entries.keys().next().value
|
|
870
|
+
if (oldest === undefined) return
|
|
871
|
+
entries.delete(oldest)
|
|
878
872
|
}
|
|
879
873
|
}
|
|
880
874
|
|
|
881
875
|
function touchLearnedRoute(segmentPath: string, learnedKey: string): void {
|
|
882
|
-
const byRoute = learnedRoutes.get(segmentPath)
|
|
883
|
-
const learned = byRoute?.get(learnedKey)
|
|
884
|
-
if (!byRoute || !learned) return
|
|
885
|
-
byRoute.delete(learnedKey)
|
|
886
|
-
byRoute.set(learnedKey, learned)
|
|
876
|
+
const byRoute = learnedRoutes.get(segmentPath)
|
|
877
|
+
const learned = byRoute?.get(learnedKey)
|
|
878
|
+
if (!byRoute || !learned) return
|
|
879
|
+
byRoute.delete(learnedKey)
|
|
880
|
+
byRoute.set(learnedKey, learned)
|
|
887
881
|
}
|
|
888
882
|
|
|
889
883
|
function trimLearnedRoutes(byRoute: Map<string, LearnedRoute>): void {
|
|
890
884
|
while (byRoute.size > SEGMENT_ENTRY_LIMIT) {
|
|
891
|
-
const oldest = byRoute.keys().next().value
|
|
892
|
-
if (oldest === undefined) return
|
|
893
|
-
byRoute.delete(oldest)
|
|
885
|
+
const oldest = byRoute.keys().next().value
|
|
886
|
+
if (oldest === undefined) return
|
|
887
|
+
byRoute.delete(oldest)
|
|
894
888
|
}
|
|
895
889
|
}
|
|
896
890
|
|
|
@@ -903,12 +897,12 @@ function trimLearnedRoutes(byRoute: Map<string, LearnedRoute>): void {
|
|
|
903
897
|
* sibling pattern.
|
|
904
898
|
*/
|
|
905
899
|
export function resolvedRouteFor(pathname: string): string | null {
|
|
906
|
-
return resolvedRoutes.get(pathname) ?? null
|
|
900
|
+
return resolvedRoutes.get(pathname) ?? null
|
|
907
901
|
}
|
|
908
902
|
|
|
909
903
|
/** Test seam: how many route/vary sets are remembered for a segment path. */
|
|
910
904
|
export function learnedRouteCount(segmentPath: string): number {
|
|
911
|
-
return learnedRoutes.get(segmentPath)?.size ?? 0
|
|
905
|
+
return learnedRoutes.get(segmentPath)?.size ?? 0
|
|
912
906
|
}
|
|
913
907
|
|
|
914
908
|
// ---------------------------------------------------------------------------
|
|
@@ -923,24 +917,24 @@ export function decodeSegmentPayload(
|
|
|
923
917
|
body: string,
|
|
924
918
|
expectedSegment: string,
|
|
925
919
|
): (SegmentPayloadMeta & { html: string }) | null {
|
|
926
|
-
const json = body.startsWith('0:') ? body.slice(2) : body
|
|
920
|
+
const json = body.startsWith('0:') ? body.slice(2) : body
|
|
927
921
|
let payload: {
|
|
928
|
-
segment?: unknown
|
|
929
|
-
html?: unknown
|
|
930
|
-
vary?: unknown
|
|
931
|
-
layoutVary?: unknown
|
|
932
|
-
pageVary?: unknown
|
|
933
|
-
route?: unknown
|
|
934
|
-
params?: unknown
|
|
935
|
-
shell?: unknown
|
|
936
|
-
}
|
|
922
|
+
segment?: unknown
|
|
923
|
+
html?: unknown
|
|
924
|
+
vary?: unknown
|
|
925
|
+
layoutVary?: unknown
|
|
926
|
+
pageVary?: unknown
|
|
927
|
+
route?: unknown
|
|
928
|
+
params?: unknown
|
|
929
|
+
shell?: unknown
|
|
930
|
+
}
|
|
937
931
|
try {
|
|
938
|
-
payload = JSON.parse(json) as typeof payload
|
|
932
|
+
payload = JSON.parse(json) as typeof payload
|
|
939
933
|
} catch {
|
|
940
|
-
return null
|
|
934
|
+
return null
|
|
941
935
|
}
|
|
942
|
-
if (payload.segment !== expectedSegment || typeof payload.html !== 'string') return null
|
|
943
|
-
const payloadSiblings = staticSiblingsFromPayloadHtml(payload.html)
|
|
936
|
+
if (payload.segment !== expectedSegment || typeof payload.html !== 'string') return null
|
|
937
|
+
const payloadSiblings = staticSiblingsFromPayloadHtml(payload.html)
|
|
944
938
|
return {
|
|
945
939
|
html: payload.html,
|
|
946
940
|
vary: Array.isArray(payload.vary) ? payload.vary.map(String) : null,
|
|
@@ -952,14 +946,14 @@ export function decodeSegmentPayload(
|
|
|
952
946
|
params: normalizeWireParams(payload.params),
|
|
953
947
|
...(payloadSiblings ? { staticSiblings: payloadSiblings } : {}),
|
|
954
948
|
...(appShellOf(payload.shell) ? { shell: appShellOf(payload.shell)! } : {}),
|
|
955
|
-
}
|
|
949
|
+
}
|
|
956
950
|
}
|
|
957
951
|
|
|
958
952
|
/** The `shell` field of a segment payload, when it carries a usable one. */
|
|
959
953
|
function appShellOf(value: unknown): { html: string; route: string } | null {
|
|
960
|
-
const shell = value as { html?: unknown; route?: unknown } | null | undefined
|
|
961
|
-
if (!shell || typeof shell.html !== 'string' || typeof shell.route !== 'string') return null
|
|
962
|
-
return { html: shell.html, route: shell.route }
|
|
954
|
+
const shell = value as { html?: unknown; route?: unknown } | null | undefined
|
|
955
|
+
if (!shell || typeof shell.html !== 'string' || typeof shell.route !== 'string') return null
|
|
956
|
+
return { html: shell.html, route: shell.route }
|
|
963
957
|
}
|
|
964
958
|
|
|
965
959
|
/**
|
|
@@ -975,33 +969,33 @@ function appShellOf(value: unknown): { html: string; route: string } | null {
|
|
|
975
969
|
* it a hard load of `/route/foo` seeds an entry no navigation to `/route/bar` can key onto.
|
|
976
970
|
*/
|
|
977
971
|
export function decodeDocumentSegmentMeta(html: string): SegmentPayloadMeta {
|
|
978
|
-
const state = documentRouteState(html)
|
|
979
|
-
if (!state) return { vary: null, route: null, params: {} }
|
|
980
|
-
const siblings = staticSiblingsOf(state)
|
|
972
|
+
const state = documentRouteState(html)
|
|
973
|
+
if (!state) return { vary: null, route: null, params: {} }
|
|
974
|
+
const siblings = staticSiblingsOf(state)
|
|
981
975
|
return {
|
|
982
976
|
vary: Array.isArray(state.pageVary) ? state.pageVary.map(String) : null,
|
|
983
977
|
route: typeof state.route === 'string' ? state.route : null,
|
|
984
978
|
params: normalizeWireParams(state.params),
|
|
985
979
|
...(siblings ? { staticSiblings: siblings } : {}),
|
|
986
|
-
}
|
|
980
|
+
}
|
|
987
981
|
}
|
|
988
982
|
|
|
989
983
|
interface WireRouteState {
|
|
990
|
-
route?: unknown
|
|
991
|
-
params?: unknown
|
|
992
|
-
pageVary?: unknown
|
|
993
|
-
staticChildren?: unknown
|
|
994
|
-
staticChildrenBySegment?: unknown
|
|
984
|
+
route?: unknown
|
|
985
|
+
params?: unknown
|
|
986
|
+
pageVary?: unknown
|
|
987
|
+
staticChildren?: unknown
|
|
988
|
+
staticChildrenBySegment?: unknown
|
|
995
989
|
}
|
|
996
990
|
|
|
997
991
|
/** The `window.__PNEXT_ROUTE__` state embedded in a rendered document. */
|
|
998
992
|
function documentRouteState(html: string): WireRouteState | null {
|
|
999
|
-
const match = /window\.__PNEXT_ROUTE__=(\{.*?\});?<\/script>/s.exec(html)
|
|
1000
|
-
if (!match?.[1]) return null
|
|
993
|
+
const match = /window\.__PNEXT_ROUTE__=(\{.*?\});?<\/script>/s.exec(html)
|
|
994
|
+
if (!match?.[1]) return null
|
|
1001
995
|
try {
|
|
1002
|
-
return JSON.parse(match[1]) as WireRouteState
|
|
996
|
+
return JSON.parse(match[1]) as WireRouteState
|
|
1003
997
|
} catch {
|
|
1004
|
-
return null
|
|
998
|
+
return null
|
|
1005
999
|
}
|
|
1006
1000
|
}
|
|
1007
1001
|
|
|
@@ -1011,27 +1005,27 @@ function documentRouteState(html: string): WireRouteState | null {
|
|
|
1011
1005
|
* deepest-level `staticChildren` at the pattern's last dynamic index).
|
|
1012
1006
|
*/
|
|
1013
1007
|
function staticSiblingsOf(state: WireRouteState): Record<string, string[]> | undefined {
|
|
1014
|
-
const siblings: Record<string, string[]> = {}
|
|
1015
|
-
const bySegment = state.staticChildrenBySegment
|
|
1008
|
+
const siblings: Record<string, string[]> = {}
|
|
1009
|
+
const bySegment = state.staticChildrenBySegment
|
|
1016
1010
|
if (typeof bySegment === 'object' && bySegment !== null) {
|
|
1017
1011
|
for (const [index, names] of Object.entries(bySegment as Record<string, unknown>)) {
|
|
1018
|
-
if (Array.isArray(names)) siblings[index] = names.map(String)
|
|
1012
|
+
if (Array.isArray(names)) siblings[index] = names.map(String)
|
|
1019
1013
|
}
|
|
1020
1014
|
}
|
|
1021
1015
|
if (Array.isArray(state.staticChildren) && typeof state.route === 'string') {
|
|
1022
|
-
const parts = state.route.split('/').filter(Boolean)
|
|
1023
|
-
const deepest = parts.reduce((last, part, index) => (part.startsWith(':') ? index : last), -1)
|
|
1016
|
+
const parts = state.route.split('/').filter(Boolean)
|
|
1017
|
+
const deepest = parts.reduce((last, part, index) => (part.startsWith(':') ? index : last), -1)
|
|
1024
1018
|
if (deepest >= 0 && siblings[String(deepest)] === undefined) {
|
|
1025
|
-
siblings[String(deepest)] = state.staticChildren.map(String)
|
|
1019
|
+
siblings[String(deepest)] = state.staticChildren.map(String)
|
|
1026
1020
|
}
|
|
1027
1021
|
}
|
|
1028
|
-
return Object.keys(siblings).length > 0 ? siblings : undefined
|
|
1022
|
+
return Object.keys(siblings).length > 0 ? siblings : undefined
|
|
1029
1023
|
}
|
|
1030
1024
|
|
|
1031
1025
|
/** A framed segment payload's static siblings, read off the HTML it carries. */
|
|
1032
1026
|
function staticSiblingsFromPayloadHtml(html: string): Record<string, string[]> | undefined {
|
|
1033
|
-
const state = documentRouteState(html)
|
|
1034
|
-
return state ? staticSiblingsOf(state) : undefined
|
|
1027
|
+
const state = documentRouteState(html)
|
|
1028
|
+
return state ? staticSiblingsOf(state) : undefined
|
|
1035
1029
|
}
|
|
1036
1030
|
|
|
1037
1031
|
/**
|
|
@@ -1048,30 +1042,30 @@ function namesStaticSibling(
|
|
|
1048
1042
|
pathname: string,
|
|
1049
1043
|
siblings: Record<string, string[]> | undefined,
|
|
1050
1044
|
): boolean {
|
|
1051
|
-
if (!siblings) return false
|
|
1052
|
-
const parts = pathname.split('/').filter(Boolean).map(decodeSegment)
|
|
1045
|
+
if (!siblings) return false
|
|
1046
|
+
const parts = pathname.split('/').filter(Boolean).map(decodeSegment)
|
|
1053
1047
|
return Object.entries(siblings).some(([index, names]) => {
|
|
1054
|
-
const segment = parts[Number(index)]
|
|
1055
|
-
return segment !== undefined && names.includes(segment)
|
|
1056
|
-
})
|
|
1048
|
+
const segment = parts[Number(index)]
|
|
1049
|
+
return segment !== undefined && names.includes(segment)
|
|
1050
|
+
})
|
|
1057
1051
|
}
|
|
1058
1052
|
|
|
1059
1053
|
function normalizeWireParams(value: unknown): Record<string, string | string[]> {
|
|
1060
|
-
if (typeof value !== 'object' || value === null) return {}
|
|
1061
|
-
const params: Record<string, string | string[]> = {}
|
|
1054
|
+
if (typeof value !== 'object' || value === null) return {}
|
|
1055
|
+
const params: Record<string, string | string[]> = {}
|
|
1062
1056
|
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
1063
|
-
if (Array.isArray(raw)) params[key] = (raw as unknown[]).map(stringifyParam)
|
|
1064
|
-
else if (raw !== undefined && raw !== null) params[key] = stringifyParam(raw)
|
|
1057
|
+
if (Array.isArray(raw)) params[key] = (raw as unknown[]).map(stringifyParam)
|
|
1058
|
+
else if (raw !== undefined && raw !== null) params[key] = stringifyParam(raw)
|
|
1065
1059
|
}
|
|
1066
|
-
return params
|
|
1060
|
+
return params
|
|
1067
1061
|
}
|
|
1068
1062
|
|
|
1069
1063
|
/** Wire params are strings; anything else is coerced defensively. */
|
|
1070
1064
|
function stringifyParam(value: unknown): string {
|
|
1071
|
-
return typeof value === 'string' ? value : JSON.stringify(value) ?? ''
|
|
1065
|
+
return typeof value === 'string' ? value : (JSON.stringify(value) ?? '')
|
|
1072
1066
|
}
|
|
1073
1067
|
|
|
1074
1068
|
/** True when a decoded vary set makes the segment query-dependent. */
|
|
1075
1069
|
export function varyIncludesSearch(vary: readonly string[] | null): boolean {
|
|
1076
|
-
return vary?.includes(SEARCH_PARAMS_SENTINEL) === true
|
|
1070
|
+
return vary?.includes(SEARCH_PARAMS_SENTINEL) === true
|
|
1077
1071
|
}
|