@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
|
@@ -34,7 +34,10 @@ import {
|
|
|
34
34
|
} from './deny-boundary.js';
|
|
35
35
|
import type { DenyPageEntry } from './deny-boundary.js';
|
|
36
36
|
import { LAYOUT_CHILDREN_SLOT } from './prebuilt/slots.js';
|
|
37
|
-
import { resolveSlotProps,
|
|
37
|
+
import { resolveSlotProps, publishSlotSegmentParams, type SlotSkipEntry } from './slot-resolver.js';
|
|
38
|
+
import { consumedUrlParts } from './chain-url-parts.js';
|
|
39
|
+
import { withPublishedParams } from './publish-params.js';
|
|
40
|
+
import type { RscPayloadRoot } from '../shared/payload-root.js';
|
|
38
41
|
import { SegmentProvider } from '../client/segment-context.js';
|
|
39
42
|
import { SegmentOutlet } from '../client/segment-outlet.js';
|
|
40
43
|
import { SlotOutlet } from '../client/slot-outlet.js';
|
|
@@ -42,93 +45,16 @@ import { SlotsProvider } from '../client/slot-provider.js';
|
|
|
42
45
|
|
|
43
46
|
import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.js';
|
|
44
47
|
import type { InterceptionContext } from './pipeline.js';
|
|
48
|
+
import { shouldSkipSegment, type ClientStateTree } from './state-tree-diff.js';
|
|
49
|
+
import { truncateSkippableForChangedSlots } from './skippable-prefix.js';
|
|
45
50
|
import {
|
|
46
|
-
shouldSkipSegment,
|
|
47
|
-
shouldSkipSlot,
|
|
48
51
|
computeSegmentKeys,
|
|
52
|
+
computeSegmentTreePaths,
|
|
49
53
|
computeSlotKey,
|
|
50
|
-
|
|
51
|
-
} from './state-tree-diff.js';
|
|
54
|
+
} from '../routing/segment-keys.js';
|
|
52
55
|
import { headElementsToReact } from './metadata.js';
|
|
53
56
|
import { loadModule } from './safe-load.js';
|
|
54
57
|
|
|
55
|
-
/**
|
|
56
|
-
* Truncate the skippable prefix when a skippable layout has slots whose
|
|
57
|
-
* content changed between the departing and destination URLs (TIM-1232).
|
|
58
|
-
*
|
|
59
|
-
* Without this, changed slots at skipped layouts are never sent to the
|
|
60
|
-
* client — the `continue` in the main loop skips slot resolution entirely.
|
|
61
|
-
* Removing the segment from the skippable set forces it to render with
|
|
62
|
-
* its slots. The prefix must stay contiguous, so all segments after the
|
|
63
|
-
* first changed-slot segment are also removed.
|
|
64
|
-
*/
|
|
65
|
-
function truncateSkippableForChangedSlots(
|
|
66
|
-
skippableSet: Set<number>,
|
|
67
|
-
segments: ManifestSegmentNode[],
|
|
68
|
-
segmentKeys: string[],
|
|
69
|
-
match: RouteMatch,
|
|
70
|
-
clientStateTree: ClientStateTree,
|
|
71
|
-
req: Request
|
|
72
|
-
): void {
|
|
73
|
-
const departingUrl = req.headers.get('X-Timber-URL');
|
|
74
|
-
let depParsed: URL | null = null;
|
|
75
|
-
if (departingUrl) {
|
|
76
|
-
try {
|
|
77
|
-
depParsed = new URL(departingUrl, 'http://localhost');
|
|
78
|
-
} catch {
|
|
79
|
-
skippableSet.clear();
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
const destPathname = new URL(req.url, 'http://localhost').pathname;
|
|
84
|
-
const depPathname = depParsed?.pathname ?? null;
|
|
85
|
-
const split = (p: string) => (p === '/' ? [] : p.slice(1).split('/'));
|
|
86
|
-
const destAll = split(destPathname);
|
|
87
|
-
const depAll = depPathname ? split(depPathname) : null;
|
|
88
|
-
const clientSlots = clientStateTree.slots ?? null;
|
|
89
|
-
const rawParams = match.rawSegmentParams ?? match.segmentParams ?? {};
|
|
90
|
-
|
|
91
|
-
for (let i = 0; i < segments.length; i++) {
|
|
92
|
-
if (!skippableSet.has(i)) continue;
|
|
93
|
-
const segment = segments[i];
|
|
94
|
-
const slotEntries = Object.entries(segment.slots ?? {});
|
|
95
|
-
if (slotEntries.length === 0) continue;
|
|
96
|
-
|
|
97
|
-
const segIdx = match.segments.indexOf(segment);
|
|
98
|
-
const parents =
|
|
99
|
-
segIdx >= 0 ? match.segments.slice(0, segIdx + 1).filter((s) => s.segmentName !== '') : [];
|
|
100
|
-
const sliceAt = extractUrlParts(parents, rawParams).length;
|
|
101
|
-
const destParts = destAll.slice(sliceAt);
|
|
102
|
-
const depParts = depAll ? depAll.slice(sliceAt) : null;
|
|
103
|
-
let parentChanged = false;
|
|
104
|
-
if (depAll) {
|
|
105
|
-
const dp = depAll.slice(0, sliceAt),
|
|
106
|
-
dd = destAll.slice(0, sliceAt);
|
|
107
|
-
parentChanged = dp.length !== dd.length || dp.some((p, j) => p !== dd[j]);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
for (const [slotName, slotNode] of slotEntries) {
|
|
111
|
-
const m = slotNode as ManifestSegmentNode;
|
|
112
|
-
const k = computeSlotKey(segmentKeys[i], `@${slotName}`);
|
|
113
|
-
if (
|
|
114
|
-
m.children.some((c) => c.segmentType === 'intercepting') ||
|
|
115
|
-
parentChanged ||
|
|
116
|
-
depParts === null ||
|
|
117
|
-
!shouldSkipSlot({
|
|
118
|
-
slotKey: k,
|
|
119
|
-
clientSlots,
|
|
120
|
-
slotNode: m,
|
|
121
|
-
departingUrlParts: depParts,
|
|
122
|
-
destinationUrlParts: destParts,
|
|
123
|
-
})
|
|
124
|
-
) {
|
|
125
|
-
for (let j = i; j < segments.length; j++) skippableSet.delete(j);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
58
|
/**
|
|
133
59
|
* Replace the outermost SegmentOutlet in a partial payload with its
|
|
134
60
|
* SegmentProvider child. Walks through AccessGate wrappers (which sit
|
|
@@ -175,6 +101,28 @@ const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
|
|
|
175
101
|
* function. Client components must go through createElement only — they are
|
|
176
102
|
* serialized as references in the RSC Flight stream, not executed on the server.
|
|
177
103
|
*/
|
|
104
|
+
/**
|
|
105
|
+
* The `segments` array handed to a layout's `SegmentProvider` — the value
|
|
106
|
+
* `useSelectedLayoutSegment(s)` reads.
|
|
107
|
+
*
|
|
108
|
+
* Only its **length** matters: both hooks take `segments.length` as a depth and
|
|
109
|
+
* index the live pathname with it. So this is really "how many URL parts had
|
|
110
|
+
* been consumed by the time this layout rendered", which is
|
|
111
|
+
* `consumedUrlParts` — the same derivation slot matching and the slot-skip
|
|
112
|
+
* slicing use, so a layout and the slots beside it cannot disagree about how
|
|
113
|
+
* deep they are. The root consumes nothing → `['']`, length 1.
|
|
114
|
+
*
|
|
115
|
+
* @internal — exported for tests; production has exactly one caller.
|
|
116
|
+
*/
|
|
117
|
+
export function computeLayoutSegmentPath(
|
|
118
|
+
segments: ManifestSegmentNode[],
|
|
119
|
+
index: number,
|
|
120
|
+
match: RouteMatch
|
|
121
|
+
): string[] {
|
|
122
|
+
const parts = consumedUrlParts(segments, index, match);
|
|
123
|
+
return parts.length === 0 ? [''] : ['', ...parts];
|
|
124
|
+
}
|
|
125
|
+
|
|
178
126
|
export function isClientReference(component: unknown): boolean {
|
|
179
127
|
return (
|
|
180
128
|
component != null &&
|
|
@@ -217,8 +165,12 @@ export interface LayoutComponentEntry {
|
|
|
217
165
|
|
|
218
166
|
/** Result of building a route element tree. */
|
|
219
167
|
export interface RouteElementResult {
|
|
220
|
-
/**
|
|
221
|
-
|
|
168
|
+
/**
|
|
169
|
+
* The payload root: the React element tree (page wrapped in layouts, access
|
|
170
|
+
* gates, error boundaries) plus the params it rendered with, as siblings.
|
|
171
|
+
* See shared/payload-root.ts for why params are not a wrapper around it.
|
|
172
|
+
*/
|
|
173
|
+
payload: RscPayloadRoot;
|
|
222
174
|
/** Layout components loaded along the segment chain. */
|
|
223
175
|
layoutComponents: LayoutComponentEntry[];
|
|
224
176
|
/** Segments from the route match. */
|
|
@@ -247,6 +199,17 @@ export interface RouteElementResult {
|
|
|
247
199
|
* enabling the client to populate its segment cache with slot entries.
|
|
248
200
|
*/
|
|
249
201
|
slotSkipInfo: SlotSkipEntry[];
|
|
202
|
+
/**
|
|
203
|
+
* Index of the first segment whose `access.ts` denied, or `Infinity`.
|
|
204
|
+
*
|
|
205
|
+
* AccessGate sits OUTSIDE the layout, so a denial means this segment and
|
|
206
|
+
* everything below it never rendered — no layout, no slots. It is the
|
|
207
|
+
* boundary of the tree that actually exists, which is why
|
|
208
|
+
* `collectMetadataRouteHeadElements` already filters on it and why
|
|
209
|
+
* `buildSegmentInfo` must too: the layouts were still *loaded*, so a list
|
|
210
|
+
* built from `layoutComponents` describes more than what mounted.
|
|
211
|
+
*/
|
|
212
|
+
firstDeniedIndex: number;
|
|
250
213
|
}
|
|
251
214
|
|
|
252
215
|
// ─── Module Processing Helpers ─────────────────────────────────────────────
|
|
@@ -554,6 +517,10 @@ export async function buildRouteElement(
|
|
|
554
517
|
// the same keys are used for skip decisions, SegmentOutlet props, and
|
|
555
518
|
// the X-Timber-Segments header (via buildSegmentInfo).
|
|
556
519
|
const segmentKeys = computeSegmentKeys(segments);
|
|
520
|
+
// The directory path of each segment — the key `setSlotParams` and codegen
|
|
521
|
+
// both use for a slot beneath it, and the identity interception scoping
|
|
522
|
+
// compares against. One derivation, so a slot cannot be keyed two ways.
|
|
523
|
+
const segmentTreePaths = computeSegmentTreePaths(segments);
|
|
557
524
|
|
|
558
525
|
// Pre-compute the contiguous skippable prefix (top-down).
|
|
559
526
|
//
|
|
@@ -645,8 +612,27 @@ export async function buildRouteElement(
|
|
|
645
612
|
|
|
646
613
|
// Slot caching: the departing URL (X-Timber-URL header) and the
|
|
647
614
|
// destination URL (request URL) are compared to determine which
|
|
648
|
-
// slots' content has changed.
|
|
649
|
-
//
|
|
615
|
+
// slots' content has changed.
|
|
616
|
+
//
|
|
617
|
+
// Pathnames only. The client sends `X-Timber-URL` as a pathname
|
|
618
|
+
// (`client/router.ts` takes `.pathname` on both branches) and
|
|
619
|
+
// `slot-resolver.ts` reads `.pathname` from both sides, so search
|
|
620
|
+
// params take no part in the skip decision.
|
|
621
|
+
//
|
|
622
|
+
// Nothing here can tell whether a slot's content depends on the query.
|
|
623
|
+
// What is supposed to make that safe lives elsewhere: a slot whose server
|
|
624
|
+
// render varies with the query must reach it through a request accessor
|
|
625
|
+
// (page components are built with no props), which the request-dep analysis
|
|
626
|
+
// is meant to catch — and that analysis is incomplete for accessors reached
|
|
627
|
+
// through indirection, so this is currently a real staleness hole. See
|
|
628
|
+
// TIM-1291 and design/19-client-navigation.md. A slot whose *client* subtree
|
|
629
|
+
// varies with the query reads useSearchParams(), which re-renders from
|
|
630
|
+
// NavigationContext whether or not the slot was skipped; that half is
|
|
631
|
+
// unconditional.
|
|
632
|
+
//
|
|
633
|
+
// Do not add search to the comparison by sending the full departing URL:
|
|
634
|
+
// the header is hashed into the public `_rsc` cache key (TIM-1268),
|
|
635
|
+
// and a digest does not anonymize a guessable query string.
|
|
650
636
|
const departingUrl = req.headers.get('X-Timber-URL');
|
|
651
637
|
const destinationUrl = req.url;
|
|
652
638
|
const slotSkipInfo: SlotSkipEntry[] = [];
|
|
@@ -661,6 +647,14 @@ export async function buildRouteElement(
|
|
|
661
647
|
// Record for X-Timber-Skipped-Segments header (outermost first, so prepend).
|
|
662
648
|
skippedSegments.unshift(segmentKeys[i]);
|
|
663
649
|
|
|
650
|
+
// A skipped layout never reaches resolveSlotProps below, but its slots
|
|
651
|
+
// stay mounted on the client — so their params still have to be
|
|
652
|
+
// published, or useSegmentParams(slotPath) falls back to the main
|
|
653
|
+
// route's record after any navigation that skipped their parent
|
|
654
|
+
// (TIM-1285). Params only: nothing renders and no access chain runs,
|
|
655
|
+
// because an unchanged slot is what made this layout skippable.
|
|
656
|
+
publishSlotSegmentParams(segment, match, destinationUrl, segmentTreePaths[i]);
|
|
657
|
+
|
|
664
658
|
// SECURITY: Even though the layout is skipped, AccessGate MUST still
|
|
665
659
|
// wrap the element tree. access.ts runs on every navigation regardless
|
|
666
660
|
// of cached layouts or state tree content.
|
|
@@ -705,13 +699,7 @@ export async function buildRouteElement(
|
|
|
705
699
|
// so slot props are never consumed. Resolving them is wasted work
|
|
706
700
|
// and could trigger side effects in slot access.ts functions.
|
|
707
701
|
const segmentId = segmentKeys[i];
|
|
708
|
-
const parentTreePath =
|
|
709
|
-
'/' +
|
|
710
|
-
segments
|
|
711
|
-
.slice(0, i + 1)
|
|
712
|
-
.map((s) => s.segmentName)
|
|
713
|
-
.filter(Boolean)
|
|
714
|
-
.join('/');
|
|
702
|
+
const parentTreePath = segmentTreePaths[i];
|
|
715
703
|
const slotProps =
|
|
716
704
|
i >= firstDeniedIndex
|
|
717
705
|
? {}
|
|
@@ -728,10 +716,7 @@ export async function buildRouteElement(
|
|
|
728
716
|
slotSkipInfo,
|
|
729
717
|
});
|
|
730
718
|
|
|
731
|
-
|
|
732
|
-
// The extra empty string makes useSelectedLayoutSegment skip the first URL segment.
|
|
733
|
-
const rawPath = segment.urlPath.split('/');
|
|
734
|
-
const segmentPath = rawPath.length === 2 && rawPath[1] === '' ? [''] : rawPath;
|
|
719
|
+
const segmentPath = computeLayoutSegmentPath(segments, i, match);
|
|
735
720
|
const parallelRouteKeys = Object.keys(segment.slots ?? {});
|
|
736
721
|
|
|
737
722
|
// Build the layout element.
|
|
@@ -854,13 +839,19 @@ export async function buildRouteElement(
|
|
|
854
839
|
shellSettled = Promise.race([Promise.all(shellPromises).then(() => {}), shortCircuit]);
|
|
855
840
|
}
|
|
856
841
|
|
|
842
|
+
// Publish the request's params beside the finished tree, as the payload's
|
|
843
|
+
// root row. Last, because the partial-navigation rewrite above rebuilds the
|
|
844
|
+
// tree and this takes it as it finally stands.
|
|
845
|
+
const payload = withPublishedParams(element);
|
|
846
|
+
|
|
857
847
|
return {
|
|
858
|
-
|
|
848
|
+
payload,
|
|
859
849
|
layoutComponents,
|
|
860
850
|
segments,
|
|
861
851
|
deferSuspenseFor,
|
|
862
852
|
skippedSegments,
|
|
863
853
|
shellSettled,
|
|
864
854
|
slotSkipInfo,
|
|
855
|
+
firstDeniedIndex,
|
|
865
856
|
};
|
|
866
857
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { RouteContext } from './types.js';
|
|
12
12
|
import { logRouteError } from './logger.js';
|
|
13
13
|
import { DenySignal, RedirectSignal } from './primitives.js';
|
|
14
|
+
import { stripHeadBody } from './head-response.js';
|
|
14
15
|
|
|
15
16
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
16
17
|
|
|
@@ -89,18 +90,17 @@ export async function handleRouteRequest(mod: RouteModule, ctx: RouteContext): P
|
|
|
89
90
|
});
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
// HEAD fallback —
|
|
93
|
+
// HEAD fallback — dispatch to GET when the module exports no HEAD, so the
|
|
94
|
+
// headers are the ones a GET would produce. The body is stripped by the
|
|
95
|
+
// shared helper (also applied at the pipeline exit, where it is a no-op the
|
|
96
|
+
// second time) rather than open-coded here — one implementation means the
|
|
97
|
+
// discarded stream is cancelled on every path. See TIM-1324.
|
|
93
98
|
if (method === 'HEAD') {
|
|
94
99
|
if (mod.HEAD) {
|
|
95
|
-
return runHandler(mod.HEAD, ctx);
|
|
100
|
+
return stripHeadBody(await runHandler(mod.HEAD, ctx), method);
|
|
96
101
|
}
|
|
97
102
|
if (mod.GET) {
|
|
98
|
-
|
|
99
|
-
// Return headers + status but no body
|
|
100
|
-
return new Response(null, {
|
|
101
|
-
status: res.status,
|
|
102
|
-
headers: res.headers,
|
|
103
|
-
});
|
|
103
|
+
return stripHeadBody(await runHandler(mod.GET, ctx), method);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -83,10 +83,27 @@ function matchPathname(root: ManifestSegmentNode, pathname: string): RouteMatch
|
|
|
83
83
|
const result = matchUrlParts(root, parts);
|
|
84
84
|
if (!result) return null;
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
return {
|
|
87
|
+
segments: result.chain,
|
|
88
|
+
segmentParams: result.params,
|
|
89
|
+
middlewareChain: collectMiddlewareChain(result.chain),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Collect middleware from a matched segment chain, root to leaf.
|
|
95
|
+
*
|
|
96
|
+
* Each entry is lazy-loaded via dynamic import on first invocation.
|
|
97
|
+
*
|
|
98
|
+
* Shared with the children-path interception resolver
|
|
99
|
+
* (`children-interception.ts`), which assembles a chain from the tree rather
|
|
100
|
+
* than from a pathname but produces the same kind of `RouteMatch` — every
|
|
101
|
+
* `middleware.ts` above an intercepted page must run exactly as it does for
|
|
102
|
+
* the canonical route.
|
|
103
|
+
*/
|
|
104
|
+
export function collectMiddlewareChain(chain: ManifestSegmentNode[]): MiddlewareFn[] {
|
|
88
105
|
const middlewareChain: MiddlewareFn[] = [];
|
|
89
|
-
for (const segment of
|
|
106
|
+
for (const segment of chain) {
|
|
90
107
|
if (segment.middleware) {
|
|
91
108
|
const loader = segment.middleware.load;
|
|
92
109
|
middlewareChain.push(async (ctx) => {
|
|
@@ -97,12 +114,7 @@ function matchPathname(root: ManifestSegmentNode, pathname: string): RouteMatch
|
|
|
97
114
|
});
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
segments: result.chain,
|
|
103
|
-
segmentParams: result.params,
|
|
104
|
-
middlewareChain,
|
|
105
|
-
};
|
|
117
|
+
return middlewareChain;
|
|
106
118
|
}
|
|
107
119
|
|
|
108
120
|
// ─── Metadata Route Matcher ─────────────────────────────────────────────
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Origin-side binding of the `_rsc` cache key to the request that produced
|
|
3
|
+
* the response (TIM-1268).
|
|
4
|
+
*
|
|
5
|
+
* The key is deterministic, and deliberately so — that is what makes RSC
|
|
6
|
+
* payloads cacheable. But deterministic also means *predictable*: anyone can
|
|
7
|
+
* compute the key another client will request. Without this guard, a caller
|
|
8
|
+
* could request `/about?_rsc=<victim's key>` while sending its own
|
|
9
|
+
* `X-Timber-State-Tree`, and a shared cache that keys by URL — the exact
|
|
10
|
+
* environment this feature targets — would store that variant under the
|
|
11
|
+
* victim's key. Every subsequent legitimate navigation would then be served
|
|
12
|
+
* a payload rendered for someone else's segment tree or deployment.
|
|
13
|
+
*
|
|
14
|
+
* The guard is a *storability* rule, not a rejection: if the claimed key
|
|
15
|
+
* does not match the key derived from the received headers, the response
|
|
16
|
+
* loses its shared-cache directives. Mismatched requests are still served
|
|
17
|
+
* normally, they just cannot be stored for anyone else.
|
|
18
|
+
*
|
|
19
|
+
* Why not Next.js's 307 self-correction: a redirect is cacheable in its own
|
|
20
|
+
* right, costs a round trip on every legitimate miss, and would make the
|
|
21
|
+
* server's behaviour depend on a parameter it otherwise never reads. This
|
|
22
|
+
* rule closes the same hole by making the poisoned entry unstorable at the
|
|
23
|
+
* source, in the response the developer already has to opt into sharing.
|
|
24
|
+
*
|
|
25
|
+
* See design/19-client-navigation.md §"The `_rsc` cache key".
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { rscCacheKey, isRscCacheKeyShape, RSC_KEY_PARAM } from '../shared/rsc-cache-key.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Legacy shared-cache control fields that predate RFC 9213 and so do not
|
|
32
|
+
* follow the `<target>-Cache-Control` naming pattern — the one part of this
|
|
33
|
+
* rule that cannot be matched by shape. `X-Accel-Expires` is nginx
|
|
34
|
+
* `proxy_cache`, which gives it precedence over `Cache-Control`.
|
|
35
|
+
*
|
|
36
|
+
* A closed historical set, unlike the vendor `*-Cache-Control` names, which
|
|
37
|
+
* keep being minted and are handled by pattern below.
|
|
38
|
+
*/
|
|
39
|
+
const LEGACY_CDN_CACHE_HEADERS = ['Surrogate-Control', 'Edge-Control', 'X-Accel-Expires'];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether a response header overrides `Cache-Control` at a shared cache.
|
|
43
|
+
*
|
|
44
|
+
* Matched by *shape*, not by vendor name. RFC 9213 defines targeted fields
|
|
45
|
+
* as `<target>-Cache-Control`, and every CDN spells its own: `CDN-`,
|
|
46
|
+
* `Cloudflare-CDN-`, `Netlify-CDN-`, `Vercel-CDN-`, and whichever ships
|
|
47
|
+
* next. A vendor list here is a denylist with an unbounded tail — it was
|
|
48
|
+
* extended twice during review, each time by one name, which is the signal
|
|
49
|
+
* that enumeration is the wrong shape. The suffix rule is total.
|
|
50
|
+
*
|
|
51
|
+
* `Cache-Control` itself does not match (no `-` prefix) and is replaced
|
|
52
|
+
* separately.
|
|
53
|
+
*/
|
|
54
|
+
function isSharedCacheOverride(name: string): boolean {
|
|
55
|
+
const lowered = name.toLowerCase();
|
|
56
|
+
return (
|
|
57
|
+
lowered.endsWith('-cache-control') ||
|
|
58
|
+
LEGACY_CDN_CACHE_HEADERS.some((legacy) => legacy.toLowerCase() === lowered)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether the request's `_rsc` parameter matches the key derived from its
|
|
64
|
+
* own headers.
|
|
65
|
+
*
|
|
66
|
+
* Requests without the parameter are not claiming a key at all, so they are
|
|
67
|
+
* left alone — the bare document URL is the developer's declared territory
|
|
68
|
+
* (see design/32-response-caching.md, TIM-1264). Neither is a parameter
|
|
69
|
+
* whose value could not be one of our digests: `_rsc` is not reserved, and
|
|
70
|
+
* an application that already uses it for its own search state
|
|
71
|
+
* (`/search?_rsc=foo`) must not have its cache policy silently revoked by a
|
|
72
|
+
* guard that mistook that for a forgery.
|
|
73
|
+
*/
|
|
74
|
+
export async function hasValidRscCacheKey(req: Request): Promise<boolean> {
|
|
75
|
+
// Fast path: this runs on every request, and the overwhelming majority
|
|
76
|
+
// (document requests) have no query string at all. Keyed on `?` and not
|
|
77
|
+
// on the literal `_rsc=`, because a substring test is not a parse: a
|
|
78
|
+
// request for `?%5Frsc=<key>` carries no literal `_rsc=`, yet
|
|
79
|
+
// `URLSearchParams` decodes it to one — and a cache that canonicalizes
|
|
80
|
+
// percent-encoded unreserved characters in its key while forwarding the
|
|
81
|
+
// original URL would store the response under the decoded, claimed key.
|
|
82
|
+
// The only sound fast path is one that cannot answer "no key" wrongly.
|
|
83
|
+
if (!req.url.includes('?')) return true;
|
|
84
|
+
let values: string[];
|
|
85
|
+
try {
|
|
86
|
+
values = new URL(req.url).searchParams.getAll(RSC_KEY_PARAM);
|
|
87
|
+
} catch {
|
|
88
|
+
// Unparseable URL — nothing to validate, and nothing safe to assume.
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
// Every value, not the first. An application that owns `_rsc` for its own
|
|
92
|
+
// search state makes the client emit `?_rsc=foo&_rsc=<key>` — reading only
|
|
93
|
+
// the first would see `foo`, exempt it as application data, and never
|
|
94
|
+
// check the real key sitting beside it.
|
|
95
|
+
const claims = values.filter(isRscCacheKeyShape);
|
|
96
|
+
if (claims.length === 0) return true;
|
|
97
|
+
const expected = await rscCacheKey((name) => req.headers.get(name));
|
|
98
|
+
// No digest available means the claim is unverifiable, which is not the
|
|
99
|
+
// same as verified. Every server runtime timber targets has
|
|
100
|
+
// `crypto.subtle`, so this is a guard against a future one that does not.
|
|
101
|
+
if (expected === null) return false;
|
|
102
|
+
// All of them, because at most one can be legitimate and a cache keys on
|
|
103
|
+
// the whole query string — a second, mismatched key would ride along
|
|
104
|
+
// inside an address that otherwise looks validated.
|
|
105
|
+
return claims.every((claim) => claim === expected);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Media type of a Flight payload. Also spelled in `rsc-entry/helpers.ts`,
|
|
110
|
+
* `deny-renderer.ts`, `action-handler.ts`, and `client/rsc-fetch.ts` — that
|
|
111
|
+
* duplication predates this module and is tracked separately.
|
|
112
|
+
*/
|
|
113
|
+
const RSC_MEDIA_TYPE = 'text/x-component';
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Ensure a Flight payload can never be interpreted as a document.
|
|
117
|
+
*
|
|
118
|
+
* Applied here, at the pipeline's single exit, rather than where each
|
|
119
|
+
* payload response is built. Six places build one — the success path, three
|
|
120
|
+
* in `deny-renderer.ts`, the action paths — and hardening them one at a time
|
|
121
|
+
* is how you end up hardening five of six. The response is Flight if it says
|
|
122
|
+
* it is; that is the only test that covers every producer, including ones
|
|
123
|
+
* added later.
|
|
124
|
+
*
|
|
125
|
+
* Matters because the payload URL is observable and, on a shared-cache hit,
|
|
126
|
+
* the origin is never consulted — so a top-level navigation can be handed
|
|
127
|
+
* these bytes with no request-time check available. See design/13-security.md
|
|
128
|
+
* #36c.
|
|
129
|
+
*/
|
|
130
|
+
export function hardenRscPayloadResponse(response: Response): void {
|
|
131
|
+
const contentType = response.headers.get('content-type');
|
|
132
|
+
if (!contentType) return;
|
|
133
|
+
if (contentType.split(';')[0].trim().toLowerCase() !== RSC_MEDIA_TYPE) return;
|
|
134
|
+
response.headers.set('X-Content-Type-Options', 'nosniff');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Make a response unstorable by any shared cache when its request claimed an
|
|
139
|
+
* `_rsc` key that does not match its own headers.
|
|
140
|
+
*
|
|
141
|
+
* The end state is unconditional and therefore statable: **a mismatched
|
|
142
|
+
* response is never stored by any cache.** Every targeted cache-control
|
|
143
|
+
* field is removed and `Cache-Control` is replaced outright, rather than
|
|
144
|
+
* downgrading only when a shared directive is spotted. Conditional variants
|
|
145
|
+
* of this were tried and were wrong twice over — they missed the CDN-
|
|
146
|
+
* specific headers, and leaving `Cache-Control` alone when it looked
|
|
147
|
+
* harmless lets a CDN apply heuristic caching to a response that has none.
|
|
148
|
+
*
|
|
149
|
+
* The cost is that a mismatched request loses developer-chosen *browser*
|
|
150
|
+
* caching too. That is a request no timber client ever makes.
|
|
151
|
+
*
|
|
152
|
+
* Purge tags (`Surrogate-Key`, `Cache-Tag`) are deliberately left alone:
|
|
153
|
+
* they address stored entries, and this response is not stored.
|
|
154
|
+
*
|
|
155
|
+
* Mutates `response.headers` in place; the pipeline guarantees mutable
|
|
156
|
+
* headers at this point (see TIM-866).
|
|
157
|
+
*/
|
|
158
|
+
export async function enforceRscCacheKey(req: Request, response: Response): Promise<void> {
|
|
159
|
+
if (await hasValidRscCacheKey(req)) return;
|
|
160
|
+
// Collected before deleting — mutating Headers while iterating its live
|
|
161
|
+
// key iterator can skip entries.
|
|
162
|
+
const overrides: string[] = [];
|
|
163
|
+
for (const name of response.headers.keys()) {
|
|
164
|
+
if (isSharedCacheOverride(name)) overrides.push(name);
|
|
165
|
+
}
|
|
166
|
+
for (const name of overrides) {
|
|
167
|
+
response.headers.delete(name);
|
|
168
|
+
}
|
|
169
|
+
response.headers.set('Cache-Control', 'private, no-store');
|
|
170
|
+
}
|
|
@@ -11,6 +11,7 @@ import type { ManifestSegmentNode } from '../route-matcher.js';
|
|
|
11
11
|
import { loadModule } from '../safe-load.js';
|
|
12
12
|
import { DenySignal, RedirectSignal } from '../primitives.js';
|
|
13
13
|
import { handleRouteRequest } from '../route-handler.js';
|
|
14
|
+
import { stripHeadBody } from '../head-response.js';
|
|
14
15
|
import type { RouteModule } from '../route-handler.js';
|
|
15
16
|
import type { RouteContext } from '../types.js';
|
|
16
17
|
|
|
@@ -89,9 +90,10 @@ export async function handleApiRoute(
|
|
|
89
90
|
* method handler into the proper API response: DenySignal → JSON status-file
|
|
90
91
|
* chain via `renderApiDeny`, RedirectSignal → bare 3xx with Location header.
|
|
91
92
|
*
|
|
92
|
-
* For HEAD requests, strip the deny body to preserve HEAD semantics —
|
|
93
|
-
*
|
|
94
|
-
*
|
|
93
|
+
* For HEAD requests, strip the deny body to preserve HEAD semantics — signals
|
|
94
|
+
* bypass the HEAD path in `handleRouteRequest`, so this exit needs its own
|
|
95
|
+
* call. Shares `stripHeadBody` with that path and with the pipeline exit; the
|
|
96
|
+
* pipeline applying it again downstream is a no-op (TIM-1324).
|
|
95
97
|
*
|
|
96
98
|
* Returns `null` for non-signal errors so the caller knows to rethrow.
|
|
97
99
|
*/
|
|
@@ -102,11 +104,7 @@ async function translateApiSignal(
|
|
|
102
104
|
responseHeaders: Headers
|
|
103
105
|
): Promise<Response | null> {
|
|
104
106
|
if (error instanceof DenySignal) {
|
|
105
|
-
|
|
106
|
-
if (req.method.toUpperCase() === 'HEAD') {
|
|
107
|
-
return new Response(null, { status: res.status, headers: res.headers });
|
|
108
|
-
}
|
|
109
|
-
return res;
|
|
107
|
+
return stripHeadBody(await renderApiDeny(error, segments, responseHeaders), req.method);
|
|
110
108
|
}
|
|
111
109
|
if (error instanceof RedirectSignal) {
|
|
112
110
|
responseHeaders.set('Location', error.location);
|