@timber-js/app 0.2.0-alpha.184 → 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-C539Pa2O.js.map +1 -0
- package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
- package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
- package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
- package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
- package/dist/_chunks/use-segment-params-ClyUNq4d.js +128 -0
- package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +1 -0
- package/dist/_chunks/{walkers-BtTlKviE.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 +872 -305
- 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 +39 -16
- 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 +915 -320
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +33 -1
- 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 +149 -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/types.d.ts +1 -3
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/routing/walkers.d.ts +6 -0
- package/dist/routing/walkers.d.ts.map +1 -1
- package/dist/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-boundary.d.ts +4 -7
- package/dist/server/deny-boundary.d.ts.map +1 -1
- 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 +528 -373
- 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/prebuilt-builder.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +0 -12
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/publish-params.d.ts +33 -0
- package/dist/server/publish-params.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +48 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +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/sitemap-generator.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/server/status-code-resolver.d.ts +3 -10
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- 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/dist/shims/navigation-client.d.ts +8 -19
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +0 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- 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 +4 -3
- 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 -690
- package/src/client/rsc-fetch.ts +180 -122
- 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 +34 -1
- 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 +60 -3
- package/src/plugins/static-build.ts +333 -63
- 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 +6 -9
- package/src/routing/scanner.ts +114 -113
- package/src/routing/segment-classify.ts +57 -0
- package/src/routing/segment-keys.ts +146 -0
- package/src/routing/slot-placement.ts +97 -0
- package/src/routing/types.ts +1 -3
- 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-boundary.ts +5 -31
- package/src/server/deny-renderer.ts +83 -22
- 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 +3 -5
- package/src/server/pipeline-phases.ts +68 -16
- package/src/server/pipeline.ts +57 -10
- package/src/server/prebuilt-builder.ts +1 -7
- package/src/server/prebuilt-runtime.ts +4 -4
- package/src/server/primitives.ts +0 -18
- 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 +12 -7
- 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/sitemap-generator.ts +1 -7
- 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 +67 -45
- package/src/server/static-not-found.ts +134 -0
- package/src/server/status-code-resolver.ts +4 -47
- 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/src/shims/navigation-client.ts +8 -26
- package/src/shims/navigation.ts +0 -4
- 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-D3naS3eH.js +0 -688
- package/dist/_chunks/cli-schema-sync-D3naS3eH.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/segment-classify-Byy425ng.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-BtTlKviE.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
|
@@ -10,47 +10,96 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { classifyUrlSegment } from '../routing/segment-classify.js';
|
|
13
|
-
|
|
14
|
-
/** Result of a successful interception match. */
|
|
15
|
-
export interface InterceptionMatchResult {
|
|
16
|
-
/** The pathname to re-match (the source/intercepting route's parent). */
|
|
17
|
-
sourcePathname: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Check if a pathname starts with a prefix on a segment boundary.
|
|
22
|
-
*
|
|
23
|
-
* Prevents /feed from matching /feed-private — the prefix must be
|
|
24
|
-
* followed by '/' or be an exact match.
|
|
25
|
-
*/
|
|
26
|
-
function hasSegmentPrefix(pathname: string, prefix: string): boolean {
|
|
27
|
-
if (prefix === '/') return true;
|
|
28
|
-
if (!pathname.startsWith(prefix)) return false;
|
|
29
|
-
return pathname.length === prefix.length || pathname[prefix.length] === '/';
|
|
30
|
-
}
|
|
13
|
+
import type { InterceptionRewrite } from '../routing/interception.js';
|
|
31
14
|
|
|
32
15
|
/**
|
|
33
16
|
* Check if an intercepting route applies for this soft navigation.
|
|
34
17
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
18
|
+
* Two conditions must hold:
|
|
19
|
+
*
|
|
20
|
+
* 1. The **target** pathname matches the rewrite's intercepted pattern.
|
|
21
|
+
* 2. The **source** route — the page the user is navigating FROM, per the
|
|
22
|
+
* validated X-Timber-URL header — renders the segment that owns the slot
|
|
23
|
+
* holding the intercepting page (`interceptingScope`). Only then does that
|
|
24
|
+
* slot exist in the rendered tree, so only then can the intercepting page
|
|
25
|
+
* render.
|
|
37
26
|
*
|
|
38
|
-
*
|
|
27
|
+
* Condition 2 replaced a URL-prefix check (TIM-1279). A prefix cannot
|
|
28
|
+
* distinguish a route-group-scoped interception from a genuinely root-level
|
|
29
|
+
* one — route groups are invisible in `urlPath`, so both collapse to "/" and
|
|
30
|
+
* the guard degenerates to always-true. Tree paths carry group identity.
|
|
31
|
+
* The scope check also subsumes the old segment-boundary rule: a source route
|
|
32
|
+
* under /feed-private has no "/feed" in its chain.
|
|
33
|
+
*
|
|
34
|
+
* A third condition lives with the caller, which owns route matching, and it
|
|
35
|
+
* applies to the `'slot'` shape only: the route produced by re-matching
|
|
36
|
+
* `interceptingPrefix` must pass through the scope too. Conditions 1–2 are
|
|
37
|
+
* about the route the user is leaving; the slot has to exist in the tree that
|
|
38
|
+
* actually renders. The `'children'` shape resolves its owner from the scope
|
|
39
|
+
* directly rather than from a pathname, so it satisfies that by construction.
|
|
40
|
+
*
|
|
41
|
+
* `resolveSourceScopes` is a thunk so its inputs are only resolved when some
|
|
42
|
+
* rewrite's pattern actually matches the target — the common case is no match
|
|
43
|
+
* at all. It returns the two client-supplied answers to "what is on screen?"
|
|
44
|
+
* as **separate sets, most authoritative first**: the tree paths the client
|
|
45
|
+
* says it has mounted, then those of the route the departing URL matches. It
|
|
46
|
+
* returns null only when neither yields anything, which is not a soft
|
|
47
|
+
* navigation this can reason about.
|
|
48
|
+
*
|
|
49
|
+
* They are two sets rather than one union because ranking needs a chain
|
|
50
|
+
* (TIM-1318). Each channel describes a single rendered chain, so within one the
|
|
51
|
+
* scopes are strictly nested and the deepest is the innermost owner. Unioned
|
|
52
|
+
* they are not: once a modal is open the address bar holds the *intercepted*
|
|
53
|
+
* URL, so the second channel describes the canonical branch — possibly an
|
|
54
|
+
* exclusive route group the client never mounted — and a deeper scope from
|
|
55
|
+
* there would outrank the owner actually on screen. Trying the mounted channel
|
|
56
|
+
* to exhaustion first keeps the fallback the union was there to provide while
|
|
57
|
+
* ranking only within one chain.
|
|
58
|
+
*
|
|
59
|
+
* Returns the winning build-time rewrite, or null. Only build-time values ever
|
|
60
|
+
* reach the renderer: the client-supplied header selects a rewrite but never
|
|
61
|
+
* becomes a rendered route or pathname.
|
|
62
|
+
*
|
|
63
|
+
* The **first** matching rewrite wins, and the caller does not fall back to the
|
|
64
|
+
* next one when the winner fails to resolve. That is deliberate and pre-dates
|
|
65
|
+
* the two shapes: a rewrite that matched the target and the scope described the
|
|
66
|
+
* author's intent, and quietly rendering some other author's interception
|
|
67
|
+
* instead would be harder to explain than rendering the canonical route.
|
|
68
|
+
*
|
|
69
|
+
* "First" is meaningful because the list arrives **deepest scope first** —
|
|
70
|
+
* `collectInterceptionRewrites` sorts it at build time, so the deepest owner a
|
|
71
|
+
* single chain passes through wins and nothing here has to score (TIM-1318).
|
|
72
|
+
* Tree-walk order used to decide it, which meant renaming a route group flipped
|
|
73
|
+
* which modal opened. The precondition is a property of the manifest this reads
|
|
74
|
+
* rather than something it can re-derive, so it is asserted at its source:
|
|
75
|
+
* `tests/interception.test.ts` §"rewrite order is decided by scope depth".
|
|
39
76
|
*/
|
|
40
77
|
export function findInterceptionMatch(
|
|
41
78
|
targetPathname: string,
|
|
42
|
-
|
|
43
|
-
rewrites:
|
|
44
|
-
):
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
79
|
+
resolveSourceScopes: () => ReadonlySet<string>[] | null,
|
|
80
|
+
rewrites: InterceptionRewrite[]
|
|
81
|
+
): InterceptionRewrite | null {
|
|
82
|
+
// Dynamic segments in the pattern match any single URL segment. Collected up
|
|
83
|
+
// front rather than inside the channel loop so a pattern is matched once
|
|
84
|
+
// however many channels are tried — and the common case, no rewrite matching
|
|
85
|
+
// at all, costs exactly the same scan it always did and resolves no scope.
|
|
86
|
+
const candidates = rewrites.filter((rewrite) =>
|
|
87
|
+
pathnameMatchesPattern(targetPathname, rewrite.interceptedPattern)
|
|
88
|
+
);
|
|
89
|
+
if (candidates.length === 0) return null;
|
|
90
|
+
|
|
91
|
+
// Nothing at all is known about the source tree — no scope can contain
|
|
92
|
+
// anything, so no rewrite can apply.
|
|
93
|
+
const scopes = resolveSourceScopes();
|
|
94
|
+
if (!scopes) return null;
|
|
49
95
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
96
|
+
// `candidates` is deepest scope first (`collectInterceptionRewrites`), so
|
|
97
|
+
// within one channel the first hit is the innermost owner that channel
|
|
98
|
+
// reports. Channels are tried in order, so a scope the client says it has
|
|
99
|
+
// mounted always beats one only the address bar implies.
|
|
100
|
+
for (const scope of scopes) {
|
|
101
|
+
for (const rewrite of candidates) {
|
|
102
|
+
if (scope.has(rewrite.interceptingScope)) return rewrite;
|
|
54
103
|
}
|
|
55
104
|
}
|
|
56
105
|
return null;
|
|
@@ -26,16 +26,14 @@ import {
|
|
|
26
26
|
import { markResponseFlushed } from './request-context.js';
|
|
27
27
|
import { RedirectSignal, DenySignal } from './primitives.js';
|
|
28
28
|
import { isDebug } from './debug.js';
|
|
29
|
+
import { rscErrorEnvelope } from '../shared/rsc-error-envelope.js';
|
|
29
30
|
import type { PipelineConfig, RouteMatch } from './pipeline.js';
|
|
30
31
|
|
|
31
32
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
32
33
|
|
|
33
34
|
function rscErrorResponse(isRsc: boolean, status: number, headers?: Headers): Response {
|
|
34
35
|
if (!isRsc) return new Response(null, { status });
|
|
35
|
-
|
|
36
|
-
h.set('X-Timber-Error', '1');
|
|
37
|
-
h.set('content-type', 'application/json; charset=utf-8');
|
|
38
|
-
return new Response(JSON.stringify({ error: true, status }), { status, headers: h });
|
|
36
|
+
return rscErrorEnvelope(status, headers);
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
// ─── Phase Outcome ─────────────────────────────────────────────────────────
|
|
@@ -132,7 +130,7 @@ export async function outcomeToResponse(
|
|
|
132
130
|
`[timber] DenySignal(${outcome.signal.status}) from ${outcome.phase} phase — ` +
|
|
133
131
|
`no renderDenyFallback configured, returning bare ${outcome.signal.status} response\n` +
|
|
134
132
|
` Request: ${ctx.method} ${ctx.path}\n` +
|
|
135
|
-
` Add a
|
|
133
|
+
` Add a status-code file (e.g. 404.tsx) or error.tsx to render a custom deny page.`
|
|
136
134
|
);
|
|
137
135
|
}
|
|
138
136
|
return new Response(null, { status: outcome.signal.status, headers });
|
|
@@ -30,6 +30,8 @@ import { checkVersionSkew, applyReloadHeaders } from './version-skew.js';
|
|
|
30
30
|
import { serveStaticMetadataFile, serializeSitemap } from './pipeline-metadata.js';
|
|
31
31
|
import { loadModule } from './safe-load.js';
|
|
32
32
|
import { findInterceptionMatch } from './pipeline-interception.js';
|
|
33
|
+
import { computeSegmentTreePaths } from '../routing/segment-keys.js';
|
|
34
|
+
import { parseClientStateTree } from './state-tree-diff.js';
|
|
33
35
|
import { applyCookieJar, cloneWithMutableHeaders, type ProxyResolver } from './pipeline-helpers.js';
|
|
34
36
|
import { coerceSegmentParams } from './param-coercion.js';
|
|
35
37
|
import { outcomeToResponse, type PhaseOutcome } from './pipeline-outcome.js';
|
|
@@ -293,17 +295,6 @@ export async function handleRequest(
|
|
|
293
295
|
// outer Server-Timing writer can append without hitting an
|
|
294
296
|
// immutable header bag (e.g. user returns Response.redirect()).
|
|
295
297
|
if (handlerResult instanceof Response) {
|
|
296
|
-
if (method === 'HEAD') {
|
|
297
|
-
const headHeaders = new Headers(handlerResult.headers);
|
|
298
|
-
if (handlerResult.ok && !headHeaders.has('Cache-Control')) {
|
|
299
|
-
headHeaders.set('Cache-Control', 'public, max-age=14400, must-revalidate');
|
|
300
|
-
}
|
|
301
|
-
return new Response(null, {
|
|
302
|
-
status: handlerResult.status,
|
|
303
|
-
statusText: handlerResult.statusText,
|
|
304
|
-
headers: headHeaders,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
298
|
const res = cloneWithMutableHeaders(handlerResult);
|
|
308
299
|
if (res.ok && !res.headers.has('Cache-Control')) {
|
|
309
300
|
res.headers.set('Cache-Control', 'public, max-age=14400, must-revalidate');
|
|
@@ -401,14 +392,75 @@ export async function handleRequest(
|
|
|
401
392
|
? validateInterceptionHeader(rawSourceUrl, stripTrailingSlash)
|
|
402
393
|
: null;
|
|
403
394
|
if (validatedSourceUrl) {
|
|
404
|
-
|
|
395
|
+
// The scope of an interception is the segment that owns the intercepting
|
|
396
|
+
// slot, so the guard asks whether the route the user is LEAVING renders
|
|
397
|
+
// that segment. Two independent answers to "what is on screen?", tried in
|
|
398
|
+
// order — deferred behind a thunk so neither runs unless a rewrite's
|
|
399
|
+
// target pattern matches.
|
|
400
|
+
//
|
|
401
|
+
// 1. What the client says it has mounted (X-Timber-State-Tree
|
|
402
|
+
// `treePaths`). Authoritative about tree shape and nothing else.
|
|
403
|
+
// 2. What re-matching X-Timber-URL implies. Correct while the address bar
|
|
404
|
+
// still holds the source page, which stops being true the moment a
|
|
405
|
+
// modal is open — the address is then the *intercepted* URL and
|
|
406
|
+
// re-matching it resolves through whichever group owns the canonical
|
|
407
|
+
// page (TIM-1282).
|
|
408
|
+
//
|
|
409
|
+
// Neither is trusted: both are client-supplied, and satisfying the scope
|
|
410
|
+
// only selects which build-time rewrite applies. The rendered pathname is
|
|
411
|
+
// always the rewrite's own `interceptingPrefix`, the server re-derives the
|
|
412
|
+
// rendered chain from it below, and every access.ts runs regardless.
|
|
413
|
+
//
|
|
414
|
+
// They are handed over as two sets in that order rather than one union:
|
|
415
|
+
// each describes a single rendered chain, and only within one chain does
|
|
416
|
+
// "the deepest scope" name the innermost owner (TIM-1318). Unioned, a
|
|
417
|
+
// deeper scope from the address-implied canonical branch — which after
|
|
418
|
+
// (2) may be a route group the client never mounted — would outrank the
|
|
419
|
+
// owner actually on screen.
|
|
420
|
+
const resolveSourceScopes = () => {
|
|
421
|
+
const mounted = new Set(parseClientStateTree(req)?.treePaths ?? []);
|
|
422
|
+
const sourceRoute = config.matchRoute(validatedSourceUrl);
|
|
423
|
+
const addressed = new Set(sourceRoute ? computeSegmentTreePaths(sourceRoute.segments) : []);
|
|
424
|
+
const scopes = [mounted, addressed].filter((scope) => scope.size > 0);
|
|
425
|
+
// Neither channel produced a tree — no scope can contain anything, and
|
|
426
|
+
// an origin that matches no route is not a legitimate soft navigation.
|
|
427
|
+
return scopes.length > 0 ? scopes : null;
|
|
428
|
+
};
|
|
429
|
+
const rewrite = findInterceptionMatch(
|
|
405
430
|
canonicalPathname,
|
|
406
|
-
|
|
431
|
+
resolveSourceScopes,
|
|
407
432
|
config.interceptionRewrites
|
|
408
433
|
);
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
434
|
+
if (rewrite?.mode === 'children') {
|
|
435
|
+
// Sibling layout swap (TIM-1280). Nothing is re-matched: the resolver
|
|
436
|
+
// addresses the owner directory by tree path and returns the chain
|
|
437
|
+
// `[…owner ancestors, interceptingNode, …descendants]`, which renders
|
|
438
|
+
// as an ordinary route. `interception` stays unset — there is no slot
|
|
439
|
+
// to intercept into, and setting it would additionally arm the modal
|
|
440
|
+
// path for every slot in the tree we are about to render.
|
|
441
|
+
//
|
|
442
|
+
// A null here means the scope named nothing in this manifest, or no
|
|
443
|
+
// intercepting child matched the target. Both fall through to the
|
|
444
|
+
// canonical route, which is what a hard navigation renders.
|
|
445
|
+
const interceptedMatch = config.matchInterceptedRoute?.(
|
|
446
|
+
canonicalPathname,
|
|
447
|
+
rewrite.interceptingScope
|
|
448
|
+
);
|
|
449
|
+
if (interceptedMatch) match = interceptedMatch;
|
|
450
|
+
} else if (rewrite) {
|
|
451
|
+
const sourceMatch = config.matchRoute(rewrite.interceptingPrefix);
|
|
452
|
+
// The scope test above was about the route the user is LEAVING. The
|
|
453
|
+
// route we are about to render is a different one —
|
|
454
|
+
// `interceptingPrefix` is URL-derived, and route groups are
|
|
455
|
+
// URL-invisible, so `/` can resolve into a sibling group that does not
|
|
456
|
+
// own the slot. Rendering that would reproduce the TIM-1278 failure on
|
|
457
|
+
// a valid tree: the rewrite hijacks the navigation and no slot exists
|
|
458
|
+
// to render the intercepting page. Fall through to the canonical
|
|
459
|
+
// target route instead.
|
|
460
|
+
const rendersTheSlot =
|
|
461
|
+
sourceMatch !== null &&
|
|
462
|
+
computeSegmentTreePaths(sourceMatch.segments).includes(rewrite.interceptingScope);
|
|
463
|
+
if (rendersTheSlot) {
|
|
412
464
|
match = sourceMatch;
|
|
413
465
|
interception = { targetPathname: canonicalPathname };
|
|
414
466
|
}
|
package/src/server/pipeline.ts
CHANGED
|
@@ -35,6 +35,9 @@ import { makeProxyResolver } from './pipeline-helpers.js';
|
|
|
35
35
|
import { handleRequest, runProxyPhase } from './pipeline-phases.js';
|
|
36
36
|
import { outcomeToResponse } from './pipeline-outcome.js';
|
|
37
37
|
import { canonicalize } from './canonicalize.js';
|
|
38
|
+
import { enforceRscCacheKey, hardenRscPayloadResponse } from './rsc-cache-key-guard.js';
|
|
39
|
+
import { stripHeadBody } from './head-response.js';
|
|
40
|
+
import { RSC_KEY_HEADERS } from '../shared/rsc-cache-key.js';
|
|
38
41
|
import { isDebug } from './debug.js';
|
|
39
42
|
|
|
40
43
|
// ─── Route Match Result ────────────────────────────────────────────────────
|
|
@@ -142,6 +145,17 @@ export interface PipelineConfig {
|
|
|
142
145
|
* See design/07-routing.md §"Intercepting Routes"
|
|
143
146
|
*/
|
|
144
147
|
interceptionRewrites?: import('../routing/interception.js').InterceptionRewrite[];
|
|
148
|
+
/**
|
|
149
|
+
* Resolver for children-path interceptions — the sibling layout swap.
|
|
150
|
+
*
|
|
151
|
+
* Separate from `matchRoute` because it addresses the owner directory by
|
|
152
|
+
* tree path rather than by pathname, which needs the manifest tree itself.
|
|
153
|
+
* Omitting it disables the `'children'` interception shape and leaves the
|
|
154
|
+
* canonical route rendering, which is the fail-safe outcome.
|
|
155
|
+
*
|
|
156
|
+
* See design/07-routing.md §"Intercepting Routes"
|
|
157
|
+
*/
|
|
158
|
+
matchInterceptedRoute?: import('./children-interception.js').InterceptedRouteMatcher;
|
|
145
159
|
/**
|
|
146
160
|
* Control Server-Timing header output.
|
|
147
161
|
*
|
|
@@ -393,17 +407,30 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
|
|
|
393
407
|
// DevSpanProcessor reads this for tree/summary output.
|
|
394
408
|
await setSpanAttribute('http.response.status_code', result.status);
|
|
395
409
|
|
|
396
|
-
// Vary
|
|
397
|
-
// separately for the same URL. Without this, a shared cache may
|
|
398
|
-
// serve an RSC Flight payload to a browser expecting HTML (or
|
|
399
|
-
// vice versa). See GHSA-wfc6-r584-vfw7, design/13-security.md.
|
|
410
|
+
// Vary on exactly the headers the `_rsc` cache key hashes.
|
|
400
411
|
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
412
|
+
// Those two mechanisms answer the same question — which request
|
|
413
|
+
// headers change this response — so they are the same list, not
|
|
414
|
+
// two lists that happen to agree. A separate hand-written list
|
|
415
|
+
// here drifted from `RSC_KEY_HEADERS` the moment the key set
|
|
416
|
+
// grew, and the drift is invisible: `Vary` is the *fallback*
|
|
417
|
+
// layer, used only when a cache omits the query from its key,
|
|
418
|
+
// which is precisely when nobody is watching (TIM-1268).
|
|
419
|
+
//
|
|
420
|
+
// Why any of it: without `Vary: Accept` a shared cache may serve
|
|
421
|
+
// an RSC Flight payload to a browser expecting HTML, or the
|
|
422
|
+
// reverse (GHSA-wfc6-r584-vfw7, design/13-security.md). The
|
|
423
|
+
// `_rsc` param puts that distinction in the URL, so correctness
|
|
424
|
+
// no longer *depends* on `Vary` — which is opt-in configuration
|
|
425
|
+
// on Cloudflare and CloudFront. `Vary` covers the requests that
|
|
426
|
+
// arrive without the param: a stripped query, a hand-rolled
|
|
427
|
+
// client.
|
|
428
|
+
//
|
|
429
|
+
// It costs no hit rate. Document requests send no `X-Timber-*`
|
|
430
|
+
// header at all, so they collapse to a single cache key; payload
|
|
431
|
+
// requests are already fragmented by these same values via the
|
|
432
|
+
// URL.
|
|
433
|
+
const varyTokens = [...RSC_KEY_HEADERS];
|
|
407
434
|
const existingVary = result.headers.get('Vary');
|
|
408
435
|
const existingTokens = existingVary
|
|
409
436
|
? existingVary
|
|
@@ -419,6 +446,26 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
|
|
|
419
446
|
);
|
|
420
447
|
}
|
|
421
448
|
|
|
449
|
+
// A request may only claim an `_rsc` cache key that matches its
|
|
450
|
+
// own headers. The key is predictable by design, so without this
|
|
451
|
+
// a caller could render its own variant under another client's
|
|
452
|
+
// key and have a shared cache store it there (TIM-1268).
|
|
453
|
+
await enforceRscCacheKey(canonicalReq, result);
|
|
454
|
+
|
|
455
|
+
// Flight payloads must never be sniffable into a renderable
|
|
456
|
+
// type — applied at this single exit so every producer is
|
|
457
|
+
// covered, not just the success path (TIM-1268).
|
|
458
|
+
hardenRscPayloadResponse(result);
|
|
459
|
+
|
|
460
|
+
// A HEAD response carries the headers its GET would produce and
|
|
461
|
+
// no content (RFC 9110 §9.3.2). Applied at this single exit so
|
|
462
|
+
// every response shape is covered — page render, API route,
|
|
463
|
+
// metadata route, deny page, error page, proxy short-circuit —
|
|
464
|
+
// rather than per-renderer, where it was missing for page
|
|
465
|
+
// renders entirely (TIM-1324). The render still runs: the
|
|
466
|
+
// headers have to be the ones a GET would have sent.
|
|
467
|
+
result = stripHeadBody(result, method);
|
|
468
|
+
|
|
422
469
|
// Append Server-Timing header based on configured mode.
|
|
423
470
|
// Header mutability is guaranteed by the producer-side clone
|
|
424
471
|
// in `outcomeToResponse` and the metadata-route / auto-sitemap
|
|
@@ -235,13 +235,7 @@ async function enumerateCombos(
|
|
|
235
235
|
): Promise<ParamCombo[] | null> {
|
|
236
236
|
if (!hasDynamicSegments(route.urlPath)) return [{}];
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
if (typeof generateFn !== 'function' && typeof pageModule.generateStaticParams === 'function') {
|
|
240
|
-
generateFn = pageModule.generateStaticParams;
|
|
241
|
-
log(
|
|
242
|
-
`[timber] ${route.page.filePath}: generateStaticParams is deprecated — rename to generateStaticSegmentParams`
|
|
243
|
-
);
|
|
244
|
-
}
|
|
238
|
+
const generateFn = pageModule.generateStaticSegmentParams;
|
|
245
239
|
if (typeof generateFn !== 'function') {
|
|
246
240
|
log(
|
|
247
241
|
`[timber] cache.component: route ${route.urlPath} has prebuilt components but no ` +
|
|
@@ -173,17 +173,17 @@ const registry = new Map<string, PrebuiltRegistration>();
|
|
|
173
173
|
*/
|
|
174
174
|
function resolveKeyParams(store: {
|
|
175
175
|
segmentParams?: Record<string, string | string[]>;
|
|
176
|
-
|
|
176
|
+
slotParams?: Record<string, Record<string, string | string[]>>;
|
|
177
177
|
}): Record<string, string | string[]> | null {
|
|
178
178
|
const main = store.segmentParams ?? {};
|
|
179
|
-
const
|
|
180
|
-
if (!
|
|
179
|
+
const slots = store.slotParams;
|
|
180
|
+
if (!slots || Object.keys(slots).length === 0) return main;
|
|
181
181
|
// Spread before canonicalizing: param records are framework-produced
|
|
182
182
|
// Object.create(null) objects (prototype-pollution defense) — normalize
|
|
183
183
|
// the records themselves, let nested values face the full allowlist.
|
|
184
184
|
const mainCanonical = tryCanonicalize({ ...main });
|
|
185
185
|
if (mainCanonical === null) return null;
|
|
186
|
-
for (const slotParams of
|
|
186
|
+
for (const slotParams of Object.values(slots)) {
|
|
187
187
|
const merged = tryCanonicalize({ ...main, ...slotParams });
|
|
188
188
|
if (merged === null || merged !== mainCanonical) return null;
|
|
189
189
|
}
|
package/src/server/primitives.ts
CHANGED
|
@@ -201,15 +201,6 @@ export function deny(statusOrOptions?: number | DenyOptions, data?: JsonSerializ
|
|
|
201
201
|
throw new DenySignal(status, resolvedData);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
/**
|
|
205
|
-
* @deprecated Use `deny(404)` instead.
|
|
206
|
-
* Kept for internal use by the Next.js shim layer.
|
|
207
|
-
* @internal
|
|
208
|
-
*/
|
|
209
|
-
export function notFound(): never {
|
|
210
|
-
deny(404);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
204
|
// Single source of truth shared with the client next/navigation shim —
|
|
214
205
|
// see shared/redirect-type.ts.
|
|
215
206
|
export { RedirectType } from '../shared/redirect-type.js';
|
|
@@ -334,15 +325,6 @@ export function redirect(path: string, statusOrOptions?: number | RedirectOption
|
|
|
334
325
|
throw new RedirectSignal(resolvedPath, status);
|
|
335
326
|
}
|
|
336
327
|
|
|
337
|
-
/**
|
|
338
|
-
* @deprecated Use `redirect(path, { permanent: true })` instead.
|
|
339
|
-
* Kept for internal use by the Next.js shim layer.
|
|
340
|
-
* @internal
|
|
341
|
-
*/
|
|
342
|
-
export function permanentRedirect(path: string, options?: Omit<RedirectOptions, 'status'>): never {
|
|
343
|
-
redirect(path, { permanent: true, ...options });
|
|
344
|
-
}
|
|
345
|
-
|
|
346
328
|
/**
|
|
347
329
|
* Redirect to an external URL. The origin must be in the provided allow-list.
|
|
348
330
|
*
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publishing the request's params onto the RSC payload.
|
|
3
|
+
*
|
|
4
|
+
* Every tree the server sends the browser goes through here: the ordinary
|
|
5
|
+
* route payload (`route-element-builder.ts`), both deny renderers
|
|
6
|
+
* (`deny-renderer.ts`) and the error renderer. One helper rather than a wrap
|
|
7
|
+
* at each site, because these previously *were* separate sites — a response
|
|
8
|
+
* header, an inline script, and a second copy of each on the deny path — and
|
|
9
|
+
* the deny path shipping empty params was the bug that came of it (TIM-1285).
|
|
10
|
+
*
|
|
11
|
+
* Reads the request store rather than taking params as an argument, so a
|
|
12
|
+
* caller cannot pass a different record than the one the request rendered
|
|
13
|
+
* with. See design/41-global-params.md §"Transport".
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { toPlainRecord } from '../shared/param-value.js';
|
|
17
|
+
import type { RscPayloadRoot } from '../shared/payload-root.js';
|
|
18
|
+
import { getSegmentParamsForClient, getSlotParamsForClient } from './request-context.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the payload root: the tree, and the params it rendered with beside it.
|
|
22
|
+
*
|
|
23
|
+
* The params are a *sibling* of the tree, not a provider around it. A provider
|
|
24
|
+
* inside the payload is shadowed by the retained tree's own provider on a
|
|
25
|
+
* partial navigation, and its props are not readable at the instant the root
|
|
26
|
+
* row resolves — see `shared/payload-root.ts` for both, and TIM-1297. The
|
|
27
|
+
* client renders the one provider readers see, above the merge point.
|
|
28
|
+
*
|
|
29
|
+
* The records are flattened to plain objects on the way out. Every param
|
|
30
|
+
* record the framework builds is `Object.create(null)` (design/13-security.md
|
|
31
|
+
* #36b/#36c), and React Flight rejects a null prototype — "Classes or null
|
|
32
|
+
* prototypes are not supported". `ParamsProvider` restores the prototype on
|
|
33
|
+
* the other side, so what the app reads is unchanged; only the wire
|
|
34
|
+
* representation differs.
|
|
35
|
+
*/
|
|
36
|
+
export function withPublishedParams(element: React.ReactElement): RscPayloadRoot {
|
|
37
|
+
return {
|
|
38
|
+
tree: element,
|
|
39
|
+
params: toPlainRecord(getSegmentParamsForClient()),
|
|
40
|
+
slotParams: toPlainRecord(getSlotParamsForClient() ?? null),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -15,9 +15,14 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { requestContextAls, type RequestContextStore } from './als-registry.js';
|
|
18
|
-
import { _setGetSearchParamsFn } from '../
|
|
19
|
-
import { _setGetSegmentParamsFn } from '../segment-params/define.js';
|
|
18
|
+
import { _setGetSearchParamsFn, _setGetSegmentParamsFn } from '../shared/als-slots.js';
|
|
20
19
|
import { consumeSeededCookies } from './cookie-context.js';
|
|
20
|
+
import { appVisibleSearch } from '../shared/rsc-cache-key.js';
|
|
21
|
+
import {
|
|
22
|
+
mergeSlotParams,
|
|
23
|
+
resolveSegmentParams,
|
|
24
|
+
type SlotParamsRecord,
|
|
25
|
+
} from '../shared/slot-params.js';
|
|
21
26
|
import { isDebug } from './debug.js';
|
|
22
27
|
|
|
23
28
|
// Re-export the ALS for framework-internal consumers that need direct access.
|
|
@@ -111,17 +116,13 @@ export function getSegmentParams(segmentPath?: string): Record<string, string |
|
|
|
111
116
|
);
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// This ensures params from segments above the slot (that the slot
|
|
122
|
-
// doesn't re-derive) are still available.
|
|
123
|
-
return { ...store.segmentParams, ...slotParams };
|
|
124
|
-
}
|
|
119
|
+
// Parallel slots may derive a param the main route never had, or the same
|
|
120
|
+
// name with a different type (catch-all [...year] → string[] vs dynamic
|
|
121
|
+
// [year] → string). `resolveSegmentParams` is the shared definition the
|
|
122
|
+
// client's useSegmentParams() also reads, so the two halves of one
|
|
123
|
+
// documented API cannot drift (TIM-1285).
|
|
124
|
+
const slotResolved = resolveSegmentParams(store.segmentParams, store.slotParams, segmentPath);
|
|
125
|
+
if (slotResolved !== store.segmentParams) return slotResolved;
|
|
125
126
|
|
|
126
127
|
// Dev-mode validation: warn when the segment path expects dynamic
|
|
127
128
|
// segments that don't exist on the actual matched segment. This catches
|
|
@@ -173,8 +174,82 @@ export function setSlotParams(
|
|
|
173
174
|
): void {
|
|
174
175
|
const store = requestContextAls.getStore();
|
|
175
176
|
if (!store) return; // non-throwing — optional diagnostic
|
|
176
|
-
if (!store.
|
|
177
|
-
store.
|
|
177
|
+
if (!store.slotParams) store.slotParams = Object.create(null) as SlotParamsRecord;
|
|
178
|
+
store.slotParams[segmentPath] = params;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Run `fn` with the segment params a parallel slot's own chain produced, so
|
|
183
|
+
* a no-arg `getSegmentParams()` inside that slot returns the slot's params
|
|
184
|
+
* rather than the main route's.
|
|
185
|
+
*
|
|
186
|
+
* ALS is the only channel available: the slot's page and layouts are plain
|
|
187
|
+
* components that receive no params prop and cannot know their own tree
|
|
188
|
+
* path, so they have no way to ask for the keyed overload. The slot resolver
|
|
189
|
+
* invokes each of them itself (page through `SafeSlotPage`, layouts and
|
|
190
|
+
* access functions through their wrappers), and this scopes those calls.
|
|
191
|
+
*
|
|
192
|
+
* The store is wrapped in a Proxy rather than cloned. Everything a slot
|
|
193
|
+
* renders can write to the *live* request store — `setDenyStatus` from an
|
|
194
|
+
* access gate, cookie writes, the flush flag — and a clone would silently
|
|
195
|
+
* strand those writes in a copy the pipeline never reads. Only the two
|
|
196
|
+
* params fields are shadowed; every other read and every write passes
|
|
197
|
+
* straight through to the real store.
|
|
198
|
+
*
|
|
199
|
+
* Returns `fn()`'s result unchanged, and no-ops outside a request context or
|
|
200
|
+
* before route matching has set the main params. See TIM-1283.
|
|
201
|
+
*
|
|
202
|
+
* @internal — framework use only
|
|
203
|
+
*/
|
|
204
|
+
export function runWithSlotSegmentParams<T>(
|
|
205
|
+
slotSegmentPath: string | undefined,
|
|
206
|
+
slotParams: Record<string, string | string[]>,
|
|
207
|
+
fn: () => T
|
|
208
|
+
): T {
|
|
209
|
+
const store = requestContextAls.getStore();
|
|
210
|
+
if (!store?.segmentParams) return fn();
|
|
211
|
+
|
|
212
|
+
const merged = mergeSlotParams(store.segmentParams, slotParams);
|
|
213
|
+
const matchedSegmentPath = slotSegmentPath ?? store.matchedSegmentPath;
|
|
214
|
+
const scoped = new Proxy(store, {
|
|
215
|
+
get(target, prop, receiver) {
|
|
216
|
+
if (prop === 'segmentParams') return merged;
|
|
217
|
+
if (prop === 'matchedSegmentPath') return matchedSegmentPath;
|
|
218
|
+
return Reflect.get(target, prop, receiver);
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
return requestContextAls.run(scoped, fn);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The main route's coerced params, or an empty record before route matching.
|
|
226
|
+
*
|
|
227
|
+
* Non-throwing, unlike `getSegmentParams()`: the deny payload path publishes
|
|
228
|
+
* params from wherever the pipeline happened to fail, which may be before
|
|
229
|
+
* matching completed.
|
|
230
|
+
*
|
|
231
|
+
* @internal — framework use only. See TIM-1285.
|
|
232
|
+
*/
|
|
233
|
+
export function getSegmentParamsForClient(): Record<string, string | string[]> {
|
|
234
|
+
return requestContextAls.getStore()?.segmentParams ?? {};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The per-slot params to publish to the client, or `undefined` when the
|
|
239
|
+
* request rendered no slot with params of its own.
|
|
240
|
+
*
|
|
241
|
+
* The client cannot derive these: a slot matches the URL through its own
|
|
242
|
+
* sub-tree, and the browser only ever sees the main route's record. Reading
|
|
243
|
+
* the map here — rather than threading it back out of the element builder —
|
|
244
|
+
* keeps one producer, since `setSlotParams` already writes it during slot
|
|
245
|
+
* resolution and every serialization site runs after that completes.
|
|
246
|
+
*
|
|
247
|
+
* @internal — framework use only. See TIM-1285.
|
|
248
|
+
*/
|
|
249
|
+
export function getSlotParamsForClient(): SlotParamsRecord | undefined {
|
|
250
|
+
const store = requestContextAls.getStore();
|
|
251
|
+
if (!store?.slotParams) return undefined;
|
|
252
|
+
return Object.keys(store.slotParams).length > 0 ? store.slotParams : undefined;
|
|
178
253
|
}
|
|
179
254
|
|
|
180
255
|
/**
|
|
@@ -247,7 +322,7 @@ export type ReadonlyHeaders = Pick<
|
|
|
247
322
|
*/
|
|
248
323
|
export function runWithRequestContext<T>(req: Request, fn: () => T): T {
|
|
249
324
|
const originalCopy = new Headers(req.headers);
|
|
250
|
-
const
|
|
325
|
+
const appVisible = appVisibleSearch(new URL(req.url));
|
|
251
326
|
const seed = consumeSeededCookies(req);
|
|
252
327
|
const store: RequestContextStore = {
|
|
253
328
|
headers: freezeHeaders(req.headers),
|
|
@@ -257,8 +332,12 @@ export function runWithRequestContext<T>(req: Request, fn: () => T): T {
|
|
|
257
332
|
// gated on `parsedCookies` being undefined.
|
|
258
333
|
cookieHeader: seed ? '' : (req.headers.get('cookie') ?? ''),
|
|
259
334
|
parsedCookies: seed,
|
|
260
|
-
searchParams
|
|
261
|
-
|
|
335
|
+
// Not `parsedUrl.searchParams` / `parsedUrl.search`: this store is what
|
|
336
|
+
// `getSearchParams()` and `redirect({ preserveSearchParams })` read, and
|
|
337
|
+
// the RSC payload URL carries the framework's `_rsc` cache key that no
|
|
338
|
+
// other request has (TIM-1272).
|
|
339
|
+
searchParams: appVisible.params,
|
|
340
|
+
searchString: appVisible.search,
|
|
262
341
|
cookieJar: new Map(),
|
|
263
342
|
flushed: false,
|
|
264
343
|
mutableContext: false,
|