@timber-js/app 0.2.0-alpha.185 → 0.2.0-alpha.187
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/dist/_chunks/{actions-O_LsyCE4.js → actions-35jnMdeJ.js} +4 -3
- package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-35jnMdeJ.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
- package/dist/_chunks/als-slots-BEEIPKYm.js +26 -0
- package/dist/_chunks/als-slots-BEEIPKYm.js.map +1 -0
- package/dist/_chunks/base-path-DaQrzbez.js +80 -0
- package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
- package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
- package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
- package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-DjNrIWRR.js} +8 -14
- package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
- package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
- package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
- package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
- package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-CGP6BZKO.js} +92 -14
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
- package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
- package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
- package/dist/_chunks/error-boundary-D-ODYX41.js +353 -0
- package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
- package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
- package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
- package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
- package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
- package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
- package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
- package/dist/_chunks/{logger-AWfuX-KJ.js → logger-D8xJZXIN.js} +34 -51
- package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
- package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
- package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
- package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-rCinWLiE.js} +8 -2
- package/dist/_chunks/plugin-context-rCinWLiE.js.map +1 -0
- package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
- package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
- package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
- package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
- package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
- package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
- package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
- package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
- package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
- package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
- package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
- package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
- package/dist/_chunks/scanner-C8b0Gcw3.js +1564 -0
- package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
- package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
- package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
- package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
- package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
- package/dist/_chunks/segment-context-CjOlyB8Y.js +97 -0
- package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
- package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
- package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
- package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
- package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
- package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
- package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
- package/dist/_chunks/use-segment-params-C4r4BD9T.js +398 -0
- package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
- package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
- package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +37 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/compress-module.d.ts +12 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/fs-identity.d.ts +64 -0
- package/dist/adapters/fs-identity.d.ts.map +1 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +56 -5
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +57 -1
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +13 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cache/fast-hash.d.ts +5 -0
- package/dist/cache/fast-hash.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cdn/cloudflare-purge.js +30 -0
- package/dist/cdn/cloudflare-purge.js.map +1 -0
- package/dist/cdn/fastly-purge.js +33 -0
- package/dist/cdn/fastly-purge.js.map +1 -0
- package/dist/cdn/index.js +94 -0
- package/dist/cdn/index.js.map +1 -0
- package/dist/cdn/workers-cache-purge.js +35 -0
- package/dist/cdn/workers-cache-purge.js.map +1 -0
- package/dist/cli-check.d.ts +153 -0
- package/dist/cli-check.d.ts.map +1 -0
- package/dist/cli.d.ts +34 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +46 -21
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts +20 -9
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +1 -1
- package/dist/client/browser-entry/index.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts +4 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +3 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +7 -0
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -162
- package/dist/client/history.d.ts +13 -6
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +42 -41
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +4 -3
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +877 -311
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts +1 -7
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-api.d.ts +2 -2
- package/dist/client/navigation-commit.d.ts +87 -0
- package/dist/client/navigation-commit.d.ts.map +1 -0
- package/dist/client/navigation-context.d.ts +37 -20
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +63 -23
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/params-context.d.ts +81 -0
- package/dist/client/params-context.d.ts.map +1 -0
- package/dist/client/router-effects.d.ts +129 -0
- package/dist/client/router-effects.d.ts.map +1 -0
- package/dist/client/router-lifecycle.d.ts +55 -0
- package/dist/client/router-lifecycle.d.ts.map +1 -0
- package/dist/client/router-pipeline.d.ts +84 -0
- package/dist/client/router-pipeline.d.ts.map +1 -0
- package/dist/client/router-ref.d.ts +1 -1
- package/dist/client/router-ref.d.ts.map +1 -1
- package/dist/client/router-skew.d.ts +21 -0
- package/dist/client/router-skew.d.ts.map +1 -0
- package/dist/client/router-types.d.ts +207 -0
- package/dist/client/router-types.d.ts.map +1 -0
- package/dist/client/router.d.ts +1 -182
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +47 -26
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +87 -34
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/ssr-data.d.ts +6 -0
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/stale-client.d.ts +69 -0
- package/dist/client/stale-client.d.ts.map +1 -0
- package/dist/client/state.d.ts +10 -1
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +2 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/unload-guard.d.ts +5 -0
- package/dist/client/unload-guard.d.ts.map +1 -1
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +9 -3
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/client/use-router.d.ts +1 -1
- package/dist/client/use-segment-params.d.ts +18 -8
- package/dist/client/use-segment-params.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +5 -5
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1009 -759
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/plugin-context.d.ts +46 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/request-dep/analysis.d.ts +106 -0
- package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
- package/dist/plugins/request-dep/index.d.ts +75 -0
- package/dist/plugins/request-dep/index.d.ts.map +1 -0
- package/dist/plugins/request-dep/origin.d.ts +61 -0
- package/dist/plugins/request-dep/origin.d.ts.map +1 -0
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +160 -8
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-shared.d.ts +3 -44
- package/dist/routing/codegen-shared.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +28 -31
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts +51 -0
- package/dist/routing/codegen-write.d.ts.map +1 -0
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/collision-probe.d.ts +104 -0
- package/dist/routing/collision-probe.d.ts.map +1 -0
- package/dist/routing/collision-spaces.d.ts +26 -0
- package/dist/routing/collision-spaces.d.ts.map +1 -0
- package/dist/routing/convention-lint.d.ts +18 -4
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/export-detect.d.ts +16 -0
- package/dist/routing/export-detect.d.ts.map +1 -1
- package/dist/routing/index.js +4 -3
- package/dist/routing/interception.d.ts +235 -6
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/link-codegen.d.ts +19 -4
- package/dist/routing/link-codegen.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts +1 -7
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +33 -0
- package/dist/routing/segment-classify.d.ts.map +1 -1
- package/dist/routing/segment-keys.d.ts +95 -0
- package/dist/routing/segment-keys.d.ts.map +1 -0
- package/dist/routing/slot-placement.d.ts +48 -0
- package/dist/routing/slot-placement.d.ts.map +1 -0
- package/dist/routing/types.d.ts +0 -6
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/routing/walkers.d.ts +6 -0
- package/dist/routing/walkers.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +60 -9
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +62 -14
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.d.ts +0 -1
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +221 -10
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/parse-total.d.ts +70 -0
- package/dist/search-params/parse-total.d.ts.map +1 -0
- package/dist/search-params/wrappers.d.ts +26 -3
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/server/access-gate.d.ts +19 -8
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +6 -3
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +18 -1
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +13 -1
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/chain-url-parts.d.ts +82 -0
- package/dist/server/chain-url-parts.d.ts.map +1 -0
- package/dist/server/children-interception.d.ts +49 -0
- package/dist/server/children-interception.d.ts.map +1 -0
- package/dist/server/compress.d.ts.map +1 -1
- package/dist/server/default-status-page.d.ts +45 -0
- package/dist/server/default-status-page.d.ts.map +1 -0
- package/dist/server/deny-boundary.d.ts +148 -15
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +15 -4
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +85 -15
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/head-response.d.ts +35 -0
- package/dist/server/head-response.d.ts.map +1 -0
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +3 -1
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +712 -418
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +52 -0
- package/dist/server/metadata-collector.d.ts.map +1 -0
- package/dist/server/param-coercion.d.ts +22 -6
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts +0 -18
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-interception.d.ts +59 -9
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +11 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +23 -0
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/publish-params.d.ts +33 -0
- package/dist/server/publish-params.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +48 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +22 -3
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-handler.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +13 -0
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-cache-key-guard.d.ts +78 -0
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +15 -3
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +2 -17
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +0 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +1 -2
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +15 -2
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/skippable-prefix.d.ts +23 -0
- package/dist/server/skippable-prefix.d.ts.map +1 -0
- package/dist/server/slot-interception.d.ts +80 -0
- package/dist/server/slot-interception.d.ts.map +1 -0
- package/dist/server/slot-resolver.d.ts +70 -8
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +17 -0
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +0 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +24 -13
- package/dist/server/ssr-wrappers.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts +6 -27
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts +6 -0
- package/dist/server/static-generator.d.ts.map +1 -1
- package/dist/server/static-not-found.d.ts +52 -0
- package/dist/server/static-not-found.d.ts.map +1 -0
- package/dist/server/status-code-resolver.d.ts +8 -1
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +28 -36
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +12 -7
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils/element-type.d.ts +40 -0
- package/dist/server/utils/element-type.d.ts.map +1 -0
- package/dist/shared/als-slots.d.ts +41 -0
- package/dist/shared/als-slots.d.ts.map +1 -0
- package/dist/shared/base-path.d.ts +56 -0
- package/dist/shared/base-path.d.ts.map +1 -0
- package/dist/shared/param-value.d.ts +80 -0
- package/dist/shared/param-value.d.ts.map +1 -0
- package/dist/shared/payload-root.d.ts +108 -0
- package/dist/shared/payload-root.d.ts.map +1 -0
- package/dist/shared/rsc-cache-key.d.ts +163 -0
- package/dist/shared/rsc-cache-key.d.ts.map +1 -0
- package/dist/shared/rsc-error-envelope.d.ts +25 -0
- package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
- package/dist/shared/rsc-manifest.d.ts +28 -0
- package/dist/shared/rsc-manifest.d.ts.map +1 -0
- package/dist/shared/rsc-media-type.d.ts +40 -0
- package/dist/shared/rsc-media-type.d.ts.map +1 -0
- package/dist/shared/rsc-payload-path.d.ts +42 -0
- package/dist/shared/rsc-payload-path.d.ts.map +1 -0
- package/dist/shared/segment-info.d.ts +59 -0
- package/dist/shared/segment-info.d.ts.map +1 -0
- package/dist/shared/slot-params.d.ts +51 -0
- package/dist/shared/slot-params.d.ts.map +1 -0
- package/dist/shared/static-platform-files.d.ts +71 -0
- package/dist/shared/static-platform-files.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +1 -1
- package/docs/api/31-api-client.mdx +4 -2
- package/docs/api/33-api-search-params.mdx +38 -16
- package/docs/api/35-api-typescript.mdx +3 -3
- package/docs/api/36-cli.mdx +34 -7
- package/docs/learn/00-introduction.mdx +1 -1
- package/docs/learn/02-pages-and-layouts.mdx +2 -2
- package/docs/learn/05-typed-params.mdx +8 -8
- package/docs/learn/07-typed-routes.mdx +12 -7
- package/docs/learn/11-error-handling.mdx +18 -0
- package/docs/learn/12-client-navigation.mdx +2 -2
- package/docs/learn/14-deploying.mdx +23 -0
- package/docs/more/01-advanced-routing.mdx +173 -3
- package/docs/more/03-coming-from-nextjs.mdx +2 -2
- package/docs/more/04-metadata-and-fonts.mdx +1 -1
- package/docs/more/50-ai-agent-instructions.mdx +6 -4
- package/package.json +9 -6
- package/src/adapters/build-output-helper.ts +116 -3
- package/src/adapters/cloudflare.ts +90 -12
- package/src/adapters/compress-module.ts +79 -1
- package/src/adapters/fs-identity.ts +156 -0
- package/src/adapters/nitro.ts +6 -6
- package/src/adapters/shared.ts +196 -21
- package/src/adapters/types.ts +13 -1
- package/src/cache/fast-hash.ts +5 -0
- package/src/cli-check.ts +458 -0
- package/src/cli.ts +59 -24
- package/src/client/browser-entry/action-dispatch.ts +11 -5
- package/src/client/browser-entry/hydrate.ts +40 -29
- package/src/client/browser-entry/index.ts +15 -6
- package/src/client/browser-entry/post-hydration.ts +10 -9
- package/src/client/browser-entry/router-init.ts +104 -48
- package/src/client/browser-entry/rsc-stream.ts +11 -2
- package/src/client/error-boundary.tsx +85 -16
- package/src/client/history.ts +13 -6
- package/src/client/index.ts +14 -3
- package/src/client/internal.ts +4 -3
- package/src/client/link.tsx +120 -72
- package/src/client/navigation-api.ts +3 -3
- package/src/client/navigation-commit.ts +197 -0
- package/src/client/navigation-context.ts +40 -67
- package/src/client/navigation-root.tsx +326 -101
- package/src/client/params-context.ts +192 -0
- package/src/client/router-effects.ts +231 -0
- package/src/client/router-lifecycle.ts +288 -0
- package/src/client/router-pipeline.ts +393 -0
- package/src/client/router-ref.ts +1 -1
- package/src/client/router-skew.ts +30 -0
- package/src/client/router-types.ts +230 -0
- package/src/client/router.ts +243 -707
- package/src/client/rsc-fetch.ts +183 -106
- package/src/client/segment-cache.ts +129 -44
- package/src/client/ssr-data.ts +6 -0
- package/src/client/stale-client.ts +191 -0
- package/src/client/state.ts +14 -1
- package/src/client/top-loader.tsx +2 -1
- package/src/client/unload-guard.ts +34 -2
- package/src/client/use-cookie.ts +9 -2
- package/src/client/use-query-states.ts +102 -39
- package/src/client/use-router.ts +1 -1
- package/src/client/use-segment-params.ts +41 -18
- package/src/cookies/define-cookie.ts +6 -1
- package/src/index.ts +25 -4
- package/src/plugin-context.ts +58 -0
- package/src/plugins/adapter-build.ts +4 -0
- package/src/plugins/build-manifest.ts +34 -13
- package/src/plugins/entries.ts +4 -0
- package/src/plugins/fonts.ts +11 -8
- package/src/plugins/request-dep/analysis.ts +447 -0
- package/src/plugins/request-dep/index.ts +376 -0
- package/src/plugins/request-dep/origin.ts +310 -0
- package/src/plugins/routing.ts +97 -102
- package/src/plugins/shims.ts +0 -1
- package/src/plugins/static-build.ts +391 -95
- package/src/routing/codegen-shared.ts +3 -79
- package/src/routing/codegen-types.ts +28 -31
- package/src/routing/codegen-write.ts +139 -0
- package/src/routing/codegen.ts +86 -172
- package/src/routing/collision-probe.ts +438 -0
- package/src/routing/collision-spaces.ts +281 -0
- package/src/routing/convention-lint.ts +139 -40
- package/src/routing/export-detect.ts +151 -7
- package/src/routing/interception.ts +896 -50
- package/src/routing/link-codegen.ts +32 -65
- package/src/routing/manifest-codegen.ts +6 -62
- package/src/routing/scanner.ts +117 -92
- package/src/routing/segment-classify.ts +57 -0
- package/src/routing/segment-keys.ts +146 -0
- package/src/routing/slot-placement.ts +97 -0
- package/src/routing/types.ts +0 -6
- package/src/routing/walkers.ts +8 -0
- package/src/schema-bridge.ts +180 -58
- package/src/search-params/define.ts +108 -66
- package/src/search-params/index.ts +0 -1
- package/src/search-params/parse-total.ts +78 -0
- package/src/search-params/wrappers.ts +60 -11
- package/src/server/access-gate.tsx +60 -40
- package/src/server/action-handler.ts +4 -5
- package/src/server/actions.ts +6 -3
- package/src/server/als-registry.ts +18 -1
- package/src/server/build-manifest.ts +14 -1
- package/src/server/chain-url-parts.ts +163 -0
- package/src/server/children-interception.ts +115 -0
- package/src/server/compress.ts +9 -1
- package/src/server/default-status-page.ts +127 -0
- package/src/server/deny-boundary.ts +269 -41
- package/src/server/deny-renderer.ts +111 -39
- package/src/server/error-boundary-wrapper.ts +166 -79
- package/src/server/head-response.ts +54 -0
- package/src/server/index.ts +1 -3
- package/src/server/internal.ts +2 -2
- package/src/server/metadata-collector.ts +115 -0
- package/src/server/param-coercion.ts +51 -69
- package/src/server/pipeline-helpers.ts +2 -47
- package/src/server/pipeline-interception.ts +81 -32
- package/src/server/pipeline-outcome.ts +4 -5
- package/src/server/pipeline-phases.ts +77 -25
- package/src/server/pipeline.ts +57 -10
- package/src/server/prebuilt-runtime.ts +4 -4
- package/src/server/primitives.ts +25 -0
- package/src/server/publish-params.ts +42 -0
- package/src/server/request-context.ts +97 -18
- package/src/server/route-element-builder.ts +241 -253
- package/src/server/route-handler.ts +8 -8
- package/src/server/route-matcher.ts +21 -9
- package/src/server/rsc-cache-key-guard.ts +164 -0
- package/src/server/rsc-entry/api-handler.ts +6 -8
- package/src/server/rsc-entry/deny-fallback.ts +3 -2
- package/src/server/rsc-entry/error-renderer.ts +94 -35
- package/src/server/rsc-entry/helpers.ts +33 -28
- package/src/server/rsc-entry/index.ts +36 -33
- package/src/server/rsc-entry/render-route.ts +18 -10
- package/src/server/rsc-entry/revalidate-renderer.ts +3 -2
- package/src/server/rsc-entry/rsc-payload.ts +113 -40
- package/src/server/rsc-entry/rsc-stream.ts +29 -5
- package/src/server/rsc-entry/ssr-renderer.ts +38 -22
- package/src/server/skippable-prefix.ts +92 -0
- package/src/server/slot-interception.ts +168 -0
- package/src/server/slot-resolver.ts +263 -158
- package/src/server/ssr-bridge-types.ts +18 -0
- package/src/server/ssr-entry.ts +20 -10
- package/src/server/ssr-wrappers.tsx +33 -32
- package/src/server/state-tree-diff.ts +14 -55
- package/src/server/static-generator.ts +68 -29
- package/src/server/static-not-found.ts +134 -0
- package/src/server/status-code-resolver.ts +28 -11
- package/src/server/tree-builder.ts +35 -218
- package/src/server/types.ts +12 -7
- package/src/server/utils/element-type.ts +72 -0
- package/src/shared/als-slots.ts +72 -0
- package/src/shared/base-path.ts +80 -0
- package/src/shared/param-value.ts +244 -0
- package/src/shared/payload-root.ts +151 -0
- package/src/shared/rsc-cache-key.ts +269 -0
- package/src/shared/rsc-error-envelope.ts +31 -0
- package/src/shared/rsc-manifest.ts +29 -0
- package/src/shared/rsc-media-type.ts +43 -0
- package/src/shared/rsc-payload-path.ts +45 -0
- package/src/shared/segment-info.ts +60 -0
- package/src/shared/slot-params.ts +111 -0
- package/src/shared/static-platform-files.ts +75 -0
- package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
- package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
- package/dist/_chunks/cache-api-B-lhk9p4.js.map +0 -1
- package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-BTWEKJXo.js +0 -668
- package/dist/_chunks/cli-schema-sync-BTWEKJXo.js.map +0 -1
- package/dist/_chunks/define-COtkxMRT.js +0 -191
- package/dist/_chunks/define-COtkxMRT.js.map +0 -1
- package/dist/_chunks/define-c4au4I9R.js +0 -82
- package/dist/_chunks/define-c4au4I9R.js.map +0 -1
- package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
- package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
- package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
- package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
- package/dist/_chunks/registry-DbJPKoBp.js +0 -20
- package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
- package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
- package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
- package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
- package/dist/_chunks/segment-classify-Byy425ng.js.map +0 -1
- package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
- package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
- package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
- package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
- package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
- package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
- package/dist/_chunks/walkers-_6zKFlch.js +0 -1002
- package/dist/_chunks/walkers-_6zKFlch.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/plugins/request-dep.d.ts +0 -32
- package/dist/plugins/request-dep.d.ts.map +0 -1
- package/dist/search-params/registry.d.ts +0 -20
- package/dist/search-params/registry.d.ts.map +0 -1
- package/dist/segment-params/define.d.ts +0 -89
- package/dist/segment-params/define.d.ts.map +0 -1
- package/dist/segment-params/index.d.ts +0 -3
- package/dist/segment-params/index.d.ts.map +0 -1
- package/dist/segment-params/index.js +0 -2
- package/src/plugins/request-dep.ts +0 -499
- package/src/search-params/registry.ts +0 -31
- package/src/segment-params/define.ts +0 -249
- package/src/segment-params/index.ts +0 -9
package/src/client/router.ts
CHANGED
|
@@ -1,220 +1,29 @@
|
|
|
1
|
-
// Segment Router —
|
|
1
|
+
// Segment Router — the operations a client-side navigation can be.
|
|
2
|
+
//
|
|
3
|
+
// `navigate`, `refresh`, `handlePopState`, `prefetch` and `applyRevalidation`
|
|
4
|
+
// live here; each is wiring over modules that own one concern apiece:
|
|
5
|
+
//
|
|
6
|
+
// router-types.ts — RouterDeps / RouterInstance / the option shapes
|
|
7
|
+
// router-lifecycle.ts — ownership, supersede, pending store, handoff set
|
|
8
|
+
// router-pipeline.ts — URL in, tree in React's hands
|
|
9
|
+
// router-effects.ts — SPA exits, error recovery, post-paint scroll
|
|
10
|
+
// navigation-commit.ts — every write that makes a page "current"
|
|
11
|
+
//
|
|
2
12
|
// See design/19-client-navigation.md for the full architecture.
|
|
3
13
|
|
|
4
|
-
import { SegmentCache, PrefetchCache
|
|
5
|
-
import type { SegmentInfo } from '
|
|
14
|
+
import { SegmentCache, PrefetchCache } from './segment-cache';
|
|
15
|
+
import type { SegmentInfo } from '../shared/segment-info.js';
|
|
6
16
|
import { HistoryStack } from './history';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
setNavigationState,
|
|
10
|
-
getNavigationState,
|
|
11
|
-
type NavigationState,
|
|
12
|
-
} from './navigation-context.js';
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
fetchRscPayload,
|
|
16
|
-
RedirectError,
|
|
17
|
-
ServerErrorResponse,
|
|
18
|
-
VersionSkewError,
|
|
19
|
-
NonRscResponse,
|
|
20
|
-
} from './rsc-fetch.js';
|
|
21
|
-
import { setHardNavigating, supersedeNavigationTransitions } from './navigation-root.js';
|
|
22
|
-
import type { FetchResult } from './rsc-fetch.js';
|
|
23
|
-
|
|
24
|
-
// ─── Types ───────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
export interface NavigationOptions {
|
|
27
|
-
/** Set to false to prevent scroll-to-top on forward navigation */
|
|
28
|
-
scroll?: boolean;
|
|
29
|
-
/** Use replaceState instead of pushState (replaces current history entry) */
|
|
30
|
-
replace?: boolean;
|
|
31
|
-
/**
|
|
32
|
-
* @internal AbortSignal from the Navigation API's NavigateEvent.
|
|
33
|
-
* When provided, the signal is linked to the router's per-navigation
|
|
34
|
-
* AbortController so in-flight RSC fetches are cancelled when a new
|
|
35
|
-
* navigation starts.
|
|
36
|
-
*/
|
|
37
|
-
_signal?: AbortSignal;
|
|
38
|
-
/**
|
|
39
|
-
* @internal Skip pushState/replaceState — the Navigation API has already
|
|
40
|
-
* updated the URL via event.intercept(). Used for external navigations
|
|
41
|
-
* intercepted by the navigate event handler.
|
|
42
|
-
*/
|
|
43
|
-
_skipHistory?: boolean;
|
|
44
|
-
/**
|
|
45
|
-
* @internal The URL the user is navigating FROM, captured before the
|
|
46
|
-
* Navigation API commits the destination. Sent as X-Timber-URL for
|
|
47
|
-
* slot skip comparison on the server (TIM-1232).
|
|
48
|
-
*/
|
|
49
|
-
_departingUrl?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Function that decodes an RSC Flight stream into a React element tree.
|
|
54
|
-
* In production: createFromFetch from @vitejs/plugin-rsc/browser.
|
|
55
|
-
* In tests: a mock that returns the raw payload.
|
|
56
|
-
*/
|
|
57
|
-
export type RscDecoder = (fetchPromise: Promise<Response>) => unknown;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Function that renders a decoded RSC element tree into the DOM.
|
|
61
|
-
* In production: reactRoot.render(element).
|
|
62
|
-
* In tests: a no-op or mock.
|
|
63
|
-
*
|
|
64
|
-
* Receives the current NavigationState explicitly — no temporal
|
|
65
|
-
* coupling with setNavigationState/getNavigationState. The renderer
|
|
66
|
-
* wraps the element in NavigationProvider with this state.
|
|
67
|
-
*/
|
|
68
|
-
export type RootRenderer = (element: unknown, navState: NavigationState) => void;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Platform dependencies injected for testability. In production these
|
|
72
|
-
* map to browser APIs; in tests they're replaced with mocks.
|
|
73
|
-
*/
|
|
74
|
-
export interface RouterDeps {
|
|
75
|
-
fetch: (url: string, init: RequestInit) => Promise<Response>;
|
|
76
|
-
pushState: (data: unknown, unused: string, url: string) => void;
|
|
77
|
-
replaceState: (data: unknown, unused: string, url: string) => void;
|
|
78
|
-
scrollTo: (x: number, y: number) => void;
|
|
79
|
-
getCurrentUrl: () => string;
|
|
80
|
-
getScrollY: () => number;
|
|
81
|
-
/** Decode RSC Flight stream into React elements. If not provided, raw response text is stored. */
|
|
82
|
-
decodeRsc?: RscDecoder;
|
|
83
|
-
/** Render decoded RSC tree into the DOM. If not provided, rendering is a no-op. */
|
|
84
|
-
renderRoot?: RootRenderer;
|
|
85
|
-
/**
|
|
86
|
-
* Schedule a callback after the next paint. In the browser, this is
|
|
87
|
-
* requestAnimationFrame + setTimeout(0) to run after React commits.
|
|
88
|
-
* In tests, this runs the callback synchronously.
|
|
89
|
-
*/
|
|
90
|
-
afterPaint?: (callback: () => void) => void;
|
|
91
|
-
/**
|
|
92
|
-
* Run a navigation inside a React transition with optimistic pending URL.
|
|
93
|
-
* The pending URL shows immediately (useOptimistic urgent update) and
|
|
94
|
-
* reverts when the transition commits (atomic with the new tree).
|
|
95
|
-
*
|
|
96
|
-
* The `perform` callback receives a `wrapPayload` function to wrap the
|
|
97
|
-
* decoded RSC payload with NavigationProvider + NuqsAdapter before
|
|
98
|
-
* NavigationRoot sets it as the new element. The `wrapPayload` function
|
|
99
|
-
* receives the NavigationState explicitly — no temporal coupling with
|
|
100
|
-
* getNavigationState().
|
|
101
|
-
*
|
|
102
|
-
* If not provided (tests), the router falls back to renderRoot.
|
|
103
|
-
*/
|
|
104
|
-
navigateTransition?: (
|
|
105
|
-
pendingUrl: string,
|
|
106
|
-
perform: (
|
|
107
|
-
wrapPayload: (
|
|
108
|
-
payload: unknown,
|
|
109
|
-
navState: NavigationState,
|
|
110
|
-
segmentUpdates?: Map<string, unknown>
|
|
111
|
-
) => unknown
|
|
112
|
-
) => Promise<{ element: unknown; decodePromise: Promise<void> | null }>
|
|
113
|
-
) => Promise<void>;
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Whether the Navigation API is active and handling traversals.
|
|
117
|
-
* When true, the popstate handler is a no-op — the Navigation API's
|
|
118
|
-
* navigate event covers back/forward button presses.
|
|
119
|
-
*/
|
|
120
|
-
navigationApiActive?: boolean;
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Called around pushState/replaceState to set a flag that prevents
|
|
124
|
-
* the Navigation API's navigate listener from double-handling
|
|
125
|
-
* router-initiated navigations.
|
|
126
|
-
*/
|
|
127
|
-
setRouterNavigating?: (value: boolean) => void;
|
|
17
|
+
import { createNavigationCommitter } from './navigation-commit.js';
|
|
128
18
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
* deferred promise that ties the browser's native loading state to the
|
|
138
|
-
* navigation lifecycle. Called in the finally block of navigate/refresh,
|
|
139
|
-
* aligned with when the TopLoader's pendingUrl clears.
|
|
140
|
-
*/
|
|
141
|
-
completeRouterNavigation?: () => void;
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Get the current unwrapped RSC payload element. Used for partial
|
|
145
|
-
* navigation: the router re-wraps the same element with new
|
|
146
|
-
* NavigationProvider context so mounted SegmentOutlets are preserved.
|
|
147
|
-
*/
|
|
148
|
-
_getCurrentPayload?: () => unknown;
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Initiate a navigation via the Navigation API (`navigation.navigate()`).
|
|
152
|
-
* Fires the navigate event BEFORE committing the URL, allowing Chrome
|
|
153
|
-
* to show its native loading indicator. Falls back to pushState when
|
|
154
|
-
* unavailable.
|
|
155
|
-
*/
|
|
156
|
-
navigationNavigate?: (url: string, replace: boolean) => void;
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Scroll the element matching a URL #fragment into view. Returns true
|
|
160
|
-
* when a matching element was found and scrolled. When absent or false,
|
|
161
|
-
* the router falls back to scroll-to-top on forward navigation — same
|
|
162
|
-
* as a full page load with an unknown fragment landing at the top.
|
|
163
|
-
*/
|
|
164
|
-
scrollToHash?: (hash: string) => boolean;
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Whether the client segment cache is enabled. When false (the default),
|
|
168
|
-
* the router does not send X-Timber-State-Tree headers and does not
|
|
169
|
-
* populate the segment cache. Every navigation gets a full RSC payload.
|
|
170
|
-
*/
|
|
171
|
-
clientSegmentCache?: boolean;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export interface RouterInstance {
|
|
175
|
-
/** Navigate to a new URL (forward navigation) */
|
|
176
|
-
navigate(url: string, options?: NavigationOptions): Promise<void>;
|
|
177
|
-
/** Full re-render of the current URL — no state tree sent */
|
|
178
|
-
refresh(): Promise<void>;
|
|
179
|
-
/** Handle a popstate event (back/forward button). scrollY is read from history.state. */
|
|
180
|
-
handlePopState(url: string, scrollY?: number, externalSignal?: AbortSignal): Promise<void>;
|
|
181
|
-
/** Whether a navigation is currently in flight */
|
|
182
|
-
isPending(): boolean;
|
|
183
|
-
/** The URL currently being navigated to, or null if idle */
|
|
184
|
-
getPendingUrl(): string | null;
|
|
185
|
-
/** Subscribe to pending state changes */
|
|
186
|
-
onPendingChange(listener: (pending: boolean) => void): () => void;
|
|
187
|
-
/** Prefetch an RSC payload for a URL (used by Link hover) */
|
|
188
|
-
prefetch(url: string): void;
|
|
189
|
-
/**
|
|
190
|
-
* Apply a piggybacked revalidation payload from a server action response.
|
|
191
|
-
* Renders the element tree and updates head elements without a server fetch.
|
|
192
|
-
* See design/08-forms-and-actions.md §"Single-Roundtrip Revalidation".
|
|
193
|
-
*/
|
|
194
|
-
applyRevalidation(element: unknown): void;
|
|
195
|
-
/**
|
|
196
|
-
* Populate the segment cache from server-provided segment metadata.
|
|
197
|
-
* Called on initial hydration with segment info embedded in the HTML.
|
|
198
|
-
*/
|
|
199
|
-
initSegmentCache(segments: SegmentInfo[]): void;
|
|
200
|
-
|
|
201
|
-
/** The segment cache (exposed for tests and <Link> prefetch) */
|
|
202
|
-
segmentCache: SegmentCache;
|
|
203
|
-
/** The prefetch cache (exposed for tests and <Link> prefetch) */
|
|
204
|
-
prefetchCache: PrefetchCache;
|
|
205
|
-
/** The history stack (exposed for tests) */
|
|
206
|
-
historyStack: HistoryStack;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Check if an error is an abort error (connection closed / fetch aborted).
|
|
211
|
-
* Browsers throw DOMException with name 'AbortError' when a fetch is aborted.
|
|
212
|
-
*/
|
|
213
|
-
function isAbortError(error: unknown): boolean {
|
|
214
|
-
if (error instanceof DOMException && error.name === 'AbortError') return true;
|
|
215
|
-
if (error instanceof Error && error.name === 'AbortError') return true;
|
|
216
|
-
return false;
|
|
217
|
-
}
|
|
19
|
+
import { fetchRscPayload, NonRscResponse } from './rsc-fetch.js';
|
|
20
|
+
import { readPayloadTree, readPublishedParams } from '../shared/payload-root.js';
|
|
21
|
+
import { isClientStale } from './stale-client.js';
|
|
22
|
+
import { createScrollEffects, createSpaExits, createNavigationRecovery } from './router-effects.js';
|
|
23
|
+
import { createNavigationLifecycle } from './router-lifecycle.js';
|
|
24
|
+
import { createNavigationPipeline, prefetchKeyFor } from './router-pipeline.js';
|
|
25
|
+
import { recordSkew } from './router-skew.js';
|
|
26
|
+
import type { NavigationOptions, RouterDeps, RouterInstance } from './router-types.js';
|
|
218
27
|
|
|
219
28
|
// ─── Router Factory ──────────────────────────────────────────────
|
|
220
29
|
|
|
@@ -222,418 +31,64 @@ function isAbortError(error: unknown): boolean {
|
|
|
222
31
|
* Create a router instance. In production, called once at app hydration
|
|
223
32
|
* with real browser APIs. In tests, called with mock dependencies.
|
|
224
33
|
*/
|
|
225
|
-
/**
|
|
226
|
-
* Router navigation phase — discriminated union replacing scattered
|
|
227
|
-
* `pending` + `pendingUrl` boolean flags.
|
|
228
|
-
*
|
|
229
|
-
* - `idle`: No navigation in flight. The committed params/pathname
|
|
230
|
-
* are current.
|
|
231
|
-
* - `navigating`: A fetch or render is in progress. `targetUrl` is
|
|
232
|
-
* the destination being navigated to.
|
|
233
|
-
*/
|
|
234
|
-
export type RouterPhase = { phase: 'idle' } | { phase: 'navigating'; targetUrl: string };
|
|
235
|
-
|
|
236
34
|
export function createRouter(deps: RouterDeps): RouterInstance {
|
|
237
35
|
const segmentCache = new SegmentCache();
|
|
238
36
|
const prefetchCache = new PrefetchCache();
|
|
239
37
|
const historyStack = new HistoryStack();
|
|
240
|
-
let routerPhase: RouterPhase = { phase: 'idle' };
|
|
241
|
-
const pendingListeners = new Set<(pending: boolean) => void>();
|
|
242
|
-
|
|
243
|
-
// AbortController for the current in-flight navigation.
|
|
244
|
-
// When a new navigation starts, the previous controller is aborted,
|
|
245
|
-
// cancelling any in-progress RSC fetch. This provides automatic
|
|
246
|
-
// cancellation of stale fetches regardless of Navigation API support.
|
|
247
|
-
let currentNavAbort: AbortController | null = null;
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Create a new AbortController for a navigation, superseding any
|
|
251
|
-
* previous in-flight navigation. Optionally links to an external
|
|
252
|
-
* signal (e.g., from the Navigation API's NavigateEvent.signal).
|
|
253
|
-
*
|
|
254
|
-
* Superseding is one operation with three parts:
|
|
255
|
-
* 1. Abort the previous navigation's fetch.
|
|
256
|
-
* 2. Invalidate its render transition so a response that already
|
|
257
|
-
* arrived can't commit a stale tree (NavigationRoot's transId guard).
|
|
258
|
-
* 3. Resolve its Navigation API deferred — the superseded navigation's
|
|
259
|
-
* finally block is staleness-guarded (see TIM-1034) and no longer
|
|
260
|
-
* cleans up after itself, so the browser's native loading state for
|
|
261
|
-
* the dead navigation is cleared here.
|
|
262
|
-
*/
|
|
263
|
-
function createNavAbort(externalSignal?: AbortSignal): AbortController {
|
|
264
|
-
if (currentNavAbort) {
|
|
265
|
-
currentNavAbort.abort();
|
|
266
|
-
supersedeNavigationTransitions();
|
|
267
|
-
deps.completeRouterNavigation?.();
|
|
268
|
-
}
|
|
269
|
-
const controller = new AbortController();
|
|
270
|
-
currentNavAbort = controller;
|
|
271
|
-
|
|
272
|
-
// If an external signal is provided (e.g., Navigation API),
|
|
273
|
-
// forward its abort to our controller.
|
|
274
|
-
if (externalSignal) {
|
|
275
|
-
if (externalSignal.aborted) {
|
|
276
|
-
controller.abort();
|
|
277
|
-
} else {
|
|
278
|
-
externalSignal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return controller;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function setPending(value: boolean, url?: string): void {
|
|
286
|
-
const next: RouterPhase =
|
|
287
|
-
value && url ? { phase: 'navigating', targetUrl: url } : { phase: 'idle' };
|
|
288
|
-
// Skip no-op updates
|
|
289
|
-
if (
|
|
290
|
-
routerPhase.phase === next.phase &&
|
|
291
|
-
(routerPhase.phase === 'idle' ||
|
|
292
|
-
(routerPhase.phase === 'navigating' &&
|
|
293
|
-
next.phase === 'navigating' &&
|
|
294
|
-
routerPhase.targetUrl === next.targetUrl))
|
|
295
|
-
) {
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
routerPhase = next;
|
|
299
|
-
// Notify external store listeners (non-React consumers).
|
|
300
|
-
// React-facing pending state is handled by useOptimistic in
|
|
301
|
-
// NavigationRoot via navigateTransition — not this function.
|
|
302
|
-
for (const listener of pendingListeners) {
|
|
303
|
-
listener(value);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** Update the segment cache from server-provided segment metadata. */
|
|
308
|
-
function updateSegmentCache(segmentInfo: SegmentInfo[] | null | undefined): void {
|
|
309
|
-
if (!deps.clientSegmentCache) return;
|
|
310
|
-
if (!segmentInfo || segmentInfo.length === 0) return;
|
|
311
|
-
const tree = buildSegmentTree(segmentInfo);
|
|
312
|
-
if (tree) {
|
|
313
|
-
segmentCache.set('/', tree);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/** Render a decoded RSC payload into the DOM if a renderer is available. */
|
|
318
|
-
function renderPayload(payload: unknown, navState: NavigationState): void {
|
|
319
|
-
if (deps.renderRoot) {
|
|
320
|
-
deps.renderRoot(payload, navState);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Atomically update all navigation-owned state for a new page. Every
|
|
326
|
-
* code path that changes the "current page" must go through this
|
|
327
|
-
* function — making "forgot a field" impossible by construction.
|
|
328
|
-
*
|
|
329
|
-
* The three operations:
|
|
330
|
-
* 1. Segment cache — update from server-provided segment metadata
|
|
331
|
-
* 2. Navigation state — params + pathname for useSegmentParams/usePathname
|
|
332
|
-
* 3. History stack — store the payload for instant back/forward replay
|
|
333
|
-
*/
|
|
334
|
-
function commitNavigation(
|
|
335
|
-
url: string,
|
|
336
|
-
opts: {
|
|
337
|
-
payload: unknown;
|
|
338
|
-
params?: Record<string, string | string[]> | null;
|
|
339
|
-
segmentInfo?: SegmentInfo[] | null;
|
|
340
|
-
/** When true, clear the segment cache if segmentInfo is empty
|
|
341
|
-
* (popstate replay for entries without layout metadata). */
|
|
342
|
-
clearSegmentCacheOnEmpty?: boolean;
|
|
343
|
-
}
|
|
344
|
-
): NavigationState {
|
|
345
|
-
if (opts.segmentInfo && opts.segmentInfo.length > 0) {
|
|
346
|
-
updateSegmentCache(opts.segmentInfo);
|
|
347
|
-
} else if (opts.clearSegmentCacheOnEmpty) {
|
|
348
|
-
segmentCache.clear();
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const navState = updateNavigationState(opts.params, url);
|
|
352
|
-
|
|
353
|
-
historyStack.push(url, {
|
|
354
|
-
payload: opts.payload,
|
|
355
|
-
params: navState.params,
|
|
356
|
-
segmentInfo: opts.segmentInfo,
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
return navState;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Wrap a navigation in the standard abort/pending/cleanup lifecycle.
|
|
364
|
-
* Consolidates the createNavAbort + setPending + staleness-guarded
|
|
365
|
-
* finally that was duplicated across navigate, refresh, and both
|
|
366
|
-
* handlePopState paths. AbortErrors are swallowed (not application
|
|
367
|
-
* errors); all other errors propagate to the caller.
|
|
368
|
-
*/
|
|
369
|
-
async function runNavigation(
|
|
370
|
-
url: string,
|
|
371
|
-
fn: (navAbort: AbortController) => Promise<void>,
|
|
372
|
-
externalSignal?: AbortSignal
|
|
373
|
-
): Promise<void> {
|
|
374
|
-
const navAbort = createNavAbort(externalSignal);
|
|
375
|
-
setPending(true, url);
|
|
376
|
-
try {
|
|
377
|
-
await fn(navAbort);
|
|
378
|
-
} catch (error) {
|
|
379
|
-
if (isAbortError(error)) return;
|
|
380
|
-
throw error;
|
|
381
|
-
} finally {
|
|
382
|
-
if (currentNavAbort === navAbort) {
|
|
383
|
-
currentNavAbort = null;
|
|
384
|
-
setPending(false);
|
|
385
|
-
deps.completeRouterNavigation?.();
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Resolve thenable payloads in the test/fallback path (no navigateTransition).
|
|
392
|
-
* In production, React handles thenables from createFromFetch directly via
|
|
393
|
-
* Suspense. In tests, renderRoot is a plain mock that expects resolved values.
|
|
394
|
-
*/
|
|
395
|
-
async function resolveForFallback(payload: unknown): Promise<unknown> {
|
|
396
|
-
if (
|
|
397
|
-
!deps.navigateTransition &&
|
|
398
|
-
payload != null &&
|
|
399
|
-
typeof payload === 'object' &&
|
|
400
|
-
'then' in payload
|
|
401
|
-
) {
|
|
402
|
-
return await (payload as PromiseLike<unknown>);
|
|
403
|
-
}
|
|
404
|
-
return payload;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function isPartialNavigation(skippedSegments: string[] | null | undefined): boolean {
|
|
408
|
-
return skippedSegments != null && skippedSegments.length > 0;
|
|
409
|
-
}
|
|
410
38
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
): NavigationState {
|
|
442
|
-
const resolvedParams = params ?? {};
|
|
443
|
-
// Module-level fallback for tests (no NavigationProvider) and SSR
|
|
444
|
-
setCurrentParams(resolvedParams);
|
|
445
|
-
// globalThis bridge — kept for backward compat
|
|
446
|
-
const parsed = new URL(url, 'http://localhost');
|
|
447
|
-
const pathname = parsed.pathname || '/';
|
|
448
|
-
const search = parsed.search;
|
|
449
|
-
const navState: NavigationState = { params: resolvedParams, pathname, search };
|
|
450
|
-
setNavigationState(navState);
|
|
451
|
-
return navState;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Render a payload via navigateTransition (production) or renderRoot (tests).
|
|
456
|
-
* The perform callback should fetch data, call commitNavigation, and return
|
|
457
|
-
* the FetchResult plus the NavigationState.
|
|
458
|
-
*
|
|
459
|
-
* State management (segmentCache, navState, historyStack) is handled by
|
|
460
|
-
* commitNavigation inside perform — this function only handles rendering.
|
|
461
|
-
*/
|
|
462
|
-
async function renderViaTransition(
|
|
463
|
-
url: string,
|
|
464
|
-
perform: () => Promise<FetchResult & { navState: NavigationState }>
|
|
465
|
-
): Promise<void> {
|
|
466
|
-
if (deps.navigateTransition) {
|
|
467
|
-
await deps.navigateTransition(url, async (wrapPayload) => {
|
|
468
|
-
const result = await perform();
|
|
469
|
-
|
|
470
|
-
if (isPartialNavigation(result.skippedSegments)) {
|
|
471
|
-
const segmentUpdates = buildSegmentUpdates(result);
|
|
472
|
-
|
|
473
|
-
// Re-wrap the CURRENT element with new context values.
|
|
474
|
-
// SegmentOutlets read updates from SegmentUpdateContext;
|
|
475
|
-
// NavigationProvider gets new params/pathname.
|
|
476
|
-
const element = wrapPayload(
|
|
477
|
-
deps._getCurrentPayload?.() ?? result.payload,
|
|
478
|
-
result.navState,
|
|
479
|
-
segmentUpdates
|
|
480
|
-
);
|
|
481
|
-
return { element, decodePromise: result.decodePromise };
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// Full navigation — empty updates, render the new tree.
|
|
485
|
-
const element = wrapPayload(result.payload, result.navState);
|
|
486
|
-
return { element, decodePromise: result.decodePromise };
|
|
487
|
-
});
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
// Fallback: no transition (tests, no React tree)
|
|
491
|
-
const result = await perform();
|
|
492
|
-
if (!isPartialNavigation(result.skippedSegments)) {
|
|
493
|
-
renderPayload(result.payload, result.navState);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Perform a hard navigation to a URL. When the target is same-document
|
|
499
|
-
* (same pathname+search, different hash), `location.href = url` is a
|
|
500
|
-
* hash change — no reload. Use `location.reload()` instead (TIM-1235).
|
|
501
|
-
*
|
|
502
|
-
* `fromUrl` is the departing URL captured before any pushState or
|
|
503
|
-
* Navigation API commit — deps.getCurrentUrl() is unreliable here
|
|
504
|
-
* because the URL may have already been updated.
|
|
505
|
-
*/
|
|
506
|
-
function hardNavigate(url: string, fromUrl: string): void {
|
|
507
|
-
const current = new URL(fromUrl, window.location.origin);
|
|
508
|
-
const target = new URL(url, window.location.origin);
|
|
509
|
-
if (target.pathname === current.pathname && target.search === current.search) {
|
|
510
|
-
// Assign first to update the hash in the address bar, then reload
|
|
511
|
-
// to force a full page load. href alone is a no-op hash change.
|
|
512
|
-
window.location.href = url;
|
|
513
|
-
window.location.reload();
|
|
514
|
-
} else {
|
|
515
|
-
window.location.href = url;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/** Run a callback after the next paint (after React commit). */
|
|
520
|
-
function afterPaint(callback: () => void): void {
|
|
521
|
-
if (deps.afterPaint) {
|
|
522
|
-
deps.afterPaint(callback);
|
|
523
|
-
} else {
|
|
524
|
-
callback();
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Schedule scroll restoration after the next paint and fire the
|
|
530
|
-
* scroll-restored event. Used by navigate, popstate, and refresh.
|
|
531
|
-
*/
|
|
532
|
-
function restoreScrollAfterPaint(scrollY: number): void {
|
|
533
|
-
afterPaint(() => {
|
|
534
|
-
deps.scrollTo(0, scrollY);
|
|
535
|
-
window.dispatchEvent(new Event('timber:scroll-restored'));
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* Scroll to the element matching the URL #fragment after paint, falling
|
|
541
|
-
* back to scroll-to-top when no element matches (matching full-page-load
|
|
542
|
-
* behavior for an unknown fragment). Used by forward navigation to a
|
|
543
|
-
* hash-bearing URL (TIM-1035).
|
|
544
|
-
*/
|
|
545
|
-
function scrollToHashAfterPaint(hash: string): void {
|
|
546
|
-
afterPaint(() => {
|
|
547
|
-
if (deps.scrollToHash?.(hash) !== true) {
|
|
548
|
-
deps.scrollTo(0, 0);
|
|
549
|
-
}
|
|
550
|
-
window.dispatchEvent(new Event('timber:scroll-restored'));
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
* Core navigation logic shared between the transition and fallback paths.
|
|
556
|
-
* Fetches the RSC payload, updates all state, and returns the result.
|
|
557
|
-
*/
|
|
558
|
-
async function performNavigationFetch(
|
|
559
|
-
url: string,
|
|
560
|
-
options: {
|
|
561
|
-
replace: boolean;
|
|
562
|
-
commitUrl?: string;
|
|
563
|
-
signal?: AbortSignal;
|
|
564
|
-
skipHistory?: boolean;
|
|
565
|
-
departingUrl?: string;
|
|
566
|
-
}
|
|
567
|
-
): Promise<FetchResult & { navState: NavigationState }> {
|
|
568
|
-
// Check prefetch cache first. A negative entry means a prior prefetch
|
|
569
|
-
// determined this URL is not a route (non-RSC Content-Type). Hard-navigate
|
|
570
|
-
// immediately without a second fetch. See TIM-1231.
|
|
571
|
-
if (prefetchCache.isNegative(url)) {
|
|
572
|
-
prefetchCache.consume(url);
|
|
573
|
-
throw new NonRscResponse(url);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// PrefetchResult has optional segmentInfo/params fields — normalize
|
|
577
|
-
// to null for FetchResult compatibility.
|
|
578
|
-
const prefetched = prefetchCache.consume(url);
|
|
579
|
-
let result: FetchResult | undefined = prefetched
|
|
580
|
-
? {
|
|
581
|
-
payload: prefetched.payload,
|
|
582
|
-
decodePromise: null,
|
|
583
|
-
segmentInfo: prefetched.segmentInfo ?? null,
|
|
584
|
-
params: prefetched.params ?? null,
|
|
585
|
-
skippedSegments: prefetched.skippedSegments ?? null,
|
|
586
|
-
}
|
|
587
|
-
: undefined;
|
|
588
|
-
|
|
589
|
-
if (result === undefined) {
|
|
590
|
-
// Fetch RSC payload with state tree for partial rendering.
|
|
591
|
-
// Send departing URL (pre-navigation) for slot skip comparison.
|
|
592
|
-
// getCurrentUrl() can't be used here because the Navigation API
|
|
593
|
-
// may have already committed the destination URL (TIM-1232).
|
|
594
|
-
const stateTree = deps.clientSegmentCache ? segmentCache.serializeStateTree() : undefined;
|
|
595
|
-
const rawDepartingUrl = options.departingUrl ?? deps.getCurrentUrl();
|
|
596
|
-
const currentUrl = rawDepartingUrl.startsWith('http')
|
|
597
|
-
? new URL(rawDepartingUrl).pathname
|
|
598
|
-
: new URL(rawDepartingUrl, 'http://localhost').pathname;
|
|
599
|
-
result = await fetchRscPayload(url, deps, stateTree, currentUrl, options.signal);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Update the browser history — skip when the Navigation API has already
|
|
603
|
-
// updated the URL via event.intercept() (external navigations).
|
|
604
|
-
// The committed URL keeps the #fragment (commitUrl) even though the
|
|
605
|
-
// fetch/history-stack URL is hash-less (TIM-1035).
|
|
606
|
-
if (!options.skipHistory) {
|
|
607
|
-
const commitUrl = options.commitUrl ?? url;
|
|
608
|
-
// Set the router-navigating flag so the Navigation API's navigate
|
|
609
|
-
// listener doesn't double-intercept this pushState/replaceState.
|
|
610
|
-
deps.setRouterNavigating?.(true);
|
|
611
|
-
if (options.replace) {
|
|
612
|
-
deps.replaceState({ timber: true, scrollY: 0 }, '', commitUrl);
|
|
613
|
-
} else {
|
|
614
|
-
deps.pushState({ timber: true, scrollY: 0 }, '', commitUrl);
|
|
615
|
-
}
|
|
616
|
-
deps.setRouterNavigating?.(false);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// Resolve thenable payloads in the test path so popstate replay
|
|
620
|
-
// and renderPayload receive plain values (see resolveForFallback).
|
|
621
|
-
const payload = await resolveForFallback(result.payload);
|
|
622
|
-
|
|
623
|
-
// Atomically update all navigation state via commitNavigation.
|
|
624
|
-
// Partial navigations and slot-skip navigations store null payload —
|
|
625
|
-
// the RSC tree contains skip holes that can't be replayed standalone;
|
|
626
|
-
// popstate will fetch fresh.
|
|
627
|
-
const isPartial = isPartialNavigation(result.skippedSegments);
|
|
628
|
-
const hasSkippedSlots = result.segmentInfo?.some((s) => s.slot && s.skipped) ?? false;
|
|
629
|
-
const navState = commitNavigation(url, {
|
|
630
|
-
payload: isPartial || hasSkippedSlots ? null : payload,
|
|
631
|
-
params: result.params,
|
|
632
|
-
segmentInfo: result.segmentInfo,
|
|
39
|
+
// Every write that makes a page "current", and the read that reports the
|
|
40
|
+
// current page back to the server. See navigation-commit.ts.
|
|
41
|
+
const { prepareNavigation, currentStateTree, updateSegmentCache } = createNavigationCommitter({
|
|
42
|
+
segmentCache,
|
|
43
|
+
historyStack,
|
|
44
|
+
clientSegmentCache: () => deps.clientSegmentCache,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Ownership of the router: who is navigating, whose fetch may be cut, and
|
|
48
|
+
// the pending store TopLoader subscribes to. See router-lifecycle.ts.
|
|
49
|
+
const {
|
|
50
|
+
currentNavAbort,
|
|
51
|
+
createNavAbort,
|
|
52
|
+
runNavigation,
|
|
53
|
+
markHandedOff,
|
|
54
|
+
forgetOlderHandoffs,
|
|
55
|
+
isPending,
|
|
56
|
+
getPendingUrl,
|
|
57
|
+
onPendingChange,
|
|
58
|
+
} = createNavigationLifecycle(deps);
|
|
59
|
+
|
|
60
|
+
// Fetch → commit → hand to React. See router-pipeline.ts.
|
|
61
|
+
const { performNavigationFetch, renderViaTransition, renderPayload, resolveForFallback } =
|
|
62
|
+
createNavigationPipeline({
|
|
63
|
+
deps,
|
|
64
|
+
prefetchCache,
|
|
65
|
+
currentStateTree,
|
|
66
|
+
prepareNavigation,
|
|
67
|
+
markHandedOff,
|
|
68
|
+
forgetOlderHandoffs,
|
|
633
69
|
});
|
|
634
70
|
|
|
635
|
-
|
|
636
|
-
}
|
|
71
|
+
// Post-paint scroll effects.
|
|
72
|
+
const { restoreScrollAfterPaint, scrollToHashAfterPaint } = createScrollEffects(deps);
|
|
73
|
+
|
|
74
|
+
// The only ways out of the SPA. Every failure branch that gives up on
|
|
75
|
+
// client-side navigation goes through one of these, so the "does this
|
|
76
|
+
// navigation still own the router?" question is asked once rather than
|
|
77
|
+
// per branch (TIM-1275, TIM-1276).
|
|
78
|
+
const { leaveSpaIfOwned, leaveSpaSuperseding } = createSpaExits({
|
|
79
|
+
currentNavAbort,
|
|
80
|
+
supersede: () => void createNavAbort(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Every way an RSC fetch can fail that the router answers rather than
|
|
84
|
+
// rethrows. Shared by navigate(), refresh(), and traversals so a path that
|
|
85
|
+
// grows a new branch grows it for all of them (TIM-1277).
|
|
86
|
+
const recoverFromNavigationError = createNavigationRecovery({
|
|
87
|
+
currentNavAbort,
|
|
88
|
+
leaveSpaIfOwned,
|
|
89
|
+
// Hoisted — `navigate` is a function declaration below.
|
|
90
|
+
navigate: (url) => navigate(url, { replace: true }),
|
|
91
|
+
});
|
|
637
92
|
|
|
638
93
|
async function navigate(url: string, options: NavigationOptions = {}): Promise<void> {
|
|
639
94
|
const scroll = options.scroll !== false;
|
|
@@ -667,6 +122,17 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
667
122
|
deps.replaceState({ timber: true, scrollY: currentScrollY }, '', deps.getCurrentUrl());
|
|
668
123
|
}
|
|
669
124
|
|
|
125
|
+
// This bundle has been superseded. Every subsequent navigation is a full
|
|
126
|
+
// document load — it is the only way to get code that matches the server,
|
|
127
|
+
// and it is the recovery for a skew detected on an earlier navigation.
|
|
128
|
+
// Deliberately *not* a reload: recovery rides on navigations the user
|
|
129
|
+
// asked for, so a rollout that keeps serving mismatched builds costs one
|
|
130
|
+
// document load per click instead of a self-issued reload loop (TIM-1275).
|
|
131
|
+
// Scroll is saved above, so back-navigation still restores correctly.
|
|
132
|
+
if (isClientStale()) {
|
|
133
|
+
await leaveSpaSuperseding(url, departingUrl);
|
|
134
|
+
}
|
|
135
|
+
|
|
670
136
|
let effectiveSkipHistory = skipHistory;
|
|
671
137
|
|
|
672
138
|
await runNavigation(
|
|
@@ -684,7 +150,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
684
150
|
}
|
|
685
151
|
|
|
686
152
|
try {
|
|
687
|
-
await renderViaTransition(fetchUrl, () =>
|
|
153
|
+
await renderViaTransition(fetchUrl, navAbort, () =>
|
|
688
154
|
performNavigationFetch(fetchUrl, {
|
|
689
155
|
replace,
|
|
690
156
|
commitUrl: url,
|
|
@@ -694,9 +160,6 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
694
160
|
})
|
|
695
161
|
);
|
|
696
162
|
|
|
697
|
-
// Notify nuqs adapter (and any other listeners) that navigation completed.
|
|
698
|
-
window.dispatchEvent(new Event('timber:navigation-end'));
|
|
699
|
-
|
|
700
163
|
// Scroll-to-top on forward navigation, scroll to the #fragment target
|
|
701
164
|
// when the URL has one, or restore captured position for scroll={false}.
|
|
702
165
|
if (scroll && hash) {
|
|
@@ -705,26 +168,12 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
705
168
|
restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
|
|
706
169
|
}
|
|
707
170
|
} catch (error) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (error
|
|
714
|
-
if (currentNavAbort !== navAbort) return;
|
|
715
|
-
await navigate(error.redirectUrl, { replace: true });
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
if (error instanceof ServerErrorResponse) {
|
|
719
|
-
setHardNavigating(true);
|
|
720
|
-
hardNavigate(url, departingUrl);
|
|
721
|
-
await new Promise(() => {});
|
|
722
|
-
}
|
|
723
|
-
if (error instanceof NonRscResponse) {
|
|
724
|
-
setHardNavigating(true);
|
|
725
|
-
hardNavigate(url, departingUrl);
|
|
726
|
-
await new Promise(() => {});
|
|
727
|
-
}
|
|
171
|
+
// `url` is where the user asked to go and `departingUrl` is where
|
|
172
|
+
// they were: a failure here finishes the click as a full document
|
|
173
|
+
// load of the destination, fragment included (TIM-1234). Reloading
|
|
174
|
+
// instead would rebuild the page they were *leaving* and discard
|
|
175
|
+
// the click (TIM-1275).
|
|
176
|
+
if (await recoverFromNavigationError(error, navAbort, url, departingUrl)) return;
|
|
728
177
|
throw error;
|
|
729
178
|
}
|
|
730
179
|
},
|
|
@@ -732,28 +181,81 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
732
181
|
);
|
|
733
182
|
}
|
|
734
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Fetch a URL's RSC payload, commit it, and render — the pipeline shared by
|
|
186
|
+
* refresh() and an uncached back/forward traversal. Both can be the path
|
|
187
|
+
* that discovers a version skew, and both recover the same way (TIM-1275).
|
|
188
|
+
*
|
|
189
|
+
* Not `performNavigationFetch`: neither caller consults the prefetch cache
|
|
190
|
+
* (there is no link to have hovered) and neither moves the address bar (the
|
|
191
|
+
* browser is already where it is going), so the fetch is a plain one.
|
|
192
|
+
*/
|
|
193
|
+
async function fetchCommitAndRender(
|
|
194
|
+
url: string,
|
|
195
|
+
opts: {
|
|
196
|
+
/** Omitted by refresh(): the server renders the complete payload. */
|
|
197
|
+
stateTree?: { segments: string[] };
|
|
198
|
+
/** Restored after paint when present — traversals only. */
|
|
199
|
+
scrollY?: number;
|
|
200
|
+
externalSignal?: AbortSignal;
|
|
201
|
+
} = {}
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
await runNavigation(
|
|
204
|
+
url,
|
|
205
|
+
async (navAbort) => {
|
|
206
|
+
try {
|
|
207
|
+
await renderViaTransition(url, navAbort, async () => {
|
|
208
|
+
const result = await fetchRscPayload(
|
|
209
|
+
url,
|
|
210
|
+
deps,
|
|
211
|
+
opts.stateTree,
|
|
212
|
+
undefined,
|
|
213
|
+
navAbort.signal
|
|
214
|
+
);
|
|
215
|
+
const payload = await resolveForFallback(result.payload);
|
|
216
|
+
const params = await result.params;
|
|
217
|
+
const { navState, commit } = prepareNavigation(url, {
|
|
218
|
+
payload,
|
|
219
|
+
params,
|
|
220
|
+
segmentInfo: result.segmentInfo,
|
|
221
|
+
status: result.status,
|
|
222
|
+
});
|
|
223
|
+
return { ...result, payload, params, navState, commit };
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
// Neither path is a navigate(), and neither has a caller that
|
|
227
|
+
// handles a rejection — `refresh()` is `void`-called everywhere it
|
|
228
|
+
// is used, and nobody awaits a traversal at all. Without the same
|
|
229
|
+
// recovery `navigate()` gets, a 500 on Back left the user on the
|
|
230
|
+
// old document with an unhandled rejection in the console
|
|
231
|
+
// (TIM-1277).
|
|
232
|
+
//
|
|
233
|
+
// `url` is both the destination and the departure point: the
|
|
234
|
+
// browser has already traversed, and `refresh()` is by definition
|
|
235
|
+
// where it already is. So `hardNavigate()` takes its same-document
|
|
236
|
+
// branch and reloads rather than pushing an entry.
|
|
237
|
+
if (await recoverFromNavigationError(error, navAbort, url, url)) return;
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (opts.scrollY !== undefined) restoreScrollAfterPaint(opts.scrollY);
|
|
242
|
+
},
|
|
243
|
+
opts.externalSignal
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
735
247
|
async function refresh(): Promise<void> {
|
|
736
248
|
const currentUrl = deps.getCurrentUrl();
|
|
737
249
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
);
|
|
748
|
-
const payload = await resolveForFallback(result.payload);
|
|
749
|
-
const navState = commitNavigation(currentUrl, {
|
|
750
|
-
payload,
|
|
751
|
-
params: result.params,
|
|
752
|
-
segmentInfo: result.segmentInfo,
|
|
753
|
-
});
|
|
754
|
-
return { ...result, payload, navState };
|
|
755
|
-
});
|
|
756
|
-
});
|
|
250
|
+
// A refresh on a stale client is a full document load of the current URL.
|
|
251
|
+
// Same rule as navigate(): the RSC fetch would only be answered with
|
|
252
|
+
// another reload signal, and this is caller-initiated, not spontaneous
|
|
253
|
+
// (TIM-1275).
|
|
254
|
+
if (isClientStale()) {
|
|
255
|
+
await leaveSpaSuperseding(currentUrl, currentUrl);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await fetchCommitAndRender(currentUrl);
|
|
757
259
|
}
|
|
758
260
|
|
|
759
261
|
async function handlePopState(
|
|
@@ -761,6 +263,18 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
761
263
|
scrollY: number = 0,
|
|
762
264
|
externalSignal?: AbortSignal
|
|
763
265
|
): Promise<void> {
|
|
266
|
+
// A stale client hard-navigates on back/forward too — this path never
|
|
267
|
+
// reaches navigate(), so it needs the same check. `url` is where the
|
|
268
|
+
// browser has *already* traversed to, so leaveSpa's same-document
|
|
269
|
+
// branch reloads there rather than pushing a new entry (TIM-1275).
|
|
270
|
+
//
|
|
271
|
+
// This overrides the cached-payload replay below on purpose. Replaying
|
|
272
|
+
// would work — the payload was decoded by this bundle — but it would keep
|
|
273
|
+
// the user on superseded code for as long as they browse within history.
|
|
274
|
+
if (isClientStale()) {
|
|
275
|
+
await leaveSpaSuperseding(url, url);
|
|
276
|
+
}
|
|
277
|
+
|
|
764
278
|
// Scroll position is read from history.state by the caller (browser-entry.ts)
|
|
765
279
|
// and passed in. This is more reliable than tracking scroll per-URL in memory
|
|
766
280
|
// because the browser maintains per-entry state even with duplicate URLs.
|
|
@@ -779,13 +293,19 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
779
293
|
// clearSegmentCacheOnEmpty: popstate to an entry without layout
|
|
780
294
|
// metadata (e.g., initial SSR page) clears the cache so the next
|
|
781
295
|
// forward navigation gets a full render.
|
|
782
|
-
|
|
296
|
+
//
|
|
297
|
+
// Committed immediately: the replay is synchronous and its render
|
|
298
|
+
// goes out through `transitionRender`, so there is no in-flight
|
|
299
|
+
// window for a successor to supersede. `runNavigation` has already
|
|
300
|
+
// superseded whatever was in flight before it (TIM-1022).
|
|
301
|
+
const { navState, commit } = prepareNavigation(url, {
|
|
783
302
|
payload: entry.payload,
|
|
784
303
|
params: entry.params,
|
|
785
304
|
segmentInfo: entry.segmentInfo,
|
|
786
305
|
clearSegmentCacheOnEmpty: true,
|
|
787
306
|
});
|
|
788
|
-
|
|
307
|
+
commit();
|
|
308
|
+
renderPayload(entry.payload, navState, entry.params);
|
|
789
309
|
restoreScrollAfterPaint(scrollY);
|
|
790
310
|
},
|
|
791
311
|
externalSignal
|
|
@@ -795,27 +315,11 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
795
315
|
// This happens when navigating back to the initial SSR'd page
|
|
796
316
|
// (its payload is null since it was rendered via SSR, not RSC fetch)
|
|
797
317
|
// or when the entry doesn't exist at all.
|
|
798
|
-
await
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
? segmentCache.serializeStateTree()
|
|
804
|
-
: undefined;
|
|
805
|
-
const result = await fetchRscPayload(url, deps, stateTree, undefined, navAbort.signal);
|
|
806
|
-
const payload = await resolveForFallback(result.payload);
|
|
807
|
-
const navState = commitNavigation(url, {
|
|
808
|
-
payload,
|
|
809
|
-
params: result.params,
|
|
810
|
-
segmentInfo: result.segmentInfo,
|
|
811
|
-
});
|
|
812
|
-
return { ...result, payload, navState };
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
restoreScrollAfterPaint(scrollY);
|
|
816
|
-
},
|
|
817
|
-
externalSignal
|
|
818
|
-
);
|
|
318
|
+
await fetchCommitAndRender(url, {
|
|
319
|
+
stateTree: currentStateTree(),
|
|
320
|
+
scrollY,
|
|
321
|
+
externalSignal,
|
|
322
|
+
});
|
|
819
323
|
}
|
|
820
324
|
}
|
|
821
325
|
|
|
@@ -824,28 +328,53 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
824
328
|
* Called on hover of <Link prefetch> elements.
|
|
825
329
|
*/
|
|
826
330
|
function prefetch(url: string): void {
|
|
331
|
+
// A stale client hard-navigates on every navigation, so a prefetched RSC
|
|
332
|
+
// payload could never be consumed — and the fetch would be answered with
|
|
333
|
+
// another reload signal anyway (TIM-1275).
|
|
334
|
+
if (isClientStale()) return;
|
|
335
|
+
|
|
827
336
|
// Strip fragment — it's client-only and would swallow the _rsc cache-bust
|
|
828
337
|
// param into the hash. The hash-less key also matches navigate()'s fetchUrl.
|
|
829
338
|
const hashIndex = url.indexOf('#');
|
|
830
339
|
const fetchUrl = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
831
340
|
|
|
341
|
+
// The page the link is on. Sent as X-Timber-URL so the prefetched payload
|
|
342
|
+
// is the one the click would have produced — an intercepting link hovered
|
|
343
|
+
// from /gallery must prefetch the modal, not the full page — and stored
|
|
344
|
+
// under the same source so only a click departing from here consumes it
|
|
345
|
+
// (TIM-1265). The key is built by the pipeline, which is also what builds
|
|
346
|
+
// the one the click looks up with, so the two cannot disagree.
|
|
347
|
+
const stateTree = currentStateTree();
|
|
348
|
+
const cacheKey = prefetchKeyFor(fetchUrl, deps.getCurrentUrl(), stateTree);
|
|
349
|
+
const from = cacheKey.from;
|
|
350
|
+
|
|
832
351
|
// Don't prefetch if already cached
|
|
833
|
-
if (prefetchCache.get(
|
|
352
|
+
if (prefetchCache.get(cacheKey) !== undefined) return;
|
|
834
353
|
if (historyStack.has(fetchUrl)) return;
|
|
835
354
|
|
|
836
355
|
// Fire-and-forget fetch
|
|
837
|
-
|
|
838
|
-
void fetchRscPayload(fetchUrl, deps, stateTree).then(
|
|
356
|
+
void fetchRscPayload(fetchUrl, deps, stateTree, from).then(
|
|
839
357
|
(result) => {
|
|
840
|
-
|
|
841
|
-
|
|
358
|
+
// The decode rejects separately from the fetch, so the stale shapes
|
|
359
|
+
// reach us here rather than in the rejection handler below — swallowing
|
|
360
|
+
// it wholesale threw away the same evidence (TIM-1275).
|
|
361
|
+
result.decodePromise?.catch((error: unknown) => void recordSkew(error));
|
|
362
|
+
prefetchCache.set(cacheKey, result);
|
|
842
363
|
},
|
|
843
364
|
(error) => {
|
|
844
365
|
if (error instanceof NonRscResponse) {
|
|
845
|
-
prefetchCache.setNegative(
|
|
366
|
+
prefetchCache.setNegative(cacheKey);
|
|
846
367
|
return;
|
|
847
368
|
}
|
|
848
|
-
//
|
|
369
|
+
// A prefetch can be the first thing to learn we are stale — during a
|
|
370
|
+
// rollout it may reach the new deployment while the click that
|
|
371
|
+
// follows reaches the old one and completes as an ordinary SPA
|
|
372
|
+
// navigation, throwing the evidence away. Record it, but never
|
|
373
|
+
// recover from here: nobody navigated, and a page load triggered by
|
|
374
|
+
// a hover is exactly the unasked-for reload this design removed. The
|
|
375
|
+
// next click takes the document-load path (TIM-1275).
|
|
376
|
+
recordSkew(error);
|
|
377
|
+
// Any other prefetch failure is non-fatal — navigation fetches fresh.
|
|
849
378
|
}
|
|
850
379
|
);
|
|
851
380
|
}
|
|
@@ -854,29 +383,36 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
854
383
|
navigate,
|
|
855
384
|
refresh,
|
|
856
385
|
handlePopState,
|
|
857
|
-
isPending
|
|
858
|
-
getPendingUrl
|
|
859
|
-
onPendingChange
|
|
860
|
-
pendingListeners.add(listener);
|
|
861
|
-
return () => pendingListeners.delete(listener);
|
|
862
|
-
},
|
|
386
|
+
isPending,
|
|
387
|
+
getPendingUrl,
|
|
388
|
+
onPendingChange,
|
|
863
389
|
prefetch,
|
|
864
|
-
applyRevalidation(
|
|
865
|
-
// Render the piggybacked
|
|
866
|
-
//
|
|
867
|
-
//
|
|
390
|
+
applyRevalidation(payloadRoot: unknown): void {
|
|
391
|
+
// Render the piggybacked payload from a server action response. Updates
|
|
392
|
+
// the current history entry with the fresh payload — same as refresh()
|
|
393
|
+
// but without a server fetch.
|
|
394
|
+
//
|
|
395
|
+
// The revalidation renderer builds its tree through the same
|
|
396
|
+
// `withPublishedParams` every route payload goes through, so it is a
|
|
397
|
+
// payload root and carries its own params. It is already decoded by the
|
|
398
|
+
// time it reaches here (action-dispatch awaits the Flight response), so
|
|
399
|
+
// the split is synchronous.
|
|
868
400
|
const currentUrl = deps.getCurrentUrl();
|
|
401
|
+
const tree = readPayloadTree(payloadRoot);
|
|
402
|
+
const params = readPublishedParams(payloadRoot);
|
|
869
403
|
|
|
870
404
|
// Preserve existing segmentInfo so away-and-back navigation replays
|
|
871
|
-
// with a correct segment cache (TIM-1037).
|
|
872
|
-
// so dynamic route params aren't cleared to {}.
|
|
405
|
+
// with a correct segment cache (TIM-1037).
|
|
873
406
|
const existingEntry = historyStack.get(currentUrl);
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
407
|
+
// Synchronous, like the popstate replay: there is no fetch to be
|
|
408
|
+
// superseded, and the payload is already decoded.
|
|
409
|
+
const { navState, commit } = prepareNavigation(currentUrl, {
|
|
410
|
+
payload: tree,
|
|
411
|
+
params,
|
|
877
412
|
segmentInfo: existingEntry?.segmentInfo,
|
|
878
413
|
});
|
|
879
|
-
|
|
414
|
+
commit();
|
|
415
|
+
renderPayload(tree, navState, params);
|
|
880
416
|
},
|
|
881
417
|
initSegmentCache: (segments: SegmentInfo[]) => updateSegmentCache(segments),
|
|
882
418
|
segmentCache,
|