@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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework default status page — the terminal fallback of the deny chain.
|
|
3
|
+
*
|
|
4
|
+
* design/10-error-handling.md §"Fallback Chain" ends every 4xx/5xx chain in a
|
|
5
|
+
* "framework default page". Before TIM-1258 that terminal did not exist: an app
|
|
6
|
+
* with no `404.tsx` / `4xx.tsx` / `error.tsx` got `Response(null, { status })` —
|
|
7
|
+
* a correct status code with an empty body. Browsers render that as a blank
|
|
8
|
+
* page, and static generation had nothing to write to `404.html`.
|
|
9
|
+
*
|
|
10
|
+
* This page is a plain HTML string rather than a React component on purpose:
|
|
11
|
+
* it is the *last* fallback, so it must not depend on the RSC → SSR pipeline
|
|
12
|
+
* (the machinery that may already have failed by the time we get here), and it
|
|
13
|
+
* must be renderable from the static build where no render environment exists.
|
|
14
|
+
* One builder is the single source of truth for both surfaces.
|
|
15
|
+
*
|
|
16
|
+
* Security: the page interpolates nothing but the status code, and the code is
|
|
17
|
+
* clamped to a plain integer before it reaches the markup. No request URL, no
|
|
18
|
+
* error message, no stack — see design/13-security.md principle 4.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Canonical reason phrases (RFC 9110 §15) for the statuses an app is likely to
|
|
23
|
+
* `deny()` with. Anything else falls back to a generic category phrase — the
|
|
24
|
+
* phrase is decoration, never a place to echo caller-supplied text.
|
|
25
|
+
*/
|
|
26
|
+
const REASON_PHRASES: Record<number, string> = {
|
|
27
|
+
400: 'Bad Request',
|
|
28
|
+
401: 'Unauthorized',
|
|
29
|
+
402: 'Payment Required',
|
|
30
|
+
403: 'Forbidden',
|
|
31
|
+
404: 'Not Found',
|
|
32
|
+
405: 'Method Not Allowed',
|
|
33
|
+
406: 'Not Acceptable',
|
|
34
|
+
408: 'Request Timeout',
|
|
35
|
+
409: 'Conflict',
|
|
36
|
+
410: 'Gone',
|
|
37
|
+
413: 'Content Too Large',
|
|
38
|
+
415: 'Unsupported Media Type',
|
|
39
|
+
418: "I'm a Teapot",
|
|
40
|
+
422: 'Unprocessable Content',
|
|
41
|
+
429: 'Too Many Requests',
|
|
42
|
+
451: 'Unavailable For Legal Reasons',
|
|
43
|
+
500: 'Internal Server Error',
|
|
44
|
+
501: 'Not Implemented',
|
|
45
|
+
502: 'Bad Gateway',
|
|
46
|
+
503: 'Service Unavailable',
|
|
47
|
+
504: 'Gateway Timeout',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** HTML content type used for every framework default page response. */
|
|
51
|
+
export const DEFAULT_STATUS_PAGE_CONTENT_TYPE = 'text/html; charset=utf-8';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Coerce a status into something safe to put in markup.
|
|
55
|
+
*
|
|
56
|
+
* `DenySignal.status` is typed `number` but nothing validates it at
|
|
57
|
+
* construction, and JS callers can hand us anything. Rather than escaping a
|
|
58
|
+
* value that should never have been a string, collapse non-integers to 500 —
|
|
59
|
+
* the page is decoration for a status already on the wire.
|
|
60
|
+
*/
|
|
61
|
+
function displayStatus(status: number): number {
|
|
62
|
+
return Number.isInteger(status) && status >= 100 && status <= 599 ? status : 500;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Human-readable phrase for a status code. Never caller-supplied text. */
|
|
66
|
+
export function statusReasonPhrase(status: number): string {
|
|
67
|
+
const code = displayStatus(status);
|
|
68
|
+
if (REASON_PHRASES[code]) return REASON_PHRASES[code];
|
|
69
|
+
return code >= 500 ? 'Server Error' : 'Request Error';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build the framework default page for a status code as a full HTML document.
|
|
74
|
+
*
|
|
75
|
+
* The document is self-contained: no scripts, no external resources, no client
|
|
76
|
+
* JavaScript. It renders identically in `server` mode, in `static` mode's
|
|
77
|
+
* generated `404.html`, and with `clientJavascript: false`.
|
|
78
|
+
*/
|
|
79
|
+
export function defaultStatusPageHtml(status: number): string {
|
|
80
|
+
const code = displayStatus(status);
|
|
81
|
+
const phrase = statusReasonPhrase(code);
|
|
82
|
+
return `<!DOCTYPE html>
|
|
83
|
+
<html lang="en">
|
|
84
|
+
<head>
|
|
85
|
+
<meta charset="utf-8">
|
|
86
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
87
|
+
<title>${code} ${phrase}</title>
|
|
88
|
+
<style>
|
|
89
|
+
:root { color-scheme: light dark; --fg: #1a1a1a; --muted: #6b6b76; --bg: #ffffff; }
|
|
90
|
+
@media (prefers-color-scheme: dark) { :root { --fg: #ededf1; --muted: #9b9baa; --bg: #111114; } }
|
|
91
|
+
html, body { height: 100%; margin: 0; }
|
|
92
|
+
body {
|
|
93
|
+
display: flex; align-items: center; justify-content: center;
|
|
94
|
+
background: var(--bg); color: var(--fg);
|
|
95
|
+
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
96
|
+
text-align: center; padding: 2rem;
|
|
97
|
+
}
|
|
98
|
+
h1 { font-size: 3rem; font-weight: 600; margin: 0; letter-spacing: -0.02em; }
|
|
99
|
+
p { margin: 0.5rem 0 0; color: var(--muted); font-size: 1rem; }
|
|
100
|
+
</style>
|
|
101
|
+
</head>
|
|
102
|
+
<body>
|
|
103
|
+
<main>
|
|
104
|
+
<h1>${code}</h1>
|
|
105
|
+
<p>${phrase}</p>
|
|
106
|
+
</main>
|
|
107
|
+
</body>
|
|
108
|
+
</html>
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build the framework default page as an HTTP `Response`.
|
|
114
|
+
*
|
|
115
|
+
* `responseHeaders` carries whatever middleware/proxy already set (cookies,
|
|
116
|
+
* cache directives); the content type is overwritten because the body is
|
|
117
|
+
* always HTML regardless of what the route would have produced.
|
|
118
|
+
*
|
|
119
|
+
* The body is unconditional. HEAD is not this function's problem: the pipeline
|
|
120
|
+
* strips HEAD bodies at its single exit for every response shape, so a
|
|
121
|
+
* `method` parameter here would be one renderer's private copy of a rule that
|
|
122
|
+
* now holds framework-wide (TIM-1324).
|
|
123
|
+
*/
|
|
124
|
+
export function defaultStatusPageResponse(status: number, responseHeaders: Headers): Response {
|
|
125
|
+
responseHeaders.set('content-type', DEFAULT_STATUS_PAGE_CONTENT_TYPE);
|
|
126
|
+
return new Response(defaultStatusPageHtml(status), { status, headers: responseHeaders });
|
|
127
|
+
}
|
|
@@ -36,7 +36,6 @@ import type { SerializableError } from '../client/error-reconstituter.js';
|
|
|
36
36
|
import { requestContextAls } from './als-registry.js';
|
|
37
37
|
import { DenySignal } from './primitives.js';
|
|
38
38
|
import { loadModule } from './safe-load.js';
|
|
39
|
-
import { LEGACY_FILE_TO_STATUS } from './status-code-resolver.js';
|
|
40
39
|
import { withSpan } from './tracing.js';
|
|
41
40
|
import { isMdxFilePath } from './utils/mdx-file.js';
|
|
42
41
|
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
@@ -50,12 +49,11 @@ export interface DenyPageEntry {
|
|
|
50
49
|
/** The component to render (server or client — both work). */
|
|
51
50
|
component: (...args: unknown[]) => unknown;
|
|
52
51
|
/**
|
|
53
|
-
* How the entry matched: a status-code file (404.tsx/4xx.tsx)
|
|
54
|
-
* compat file (not-found.tsx/forbidden.tsx/unauthorized.tsx), or the
|
|
52
|
+
* How the entry matched: a status-code file (404.tsx/4xx.tsx) or the
|
|
55
53
|
* error.tsx catch-all. error.tsx entries render with their documented
|
|
56
54
|
* { error, digest, reset } contract, not bare deny props. See TIM-1081.
|
|
57
55
|
*/
|
|
58
|
-
kind: 'status' | '
|
|
56
|
+
kind: 'status' | 'error';
|
|
59
57
|
/** MDX files are server components — rendered with plain props, never ErrorReconstituter. */
|
|
60
58
|
isMdx: boolean;
|
|
61
59
|
}
|
|
@@ -69,9 +67,7 @@ export interface DenyPageEntry {
|
|
|
69
67
|
* status-code file components in fallback order:
|
|
70
68
|
* 1. Specific status files (403.tsx, 404.tsx) — exact match
|
|
71
69
|
* 2. Category catch-alls (4xx.tsx) — matches any 4xx
|
|
72
|
-
* 3.
|
|
73
|
-
* unauthorized.tsx → 401) — exact match
|
|
74
|
-
* 4. error.tsx — catches everything
|
|
70
|
+
* 3. error.tsx — catches everything
|
|
75
71
|
*
|
|
76
72
|
* Each segment is checked in this order. The chain is ordered so the
|
|
77
73
|
* FIRST match wins at catch time. This mirrors resolveStatusFile's 4xx
|
|
@@ -127,29 +123,7 @@ export async function buildDenyPageChain(
|
|
|
127
123
|
}
|
|
128
124
|
}
|
|
129
125
|
|
|
130
|
-
// Pass 2:
|
|
131
|
-
// unauthorized.tsx → 401). Lower priority than status files (a root
|
|
132
|
-
// 4xx.tsx beats a leaf not-found.tsx) but higher than error.tsx — the
|
|
133
|
-
// same ordering as resolveStatusFile's resolve4xx. See TIM-1081.
|
|
134
|
-
for (let i = startIndex; i >= 0; i--) {
|
|
135
|
-
const segment = segments[i];
|
|
136
|
-
if (!segment.legacyStatusFiles) continue;
|
|
137
|
-
for (const [name, status] of Object.entries(LEGACY_FILE_TO_STATUS)) {
|
|
138
|
-
const file = segment.legacyStatusFiles[name];
|
|
139
|
-
if (!file) continue;
|
|
140
|
-
const mod = await loadModule(file).catch(() => null);
|
|
141
|
-
if (mod?.default) {
|
|
142
|
-
chain.push({
|
|
143
|
-
status,
|
|
144
|
-
component: mod.default as (...args: unknown[]) => unknown,
|
|
145
|
-
kind: 'legacy',
|
|
146
|
-
isMdx: isMdxFilePath(file.filePath),
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Pass 3: error.tsx files — lowest priority catch-all.
|
|
126
|
+
// Pass 2: error.tsx files — lowest priority catch-all.
|
|
153
127
|
// Only added AFTER all status and legacy files so they never shadow a
|
|
154
128
|
// more specific file from an ancestor segment.
|
|
155
129
|
for (let i = startIndex; i >= 0; i--) {
|
|
@@ -202,7 +176,7 @@ export function renderMatchingDenyPage(
|
|
|
202
176
|
* Build the element for a matched deny chain entry with the props contract
|
|
203
177
|
* the file's convention documents:
|
|
204
178
|
*
|
|
205
|
-
* - Status-code
|
|
179
|
+
* - Status-code files: { status, dangerouslyPassData }
|
|
206
180
|
* - error.tsx (TSX): { error, digest, reset } via ErrorReconstituter, plus
|
|
207
181
|
* { status, dangerouslyPassData } for dual-shape implementations. Without
|
|
208
182
|
* the Error prop, any error.tsx written per the docs (reading
|
|
@@ -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';
|
|
@@ -76,24 +81,36 @@ function isApiRoute(segments: ReadonlyArray<ManifestSegmentNode>): boolean {
|
|
|
76
81
|
function buildDenyElement(
|
|
77
82
|
deny: DenySignal,
|
|
78
83
|
component: (...args: unknown[]) => unknown,
|
|
79
|
-
resolutionKind: 'exact' | 'category' | '
|
|
84
|
+
resolutionKind: 'exact' | 'category' | 'error',
|
|
80
85
|
filePath: string
|
|
81
86
|
): React.ReactElement {
|
|
82
87
|
const entry: DenyPageEntry = {
|
|
83
88
|
status: deny.status,
|
|
84
89
|
component,
|
|
85
|
-
kind: resolutionKind === 'error' ? 'error' :
|
|
90
|
+
kind: resolutionKind === 'error' ? 'error' : 'status',
|
|
86
91
|
isMdx: isMdxFilePath(filePath),
|
|
87
92
|
};
|
|
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
|
/**
|