@timber-js/app 0.2.0-alpha.185 → 0.2.0-alpha.186
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-C-Rw9vPc.js} +3 -3
- package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-C-Rw9vPc.js.map} +1 -1
- package/dist/_chunks/als-slots-mFweg276.js +27 -0
- package/dist/_chunks/als-slots-mFweg276.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-Cd0VZ_Pd.js} +3 -3
- package/dist/_chunks/{cache-api-B-lhk9p4.js.map → cache-api-Cd0VZ_Pd.js.map} +1 -1
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js +1857 -0
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +1 -0
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-Cs0uZXea.js} +89 -12
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-Cs0uZXea.js.map} +1 -1
- package/dist/_chunks/error-boundary-DpYRI_I1.js +330 -0
- package/dist/_chunks/error-boundary-DpYRI_I1.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/fs-identity-D7vb6GdI.js +208 -0
- package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
- package/dist/_chunks/{logger-AWfuX-KJ.js → logger-N7e5auP0.js} +9 -13
- package/dist/_chunks/logger-N7e5auP0.js.map +1 -0
- package/dist/_chunks/navigation-root-B29qg0_T.js +274 -0
- package/dist/_chunks/navigation-root-B29qg0_T.js.map +1 -0
- package/dist/_chunks/param-value-C8TNYchQ.js +66 -0
- package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
- package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-DEGLSJs3.js} +2 -1
- package/dist/_chunks/plugin-context-DEGLSJs3.js.map +1 -0
- 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-payload-path-B_LBodc2.js +48 -0
- package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
- package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
- package/dist/_chunks/{segment-classify-Byy425ng.js.map → segment-classify-C539Pa2O.js.map} +1 -1
- 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-segment-params-ClyUNq4d.js +128 -0
- package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +1 -0
- package/dist/_chunks/{walkers-_6zKFlch.js → walkers-BhhwI9TD.js} +21 -87
- package/dist/_chunks/walkers-BhhwI9TD.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/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 +2 -3
- 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/cli.js +2 -2
- 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/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 +1 -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.js +18 -11
- 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 +865 -306
- package/dist/client/internal.js.map +1 -1
- 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 +71 -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 +38 -25
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +79 -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-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/cookies/index.js +3 -3
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +906 -321
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +31 -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 +9 -0
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +136 -8
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +18 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- 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/index.js +3 -3
- package/dist/routing/interception.d.ts +235 -6
- package/dist/routing/interception.d.ts.map +1 -1
- 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/walkers.d.ts +6 -0
- package/dist/routing/walkers.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +0 -6
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +176 -2
- package/dist/search-params/index.js.map +1 -1
- package/dist/segment-params/define.d.ts +0 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +69 -1
- package/dist/segment-params/index.js.map +1 -0
- package/dist/server/actions.d.ts +6 -3
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +2 -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/default-status-page.d.ts +45 -0
- package/dist/server/default-status-page.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts +14 -3
- package/dist/server/deny-renderer.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.js +2 -2
- package/dist/server/internal.js +393 -212
- package/dist/server/internal.js.map +1 -1
- package/dist/server/param-coercion.d.ts +10 -1
- 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/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 +32 -2
- 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/error-renderer.d.ts +14 -2
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +2 -10
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -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/rsc-payload.d.ts +3 -2
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +3 -2
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +2 -0
- 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 +6 -0
- package/dist/server/ssr-bridge-types.d.ts.map +1 -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/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-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/31-api-client.mdx +4 -2
- package/docs/learn/02-pages-and-layouts.mdx +1 -1
- package/docs/learn/11-error-handling.mdx +2 -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 +172 -2
- package/docs/more/04-metadata-and-fonts.mdx +1 -1
- package/package.json +2 -2
- package/src/adapters/build-output-helper.ts +116 -3
- package/src/adapters/cloudflare.ts +86 -11
- 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/client/browser-entry/action-dispatch.ts +9 -4
- package/src/client/browser-entry/hydrate.ts +40 -29
- package/src/client/browser-entry/index.ts +15 -1
- 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 +20 -15
- package/src/client/history.ts +13 -6
- package/src/client/internal.ts +4 -3
- package/src/client/link.tsx +55 -8
- package/src/client/navigation-api.ts +3 -3
- package/src/client/navigation-commit.ts +174 -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 +389 -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 +242 -707
- package/src/client/rsc-fetch.ts +169 -102
- package/src/client/segment-cache.ts +121 -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-router.ts +1 -1
- package/src/client/use-segment-params.ts +41 -18
- package/src/index.ts +5 -1
- package/src/plugin-context.ts +32 -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 +59 -2
- package/src/plugins/static-build.ts +315 -73
- package/src/routing/codegen-types.ts +18 -0
- package/src/routing/codegen.ts +50 -10
- package/src/routing/collision-probe.ts +438 -0
- package/src/routing/collision-spaces.ts +281 -0
- package/src/routing/interception.ts +896 -50
- package/src/routing/manifest-codegen.ts +5 -3
- package/src/routing/scanner.ts +114 -89
- 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/walkers.ts +8 -0
- package/src/search-params/define.ts +6 -29
- package/src/segment-params/define.ts +4 -27
- package/src/server/action-handler.ts +3 -1
- package/src/server/actions.ts +6 -3
- package/src/server/als-registry.ts +2 -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/default-status-page.ts +127 -0
- package/src/server/deny-renderer.ts +81 -20
- package/src/server/head-response.ts +54 -0
- package/src/server/param-coercion.ts +48 -18
- package/src/server/pipeline-helpers.ts +0 -45
- package/src/server/pipeline-interception.ts +81 -32
- package/src/server/pipeline-outcome.ts +2 -4
- package/src/server/pipeline-phases.ts +68 -16
- package/src/server/pipeline.ts +57 -10
- package/src/server/prebuilt-runtime.ts +4 -4
- package/src/server/publish-params.ts +42 -0
- package/src/server/request-context.ts +97 -18
- package/src/server/route-element-builder.ts +89 -98
- package/src/server/route-handler.ts +8 -8
- package/src/server/route-matcher.ts +21 -9
- package/src/server/rsc-cache-key-guard.ts +170 -0
- package/src/server/rsc-entry/api-handler.ts +6 -8
- package/src/server/rsc-entry/error-renderer.ts +64 -25
- package/src/server/rsc-entry/helpers.ts +31 -16
- package/src/server/rsc-entry/index.ts +36 -28
- package/src/server/rsc-entry/render-route.ts +9 -4
- package/src/server/rsc-entry/revalidate-renderer.ts +1 -1
- package/src/server/rsc-entry/rsc-payload.ts +14 -25
- package/src/server/rsc-entry/rsc-stream.ts +4 -3
- package/src/server/rsc-entry/ssr-renderer.ts +13 -9
- package/src/server/skippable-prefix.ts +92 -0
- package/src/server/slot-interception.ts +168 -0
- package/src/server/slot-resolver.ts +253 -156
- package/src/server/ssr-bridge-types.ts +6 -0
- package/src/server/ssr-entry.ts +12 -4
- package/src/server/ssr-wrappers.tsx +33 -32
- package/src/server/state-tree-diff.ts +14 -55
- package/src/server/static-generator.ts +65 -27
- package/src/server/static-not-found.ts +134 -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-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/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/router-ref-CtmF-aPv.js.map +0 -1
- package/dist/_chunks/ssr-data-BOWsq18U.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.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/src/plugins/request-dep.ts +0 -499
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
// Segment Cache — stores the mounted segment tree and prefetched payloads
|
|
2
2
|
// See design/19-client-navigation.md for architecture details.
|
|
3
3
|
|
|
4
|
+
import type { PublishedParams } from '../shared/payload-root.js';
|
|
5
|
+
import type { SegmentInfo, StateTree } from '../shared/segment-info.js';
|
|
6
|
+
|
|
4
7
|
// ─── Types ───────────────────────────────────────────────────────
|
|
5
8
|
|
|
6
9
|
/** A prefetched RSC result with optional segment metadata. */
|
|
7
10
|
export interface PrefetchResult {
|
|
8
11
|
payload: unknown;
|
|
12
|
+
/**
|
|
13
|
+
* Completion of the full Flight stream, when the prefetch is still decoding.
|
|
14
|
+
* Carried through to the navigation that consumes the entry so a decode
|
|
15
|
+
* failure reaches *that* navigation's recovery rather than dying with the
|
|
16
|
+
* prefetch (TIM-1275).
|
|
17
|
+
*/
|
|
18
|
+
decodePromise?: Promise<void> | null;
|
|
9
19
|
/** Segment metadata from X-Timber-Segments header for populating the segment cache. */
|
|
10
20
|
segmentInfo?: SegmentInfo[] | null;
|
|
11
|
-
/**
|
|
12
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The params published beside `payload`, split off the same payload root.
|
|
23
|
+
* Carried so a consumed prefetch publishes the params of the tree it is
|
|
24
|
+
* about to render, and not the ones already on screen.
|
|
25
|
+
*/
|
|
26
|
+
params?: PublishedParams | Promise<PublishedParams>;
|
|
13
27
|
/** Segment paths skipped by the server (for client-side merging). */
|
|
14
28
|
skippedSegments?: string[] | null;
|
|
15
29
|
}
|
|
@@ -21,6 +35,11 @@ export interface PrefetchResult {
|
|
|
21
35
|
export interface SegmentNode {
|
|
22
36
|
/** The segment's URL pattern (e.g., "/", "/dashboard", "/projects/[id]") */
|
|
23
37
|
segment: string;
|
|
38
|
+
/**
|
|
39
|
+
* The segment's `app/` directory path, groups included (e.g. `/(browse)/feed`).
|
|
40
|
+
* Undefined for slot nodes and for servers that predate the field.
|
|
41
|
+
*/
|
|
42
|
+
treePath?: string;
|
|
24
43
|
/** The RSC flight payload for this segment (opaque to the cache) */
|
|
25
44
|
payload: unknown;
|
|
26
45
|
/**
|
|
@@ -39,15 +58,6 @@ export interface SegmentNode {
|
|
|
39
58
|
denied?: boolean;
|
|
40
59
|
}
|
|
41
60
|
|
|
42
|
-
/**
|
|
43
|
-
* Serialized state tree sent via X-Timber-State-Tree header.
|
|
44
|
-
* Only sync segments are included — async segments always re-render.
|
|
45
|
-
*/
|
|
46
|
-
export interface StateTree {
|
|
47
|
-
segments: string[];
|
|
48
|
-
slots?: string[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
61
|
// ─── Segment Cache ───────────────────────────────────────────────
|
|
52
62
|
|
|
53
63
|
/**
|
|
@@ -85,20 +95,32 @@ export class SegmentCache {
|
|
|
85
95
|
* client can actually merge (i.e., segments whose cached element tree
|
|
86
96
|
* contains an inner SegmentProvider the merger can splice into).
|
|
87
97
|
*
|
|
98
|
+
* `treePaths` is collected UNFILTERED, unlike `segments` and `slots`. Those
|
|
99
|
+
* two answer "what may the server skip re-rendering?", so every reason a
|
|
100
|
+
* segment cannot be reused is a reason to leave it out. `treePaths` answers
|
|
101
|
+
* "what is mounted right now?" for interception scoping — a request-dependent
|
|
102
|
+
* layout is still on screen, and dropping it would silently narrow the scope
|
|
103
|
+
* the server sees and cancel a modal that should open (TIM-1282).
|
|
104
|
+
*
|
|
88
105
|
* This is a performance optimization only, NOT a security boundary.
|
|
89
106
|
* The server always runs all access.ts files regardless of the state tree.
|
|
90
107
|
*/
|
|
91
108
|
serializeStateTree(mergeableFilter?: Set<string>): StateTree {
|
|
92
109
|
const segments: string[] = [];
|
|
93
110
|
const slots: string[] = [];
|
|
111
|
+
const treePaths: string[] = [];
|
|
94
112
|
if (this.root) {
|
|
95
113
|
collectSyncSegments(this.root, segments, mergeableFilter);
|
|
96
114
|
collectSyncSlots(this.root, slots);
|
|
115
|
+
collectTreePaths(this.root, treePaths);
|
|
97
116
|
}
|
|
98
117
|
const tree: StateTree = { segments };
|
|
99
118
|
if (slots.length > 0) {
|
|
100
119
|
tree.slots = slots;
|
|
101
120
|
}
|
|
121
|
+
if (treePaths.length > 0) {
|
|
122
|
+
tree.treePaths = treePaths;
|
|
123
|
+
}
|
|
102
124
|
return tree;
|
|
103
125
|
}
|
|
104
126
|
}
|
|
@@ -117,6 +139,20 @@ function collectSyncSegments(
|
|
|
117
139
|
}
|
|
118
140
|
}
|
|
119
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Recursively collect the `app/` directory path of every mounted segment.
|
|
144
|
+
*
|
|
145
|
+
* Slots are not walked: a slot's own sub-tree can never own an intercepting
|
|
146
|
+
* slot's scope, because scopes are always ordinary segments (`interception.ts`
|
|
147
|
+
* derives one from the slot owner's ancestor chain).
|
|
148
|
+
*/
|
|
149
|
+
function collectTreePaths(node: SegmentNode, out: string[]): void {
|
|
150
|
+
if (node.treePath) out.push(node.treePath);
|
|
151
|
+
for (const child of node.children.values()) {
|
|
152
|
+
collectTreePaths(child, out);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
120
156
|
/** Recursively collect cacheable slot paths from the tree */
|
|
121
157
|
function collectSyncSlots(node: SegmentNode, out: string[]): void {
|
|
122
158
|
if (node.slots) {
|
|
@@ -135,25 +171,6 @@ function collectSyncSlots(node: SegmentNode, out: string[]): void {
|
|
|
135
171
|
|
|
136
172
|
// ─── Segment Tree Builder ────────────────────────────────────────
|
|
137
173
|
|
|
138
|
-
/**
|
|
139
|
-
* Segment metadata from the server, sent via X-Timber-Segments header.
|
|
140
|
-
* Describes a rendered segment's path and whether it's async.
|
|
141
|
-
*/
|
|
142
|
-
export interface SegmentInfo {
|
|
143
|
-
path: string;
|
|
144
|
-
/** Outlet key — includes route group name when applicable (e.g., "/(marketing)"). */
|
|
145
|
-
segmentId?: string;
|
|
146
|
-
isRequestDependent: boolean;
|
|
147
|
-
/** True for parallel route slot entries. Slots are keyed by their slot path (e.g., "/@sidebar"). */
|
|
148
|
-
slot?: boolean;
|
|
149
|
-
/** Parent segment path for slot entries. Used to attach the slot to the correct SegmentNode. */
|
|
150
|
-
parentSegment?: string;
|
|
151
|
-
/** True when the slot's access.ts denied on this render. Denied slots are excluded from the state tree. */
|
|
152
|
-
denied?: boolean;
|
|
153
|
-
/** True when the slot was skipped (cached content reused). Payloads with skipped slots are not replayable. */
|
|
154
|
-
skipped?: boolean;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
174
|
/**
|
|
158
175
|
* Build a SegmentNode tree from flat segment metadata.
|
|
159
176
|
*
|
|
@@ -190,6 +207,7 @@ export function buildSegmentTree(segments: SegmentInfo[]): SegmentNode | undefin
|
|
|
190
207
|
const id = info.segmentId ?? info.path;
|
|
191
208
|
const node: SegmentNode = {
|
|
192
209
|
segment: id,
|
|
210
|
+
treePath: info.treePath,
|
|
193
211
|
payload: null,
|
|
194
212
|
isRequestDependent: info.isRequestDependent,
|
|
195
213
|
children: new Map(),
|
|
@@ -242,6 +260,58 @@ interface PrefetchEntry {
|
|
|
242
260
|
/** Sentinel value for negative cache entries (URL is not a route). */
|
|
243
261
|
const NEGATIVE_ENTRY: PrefetchResult = Object.freeze({ payload: null });
|
|
244
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Identifies a prefetched payload. A payload is addressed by *every* input that
|
|
265
|
+
* changes the server's answer, not by the destination alone.
|
|
266
|
+
*
|
|
267
|
+
* The server's answer for a destination depends on where the request came
|
|
268
|
+
* from — that is the whole mechanism behind intercepting routes, and it also
|
|
269
|
+
* drives slot-skip resolution. A prefetch issued from `/gallery` is a modal
|
|
270
|
+
* payload; the same URL prefetched from `/` is the full page. Keying on the
|
|
271
|
+
* destination alone let the first click after a hover be answered with
|
|
272
|
+
* whichever variant happened to be fetched (TIM-1265).
|
|
273
|
+
*
|
|
274
|
+
* `from` alone stopped being enough in TIM-1282. It is a pathname, and once
|
|
275
|
+
* interception scope comes from the rendered tree rather than the address, one
|
|
276
|
+
* pathname no longer names one source tree: `/photo/1` is the address both for
|
|
277
|
+
* a modal open over `(browse)` and for `(landing)`'s canonical page. A key
|
|
278
|
+
* without the scope hands the modal payload to the canonical page's click,
|
|
279
|
+
* which renders it without the server's scope check ever running.
|
|
280
|
+
*
|
|
281
|
+
* The two reads happen at different instants — `scope` is sampled at hover and
|
|
282
|
+
* again at click — which is why a tree that changed in between produces a
|
|
283
|
+
* *miss* and a refetch rather than a wrong hit.
|
|
284
|
+
*/
|
|
285
|
+
export interface PrefetchKey {
|
|
286
|
+
/** Destination URL — hash-less `pathname + search`, as navigate() fetches it. */
|
|
287
|
+
url: string;
|
|
288
|
+
/** Departing pathname — the `X-Timber-URL` the prefetch was issued with. */
|
|
289
|
+
from: string;
|
|
290
|
+
/**
|
|
291
|
+
* The rendered scope the prefetch was issued with — the `treePaths` half of
|
|
292
|
+
* `X-Timber-State-Tree`, joined. Empty when none crossed.
|
|
293
|
+
*/
|
|
294
|
+
scope: string;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Compose the map key. Length-prefixed rather than delimiter-joined for the
|
|
299
|
+
* same reason `shared/rsc-cache-key.ts` is: any delimiter is forgeable by a
|
|
300
|
+
* value containing it, and a URL may contain any character a delimiter could.
|
|
301
|
+
*/
|
|
302
|
+
function prefetchMapKey(key: PrefetchKey): string {
|
|
303
|
+
return `${key.from.length}:${key.from}:${key.scope.length}:${key.scope}:${key.url}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The `scope` half of a `PrefetchKey`, derived from the state tree that will
|
|
308
|
+
* actually be sent. Taking it from the header value rather than re-walking the
|
|
309
|
+
* cache is the point: the key varies by exactly what the request varies by.
|
|
310
|
+
*/
|
|
311
|
+
export function prefetchScopeOf(stateTree: StateTree | undefined): string {
|
|
312
|
+
return stateTree?.treePaths?.join('\u0000') ?? '';
|
|
313
|
+
}
|
|
314
|
+
|
|
245
315
|
/**
|
|
246
316
|
* Short-lived cache for hover-triggered prefetches. Entries expire after
|
|
247
317
|
* 30 seconds. When a link is clicked, the prefetched payload is consumed
|
|
@@ -254,40 +324,47 @@ export class PrefetchCache {
|
|
|
254
324
|
private static readonly TTL_MS = 30_000;
|
|
255
325
|
private entries = new Map<string, PrefetchEntry>();
|
|
256
326
|
|
|
257
|
-
set(
|
|
258
|
-
this.entries.set(
|
|
327
|
+
set(key: PrefetchKey, result: PrefetchResult): void {
|
|
328
|
+
this.entries.set(prefetchMapKey(key), {
|
|
259
329
|
result,
|
|
260
330
|
expiresAt: Date.now() + PrefetchCache.TTL_MS,
|
|
261
331
|
});
|
|
262
332
|
}
|
|
263
333
|
|
|
264
|
-
get(
|
|
265
|
-
const
|
|
334
|
+
get(key: PrefetchKey): PrefetchResult | undefined {
|
|
335
|
+
const mapKey = prefetchMapKey(key);
|
|
336
|
+
const entry = this.entries.get(mapKey);
|
|
266
337
|
if (!entry) return undefined;
|
|
267
338
|
if (Date.now() >= entry.expiresAt) {
|
|
268
|
-
this.entries.delete(
|
|
339
|
+
this.entries.delete(mapKey);
|
|
269
340
|
return undefined;
|
|
270
341
|
}
|
|
271
342
|
return entry.result;
|
|
272
343
|
}
|
|
273
344
|
|
|
274
345
|
/** Get and remove the entry (used when navigation consumes a prefetch) */
|
|
275
|
-
consume(
|
|
276
|
-
const result = this.get(
|
|
346
|
+
consume(key: PrefetchKey): PrefetchResult | undefined {
|
|
347
|
+
const result = this.get(key);
|
|
277
348
|
if (result !== undefined) {
|
|
278
|
-
this.entries.delete(
|
|
349
|
+
this.entries.delete(prefetchMapKey(key));
|
|
279
350
|
}
|
|
280
351
|
return result;
|
|
281
352
|
}
|
|
282
353
|
|
|
283
|
-
/**
|
|
284
|
-
|
|
285
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Store a negative entry — the URL is not a route (non-RSC Content-Type).
|
|
356
|
+
*
|
|
357
|
+
* Keyed like every other entry even though "not a route" does not actually
|
|
358
|
+
* vary by departing URL: a source-independent negative would be a second
|
|
359
|
+
* keying rule to keep correct, and the only cost of the uniform one is a
|
|
360
|
+
* repeated fetch for a link hovered from a second page.
|
|
361
|
+
*/
|
|
362
|
+
setNegative(key: PrefetchKey): void {
|
|
363
|
+
this.set(key, NEGATIVE_ENTRY);
|
|
286
364
|
}
|
|
287
365
|
|
|
288
366
|
/** Check if the entry is a negative cache entry (URL is not a route). */
|
|
289
|
-
isNegative(
|
|
290
|
-
|
|
291
|
-
return entry === NEGATIVE_ENTRY;
|
|
367
|
+
isNegative(key: PrefetchKey): boolean {
|
|
368
|
+
return this.get(key) === NEGATIVE_ENTRY;
|
|
292
369
|
}
|
|
293
370
|
}
|
package/src/client/ssr-data.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
_setSsrDataProvider,
|
|
29
29
|
_setCurrentSsrData,
|
|
30
30
|
} from './state.js';
|
|
31
|
+
import type { SlotParamsRecord } from '../shared/slot-params.js';
|
|
31
32
|
|
|
32
33
|
// ─── Types ────────────────────────────────────────────────────────
|
|
33
34
|
|
|
@@ -40,6 +41,11 @@ export interface SsrData {
|
|
|
40
41
|
cookies: Map<string, string>;
|
|
41
42
|
/** The request's route params (e.g. { id: '123' }) */
|
|
42
43
|
params: Record<string, string | string[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Per-slot params keyed by slot tree path, absent when the request rendered
|
|
46
|
+
* no slot with params of its own (TIM-1285).
|
|
47
|
+
*/
|
|
48
|
+
slotParams?: SlotParamsRecord;
|
|
43
49
|
/**
|
|
44
50
|
* Mutable reference to NavContext for error boundary → pipeline communication.
|
|
45
51
|
*
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stale Client — knowing when this bundle has been superseded.
|
|
3
|
+
*
|
|
4
|
+
* Layer 1 of version skew handling (see design/33-version-skew.md) is
|
|
5
|
+
* proactive: the server compares `X-Timber-Deployment-Id` against the current
|
|
6
|
+
* build and answers a stale client with `X-Timber-Reload: 1`. That covers
|
|
7
|
+
* every RSC navigation and every server action — as long as there is a server
|
|
8
|
+
* in the request path.
|
|
9
|
+
*
|
|
10
|
+
* This module is the reactive half, plus the state both halves feed. It
|
|
11
|
+
* recognizes the two error shapes a stale client produces on its own:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Stale client references** — React's Flight client throws
|
|
14
|
+
* "Could not find the module …" when an RSC payload names a client module
|
|
15
|
+
* ID that isn't in this bundle's manifest.
|
|
16
|
+
* 2. **Chunk load failures** — a content-hashed chunk that no longer exists on
|
|
17
|
+
* the CDN fails to `import()`. Every engine words this differently.
|
|
18
|
+
*
|
|
19
|
+
* It matters most for `output: 'static'`, where there is no server to send the
|
|
20
|
+
* reload header and this is the *only* skew detection that can run.
|
|
21
|
+
*
|
|
22
|
+
* **Nothing here reloads the page.** A stale client is recorded, not acted on;
|
|
23
|
+
* the router turns the next user-initiated navigation into a full document
|
|
24
|
+
* load. See design/33-version-skew.md §"Recovery on the next navigation" for
|
|
25
|
+
* why — a self-issued `location.reload()` cannot tell a recovery from a bounce
|
|
26
|
+
* between two live deployments, and mid-rollout it produces exactly that loop.
|
|
27
|
+
*
|
|
28
|
+
* See TIM-1260, TIM-1275.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// The only import: the shared unload state. Kept minimal because this module
|
|
32
|
+
// is reached from the error boundary, a `'use client'` component evaluated in
|
|
33
|
+
// the SSR graph too — a dependency on the client fetch layer would drag that
|
|
34
|
+
// graph along with it.
|
|
35
|
+
import { isPageUnloading } from './unload-guard.js';
|
|
36
|
+
|
|
37
|
+
// ─── Error Shapes ────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Substrings that identify a failed dynamic import, lowercased for
|
|
41
|
+
* case-insensitive matching. Engines and bundlers each word this
|
|
42
|
+
* differently and none of them expose a stable error code.
|
|
43
|
+
*/
|
|
44
|
+
const CHUNK_LOAD_PATTERNS = [
|
|
45
|
+
// Chromium
|
|
46
|
+
'failed to fetch dynamically imported module',
|
|
47
|
+
// Firefox
|
|
48
|
+
'error loading dynamically imported module',
|
|
49
|
+
// Safari
|
|
50
|
+
'importing a module script failed',
|
|
51
|
+
// Vite's preload helper, for the stylesheet of a lazily-imported chunk
|
|
52
|
+
'unable to preload css for',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/** Substring React's Flight client uses when a client module ID is unknown. */
|
|
56
|
+
const STALE_REFERENCE_PATTERN = 'could not find the module';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Pull a comparable message out of an unknown thrown value. Rejections cross
|
|
60
|
+
* enough boundaries (event handlers, `unhandledrejection`, Vite's preload
|
|
61
|
+
* event) that the value is not reliably an `Error`.
|
|
62
|
+
*/
|
|
63
|
+
function messageOf(error: unknown): string {
|
|
64
|
+
if (typeof error === 'string') return error.toLowerCase();
|
|
65
|
+
if (error instanceof Error) return error.message.toLowerCase();
|
|
66
|
+
if (error && typeof error === 'object' && 'message' in error) {
|
|
67
|
+
const message = (error as { message: unknown }).message;
|
|
68
|
+
if (typeof message === 'string') return message.toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* True when the error is React's "client reference not in this manifest"
|
|
75
|
+
* failure — the payload was produced by a build this bundle doesn't match.
|
|
76
|
+
*/
|
|
77
|
+
export function isStaleClientReference(error: unknown): boolean {
|
|
78
|
+
return messageOf(error).includes(STALE_REFERENCE_PATTERN);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* True when the error is a failed dynamic import or CSS preload — the
|
|
83
|
+
* content-hashed chunk this bundle asks for is gone from the CDN.
|
|
84
|
+
*/
|
|
85
|
+
export function isChunkLoadError(error: unknown): boolean {
|
|
86
|
+
if (error instanceof Error && error.name === 'ChunkLoadError') return true;
|
|
87
|
+
const message = messageOf(error);
|
|
88
|
+
return CHUNK_LOAD_PATTERNS.some((pattern) => message.includes(pattern));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Staleness ───────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* True once this client is known to be running against a superseded
|
|
95
|
+
* deployment. One-way: a document that has gone stale cannot come back, since
|
|
96
|
+
* the only way to get fresh code is to load a new document.
|
|
97
|
+
*
|
|
98
|
+
* In-memory on purpose. Persisting it would be wrong — the next document gets
|
|
99
|
+
* whatever build the server hands it, and if that one is stale too it will
|
|
100
|
+
* find out the same way.
|
|
101
|
+
*/
|
|
102
|
+
let stale = false;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* True once {@link initStaleClient} has seen a deployment ID — i.e. this is a
|
|
106
|
+
* production build. False in dev, where nothing is ever stale: HMR owns code
|
|
107
|
+
* updates, and a failed dynamic import is a real error the developer needs to
|
|
108
|
+
* see rather than something a page load fixes.
|
|
109
|
+
*/
|
|
110
|
+
let deployed = false;
|
|
111
|
+
|
|
112
|
+
/** Whether this client has been superseded by a newer deployment. */
|
|
113
|
+
export function isClientStale(): boolean {
|
|
114
|
+
return stale;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Record that the origin has told us this client is stale — the `X-Timber-Reload`
|
|
119
|
+
* header on an RSC or action response. Authoritative, so it is not gated on
|
|
120
|
+
* anything: a dev server never sends the header in the first place.
|
|
121
|
+
*/
|
|
122
|
+
export function markClientStale(): void {
|
|
123
|
+
stale = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Record staleness if `error` is one of the shapes a superseded bundle
|
|
128
|
+
* produces. Returns whether it did, so a caller mid-navigation can decide to
|
|
129
|
+
* hard-navigate instead of surfacing the error.
|
|
130
|
+
*
|
|
131
|
+
* Inference, unlike {@link markClientStale}, so it is gated on being a
|
|
132
|
+
* production build. This is the single place that gate lives — call sites
|
|
133
|
+
* cannot see each other, and "remember to gate the next one too" is not a
|
|
134
|
+
* guarantee.
|
|
135
|
+
*/
|
|
136
|
+
export function markStaleFromError(error: unknown): boolean {
|
|
137
|
+
if (!deployed) return false;
|
|
138
|
+
|
|
139
|
+
// A document being torn down aborts its in-flight dynamic imports, and an
|
|
140
|
+
// aborted import reads exactly like a chunk the CDN has dropped. That is not
|
|
141
|
+
// evidence of staleness, and the bit outlives the teardown that produced it:
|
|
142
|
+
// a document entering the back-forward cache is frozen, not destroyed, so a
|
|
143
|
+
// false positive recorded on the way out would cost the *restored* page its
|
|
144
|
+
// client-side navigation for good. `unload-guard.ts` clears its own flag on
|
|
145
|
+
// a persisted `pageshow`; this one is one-way by design, so it has to not be
|
|
146
|
+
// set in the first place.
|
|
147
|
+
if (isPageUnloading()) return false;
|
|
148
|
+
|
|
149
|
+
if (!isStaleClientReference(error) && !isChunkLoadError(error)) return false;
|
|
150
|
+
stale = true;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Initialization ──────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The underlying error Vite attaches to a `vite:preloadError` event.
|
|
158
|
+
*
|
|
159
|
+
* Narrowed at runtime rather than cast: the property is Vite's, not ours, and
|
|
160
|
+
* a shape change should fall back to matching the event itself rather than
|
|
161
|
+
* silently reading `undefined` forever.
|
|
162
|
+
*/
|
|
163
|
+
function preloadErrorPayload(event: Event): unknown {
|
|
164
|
+
return 'payload' in event ? (event as Event & { payload: unknown }).payload : event;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Arm the module, and register the listeners that catch stale-client errors
|
|
169
|
+
* never passing through the router: a lazily imported client component, a
|
|
170
|
+
* preloaded stylesheet, anything that rejects outside a navigation.
|
|
171
|
+
*
|
|
172
|
+
* No-op when `deploymentId` is null — that is dev. Called from the browser
|
|
173
|
+
* entry before hydration, so it precedes any render or navigation that could
|
|
174
|
+
* throw.
|
|
175
|
+
*/
|
|
176
|
+
export function initStaleClient(deploymentId: string | null): void {
|
|
177
|
+
if (!deploymentId) return;
|
|
178
|
+
deployed = true;
|
|
179
|
+
|
|
180
|
+
// Vite dispatches this for failed chunk/CSS preloads. Note what is *not*
|
|
181
|
+
// here: `preventDefault()`. Recording staleness is not recovering from this
|
|
182
|
+
// error — the page is still broken until the user navigates — so Vite must
|
|
183
|
+
// go on rethrowing it rather than have it silently swallowed.
|
|
184
|
+
window.addEventListener('vite:preloadError', (event) => {
|
|
185
|
+
markStaleFromError(preloadErrorPayload(event));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
|
|
189
|
+
markStaleFromError(event.reason);
|
|
190
|
+
});
|
|
191
|
+
}
|
package/src/client/state.ts
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* §"Singleton State Registry".
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import type {
|
|
22
|
+
import type { SlotParamsRecord } from '../shared/slot-params.js';
|
|
23
|
+
import type { RouterInstance } from './router-types.js';
|
|
23
24
|
import type { SsrData } from './ssr-data.js';
|
|
24
25
|
|
|
25
26
|
// ─── Router (from router-ref.ts) ──────────────────────────────────────────
|
|
@@ -59,6 +60,18 @@ export function _setCurrentParams(params: Record<string, string | string[]>): vo
|
|
|
59
60
|
currentParams = params;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Per-slot params snapshot, keyed by slot tree path — the module-level
|
|
65
|
+
* fallback behind `useSegmentParams(slotPath)` when no NavigationContext is
|
|
66
|
+
* mounted (tests, calls outside a component). Replaced, never mutated, on
|
|
67
|
+
* each navigation. See TIM-1285.
|
|
68
|
+
*/
|
|
69
|
+
export let currentSlotParams: SlotParamsRecord | null = null;
|
|
70
|
+
|
|
71
|
+
export function _setCurrentSlotParams(slotParams: SlotParamsRecord | null): void {
|
|
72
|
+
currentSlotParams = slotParams;
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
/** Listeners notified when currentParams changes. */
|
|
63
76
|
export const paramsListeners = new Set<() => void>();
|
|
64
77
|
|
|
@@ -99,7 +99,8 @@ function ensureKeyframes(): void {
|
|
|
99
99
|
/**
|
|
100
100
|
* Internal top-loader component. Injected by NavigationRoot.
|
|
101
101
|
*
|
|
102
|
-
* Reads pending navigation state from
|
|
102
|
+
* Reads pending navigation state from the router's external store, via
|
|
103
|
+
* usePendingNavigation() — the one pending representation (TIM-1307).
|
|
103
104
|
* Phase transitions are derived synchronously during render:
|
|
104
105
|
*
|
|
105
106
|
* hidden → crawling: when isPending becomes true
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* unloaded so error boundaries and error handlers can suppress abort-related
|
|
9
9
|
* errors during the unload window.
|
|
10
10
|
*
|
|
11
|
+
* This is the single owner of "is this document going away". Anything that
|
|
12
|
+
* needs the answer imports `isPageUnloading()` — an unload flag maintained a
|
|
13
|
+
* second time somewhere else is a flag that will drift out of agreement with
|
|
14
|
+
* this one (TIM-1260).
|
|
15
|
+
*
|
|
11
16
|
* Mutable state is delegated to client/state.ts for singleton guarantees.
|
|
12
17
|
* See design/18-build-system.md §"Singleton State Registry"
|
|
13
18
|
*
|
|
@@ -16,16 +21,43 @@
|
|
|
16
21
|
|
|
17
22
|
import { unloading, _setUnloading } from './state.js';
|
|
18
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Departure is *committed*: `pagehide` has fired, or this document has called
|
|
26
|
+
* reload() on itself. `beforeunload` alone doesn't qualify — it is
|
|
27
|
+
* speculative, and the user can still cancel out of it.
|
|
28
|
+
*/
|
|
29
|
+
let committed = false;
|
|
30
|
+
|
|
19
31
|
if (typeof window !== 'undefined') {
|
|
20
32
|
window.addEventListener('beforeunload', () => {
|
|
21
33
|
_setUnloading(true);
|
|
34
|
+
|
|
35
|
+
// An app with an unsaved-changes prompt can have this cancelled. Nothing
|
|
36
|
+
// fires to say so, so the only signal is that the page is still running
|
|
37
|
+
// afterwards — and timers are blocked while the modal is up, so this task
|
|
38
|
+
// cannot run until the user has chosen. If they left, `pagehide` has
|
|
39
|
+
// already committed and this is a no-op.
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
if (!committed) _setUnloading(false);
|
|
42
|
+
}, 0);
|
|
22
43
|
});
|
|
23
44
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
45
|
+
// pagehide is the committed-departure event, and fires for both navigations
|
|
46
|
+
// and bfcache entry. Safari in particular may skip beforeunload entirely.
|
|
26
47
|
window.addEventListener('pagehide', () => {
|
|
48
|
+
committed = true;
|
|
27
49
|
_setUnloading(true);
|
|
28
50
|
});
|
|
51
|
+
|
|
52
|
+
window.addEventListener('pageshow', (event) => {
|
|
53
|
+
// A bfcache restore un-does the teardown `pagehide` announced: the same
|
|
54
|
+
// document is alive again, with this module's state intact. Without the
|
|
55
|
+
// reset, every consumer would go on suppressing errors on a live page.
|
|
56
|
+
if (event.persisted) {
|
|
57
|
+
committed = false;
|
|
58
|
+
_setUnloading(false);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
29
61
|
}
|
|
30
62
|
|
|
31
63
|
/**
|
package/src/client/use-cookie.ts
CHANGED
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
* See design/29-cookies.md §"useCookie(name) Hook"
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useSyncExternalStore }
|
|
11
|
+
// Namespace access, not `import { useSyncExternalStore }`. This module is
|
|
12
|
+
// reachable from `@timber-js/app/cookies` — `defineCookie()` exposes both a
|
|
13
|
+
// server `.get()` and a client `.useCookie()`, so the entry is isomorphic and
|
|
14
|
+
// loads in the RSC environment. React's `react-server` export withholds
|
|
15
|
+
// `useSyncExternalStore` exactly as it withholds `createContext`, and a *named*
|
|
16
|
+
// ESM import of a missing export fails at module instantiation, before any
|
|
17
|
+
// code runs. Same rule as `client/params-context.ts` (TIM-1297, TIM-1298).
|
|
18
|
+
import * as React from 'react';
|
|
12
19
|
import { parseCookie, stringifySetCookie } from 'cookie';
|
|
13
20
|
import { getSsrData } from './ssr-data.js';
|
|
14
21
|
import { assertValidCookieName } from '../cookies/validation.js';
|
|
@@ -129,7 +136,7 @@ export function useCookie(
|
|
|
129
136
|
return getCookieValue(name);
|
|
130
137
|
};
|
|
131
138
|
|
|
132
|
-
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
139
|
+
const value = React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
133
140
|
|
|
134
141
|
// Validate the name once per hook instance — names are typically
|
|
135
142
|
// stable string literals, so this catches the bug at first render
|
package/src/client/use-router.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* In timber, navigation calls reactRoot.render() which is a root-level
|
|
15
15
|
* render — startTransition has no effect on root renders.
|
|
16
16
|
*
|
|
17
|
-
* Navigation state (
|
|
17
|
+
* Navigation state (pathname, search) is delivered atomically via
|
|
18
18
|
* NavigationContext embedded in the element tree passed to
|
|
19
19
|
* reactRoot.render(). See design/19-client-navigation.md §"NavigationContext".
|
|
20
20
|
*
|