@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
|
@@ -14,35 +14,12 @@ import { useQueryStates as clientUseQueryStates } from '../client/use-query-stat
|
|
|
14
14
|
import { fromSchema, isStandardSchema, isCodec } from '../schema-bridge.js';
|
|
15
15
|
import type { StandardSchemaV1 } from '../schema-bridge.js';
|
|
16
16
|
import type { Codec } from '../codec.js';
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
// dynamic `await import()` at call time because the async microtask from the
|
|
24
|
-
// dynamic import loses AsyncLocalStorage context in React's RSC Flight renderer,
|
|
25
|
-
// breaking getSearchParams() in parallel slot pages. See TIM-523.
|
|
26
|
-
let _getSearchParamsFn: (() => URLSearchParams) | undefined;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Register the getSearchParams function. Called once at module load time
|
|
30
|
-
* from request-context.ts to avoid dynamic import at call time.
|
|
31
|
-
* @internal
|
|
32
|
-
*/
|
|
33
|
-
export function _setGetSearchParamsFn(fn: () => URLSearchParams): void {
|
|
34
|
-
_getSearchParamsFn = fn;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function getSearchParamsFromAls(): URLSearchParams {
|
|
38
|
-
if (!_getSearchParamsFn) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
'[timber] searchParams.get() is only available on the server. ' +
|
|
41
|
-
'Use searchParams.useQueryStates() on the client.'
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
return _getSearchParamsFn();
|
|
45
|
-
}
|
|
17
|
+
// Server-only reference for .get() — avoids pulling server ALS into client
|
|
18
|
+
// bundles. In client environments, .get() throws before reaching this code
|
|
19
|
+
// path. The slot lives in its own leaf module so that `request-context.ts`
|
|
20
|
+
// can register the getter without importing this file, which would drag
|
|
21
|
+
// `nuqs` into every server entry (TIM-1298). See shared/als-slots.ts.
|
|
22
|
+
import { getSearchParamsFromAls } from '../shared/als-slots.js';
|
|
46
23
|
|
|
47
24
|
// ---------------------------------------------------------------------------
|
|
48
25
|
// Types
|
|
@@ -16,33 +16,10 @@
|
|
|
16
16
|
import type { Codec } from '../codec.js';
|
|
17
17
|
import { type StandardSchemaV1, resolveCodecOrSchema } from '../schema-bridge.js';
|
|
18
18
|
import { useSegmentParams as clientUseSegmentParams } from '../client/use-segment-params.js';
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Same pattern as search-params: eagerly registered at server startup
|
|
25
|
-
// to avoid dynamic imports that lose ALS context. See TIM-523.
|
|
26
|
-
let _getSegmentParamsFn: (() => Record<string, string | string[]>) | undefined;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Register the getSegmentParams function. Called once at module load time
|
|
30
|
-
* from request-context.ts to avoid dynamic import at call time.
|
|
31
|
-
* @internal
|
|
32
|
-
*/
|
|
33
|
-
export function _setGetSegmentParamsFn(fn: () => Record<string, string | string[]>): void {
|
|
34
|
-
_getSegmentParamsFn = fn;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function getSegmentParamsFromAls(): Record<string, string | string[]> {
|
|
38
|
-
if (!_getSegmentParamsFn) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
'[timber] segmentParams.get() is only available on the server. ' +
|
|
41
|
-
'Use segmentParams.useSegmentParams() on the client.'
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
return _getSegmentParamsFn();
|
|
45
|
-
}
|
|
19
|
+
// Server-only ALS reference for .get(), eagerly registered at server startup
|
|
20
|
+
// from a leaf module so this file stays out of the server graph. Same slot
|
|
21
|
+
// mechanism as search-params. See shared/als-slots.ts, TIM-523, TIM-1298.
|
|
22
|
+
import { getSegmentParamsFromAls } from '../shared/als-slots.js';
|
|
46
23
|
|
|
47
24
|
// ---------------------------------------------------------------------------
|
|
48
25
|
// Types
|
|
@@ -296,7 +296,9 @@ async function handleRscAction(
|
|
|
296
296
|
// serialized together so React Flight handles both in one stream.
|
|
297
297
|
payload = {
|
|
298
298
|
_action: result.actionResult,
|
|
299
|
-
|
|
299
|
+
// The payload root, not a bare tree: it carries the revalidated
|
|
300
|
+
// route's params too, and the client publishes them (TIM-1297).
|
|
301
|
+
_tree: result.revalidation.payload,
|
|
300
302
|
};
|
|
301
303
|
headers['X-Timber-Revalidation'] = '1';
|
|
302
304
|
// Metadata (<title>/<meta>/<link>) now rides the revalidation element
|
package/src/server/actions.ts
CHANGED
|
@@ -20,10 +20,13 @@ import { withSpan } from './tracing';
|
|
|
20
20
|
import { revalidationAls, type RevalidationState } from './als-registry.js';
|
|
21
21
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
22
22
|
|
|
23
|
-
/** Result of rendering a revalidation —
|
|
23
|
+
/** Result of rendering a revalidation — payload root before RSC serialization. */
|
|
24
24
|
export interface RevalidationResult {
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The payload root — tree plus the params it rendered with, exactly as an
|
|
27
|
+
* ordinary route payload. The client splits it in `applyRevalidation`.
|
|
28
|
+
*/
|
|
29
|
+
payload: unknown;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/** Renderer function that builds a React element tree for a given path. */
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
import '#server-only-guard';
|
|
35
|
+
import type { SlotParamsRecord } from '../shared/slot-params.js';
|
|
35
36
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
36
37
|
import type { DebugComponentEntry } from './rsc-entry/helpers.js';
|
|
37
38
|
|
|
@@ -113,7 +114,7 @@ export interface RequestContextStore {
|
|
|
113
114
|
*
|
|
114
115
|
* See design/07-routing.md §"Parallel Slot Params"
|
|
115
116
|
*/
|
|
116
|
-
|
|
117
|
+
slotParams?: SlotParamsRecord;
|
|
117
118
|
/** Outgoing Set-Cookie entries (name → serialized value + options). Last write wins. */
|
|
118
119
|
cookieJar: Map<string, CookieEntry>;
|
|
119
120
|
/** Whether the response has flushed (headers committed). */
|
|
@@ -49,13 +49,26 @@ export interface BuildManifest {
|
|
|
49
49
|
requestDep?: Record<string, boolean>;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Empty build manifest used in dev mode, and the single source of truth for
|
|
54
|
+
* what an empty manifest contains.
|
|
55
|
+
*
|
|
56
|
+
* `virtual:timber-build-manifest` serializes this constant into its generated
|
|
57
|
+
* source rather than restating it — one literal for the dev module, one for the
|
|
58
|
+
* production module's `globalThis.__TIMBER_BUILD_MANIFEST__ ?? …` fallback.
|
|
59
|
+
* They were three hand-written objects that had already drifted: this one and
|
|
60
|
+
* the dev literal were missing `requestDep` while the production fallback had
|
|
61
|
+
* it, so a reader who grepped the built bundle for `requestDep` found the one
|
|
62
|
+
* copy that mentioned it and read the fallback as the shipped map (TIM-1300).
|
|
63
|
+
* A field added here now reaches every copy.
|
|
64
|
+
*/
|
|
53
65
|
export const EMPTY_BUILD_MANIFEST: BuildManifest = {
|
|
54
66
|
css: {},
|
|
55
67
|
js: {},
|
|
56
68
|
modulepreload: {},
|
|
57
69
|
fonts: {},
|
|
58
70
|
metadataRouteHashes: {},
|
|
71
|
+
requestDep: {},
|
|
59
72
|
};
|
|
60
73
|
|
|
61
74
|
/** Segment shape expected by collectRouteCss (matches ManifestSegmentNode). */
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much of the URL a matched segment chain has consumed.
|
|
3
|
+
*
|
|
4
|
+
* One question — "which URL parts had been consumed by the time segment `i`
|
|
5
|
+
* rendered?" — asked by four things that must all agree:
|
|
6
|
+
*
|
|
7
|
+
* - the layout depth handed to `SegmentProvider` (`route-element-builder.ts`),
|
|
8
|
+
* which `useSelectedLayoutSegment(s)` indexes the live pathname with;
|
|
9
|
+
* - `slotUrlParts`, which slices the destination pathname for slot matching;
|
|
10
|
+
* - `resolveSlotProps`, which slices the *departing* pathname the same way to
|
|
11
|
+
* decide whether a slot can be skipped;
|
|
12
|
+
* - `skippable-prefix.ts`, which does the same for the contiguous skip prefix.
|
|
13
|
+
*
|
|
14
|
+
* They were four separate `extractUrlParts(...).length` calls. That was fine
|
|
15
|
+
* while the answer was a plain walk, and stopped being fine when interception
|
|
16
|
+
* gave it a second term (TIM-1280): the fix landed on the layout depth alone
|
|
17
|
+
* and the other three kept the old walk, so a layout and the slots beside it
|
|
18
|
+
* disagreed about how deep they were. Hence one function, in its own module,
|
|
19
|
+
* rather than a helper on the side of whichever caller was fixed first.
|
|
20
|
+
*
|
|
21
|
+
* See design/07-routing.md §"Intercepting Routes".
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
25
|
+
import type { RouteMatch } from './pipeline.js';
|
|
26
|
+
import { effectiveUrlSegment } from '../routing/segment-classify.js';
|
|
27
|
+
import { computeInterceptedBase } from '../routing/interception.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract raw URL path parts from a segment chain.
|
|
31
|
+
*
|
|
32
|
+
* Each dynamic segment consumed one URL part (its param value). Static
|
|
33
|
+
* segments consumed their own name. Groups are transparent. Catch-all segments
|
|
34
|
+
* consumed multiple parts (their param value is `string[]`).
|
|
35
|
+
*
|
|
36
|
+
* The root segment is named '' and consumes nothing, so it is skipped here
|
|
37
|
+
* rather than by each caller — callers that pass a chain including the root
|
|
38
|
+
* (interception anchoring, slot-skip slicing) would otherwise have to remember
|
|
39
|
+
* to filter it, and one that forgot would push an empty part and shift every
|
|
40
|
+
* subsequent index by one.
|
|
41
|
+
*
|
|
42
|
+
* Uses rawSegmentParams (pre-coercion) to get actual URL strings. Values are
|
|
43
|
+
* coerced to `String()` defensively in case a codec leaked a non-string value
|
|
44
|
+
* into the params map.
|
|
45
|
+
*
|
|
46
|
+
* Each node is read through `effectiveUrlSegment`, so an intercepting
|
|
47
|
+
* directory contributes the URL part it intercepts (`(.)item` → `item`,
|
|
48
|
+
* `(.)[id]` → the bound value). A children-path interception puts such a node
|
|
49
|
+
* in the *main* chain, and reading it directly would classify it as neither
|
|
50
|
+
* static nor param-bearing — it would contribute nothing, and every part after
|
|
51
|
+
* it would shift by one.
|
|
52
|
+
*/
|
|
53
|
+
export function extractUrlParts(
|
|
54
|
+
segments: ManifestSegmentNode[],
|
|
55
|
+
mainParams: Record<string, string | string[]>
|
|
56
|
+
): string[] {
|
|
57
|
+
const parts: string[] = [];
|
|
58
|
+
for (const node of segments) {
|
|
59
|
+
const seg = effectiveUrlSegment(node);
|
|
60
|
+
// The root's *directory* name is empty; read it off the node, since an
|
|
61
|
+
// intercepting node's effective name is the one it intercepts.
|
|
62
|
+
if (seg.segmentType === 'group') continue;
|
|
63
|
+
if (node.segmentName === '') continue;
|
|
64
|
+
if (seg.segmentType === 'static') {
|
|
65
|
+
parts.push(seg.segmentName);
|
|
66
|
+
} else if (seg.paramName) {
|
|
67
|
+
const value = mainParams[seg.paramName];
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
parts.push(...value.map(String));
|
|
70
|
+
} else if (value !== undefined) {
|
|
71
|
+
// Reconstruct the original URL part for affixed segments.
|
|
72
|
+
// The param value is the stripped middle (e.g. "123" for [id].png
|
|
73
|
+
// matching "123.png"), so we re-attach prefix/suffix so that slot
|
|
74
|
+
// matching through matchUrlParts sees the original part.
|
|
75
|
+
const prefix = seg.paramPrefix ?? '';
|
|
76
|
+
const suffix = seg.paramSuffix ?? '';
|
|
77
|
+
parts.push(prefix + String(value) + suffix);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return parts;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Where a children-path interception sits in the target pathname, or null when
|
|
86
|
+
* the chain contains none.
|
|
87
|
+
*
|
|
88
|
+
* `anchor` is the intercepted base's length — the URL index the intercepting
|
|
89
|
+
* node occupies — from the same `computeInterceptedBase` that rewrite
|
|
90
|
+
* generation and the slot resolver both go through, so the three cannot
|
|
91
|
+
* disagree about which part a marker points at.
|
|
92
|
+
*/
|
|
93
|
+
function interceptionAnchor(
|
|
94
|
+
segments: ManifestSegmentNode[]
|
|
95
|
+
): { index: number; anchor: number } | null {
|
|
96
|
+
const index = segments.findIndex((s) => s.segmentType === 'intercepting');
|
|
97
|
+
if (index <= 0) return null;
|
|
98
|
+
|
|
99
|
+
const owner = segments[index - 1];
|
|
100
|
+
const base = computeInterceptedBase(owner.urlPath, segments[index].interceptionMarker ?? '(.)');
|
|
101
|
+
return { index, anchor: base === '/' ? 0 : base.slice(1).split('/').length };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The URL parts the chain has consumed through `index`, inclusive.
|
|
106
|
+
*
|
|
107
|
+
* For an ordinary chain this is just the walk. Two things make it more than
|
|
108
|
+
* that, and both are cases where a node stands for a different number of URL
|
|
109
|
+
* parts than itself:
|
|
110
|
+
*
|
|
111
|
+
* - A **catch-all** consumes as many parts as the request supplies, which the
|
|
112
|
+
* walk already handles (its param value is an array).
|
|
113
|
+
* - An **interception with a climbing marker** intercepts a URL that starts
|
|
114
|
+
* *above* its owner, so the owner's own URL segment — and any static
|
|
115
|
+
* ancestor between the intercepted base and the owner — is not in the target
|
|
116
|
+
* at all. `feed/(..)photo/[id]` at `/photo/42` still has `feed` in the
|
|
117
|
+
* chain, and counting it puts everything after it off by one.
|
|
118
|
+
*
|
|
119
|
+
* So everything above the interception clamps to the anchor, and everything
|
|
120
|
+
* from it down counts forward from there. The owner chain is param-free by
|
|
121
|
+
* build-time rule and the intercepted base is a prefix of the owner's
|
|
122
|
+
* `urlPath`, so the target's leading `anchor` parts *are* the owner chain's
|
|
123
|
+
* leading `anchor` parts — which is what makes the clamped prefix real values
|
|
124
|
+
* rather than placeholders. For `(.)` the base is the owner's own path, so the
|
|
125
|
+
* clamp is a no-op and this reduces to the plain walk.
|
|
126
|
+
*/
|
|
127
|
+
export function consumedUrlParts(
|
|
128
|
+
segments: ManifestSegmentNode[],
|
|
129
|
+
index: number,
|
|
130
|
+
match: RouteMatch
|
|
131
|
+
): string[] {
|
|
132
|
+
const rawParams = match.rawSegmentParams ?? match.segmentParams ?? {};
|
|
133
|
+
const partsThrough = (from: number, to: number) =>
|
|
134
|
+
extractUrlParts(segments.slice(from, to + 1), rawParams);
|
|
135
|
+
|
|
136
|
+
const interception = interceptionAnchor(segments);
|
|
137
|
+
if (!interception) return partsThrough(0, index);
|
|
138
|
+
|
|
139
|
+
const targetPrefix = partsThrough(0, interception.index - 1).slice(0, interception.anchor);
|
|
140
|
+
|
|
141
|
+
if (index < interception.index) {
|
|
142
|
+
// Above the interception: an ancestor is never deeper than the anchor.
|
|
143
|
+
return targetPrefix.slice(0, Math.min(partsThrough(0, index).length, interception.anchor));
|
|
144
|
+
}
|
|
145
|
+
// At or below it: the intercepting subtree's parts ARE the target's.
|
|
146
|
+
return [...targetPrefix, ...partsThrough(interception.index, index)];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The index in `match.segments` of a segment, or -1.
|
|
151
|
+
*
|
|
152
|
+
* Split out so the three slot call sites and the layout-depth one derive their
|
|
153
|
+
* slice point identically — `indexOf` plus `consumedUrlParts` — instead of
|
|
154
|
+
* each inlining the pair.
|
|
155
|
+
*/
|
|
156
|
+
export function consumedPartsThroughSegment(
|
|
157
|
+
segment: ManifestSegmentNode,
|
|
158
|
+
match: RouteMatch
|
|
159
|
+
): string[] {
|
|
160
|
+
const index = match.segments.indexOf(segment);
|
|
161
|
+
if (index < 0) return [];
|
|
162
|
+
return consumedUrlParts(match.segments as ManifestSegmentNode[], index, match);
|
|
163
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Children-path interception — the sibling layout swap (TIM-1280).
|
|
3
|
+
*
|
|
4
|
+
* The second shape interception has. An intercepting directory that is a
|
|
5
|
+
* direct child of an ordinary segment or route group renders *in place of*
|
|
6
|
+
* `children` for that directory's own layout chain:
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* app/(landing)/layout.tsx
|
|
10
|
+
* app/(landing)/[artistSlug]/[year]/[month]/[day]/page.tsx ← hard nav
|
|
11
|
+
* app/(browse)/layout.tsx
|
|
12
|
+
* app/(browse)/(...)[artistSlug]/[year]/[month]/[day]/page.tsx ← soft nav from (browse)
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* There is no overlay, no slot, and no `default.tsx`. Nothing is re-matched
|
|
16
|
+
* either — and that is the point. The modal shape re-matches
|
|
17
|
+
* `interceptingPrefix`, a URL, and route groups contribute nothing to a URL,
|
|
18
|
+
* so an owner like `app/(browse)/` serializes to "/" and re-matching it
|
|
19
|
+
* resolves through whichever group owns the canonical page. This resolver
|
|
20
|
+
* addresses the owner by **tree path** instead, which never collapses.
|
|
21
|
+
*
|
|
22
|
+
* The result is an ordinary `RouteMatch` whose chain runs
|
|
23
|
+
* `[…owner ancestors, interceptingNode, …its matched descendants]`. Every
|
|
24
|
+
* consumer downstream — `tree-builder.ts`, the element builder, access gates,
|
|
25
|
+
* middleware — sees a segment chain and needs to know nothing about
|
|
26
|
+
* interception. That is why this feature adds no rendering path: the chain
|
|
27
|
+
* *is* the feature.
|
|
28
|
+
*
|
|
29
|
+
* See design/07-routing.md §"Intercepting Routes".
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { RouteMatch } from './pipeline.js';
|
|
33
|
+
import { collectMiddlewareChain, type ManifestSegmentNode } from './route-matcher.js';
|
|
34
|
+
import { findInterceptingMatch } from './slot-interception.js';
|
|
35
|
+
import { treePathNames } from '../routing/segment-keys.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a children-path interception to a complete route match.
|
|
39
|
+
*
|
|
40
|
+
* `interceptingScope` is the rewrite's own build-time field — the tree path of
|
|
41
|
+
* the directory that owns the interception. The pipeline has already
|
|
42
|
+
* established that the client's rendered tree passes through it; this
|
|
43
|
+
* translates it into the chain to render.
|
|
44
|
+
*
|
|
45
|
+
* Returns null when the scope names no directory in this manifest, or when no
|
|
46
|
+
* intercepting child of that directory matches the target. Both are fail-safe:
|
|
47
|
+
* the caller keeps the canonical target match, so the page the user gets is
|
|
48
|
+
* the one a hard navigation would have produced.
|
|
49
|
+
*/
|
|
50
|
+
export type InterceptedRouteMatcher = (
|
|
51
|
+
targetPathname: string,
|
|
52
|
+
interceptingScope: string
|
|
53
|
+
) => RouteMatch | null;
|
|
54
|
+
|
|
55
|
+
/** Build a children-path interception resolver over a runtime manifest. */
|
|
56
|
+
export function createInterceptedRouteMatcher(root: ManifestSegmentNode): InterceptedRouteMatcher {
|
|
57
|
+
return (targetPathname, interceptingScope) => {
|
|
58
|
+
const ancestors = findChainByTreePath(root, interceptingScope);
|
|
59
|
+
if (!ancestors) return null;
|
|
60
|
+
|
|
61
|
+
const owner = ancestors[ancestors.length - 1];
|
|
62
|
+
// The same resolver the slot path uses. It takes an arbitrary node, buckets
|
|
63
|
+
// its intercepting children by the URL index their marker anchors them to,
|
|
64
|
+
// and walks each bucket with the shared tree walker — so marker depth,
|
|
65
|
+
// static-before-dynamic sibling ordering and catch-all handling are one
|
|
66
|
+
// implementation across both shapes rather than two that must agree.
|
|
67
|
+
const result = findInterceptingMatch(owner, targetPathname);
|
|
68
|
+
if (!result) return null;
|
|
69
|
+
|
|
70
|
+
// `result.chain` is [owner, interceptingNode, …descendants]; splice the
|
|
71
|
+
// owner's own ancestry in front of it to get a root-anchored chain.
|
|
72
|
+
const segments = [...ancestors, ...result.chain.slice(1)];
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
segments,
|
|
76
|
+
// Every segment above the intercepting node is static or a route group —
|
|
77
|
+
// `validateInterceptionPlacement` rejects a dynamic one at build time,
|
|
78
|
+
// precisely because the intercepted URL carries no value to bind it
|
|
79
|
+
// with. So the intercepting subtree's params are the whole record.
|
|
80
|
+
segmentParams: result.slotParams,
|
|
81
|
+
middlewareChain: collectMiddlewareChain(segments),
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Find the chain from the root to the segment with this tree path.
|
|
88
|
+
*
|
|
89
|
+
* Tree paths are `computeSegmentTreePaths` — the directory path with slots
|
|
90
|
+
* elided — so this walks `children` only, which is also exactly the set of
|
|
91
|
+
* positions a children-path interception can own: an intercepting directory
|
|
92
|
+
* inside a slot subtree is rejected at build time unless it is a direct slot
|
|
93
|
+
* child, and that shape is the modal one.
|
|
94
|
+
*
|
|
95
|
+
* Sibling directories cannot share a name, so a tree path names at most one
|
|
96
|
+
* node and the walk needs no tie-break. This is the property `urlPath` and
|
|
97
|
+
* `computeSegmentKeys` both lack — see the note on `computeSegmentTreePaths`.
|
|
98
|
+
*/
|
|
99
|
+
function findChainByTreePath(
|
|
100
|
+
root: ManifestSegmentNode,
|
|
101
|
+
treePath: string
|
|
102
|
+
): ManifestSegmentNode[] | null {
|
|
103
|
+
const names = treePathNames(treePath);
|
|
104
|
+
const chain: ManifestSegmentNode[] = [root];
|
|
105
|
+
let node = root;
|
|
106
|
+
|
|
107
|
+
for (const name of names) {
|
|
108
|
+
const next = node.children.find((child) => child.segmentName === name);
|
|
109
|
+
if (!next) return null;
|
|
110
|
+
chain.push(next);
|
|
111
|
+
node = next;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return chain;
|
|
115
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework default status page — the terminal fallback of the deny chain.
|
|
3
|
+
*
|
|
4
|
+
* design/10-error-handling.md §"Fallback Chain" ends every 4xx/5xx chain in a
|
|
5
|
+
* "framework default page". Before TIM-1258 that terminal did not exist: an app
|
|
6
|
+
* with no `404.tsx` / `4xx.tsx` / `error.tsx` got `Response(null, { status })` —
|
|
7
|
+
* a correct status code with an empty body. Browsers render that as a blank
|
|
8
|
+
* page, and static generation had nothing to write to `404.html`.
|
|
9
|
+
*
|
|
10
|
+
* This page is a plain HTML string rather than a React component on purpose:
|
|
11
|
+
* it is the *last* fallback, so it must not depend on the RSC → SSR pipeline
|
|
12
|
+
* (the machinery that may already have failed by the time we get here), and it
|
|
13
|
+
* must be renderable from the static build where no render environment exists.
|
|
14
|
+
* One builder is the single source of truth for both surfaces.
|
|
15
|
+
*
|
|
16
|
+
* Security: the page interpolates nothing but the status code, and the code is
|
|
17
|
+
* clamped to a plain integer before it reaches the markup. No request URL, no
|
|
18
|
+
* error message, no stack — see design/13-security.md principle 4.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Canonical reason phrases (RFC 9110 §15) for the statuses an app is likely to
|
|
23
|
+
* `deny()` with. Anything else falls back to a generic category phrase — the
|
|
24
|
+
* phrase is decoration, never a place to echo caller-supplied text.
|
|
25
|
+
*/
|
|
26
|
+
const REASON_PHRASES: Record<number, string> = {
|
|
27
|
+
400: 'Bad Request',
|
|
28
|
+
401: 'Unauthorized',
|
|
29
|
+
402: 'Payment Required',
|
|
30
|
+
403: 'Forbidden',
|
|
31
|
+
404: 'Not Found',
|
|
32
|
+
405: 'Method Not Allowed',
|
|
33
|
+
406: 'Not Acceptable',
|
|
34
|
+
408: 'Request Timeout',
|
|
35
|
+
409: 'Conflict',
|
|
36
|
+
410: 'Gone',
|
|
37
|
+
413: 'Content Too Large',
|
|
38
|
+
415: 'Unsupported Media Type',
|
|
39
|
+
418: "I'm a Teapot",
|
|
40
|
+
422: 'Unprocessable Content',
|
|
41
|
+
429: 'Too Many Requests',
|
|
42
|
+
451: 'Unavailable For Legal Reasons',
|
|
43
|
+
500: 'Internal Server Error',
|
|
44
|
+
501: 'Not Implemented',
|
|
45
|
+
502: 'Bad Gateway',
|
|
46
|
+
503: 'Service Unavailable',
|
|
47
|
+
504: 'Gateway Timeout',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** HTML content type used for every framework default page response. */
|
|
51
|
+
export const DEFAULT_STATUS_PAGE_CONTENT_TYPE = 'text/html; charset=utf-8';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Coerce a status into something safe to put in markup.
|
|
55
|
+
*
|
|
56
|
+
* `DenySignal.status` is typed `number` but nothing validates it at
|
|
57
|
+
* construction, and JS callers can hand us anything. Rather than escaping a
|
|
58
|
+
* value that should never have been a string, collapse non-integers to 500 —
|
|
59
|
+
* the page is decoration for a status already on the wire.
|
|
60
|
+
*/
|
|
61
|
+
function displayStatus(status: number): number {
|
|
62
|
+
return Number.isInteger(status) && status >= 100 && status <= 599 ? status : 500;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Human-readable phrase for a status code. Never caller-supplied text. */
|
|
66
|
+
export function statusReasonPhrase(status: number): string {
|
|
67
|
+
const code = displayStatus(status);
|
|
68
|
+
if (REASON_PHRASES[code]) return REASON_PHRASES[code];
|
|
69
|
+
return code >= 500 ? 'Server Error' : 'Request Error';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build the framework default page for a status code as a full HTML document.
|
|
74
|
+
*
|
|
75
|
+
* The document is self-contained: no scripts, no external resources, no client
|
|
76
|
+
* JavaScript. It renders identically in `server` mode, in `static` mode's
|
|
77
|
+
* generated `404.html`, and with `clientJavascript: false`.
|
|
78
|
+
*/
|
|
79
|
+
export function defaultStatusPageHtml(status: number): string {
|
|
80
|
+
const code = displayStatus(status);
|
|
81
|
+
const phrase = statusReasonPhrase(code);
|
|
82
|
+
return `<!DOCTYPE html>
|
|
83
|
+
<html lang="en">
|
|
84
|
+
<head>
|
|
85
|
+
<meta charset="utf-8">
|
|
86
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
87
|
+
<title>${code} ${phrase}</title>
|
|
88
|
+
<style>
|
|
89
|
+
:root { color-scheme: light dark; --fg: #1a1a1a; --muted: #6b6b76; --bg: #ffffff; }
|
|
90
|
+
@media (prefers-color-scheme: dark) { :root { --fg: #ededf1; --muted: #9b9baa; --bg: #111114; } }
|
|
91
|
+
html, body { height: 100%; margin: 0; }
|
|
92
|
+
body {
|
|
93
|
+
display: flex; align-items: center; justify-content: center;
|
|
94
|
+
background: var(--bg); color: var(--fg);
|
|
95
|
+
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
96
|
+
text-align: center; padding: 2rem;
|
|
97
|
+
}
|
|
98
|
+
h1 { font-size: 3rem; font-weight: 600; margin: 0; letter-spacing: -0.02em; }
|
|
99
|
+
p { margin: 0.5rem 0 0; color: var(--muted); font-size: 1rem; }
|
|
100
|
+
</style>
|
|
101
|
+
</head>
|
|
102
|
+
<body>
|
|
103
|
+
<main>
|
|
104
|
+
<h1>${code}</h1>
|
|
105
|
+
<p>${phrase}</p>
|
|
106
|
+
</main>
|
|
107
|
+
</body>
|
|
108
|
+
</html>
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build the framework default page as an HTTP `Response`.
|
|
114
|
+
*
|
|
115
|
+
* `responseHeaders` carries whatever middleware/proxy already set (cookies,
|
|
116
|
+
* cache directives); the content type is overwritten because the body is
|
|
117
|
+
* always HTML regardless of what the route would have produced.
|
|
118
|
+
*
|
|
119
|
+
* The body is unconditional. HEAD is not this function's problem: the pipeline
|
|
120
|
+
* strips HEAD bodies at its single exit for every response shape, so a
|
|
121
|
+
* `method` parameter here would be one renderer's private copy of a rule that
|
|
122
|
+
* now holds framework-wide (TIM-1324).
|
|
123
|
+
*/
|
|
124
|
+
export function defaultStatusPageResponse(status: number, responseHeaders: Headers): Response {
|
|
125
|
+
responseHeaders.set('content-type', DEFAULT_STATUS_PAGE_CONTENT_TYPE);
|
|
126
|
+
return new Response(defaultStatusPageHtml(status), { status, headers: responseHeaders });
|
|
127
|
+
}
|