@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
|
@@ -19,6 +19,9 @@ import { createElement, Fragment } from 'react';
|
|
|
19
19
|
import { renderToReadableStream } from '../rsc-runtime/rsc.js';
|
|
20
20
|
|
|
21
21
|
import { DenySignal } from './primitives.js';
|
|
22
|
+
import { appVisibleSearch } from '../shared/rsc-cache-key.js';
|
|
23
|
+
import { getSlotParamsForClient } from './request-context.js';
|
|
24
|
+
import { withPublishedParams } from './publish-params.js';
|
|
22
25
|
import { renderDenyEntry, type DenyPageEntry } from './deny-boundary.js';
|
|
23
26
|
import { getCookiesForSsr } from './cookie-context.js';
|
|
24
27
|
import { logRenderError } from './logger.js';
|
|
@@ -28,6 +31,8 @@ import { isDebug } from './debug.js';
|
|
|
28
31
|
import { resolveMetadata, renderMetadataToElements } from './metadata.js';
|
|
29
32
|
import { headElementsToReact } from './metadata.js';
|
|
30
33
|
import { resolveStatusFile } from './status-code-resolver.js';
|
|
34
|
+
import { defaultStatusPageResponse } from './default-status-page.js';
|
|
35
|
+
import { rscErrorEnvelope } from '../shared/rsc-error-envelope.js';
|
|
31
36
|
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
32
37
|
import type { RouteMatch } from './pipeline.js';
|
|
33
38
|
import type { NavContext } from './ssr-bridge-types.js';
|
|
@@ -88,12 +93,24 @@ function buildDenyElement(
|
|
|
88
93
|
return renderDenyEntry(entry, deny.status, deny.data);
|
|
89
94
|
}
|
|
90
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Supplies a replacement for the framework default page at the end of the
|
|
98
|
+
* chain. Returning `null` keeps the framework default.
|
|
99
|
+
*
|
|
100
|
+
* The dev server uses this to substitute its route-listing 404 page. It is a
|
|
101
|
+
* parameter rather than a check on the produced Response because "the app
|
|
102
|
+
* authored no status page" is a fact about the *chain*, and inspecting the
|
|
103
|
+
* result for it (e.g. an empty body) stops being true the moment the chain
|
|
104
|
+
* grows a real terminal — which is exactly what TIM-1258 did.
|
|
105
|
+
*/
|
|
106
|
+
export type DefaultPageRenderer = (status: number, responseHeaders: Headers) => Response | null;
|
|
107
|
+
|
|
91
108
|
/**
|
|
92
109
|
* Render a status-code error page for a DenySignal.
|
|
93
110
|
*
|
|
94
111
|
* Resolves the appropriate status-code file from the segment chain based
|
|
95
|
-
* on format preference. Returns an HTML Response (component), JSON Response,
|
|
96
|
-
* or
|
|
112
|
+
* on format preference. Returns an HTML Response (component), a JSON Response,
|
|
113
|
+
* or the framework default status page.
|
|
97
114
|
*/
|
|
98
115
|
export async function renderDenyPage(
|
|
99
116
|
deny: DenySignal,
|
|
@@ -104,7 +121,8 @@ export async function renderDenyPage(
|
|
|
104
121
|
responseHeaders: Headers,
|
|
105
122
|
clientBootstrap: ClientBootstrapConfig,
|
|
106
123
|
createDebugChannelSink: DebugChannelFactory,
|
|
107
|
-
callSsr: CallSsrFn
|
|
124
|
+
callSsr: CallSsrFn,
|
|
125
|
+
defaultPage?: DefaultPageRenderer
|
|
108
126
|
): Promise<Response> {
|
|
109
127
|
// API routes (route.ts) → JSON only, never render components
|
|
110
128
|
if (isApiRoute(segments)) {
|
|
@@ -116,11 +134,14 @@ export async function renderDenyPage(
|
|
|
116
134
|
// Page routes → component chain first, JSON fallback only if no component found.
|
|
117
135
|
const resolution = resolveStatusFile(deny.status, segments, 'component');
|
|
118
136
|
|
|
119
|
-
// No component status file — try JSON chain
|
|
137
|
+
// No component status file — try JSON chain, then the framework default page.
|
|
120
138
|
if (!resolution) {
|
|
121
139
|
const jsonResponse = await renderDenyPageJson(deny, segments, responseHeaders);
|
|
122
140
|
if (jsonResponse) return jsonResponse;
|
|
123
|
-
return
|
|
141
|
+
return (
|
|
142
|
+
defaultPage?.(deny.status, responseHeaders) ??
|
|
143
|
+
defaultStatusPageResponse(deny.status, responseHeaders)
|
|
144
|
+
);
|
|
124
145
|
}
|
|
125
146
|
|
|
126
147
|
// Dev warning: JSON status file exists but is shadowed by the component chain.
|
|
@@ -137,10 +158,20 @@ export async function renderDenyPage(
|
|
|
137
158
|
}
|
|
138
159
|
}
|
|
139
160
|
|
|
140
|
-
// Load the status-code page component
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
161
|
+
// Load the status-code page component. A file that resolved but exports no
|
|
162
|
+
// default — or whose module fails to import or evaluate — is not a usable
|
|
163
|
+
// component match: continue down the documented chain (JSON, then framework
|
|
164
|
+
// default) rather than returning an empty body or letting the loader's
|
|
165
|
+
// rejection escape request handling. Mirrors buildDenyPageChain and the
|
|
166
|
+
// error-file resolver, which both swallow the load failure.
|
|
167
|
+
const mod = await loadModule(resolution.file).catch(() => null);
|
|
168
|
+
if (!mod?.default) {
|
|
169
|
+
const jsonResponse = await renderDenyPageJson(deny, segments, responseHeaders);
|
|
170
|
+
if (jsonResponse) return jsonResponse;
|
|
171
|
+
return (
|
|
172
|
+
defaultPage?.(deny.status, responseHeaders) ??
|
|
173
|
+
defaultStatusPageResponse(deny.status, responseHeaders)
|
|
174
|
+
);
|
|
144
175
|
}
|
|
145
176
|
|
|
146
177
|
const ErrorPageComponent = mod.default as (...args: unknown[]) => unknown;
|
|
@@ -176,7 +207,7 @@ export async function renderDenyPage(
|
|
|
176
207
|
}
|
|
177
208
|
|
|
178
209
|
// Render the error page to a fresh RSC Flight stream.
|
|
179
|
-
const rscStream = renderToReadableStream(element, {
|
|
210
|
+
const rscStream = renderToReadableStream(withPublishedParams(element), {
|
|
180
211
|
signal: req.signal,
|
|
181
212
|
onError(error: unknown) {
|
|
182
213
|
logRenderError({ method: req.method, path: new URL(req.url).pathname, error });
|
|
@@ -186,10 +217,13 @@ export async function renderDenyPage(
|
|
|
186
217
|
|
|
187
218
|
const [ssrStream, inlineStream] = teeWithErrorPropagation(rscStream);
|
|
188
219
|
|
|
220
|
+
const slotParams = getSlotParamsForClient();
|
|
221
|
+
|
|
189
222
|
const navContext: NavContext = {
|
|
190
223
|
pathname: new URL(req.url).pathname,
|
|
191
224
|
segmentParams: match.segmentParams,
|
|
192
|
-
searchParams: Object.fromEntries(new URL(req.url).
|
|
225
|
+
searchParams: Object.fromEntries(appVisibleSearch(new URL(req.url)).params),
|
|
226
|
+
slotParams,
|
|
193
227
|
statusCode: deny.status,
|
|
194
228
|
responseHeaders,
|
|
195
229
|
headHtml: flightInitScript(),
|
|
@@ -199,7 +233,32 @@ export async function renderDenyPage(
|
|
|
199
233
|
cookies: getCookiesForSsr(),
|
|
200
234
|
};
|
|
201
235
|
|
|
202
|
-
|
|
236
|
+
try {
|
|
237
|
+
// `await` inside the try on purpose — see the same note in
|
|
238
|
+
// rsc-entry/error-renderer.ts. A status page that crashes during SSR
|
|
239
|
+
// ends in the framework default page, not a blank body: the pipeline's
|
|
240
|
+
// own deny fallback catches this and can only produce an empty response,
|
|
241
|
+
// and a crashing 404.tsx is exactly when the reader has nothing else to
|
|
242
|
+
// go on (TIM-1258).
|
|
243
|
+
return await callSsr(ssrStream, navContext);
|
|
244
|
+
} catch (renderError) {
|
|
245
|
+
logRenderError({ method: req.method, path: new URL(req.url).pathname, error: renderError });
|
|
246
|
+
return defaultStatusPageResponse(deny.status, responseHeaders);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Terminal fallback for a client-navigation deny with no status-code file.
|
|
252
|
+
*
|
|
253
|
+
* The HTML path answers this case with the framework default page, but a
|
|
254
|
+
* client-navigation response cannot carry HTML — the Flight decoder would
|
|
255
|
+
* choke on it. Emit the hard-navigate envelope instead: the client abandons
|
|
256
|
+
* SPA navigation and the resulting document request renders that same default
|
|
257
|
+
* page. Before TIM-1258 this returned an empty Flight body, which the decoder
|
|
258
|
+
* hit as a malformed stream.
|
|
259
|
+
*/
|
|
260
|
+
function denyHardNavigateResponse(status: number, responseHeaders: Headers): Response {
|
|
261
|
+
return rscErrorEnvelope(status, responseHeaders);
|
|
203
262
|
}
|
|
204
263
|
|
|
205
264
|
/**
|
|
@@ -219,14 +278,12 @@ export async function renderDenyPageAsRsc(
|
|
|
219
278
|
const resolution = resolveStatusFile(deny.status, segments, 'component');
|
|
220
279
|
|
|
221
280
|
if (!resolution) {
|
|
222
|
-
|
|
223
|
-
return new Response(null, { status: deny.status, headers: responseHeaders });
|
|
281
|
+
return denyHardNavigateResponse(deny.status, responseHeaders);
|
|
224
282
|
}
|
|
225
283
|
|
|
226
|
-
const mod = await loadModule(resolution.file);
|
|
227
|
-
if (!mod
|
|
228
|
-
|
|
229
|
-
return new Response(null, { status: deny.status, headers: responseHeaders });
|
|
284
|
+
const mod = await loadModule(resolution.file).catch(() => null);
|
|
285
|
+
if (!mod?.default) {
|
|
286
|
+
return denyHardNavigateResponse(deny.status, responseHeaders);
|
|
230
287
|
}
|
|
231
288
|
|
|
232
289
|
const ErrorPageComponent = mod.default as (...args: unknown[]) => unknown;
|
|
@@ -247,7 +304,7 @@ export async function renderDenyPageAsRsc(
|
|
|
247
304
|
element = h(component, null, element);
|
|
248
305
|
}
|
|
249
306
|
|
|
250
|
-
const rscStream = renderToReadableStream(element, {
|
|
307
|
+
const rscStream = renderToReadableStream(withPublishedParams(element), {
|
|
251
308
|
signal: req.signal,
|
|
252
309
|
onError(error: unknown) {
|
|
253
310
|
console.error('[timber] Error page RSC render error:', error);
|
|
@@ -286,7 +343,11 @@ async function renderDenyPageJson(
|
|
|
286
343
|
// JSON status files are loaded as modules that export the JSON content.
|
|
287
344
|
// The manifest's load() imports the .json file, which Vite handles as a
|
|
288
345
|
// default export of the parsed JSON object.
|
|
289
|
-
|
|
346
|
+
// A .json status file that fails to load is not a usable match — return
|
|
347
|
+
// null so the caller continues to the framework default rather than
|
|
348
|
+
// rejecting out of the chain.
|
|
349
|
+
const mod = await loadModule(resolution.file).catch(() => null);
|
|
350
|
+
if (!mod) return null;
|
|
290
351
|
const jsonContent = mod.default ?? mod;
|
|
291
352
|
|
|
292
353
|
responseHeaders.set('content-type', 'application/json; charset=utf-8');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HEAD body stripping — RFC 9110 §9.3.2.
|
|
3
|
+
*
|
|
4
|
+
* "The HEAD method is identical to GET except that the server MUST NOT send
|
|
5
|
+
* content in the response." The headers, however, must be the ones the
|
|
6
|
+
* corresponding GET would have produced — which is why timber renders the
|
|
7
|
+
* route in full and discards the body here rather than short-circuiting the
|
|
8
|
+
* render. Content-Type, Content-Length, Set-Cookie, cache directives and
|
|
9
|
+
* everything else ride through untouched.
|
|
10
|
+
*
|
|
11
|
+
* Node's `ServerResponse` happens to discard writes for HEAD, so the wire is
|
|
12
|
+
* usually correct even without this. That is a property of one adapter, not of
|
|
13
|
+
* the `Response` object: static generation, the Cloudflare and Nitro adapters,
|
|
14
|
+
* tests, and anything calling the pipeline handler as a function all observe
|
|
15
|
+
* the `Response` directly and saw the full body before TIM-1324.
|
|
16
|
+
*
|
|
17
|
+
* The pipeline applies this at its single exit so every response shape is
|
|
18
|
+
* covered — page render, API route, metadata route, deny page, error page,
|
|
19
|
+
* proxy short-circuit. The two unit-level producers that are also reachable as
|
|
20
|
+
* standalone functions (`handleRouteRequest`, `handleApiRoute`) call it too, so
|
|
21
|
+
* each honours HEAD on its own; they share this implementation rather than
|
|
22
|
+
* repeating it, and applying it twice is a no-op.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return a bodyless copy of `response` when `method` is HEAD.
|
|
27
|
+
*
|
|
28
|
+
* A no-op for every other method and for responses that already carry no body
|
|
29
|
+
* (204, 304, redirects), so callers do not need to pre-check either.
|
|
30
|
+
*
|
|
31
|
+
* The discarded stream is cancelled: an SSR render that reached this point has
|
|
32
|
+
* a live producer attached, and dropping the reference without cancelling
|
|
33
|
+
* leaks it for the lifetime of the render (design/13-security.md A4).
|
|
34
|
+
*/
|
|
35
|
+
export function stripHeadBody(response: Response, method: string): Response {
|
|
36
|
+
if (method.toUpperCase() !== 'HEAD') return response;
|
|
37
|
+
const body = response.body;
|
|
38
|
+
if (body === null) return response;
|
|
39
|
+
|
|
40
|
+
// `cancel()` rejects if the stream is already errored and throws
|
|
41
|
+
// synchronously if it is locked — neither is actionable here, the body is
|
|
42
|
+
// being discarded either way.
|
|
43
|
+
try {
|
|
44
|
+
body.cancel().catch(() => {});
|
|
45
|
+
} catch {
|
|
46
|
+
/* stream already locked or disturbed */
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return new Response(null, {
|
|
50
|
+
status: response.status,
|
|
51
|
+
statusText: response.statusText,
|
|
52
|
+
headers: response.headers,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { Codec } from '../codec.js';
|
|
16
|
+
import type { SegmentType } from '../routing/types.js';
|
|
17
|
+
import { effectiveUrlSegment } from '../routing/segment-classify.js';
|
|
16
18
|
import { toBracketKey } from '../params/resolve-schema.js';
|
|
17
19
|
import type { RouteMatch } from './pipeline.js';
|
|
18
|
-
import {
|
|
20
|
+
import { normalizeParamValue } from '../shared/param-value.js';
|
|
19
21
|
import { loadModule } from './safe-load.js';
|
|
20
22
|
import { ParamCoercionError } from './route-element-builder.js';
|
|
21
23
|
import { isDebug } from './debug.js';
|
|
@@ -44,6 +46,12 @@ export function setGlobalSchemaCodecs(codecs: Record<string, Codec<unknown>> | n
|
|
|
44
46
|
* exists, the raw value is parsed through it. If no codec exists or no
|
|
45
47
|
* global codecs are registered, the raw value passes through unchanged.
|
|
46
48
|
*
|
|
49
|
+
* Segments are read through `effectiveUrlSegment` because an intercepting
|
|
50
|
+
* node's own `segmentType` is `'intercepting'` and its `paramName` is unset
|
|
51
|
+
* — the param it binds lives in the intercepted name (`(.)[id]` → `[id]`).
|
|
52
|
+
* The slot resolver matches through the same helper, so the key a param is
|
|
53
|
+
* coerced under is always the key it was matched under (TIM-1281).
|
|
54
|
+
*
|
|
47
55
|
* Unlike main route coercion, this does NOT throw ParamCoercionError on
|
|
48
56
|
* failure — slots degrade gracefully. A failed coercion logs a warning
|
|
49
57
|
* in dev mode and keeps the raw value.
|
|
@@ -51,7 +59,12 @@ export function setGlobalSchemaCodecs(codecs: Record<string, Codec<unknown>> | n
|
|
|
51
59
|
* @internal — framework use only
|
|
52
60
|
*/
|
|
53
61
|
export function coerceSlotParams(
|
|
54
|
-
slotChain: Array<{
|
|
62
|
+
slotChain: Array<{
|
|
63
|
+
segmentName: string;
|
|
64
|
+
segmentType: SegmentType;
|
|
65
|
+
paramName?: string;
|
|
66
|
+
interceptedSegmentName?: string;
|
|
67
|
+
}>,
|
|
55
68
|
rawParams: Record<string, string | string[]>
|
|
56
69
|
): Record<string, string | string[]> {
|
|
57
70
|
const globalCodecs = _globalCodecs;
|
|
@@ -62,7 +75,8 @@ export function coerceSlotParams(
|
|
|
62
75
|
result[key] = rawParams[key];
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
for (const
|
|
78
|
+
for (const node of slotChain) {
|
|
79
|
+
const segment = effectiveUrlSegment(node);
|
|
66
80
|
if (!segment.paramName) continue;
|
|
67
81
|
const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
|
|
68
82
|
const codec = globalCodecs[bracketKey];
|
|
@@ -71,10 +85,9 @@ export function coerceSlotParams(
|
|
|
71
85
|
const key = segment.paramName;
|
|
72
86
|
if (!(key in result)) continue;
|
|
73
87
|
|
|
88
|
+
let parsed: unknown;
|
|
74
89
|
try {
|
|
75
|
-
|
|
76
|
-
| string
|
|
77
|
-
| string[];
|
|
90
|
+
parsed = codec.parse(result[key] as string | string[]);
|
|
78
91
|
} catch (err) {
|
|
79
92
|
// Slot param coercion failures are non-fatal — keep the raw value.
|
|
80
93
|
// The main route already validated the URL; the slot just interprets
|
|
@@ -88,7 +101,9 @@ export function coerceSlotParams(
|
|
|
88
101
|
` Keeping raw value.`
|
|
89
102
|
);
|
|
90
103
|
}
|
|
104
|
+
continue;
|
|
91
105
|
}
|
|
106
|
+
result[key] = normalizeParamValue(parsed, key) as string | string[];
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
return result as Record<string, string | string[]>;
|
|
@@ -125,7 +140,17 @@ export async function coerceSegmentParams(match: RouteMatch): Promise<void> {
|
|
|
125
140
|
// by bracket name (e.g. '[id]', '[...slug]') to avoid collisions between
|
|
126
141
|
// distinct segment types that share a param name.
|
|
127
142
|
if (globalCodecs) {
|
|
128
|
-
for (const
|
|
143
|
+
for (const node of match.segments) {
|
|
144
|
+
// Read through `effectiveUrlSegment` for the same reason
|
|
145
|
+
// `coerceSlotParams` does: an intercepting node's own `segmentType` is
|
|
146
|
+
// 'intercepting' and its `paramName` is unset — the param it binds lives
|
|
147
|
+
// in the intercepted name (`(.)[id]` → `[id]`). Until TIM-1280 an
|
|
148
|
+
// intercepting node could only appear in a *slot* chain, so only that
|
|
149
|
+
// path needed it; a children-path interception puts one in the main
|
|
150
|
+
// chain, and reading the node directly would silently skip coercion for
|
|
151
|
+
// exactly the param the intercepting directory declares — so the swapped
|
|
152
|
+
// route and the canonical one would disagree about the param's type.
|
|
153
|
+
const segment = effectiveUrlSegment(node);
|
|
129
154
|
if (!segment.paramName) continue;
|
|
130
155
|
const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
|
|
131
156
|
const codec = globalCodecs[bracketKey];
|
|
@@ -133,8 +158,9 @@ export async function coerceSegmentParams(match: RouteMatch): Promise<void> {
|
|
|
133
158
|
|
|
134
159
|
const key = segment.paramName;
|
|
135
160
|
if (!(key in mergeTarget)) continue;
|
|
161
|
+
let parsed: unknown;
|
|
136
162
|
try {
|
|
137
|
-
|
|
163
|
+
parsed = codec.parse(mergeTarget[key] as string | string[]);
|
|
138
164
|
} catch (err) {
|
|
139
165
|
const message = err instanceof Error ? err.message : String(err);
|
|
140
166
|
if (isDebug()) {
|
|
@@ -147,6 +173,10 @@ export async function coerceSegmentParams(match: RouteMatch): Promise<void> {
|
|
|
147
173
|
}
|
|
148
174
|
throw new ParamCoercionError(message);
|
|
149
175
|
}
|
|
176
|
+
// Outside the catch: a codec that rejects a URL value is a 404, a codec
|
|
177
|
+
// that returns an unrepresentable one is a bug in the app. Normalizing
|
|
178
|
+
// inside would report the second as the first.
|
|
179
|
+
mergeTarget[key] = normalizeParamValue(parsed, key);
|
|
150
180
|
}
|
|
151
181
|
return;
|
|
152
182
|
}
|
|
@@ -177,17 +207,9 @@ export async function coerceSegmentParams(match: RouteMatch): Promise<void> {
|
|
|
177
207
|
|
|
178
208
|
if (!segmentParamsDef || typeof segmentParamsDef.parse !== 'function') continue;
|
|
179
209
|
|
|
210
|
+
let coerced: Record<string, unknown>;
|
|
180
211
|
try {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// Deep-sanitize codec output: every nested plain object becomes
|
|
184
|
-
// null-prototype with dangerous keys stripped at every depth.
|
|
185
|
-
// See TIM-873, design/13-security.md
|
|
186
|
-
for (const key of Object.keys(coerced as Record<string, unknown>)) {
|
|
187
|
-
if (key !== '__proto__') {
|
|
188
|
-
mergeTarget[key] = sanitizeParamValue((coerced as Record<string, unknown>)[key]);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
212
|
+
coerced = segmentParamsDef.parse(match.segmentParams) as Record<string, unknown>;
|
|
191
213
|
} catch (err) {
|
|
192
214
|
const message = err instanceof Error ? err.message : String(err);
|
|
193
215
|
if (isDebug()) {
|
|
@@ -203,5 +225,13 @@ export async function coerceSegmentParams(match: RouteMatch): Promise<void> {
|
|
|
203
225
|
}
|
|
204
226
|
throw new ParamCoercionError(message);
|
|
205
227
|
}
|
|
228
|
+
|
|
229
|
+
// Normalize outside the catch, for the same reason as above. `__proto__`
|
|
230
|
+
// is skipped here as well as inside `normalizeParamValue`, because this
|
|
231
|
+
// loop assigns into `mergeTarget` directly.
|
|
232
|
+
for (const key of Object.keys(coerced)) {
|
|
233
|
+
if (key === '__proto__') continue;
|
|
234
|
+
mergeTarget[key] = normalizeParamValue(coerced[key], key);
|
|
235
|
+
}
|
|
206
236
|
}
|
|
207
237
|
}
|
|
@@ -18,51 +18,6 @@ import type { ProxyConfig } from './pipeline.js';
|
|
|
18
18
|
|
|
19
19
|
// ─── Prototype-Pollution-Safe Sanitizer ────────────────────────────────────
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* Only __proto__ needs stripping — it has a language-level setter that
|
|
23
|
-
* changes the prototype chain of spread copies. constructor and prototype
|
|
24
|
-
* are harmless own properties on null-prototype objects.
|
|
25
|
-
*/
|
|
26
|
-
const DANGEROUS_KEYS = new Set(['__proto__']);
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Deep-walk a value returned by a segment param codec, producing a
|
|
30
|
-
* sanitized copy where every plain object is null-prototype and
|
|
31
|
-
* dangerous keys (__proto__, constructor, prototype) are stripped at
|
|
32
|
-
* every depth.
|
|
33
|
-
*
|
|
34
|
-
* Non-plain objects (Date, Map, class instances, etc.) are returned
|
|
35
|
-
* as-is — they cannot be poisoned by `{...x}` spread and may carry
|
|
36
|
-
* author-intended prototype methods.
|
|
37
|
-
*
|
|
38
|
-
* Arrays are walked element-wise.
|
|
39
|
-
*
|
|
40
|
-
* Performance: URL params are bounded by URL length (~8 KB). Realistic
|
|
41
|
-
* trees are <1 KB. The recursive walk is sub-microsecond.
|
|
42
|
-
*
|
|
43
|
-
* See TIM-655, TIM-855, TIM-873, design/13-security.md
|
|
44
|
-
*/
|
|
45
|
-
export function sanitizeParamValue(value: unknown): unknown {
|
|
46
|
-
if (value === null || typeof value !== 'object') return value;
|
|
47
|
-
|
|
48
|
-
if (Array.isArray(value)) {
|
|
49
|
-
return value.map(sanitizeParamValue);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Only walk plain objects — anything with a custom prototype (Date, Map,
|
|
53
|
-
// class instances) is left untouched.
|
|
54
|
-
const proto = Object.getPrototypeOf(value);
|
|
55
|
-
if (proto !== Object.prototype && proto !== null) return value;
|
|
56
|
-
|
|
57
|
-
const out: Record<string, unknown> = Object.create(null);
|
|
58
|
-
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
59
|
-
if (!DANGEROUS_KEYS.has(key)) {
|
|
60
|
-
out[key] = sanitizeParamValue((value as Record<string, unknown>)[key]);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return out;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
21
|
// ─── Proxy Resolver ────────────────────────────────────────────────────────
|
|
67
22
|
|
|
68
23
|
/**
|
|
@@ -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 ─────────────────────────────────────────────────────────
|