@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,243 @@
|
|
|
1
|
+
//#region src/shared/rsc-cache-key.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `_rsc` payload cache key — derived identically on both sides.
|
|
4
|
+
*
|
|
5
|
+
* The client puts this in the payload URL so the document and its Flight
|
|
6
|
+
* payload occupy different cache keys (TIM-1268). The server recomputes it
|
|
7
|
+
* from the received headers so a request cannot claim one client's key while
|
|
8
|
+
* sending another client's headers — see `server/rsc-cache-key-guard.ts`.
|
|
9
|
+
* Both sides must agree exactly, which is why the derivation lives here and
|
|
10
|
+
* not in either one.
|
|
11
|
+
*
|
|
12
|
+
* Isomorphic: no server or client imports. The browser bundle audit
|
|
13
|
+
* (`tests/client-bundle-audit.test.ts`) enforces the client half of that.
|
|
14
|
+
*
|
|
15
|
+
* See design/19-client-navigation.md §"The `_rsc` cache key".
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Digest length in hex characters. 128 bits: the origin guard compares a
|
|
19
|
+
* caller-supplied key against a recomputed one, so the relevant bound is a
|
|
20
|
+
* birthday attack (an attacker who can influence a victim's `X-Timber-URL`
|
|
21
|
+
* gets to search both sides), not second preimage. 64 bits would fall to
|
|
22
|
+
* ~2^32 work; 128 puts it at 2^64.
|
|
23
|
+
*/
|
|
24
|
+
var KEY_HEX_LENGTH = 32;
|
|
25
|
+
/**
|
|
26
|
+
* Marks a value as a framework-issued key, and versions the derivation.
|
|
27
|
+
*
|
|
28
|
+
* Without it, an application value that happened to be 32 lowercase hex — an
|
|
29
|
+
* MD5, a token — is indistinguishable from a forged claim: both are "the
|
|
30
|
+
* right shape, the wrong digest", and no information in the value separates
|
|
31
|
+
* them. So the exemption cannot key on shape alone.
|
|
32
|
+
*
|
|
33
|
+
* The prefix resolves that because it is anchored to the address the
|
|
34
|
+
* victim's own client uses. A payload lives at `?_rsc=1.<digest>`, so
|
|
35
|
+
* anything aimed at that cache entry must carry the prefix and therefore
|
|
36
|
+
* gets validated; anything without it cannot be addressing a payload URL at
|
|
37
|
+
* all, and is exempt as application data. An attacker cannot both hit the
|
|
38
|
+
* victim's cache key and dodge the check.
|
|
39
|
+
*
|
|
40
|
+
* The version component also gives the derivation a migration path: the
|
|
41
|
+
* client and the origin must agree byte for byte, so a future change to the
|
|
42
|
+
* canonical form can be rolled out by bumping this instead of a flag day.
|
|
43
|
+
*/
|
|
44
|
+
var KEY_PREFIX = "1.";
|
|
45
|
+
/**
|
|
46
|
+
* The request headers that vary an RSC payload response, in canonical
|
|
47
|
+
* casing — this list is also emitted verbatim as the response's `Vary`
|
|
48
|
+
* tokens (`server/pipeline.ts`), since "what varies the response" is one
|
|
49
|
+
* question with one answer, not two lists that happen to agree.
|
|
50
|
+
*
|
|
51
|
+
* This list is the contract, and it is enforced rather than trusted:
|
|
52
|
+
* `tests/rsc-cache-key.test.ts` asserts that the header names
|
|
53
|
+
* `buildRscHeaders()` can emit are exactly this set. A header added to the
|
|
54
|
+
* request without being added here fails that test, because a varying input
|
|
55
|
+
* outside the key is precisely the cache-poisoning hazard the key exists to
|
|
56
|
+
* close.
|
|
57
|
+
*
|
|
58
|
+
* **No credentials, session identifiers, or user-supplied content may be
|
|
59
|
+
* added.** Every value here is hashed into a URL, and URLs are logged by
|
|
60
|
+
* proxies, CDNs, and origin access logs. `fnv1aHash` is a cache-key hash,
|
|
61
|
+
* not a KDF — a low-entropy secret would be recoverable by brute force.
|
|
62
|
+
* Authentication travels on cookies, which never enter the key.
|
|
63
|
+
*/
|
|
64
|
+
var RSC_KEY_HEADERS = [
|
|
65
|
+
"Accept",
|
|
66
|
+
"X-Timber-State-Tree",
|
|
67
|
+
"X-Timber-URL",
|
|
68
|
+
"X-Timber-Deployment-Id"
|
|
69
|
+
];
|
|
70
|
+
/**
|
|
71
|
+
* Whether a value could be a key this module produced.
|
|
72
|
+
*
|
|
73
|
+
* `_rsc` is not a reserved parameter name — an application may already use
|
|
74
|
+
* it for its own search state, including for a value that happens to look
|
|
75
|
+
* like a digest. Anything without the `KEY_PREFIX` is therefore application
|
|
76
|
+
* data, not a claim on a payload cache key, and the origin guard leaves it
|
|
77
|
+
* alone rather than penalizing the response.
|
|
78
|
+
*
|
|
79
|
+
* Derived from `KEY_PREFIX` and `KEY_HEX_LENGTH` rather than spelled again,
|
|
80
|
+
* so the shape cannot drift from the digest that has to satisfy it.
|
|
81
|
+
*
|
|
82
|
+
* Ignoring a malformed value costs no protection: clients only ever send
|
|
83
|
+
* well-formed keys, so a URL bearing a malformed one is an address no
|
|
84
|
+
* legitimate request will ever ask for, and nothing can be poisoned at an
|
|
85
|
+
* address nobody reads.
|
|
86
|
+
*/
|
|
87
|
+
function isRscCacheKeyShape(value) {
|
|
88
|
+
if (!value.startsWith(KEY_PREFIX)) return false;
|
|
89
|
+
const digest = value.slice(2);
|
|
90
|
+
return digest.length === KEY_HEX_LENGTH && /^[0-9a-f]+$/.test(digest);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The query parameter carrying the payload cache key.
|
|
94
|
+
*
|
|
95
|
+
* Spelled here rather than at each of the four places that read or write it
|
|
96
|
+
* — the client that appends it, the origin guard that validates it, and the
|
|
97
|
+
* strip below — because it is a wire contract between two independent
|
|
98
|
+
* implementations, exactly like the digest itself.
|
|
99
|
+
*/
|
|
100
|
+
var RSC_KEY_PARAM = "_rsc";
|
|
101
|
+
/**
|
|
102
|
+
* Decode one raw `name` or `value` the way `URLSearchParams` does:
|
|
103
|
+
* `application/x-www-form-urlencoded`, so `+` is a space.
|
|
104
|
+
*
|
|
105
|
+
* Malformed percent-escapes throw in `decodeURIComponent`. `URLSearchParams`
|
|
106
|
+
* leaves those bytes as-is rather than throwing, and so do we — a pair we
|
|
107
|
+
* cannot decode is by definition not the framework's, so it is kept.
|
|
108
|
+
*/
|
|
109
|
+
function formDecode(raw) {
|
|
110
|
+
try {
|
|
111
|
+
return decodeURIComponent(raw.replace(/\+/g, " "));
|
|
112
|
+
} catch {
|
|
113
|
+
return raw;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Remove framework-issued `_rsc` values from a raw query string.
|
|
118
|
+
*
|
|
119
|
+
* Only values matching `isRscCacheKeyShape` are removed. `_rsc` is not a
|
|
120
|
+
* reserved name (see that function), so `/search?_rsc=foo` is application
|
|
121
|
+
* state and survives — the same exemption the origin guard applies, derived
|
|
122
|
+
* from the same predicate so the two cannot disagree about what "ours"
|
|
123
|
+
* means.
|
|
124
|
+
*
|
|
125
|
+
* Operates on the raw string rather than round-tripping through
|
|
126
|
+
* `URLSearchParams`, so every surviving pair keeps its original bytes.
|
|
127
|
+
* Re-serializing would rewrite `?b` as `?b=` and `?a=%7E` as `?a=~` on RSC
|
|
128
|
+
* navigations only, which is the same class of inconsistency this function
|
|
129
|
+
* exists to remove.
|
|
130
|
+
*
|
|
131
|
+
* Matching decodes the name, because `?%5Frsc=<key>` is a `_rsc` parameter
|
|
132
|
+
* as far as `URLSearchParams` is concerned — the same reasoning as the
|
|
133
|
+
* guard's fast path.
|
|
134
|
+
*
|
|
135
|
+
* @param search - `url.search`, with or without the leading `?`.
|
|
136
|
+
* @returns The query string including a leading `?`, or `''` if empty.
|
|
137
|
+
*/
|
|
138
|
+
function stripRscCacheKey(search) {
|
|
139
|
+
const query = search.startsWith("?") ? search.slice(1) : search;
|
|
140
|
+
if (query === "") return "";
|
|
141
|
+
const result = query.split("&").filter((pair) => {
|
|
142
|
+
const eq = pair.indexOf("=");
|
|
143
|
+
if (formDecode(eq === -1 ? pair : pair.slice(0, eq)) !== "_rsc") return true;
|
|
144
|
+
return !isRscCacheKeyShape(eq === -1 ? "" : formDecode(pair.slice(eq + 1)));
|
|
145
|
+
}).join("&");
|
|
146
|
+
return result === "" ? "" : `?${result}`;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The search state application code sees, for any request URL.
|
|
150
|
+
*
|
|
151
|
+
* The `_rsc` key rides on the payload URL of every RSC navigation and on no
|
|
152
|
+
* other request, so reading search params straight off `req.url` makes the
|
|
153
|
+
* same page observe different search state depending on whether it was
|
|
154
|
+
* reached by a full load or a client navigation — and lets the cache key
|
|
155
|
+
* leak into whatever the page builds from it: a canonical URL, a pagination
|
|
156
|
+
* link, an analytics payload (TIM-1272).
|
|
157
|
+
*
|
|
158
|
+
* Every application-facing derivation of search state goes through here.
|
|
159
|
+
* `params` and `search` are returned together, from one strip, because they
|
|
160
|
+
* are two views of one answer and were previously two expressions that had
|
|
161
|
+
* to agree.
|
|
162
|
+
*
|
|
163
|
+
* The raw request URL is deliberately untouched: `server/rsc-cache-key-guard.ts`
|
|
164
|
+
* still reads the parameter from `req.url` to bind the key to the request.
|
|
165
|
+
*/
|
|
166
|
+
function appVisibleSearch(url) {
|
|
167
|
+
const search = stripRscCacheKey(url.search);
|
|
168
|
+
return {
|
|
169
|
+
params: new URLSearchParams(search),
|
|
170
|
+
search
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Compute the `_rsc` cache key for a request.
|
|
175
|
+
*
|
|
176
|
+
* Absent headers are omitted rather than sent as empty, so "header missing"
|
|
177
|
+
* and "header present but empty" produce different keys.
|
|
178
|
+
*
|
|
179
|
+
* Values are length-prefixed rather than delimiter-separated. Any delimiter
|
|
180
|
+
* can be forged by a value that contains it: with a `\0` separator, an
|
|
181
|
+
* `accept` of `a\nx-timber-url\0b` serializes identically to an `accept` of
|
|
182
|
+
* `a` alongside an `x-timber-url` of `b`, which would let one header claim
|
|
183
|
+
* another's key. HTTP forbids control characters in header values and both
|
|
184
|
+
* `fetch()` and the server runtime reject them, so this is defense in depth
|
|
185
|
+
* — but a length prefix is unambiguous for *any* content, which is a
|
|
186
|
+
* property worth having in a function whose whole job is to be agreed on by
|
|
187
|
+
* two independent implementations.
|
|
188
|
+
*
|
|
189
|
+
* SHA-256, truncated to 128 bits. A fast non-cryptographic hash is the
|
|
190
|
+
* obvious choice for a cache key and is the wrong one here: the origin
|
|
191
|
+
* *compares* this value against a recomputed one, which makes it a check.
|
|
192
|
+
* FNV-1a — used for timber's other cache keys — is a multiply-xor over a
|
|
193
|
+
* fixed field, so an attacker controlling a few bytes of any input (their
|
|
194
|
+
* own `X-Timber-URL`, say) can solve for a chosen digest rather than search
|
|
195
|
+
* for one, and forge a key belonging to somebody else.
|
|
196
|
+
*
|
|
197
|
+
* Returns `null` when no digest is available — `crypto.subtle` is absent in
|
|
198
|
+
* non-secure browsing contexts (plain `http://` on a LAN address). Callers
|
|
199
|
+
* must treat that as "no key", never as "key matches": the client omits the
|
|
200
|
+
* parameter and the server refuses to share-cache. That returns those
|
|
201
|
+
* clients to the pre-TIM-1268 posture, where `Vary` alone separates HTML
|
|
202
|
+
* from Flight — acceptable, because a non-secure origin has no shared-CDN
|
|
203
|
+
* caching story to protect in the first place.
|
|
204
|
+
*/
|
|
205
|
+
async function rscCacheKey(lookup) {
|
|
206
|
+
const subtle = globalThis.crypto?.subtle;
|
|
207
|
+
if (!subtle) return null;
|
|
208
|
+
let canonical = "";
|
|
209
|
+
for (const name of RSC_KEY_HEADERS) {
|
|
210
|
+
const value = lookup(name);
|
|
211
|
+
if (value == null) continue;
|
|
212
|
+
canonical += `${name.toLowerCase()}:${value.length}:${value}`;
|
|
213
|
+
}
|
|
214
|
+
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(canonical));
|
|
215
|
+
let hex = "";
|
|
216
|
+
for (const byte of new Uint8Array(digest, 0, KEY_HEX_LENGTH / 2)) hex += byte.toString(16).padStart(2, "0");
|
|
217
|
+
return KEY_PREFIX + hex;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* An unverifiable stand-in key, for the one case where the digest cannot be
|
|
221
|
+
* computed: `crypto.subtle` is absent in non-secure browsing contexts.
|
|
222
|
+
* `crypto.getRandomValues` is not gated that way, so it is available exactly
|
|
223
|
+
* where the digest is not.
|
|
224
|
+
*
|
|
225
|
+
* Carries `KEY_PREFIX` like any other key, so the origin reads it as a claim
|
|
226
|
+
* it cannot verify — and refuses to share-cache the response — rather than
|
|
227
|
+
* exempting it as application data. Lives here so the prefix has one home.
|
|
228
|
+
*/
|
|
229
|
+
function randomRscCacheKey() {
|
|
230
|
+
const bytes = new Uint8Array(KEY_HEX_LENGTH / 2);
|
|
231
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
232
|
+
return KEY_PREFIX + [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
233
|
+
}
|
|
234
|
+
/** Build a case-insensitive lookup over a plain header record. */
|
|
235
|
+
function recordLookup(headers) {
|
|
236
|
+
const lowered = {};
|
|
237
|
+
for (const name of Object.keys(headers)) lowered[name.toLowerCase()] = headers[name];
|
|
238
|
+
return (name) => lowered[name.toLowerCase()];
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
241
|
+
export { randomRscCacheKey as a, isRscCacheKeyShape as i, RSC_KEY_PARAM as n, recordLookup as o, appVisibleSearch as r, rscCacheKey as s, RSC_KEY_HEADERS as t };
|
|
242
|
+
|
|
243
|
+
//# sourceMappingURL=rsc-cache-key-DD0fl_-s.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rsc-cache-key-DD0fl_-s.js","names":[],"sources":["../../src/shared/rsc-cache-key.ts"],"sourcesContent":["/**\n * The `_rsc` payload cache key — derived identically on both sides.\n *\n * The client puts this in the payload URL so the document and its Flight\n * payload occupy different cache keys (TIM-1268). The server recomputes it\n * from the received headers so a request cannot claim one client's key while\n * sending another client's headers — see `server/rsc-cache-key-guard.ts`.\n * Both sides must agree exactly, which is why the derivation lives here and\n * not in either one.\n *\n * Isomorphic: no server or client imports. The browser bundle audit\n * (`tests/client-bundle-audit.test.ts`) enforces the client half of that.\n *\n * See design/19-client-navigation.md §\"The `_rsc` cache key\".\n */\n\n/**\n * Digest length in hex characters. 128 bits: the origin guard compares a\n * caller-supplied key against a recomputed one, so the relevant bound is a\n * birthday attack (an attacker who can influence a victim's `X-Timber-URL`\n * gets to search both sides), not second preimage. 64 bits would fall to\n * ~2^32 work; 128 puts it at 2^64.\n */\nconst KEY_HEX_LENGTH = 32;\n\n/**\n * Marks a value as a framework-issued key, and versions the derivation.\n *\n * Without it, an application value that happened to be 32 lowercase hex — an\n * MD5, a token — is indistinguishable from a forged claim: both are \"the\n * right shape, the wrong digest\", and no information in the value separates\n * them. So the exemption cannot key on shape alone.\n *\n * The prefix resolves that because it is anchored to the address the\n * victim's own client uses. A payload lives at `?_rsc=1.<digest>`, so\n * anything aimed at that cache entry must carry the prefix and therefore\n * gets validated; anything without it cannot be addressing a payload URL at\n * all, and is exempt as application data. An attacker cannot both hit the\n * victim's cache key and dodge the check.\n *\n * The version component also gives the derivation a migration path: the\n * client and the origin must agree byte for byte, so a future change to the\n * canonical form can be rolled out by bumping this instead of a flag day.\n */\nconst KEY_PREFIX = '1.';\n\n/**\n * The request headers that vary an RSC payload response, in canonical\n * casing — this list is also emitted verbatim as the response's `Vary`\n * tokens (`server/pipeline.ts`), since \"what varies the response\" is one\n * question with one answer, not two lists that happen to agree.\n *\n * This list is the contract, and it is enforced rather than trusted:\n * `tests/rsc-cache-key.test.ts` asserts that the header names\n * `buildRscHeaders()` can emit are exactly this set. A header added to the\n * request without being added here fails that test, because a varying input\n * outside the key is precisely the cache-poisoning hazard the key exists to\n * close.\n *\n * **No credentials, session identifiers, or user-supplied content may be\n * added.** Every value here is hashed into a URL, and URLs are logged by\n * proxies, CDNs, and origin access logs. `fnv1aHash` is a cache-key hash,\n * not a KDF — a low-entropy secret would be recoverable by brute force.\n * Authentication travels on cookies, which never enter the key.\n */\nexport const RSC_KEY_HEADERS = [\n 'Accept',\n 'X-Timber-State-Tree',\n 'X-Timber-URL',\n 'X-Timber-Deployment-Id',\n] as const;\n\n/**\n * Whether a value could be a key this module produced.\n *\n * `_rsc` is not a reserved parameter name — an application may already use\n * it for its own search state, including for a value that happens to look\n * like a digest. Anything without the `KEY_PREFIX` is therefore application\n * data, not a claim on a payload cache key, and the origin guard leaves it\n * alone rather than penalizing the response.\n *\n * Derived from `KEY_PREFIX` and `KEY_HEX_LENGTH` rather than spelled again,\n * so the shape cannot drift from the digest that has to satisfy it.\n *\n * Ignoring a malformed value costs no protection: clients only ever send\n * well-formed keys, so a URL bearing a malformed one is an address no\n * legitimate request will ever ask for, and nothing can be poisoned at an\n * address nobody reads.\n */\nexport function isRscCacheKeyShape(value: string): boolean {\n if (!value.startsWith(KEY_PREFIX)) return false;\n const digest = value.slice(KEY_PREFIX.length);\n return digest.length === KEY_HEX_LENGTH && /^[0-9a-f]+$/.test(digest);\n}\n\n/**\n * The query parameter carrying the payload cache key.\n *\n * Spelled here rather than at each of the four places that read or write it\n * — the client that appends it, the origin guard that validates it, and the\n * strip below — because it is a wire contract between two independent\n * implementations, exactly like the digest itself.\n */\nexport const RSC_KEY_PARAM = '_rsc';\n\n// ─── Keeping the key out of application state ────────────────────────────\n\n/**\n * Decode one raw `name` or `value` the way `URLSearchParams` does:\n * `application/x-www-form-urlencoded`, so `+` is a space.\n *\n * Malformed percent-escapes throw in `decodeURIComponent`. `URLSearchParams`\n * leaves those bytes as-is rather than throwing, and so do we — a pair we\n * cannot decode is by definition not the framework's, so it is kept.\n */\nfunction formDecode(raw: string): string {\n try {\n return decodeURIComponent(raw.replace(/\\+/g, ' '));\n } catch {\n return raw;\n }\n}\n\n/**\n * Remove framework-issued `_rsc` values from a raw query string.\n *\n * Only values matching `isRscCacheKeyShape` are removed. `_rsc` is not a\n * reserved name (see that function), so `/search?_rsc=foo` is application\n * state and survives — the same exemption the origin guard applies, derived\n * from the same predicate so the two cannot disagree about what \"ours\"\n * means.\n *\n * Operates on the raw string rather than round-tripping through\n * `URLSearchParams`, so every surviving pair keeps its original bytes.\n * Re-serializing would rewrite `?b` as `?b=` and `?a=%7E` as `?a=~` on RSC\n * navigations only, which is the same class of inconsistency this function\n * exists to remove.\n *\n * Matching decodes the name, because `?%5Frsc=<key>` is a `_rsc` parameter\n * as far as `URLSearchParams` is concerned — the same reasoning as the\n * guard's fast path.\n *\n * @param search - `url.search`, with or without the leading `?`.\n * @returns The query string including a leading `?`, or `''` if empty.\n */\nexport function stripRscCacheKey(search: string): string {\n const query = search.startsWith('?') ? search.slice(1) : search;\n if (query === '') return '';\n const kept = query.split('&').filter((pair) => {\n const eq = pair.indexOf('=');\n if (formDecode(eq === -1 ? pair : pair.slice(0, eq)) !== RSC_KEY_PARAM) return true;\n return !isRscCacheKeyShape(eq === -1 ? '' : formDecode(pair.slice(eq + 1)));\n });\n const result = kept.join('&');\n return result === '' ? '' : `?${result}`;\n}\n\n/** A request's search state as application code should see it. */\nexport interface AppVisibleSearch {\n /** Parsed params, with the framework's cache key removed. */\n params: URLSearchParams;\n /** The matching raw query string — `''` or `?…`. */\n search: string;\n}\n\n/**\n * The search state application code sees, for any request URL.\n *\n * The `_rsc` key rides on the payload URL of every RSC navigation and on no\n * other request, so reading search params straight off `req.url` makes the\n * same page observe different search state depending on whether it was\n * reached by a full load or a client navigation — and lets the cache key\n * leak into whatever the page builds from it: a canonical URL, a pagination\n * link, an analytics payload (TIM-1272).\n *\n * Every application-facing derivation of search state goes through here.\n * `params` and `search` are returned together, from one strip, because they\n * are two views of one answer and were previously two expressions that had\n * to agree.\n *\n * The raw request URL is deliberately untouched: `server/rsc-cache-key-guard.ts`\n * still reads the parameter from `req.url` to bind the key to the request.\n */\nexport function appVisibleSearch(url: URL): AppVisibleSearch {\n const search = stripRscCacheKey(url.search);\n return { params: new URLSearchParams(search), search };\n}\n\n/**\n * Reads a header value by name. Must be case-insensitive — `Headers.get()`\n * already is, and `recordLookup` below makes a plain record so.\n */\nexport type HeaderLookup = (name: string) => string | null | undefined;\n\n/**\n * Compute the `_rsc` cache key for a request.\n *\n * Absent headers are omitted rather than sent as empty, so \"header missing\"\n * and \"header present but empty\" produce different keys.\n *\n * Values are length-prefixed rather than delimiter-separated. Any delimiter\n * can be forged by a value that contains it: with a `\\0` separator, an\n * `accept` of `a\\nx-timber-url\\0b` serializes identically to an `accept` of\n * `a` alongside an `x-timber-url` of `b`, which would let one header claim\n * another's key. HTTP forbids control characters in header values and both\n * `fetch()` and the server runtime reject them, so this is defense in depth\n * — but a length prefix is unambiguous for *any* content, which is a\n * property worth having in a function whose whole job is to be agreed on by\n * two independent implementations.\n *\n * SHA-256, truncated to 128 bits. A fast non-cryptographic hash is the\n * obvious choice for a cache key and is the wrong one here: the origin\n * *compares* this value against a recomputed one, which makes it a check.\n * FNV-1a — used for timber's other cache keys — is a multiply-xor over a\n * fixed field, so an attacker controlling a few bytes of any input (their\n * own `X-Timber-URL`, say) can solve for a chosen digest rather than search\n * for one, and forge a key belonging to somebody else.\n *\n * Returns `null` when no digest is available — `crypto.subtle` is absent in\n * non-secure browsing contexts (plain `http://` on a LAN address). Callers\n * must treat that as \"no key\", never as \"key matches\": the client omits the\n * parameter and the server refuses to share-cache. That returns those\n * clients to the pre-TIM-1268 posture, where `Vary` alone separates HTML\n * from Flight — acceptable, because a non-secure origin has no shared-CDN\n * caching story to protect in the first place.\n */\nexport async function rscCacheKey(lookup: HeaderLookup): Promise<string | null> {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) return null;\n let canonical = '';\n for (const name of RSC_KEY_HEADERS) {\n const value = lookup(name);\n if (value == null) continue;\n // Lowercased in the canonical form so the two implementations cannot\n // disagree over the casing of the constant they share.\n canonical += `${name.toLowerCase()}:${value.length}:${value}`;\n }\n const digest = await subtle.digest('SHA-256', new TextEncoder().encode(canonical));\n let hex = '';\n for (const byte of new Uint8Array(digest, 0, KEY_HEX_LENGTH / 2)) {\n hex += byte.toString(16).padStart(2, '0');\n }\n return KEY_PREFIX + hex;\n}\n\n/**\n * An unverifiable stand-in key, for the one case where the digest cannot be\n * computed: `crypto.subtle` is absent in non-secure browsing contexts.\n * `crypto.getRandomValues` is not gated that way, so it is available exactly\n * where the digest is not.\n *\n * Carries `KEY_PREFIX` like any other key, so the origin reads it as a claim\n * it cannot verify — and refuses to share-cache the response — rather than\n * exempting it as application data. Lives here so the prefix has one home.\n */\nexport function randomRscCacheKey(): string {\n const bytes = new Uint8Array(KEY_HEX_LENGTH / 2);\n globalThis.crypto.getRandomValues(bytes);\n return KEY_PREFIX + [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');\n}\n\n/** Build a case-insensitive lookup over a plain header record. */\nexport function recordLookup(headers: Record<string, string>): HeaderLookup {\n const lowered: Record<string, string> = {};\n for (const name of Object.keys(headers)) {\n lowered[name.toLowerCase()] = headers[name];\n }\n return (name) => lowered[name.toLowerCase()];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;AAqBvB,IAAM,aAAa;;;;;;;;;;;;;;;;;;;;AAqBnB,IAAa,kBAAkB;CAC7B;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,mBAAmB,OAAwB;CACzD,IAAI,CAAC,MAAM,WAAW,UAAU,GAAG,OAAO;CAC1C,MAAM,SAAS,MAAM,MAAM,CAAiB;CAC5C,OAAO,OAAO,WAAW,kBAAkB,cAAc,KAAK,MAAM;AACtE;;;;;;;;;AAUA,IAAa,gBAAgB;;;;;;;;;AAY7B,SAAS,WAAW,KAAqB;CACvC,IAAI;EACF,OAAO,mBAAmB,IAAI,QAAQ,OAAO,GAAG,CAAC;CACnD,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,iBAAiB,QAAwB;CACvD,MAAM,QAAQ,OAAO,WAAW,GAAG,IAAI,OAAO,MAAM,CAAC,IAAI;CACzD,IAAI,UAAU,IAAI,OAAO;CAMzB,MAAM,SALO,MAAM,MAAM,GAAG,CAAC,CAAC,QAAQ,SAAS;EAC7C,MAAM,KAAK,KAAK,QAAQ,GAAG;EAC3B,IAAI,WAAW,OAAO,KAAK,OAAO,KAAK,MAAM,GAAG,EAAE,CAAC,MAAA,QAAqB,OAAO;EAC/E,OAAO,CAAC,mBAAmB,OAAO,KAAK,KAAK,WAAW,KAAK,MAAM,KAAK,CAAC,CAAC,CAAC;CAC5E,CACe,CAAA,CAAK,KAAK,GAAG;CAC5B,OAAO,WAAW,KAAK,KAAK,IAAI;AAClC;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,iBAAiB,KAA4B;CAC3D,MAAM,SAAS,iBAAiB,IAAI,MAAM;CAC1C,OAAO;EAAE,QAAQ,IAAI,gBAAgB,MAAM;EAAG;CAAO;AACvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,eAAsB,YAAY,QAA8C;CAC9E,MAAM,SAAS,WAAW,QAAQ;CAClC,IAAI,CAAC,QAAQ,OAAO;CACpB,IAAI,YAAY;CAChB,KAAK,MAAM,QAAQ,iBAAiB;EAClC,MAAM,QAAQ,OAAO,IAAI;EACzB,IAAI,SAAS,MAAM;EAGnB,aAAa,GAAG,KAAK,YAAY,EAAE,GAAG,MAAM,OAAO,GAAG;CACxD;CACA,MAAM,SAAS,MAAM,OAAO,OAAO,WAAW,IAAI,YAAY,CAAC,CAAC,OAAO,SAAS,CAAC;CACjF,IAAI,MAAM;CACV,KAAK,MAAM,QAAQ,IAAI,WAAW,QAAQ,GAAG,iBAAiB,CAAC,GAC7D,OAAO,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;CAE1C,OAAO,aAAa;AACtB;;;;;;;;;;;AAYA,SAAgB,oBAA4B;CAC1C,MAAM,QAAQ,IAAI,WAAW,iBAAiB,CAAC;CAC/C,WAAW,OAAO,gBAAgB,KAAK;CACvC,OAAO,aAAa,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;AACpF;;AAGA,SAAgB,aAAa,SAA+C;CAC1E,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,QAAQ,OAAO,KAAK,OAAO,GACpC,QAAQ,KAAK,YAAY,KAAK,QAAQ;CAExC,QAAQ,SAAS,QAAQ,KAAK,YAAY;AAC5C"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/shared/rsc-error-envelope.ts
|
|
2
|
+
/**
|
|
3
|
+
* The hard-navigate error envelope shared by the server and the client router.
|
|
4
|
+
*
|
|
5
|
+
* A client-navigation (RSC) request cannot carry an error *page* — the response
|
|
6
|
+
* body must be Flight data or the decoder crashes. Instead the server answers
|
|
7
|
+
* with this envelope: an `X-Timber-Error: 1` header and a minimal JSON body.
|
|
8
|
+
* The client treats it as "give up on SPA navigation and hard-navigate", and
|
|
9
|
+
* the resulting document request renders the real error page as HTML.
|
|
10
|
+
*
|
|
11
|
+
* The header name lives here so the four server producers and the single
|
|
12
|
+
* client consumer cannot drift apart. See design/10-error-handling.md
|
|
13
|
+
* §"Error Page Rendering for Client Navigation".
|
|
14
|
+
*/
|
|
15
|
+
/** Header the client checks to decide whether to hard-navigate. */
|
|
16
|
+
var TIMBER_ERROR_HEADER = "X-Timber-Error";
|
|
17
|
+
/**
|
|
18
|
+
* Build the hard-navigate envelope for a status code.
|
|
19
|
+
*
|
|
20
|
+
* `responseHeaders` carries anything middleware already set (cookies, cache
|
|
21
|
+
* directives); the content type and error header are overwritten because the
|
|
22
|
+
* body is always this envelope. The body deliberately carries no message —
|
|
23
|
+
* only the status already on the wire (design/13-security.md principle 4).
|
|
24
|
+
*/
|
|
25
|
+
function rscErrorEnvelope(status, responseHeaders) {
|
|
26
|
+
const headers = responseHeaders ?? new Headers();
|
|
27
|
+
headers.set(TIMBER_ERROR_HEADER, "1");
|
|
28
|
+
headers.set("content-type", "application/json; charset=utf-8");
|
|
29
|
+
return new Response(JSON.stringify({
|
|
30
|
+
error: true,
|
|
31
|
+
status
|
|
32
|
+
}), {
|
|
33
|
+
status,
|
|
34
|
+
headers
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { rscErrorEnvelope as n, TIMBER_ERROR_HEADER as t };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=rsc-error-envelope-tT5PJs4q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rsc-error-envelope-tT5PJs4q.js","names":[],"sources":["../../src/shared/rsc-error-envelope.ts"],"sourcesContent":["/**\n * The hard-navigate error envelope shared by the server and the client router.\n *\n * A client-navigation (RSC) request cannot carry an error *page* — the response\n * body must be Flight data or the decoder crashes. Instead the server answers\n * with this envelope: an `X-Timber-Error: 1` header and a minimal JSON body.\n * The client treats it as \"give up on SPA navigation and hard-navigate\", and\n * the resulting document request renders the real error page as HTML.\n *\n * The header name lives here so the four server producers and the single\n * client consumer cannot drift apart. See design/10-error-handling.md\n * §\"Error Page Rendering for Client Navigation\".\n */\n\n/** Header the client checks to decide whether to hard-navigate. */\nexport const TIMBER_ERROR_HEADER = 'X-Timber-Error';\n\n/**\n * Build the hard-navigate envelope for a status code.\n *\n * `responseHeaders` carries anything middleware already set (cookies, cache\n * directives); the content type and error header are overwritten because the\n * body is always this envelope. The body deliberately carries no message —\n * only the status already on the wire (design/13-security.md principle 4).\n */\nexport function rscErrorEnvelope(status: number, responseHeaders?: Headers): Response {\n const headers = responseHeaders ?? new Headers();\n headers.set(TIMBER_ERROR_HEADER, '1');\n headers.set('content-type', 'application/json; charset=utf-8');\n return new Response(JSON.stringify({ error: true, status }), { status, headers });\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,IAAa,sBAAsB;;;;;;;;;AAUnC,SAAgB,iBAAiB,QAAgB,iBAAqC;CACpF,MAAM,UAAU,mBAAmB,IAAI,QAAQ;CAC/C,QAAQ,IAAI,qBAAqB,GAAG;CACpC,QAAQ,IAAI,gBAAgB,iCAAiC;CAC7D,OAAO,IAAI,SAAS,KAAK,UAAU;EAAE,OAAO;EAAM;CAAO,CAAC,GAAG;EAAE;EAAQ;CAAQ,CAAC;AAClF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/shared/rsc-payload-path.ts
|
|
2
|
+
/**
|
|
3
|
+
* Where a route's Flight payload lives in `output: 'static'`.
|
|
4
|
+
*
|
|
5
|
+
* The static build writes these files and the client fetches them, so the two
|
|
6
|
+
* halves must agree byte for byte. They used to state the naming rule
|
|
7
|
+
* separately — `plugins/static-build.ts` for the write and
|
|
8
|
+
* `client/rsc-fetch.ts` for the read — which is a duplicate representation with
|
|
9
|
+
* nothing checking it. One function now, called by both.
|
|
10
|
+
*
|
|
11
|
+
* ## The shape
|
|
12
|
+
*
|
|
13
|
+
* The payload path mirrors the HTML path: every route gets a directory and the
|
|
14
|
+
* artifact is the `index` inside it.
|
|
15
|
+
*
|
|
16
|
+
* / → /_rsc/index.rsc (cf. index.html)
|
|
17
|
+
* /about → /_rsc/about/index.rsc (cf. about/index.html)
|
|
18
|
+
* /blog/hello → /_rsc/blog/hello/index.rsc
|
|
19
|
+
*
|
|
20
|
+
* That shape is **injective**, which the previous one (`/about` →
|
|
21
|
+
* `/_rsc/about.rsc`, root → `/_rsc/index.rsc`) was not: a site with both `/`
|
|
22
|
+
* and `/index` produced two payloads at `_rsc/index.rsc`, and the second write
|
|
23
|
+
* won silently — a client navigation to one route was served the other route's
|
|
24
|
+
* payload, same content type and same shape, undetectable downstream
|
|
25
|
+
* (TIM-1266).
|
|
26
|
+
*
|
|
27
|
+
* Injectivity holds because two distinct non-root routes give distinct
|
|
28
|
+
* directories, and the root's `/_rsc/index.rsc` has no directory component for
|
|
29
|
+
* a non-root route to occupy — reaching it would need an empty route path,
|
|
30
|
+
* which is not a route.
|
|
31
|
+
*
|
|
32
|
+
* Isomorphic: no server, client, or build-time imports.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* The route-space path of a route's static Flight payload.
|
|
36
|
+
*
|
|
37
|
+
* @param routePath A route-space pathname: leading slash, no deployment base,
|
|
38
|
+
* no query or fragment, no trailing slash (except the root `/`). Callers
|
|
39
|
+
* normalize; this function does not, because the two callers normalize from
|
|
40
|
+
* different sources (a browser URL, a build-time route entry).
|
|
41
|
+
*/
|
|
42
|
+
function rscPayloadPath(routePath) {
|
|
43
|
+
return `/_rsc${routePath === "/" ? "" : routePath}/index.rsc`;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { rscPayloadPath as t };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=rsc-payload-path-B_LBodc2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rsc-payload-path-B_LBodc2.js","names":[],"sources":["../../src/shared/rsc-payload-path.ts"],"sourcesContent":["/**\n * Where a route's Flight payload lives in `output: 'static'`.\n *\n * The static build writes these files and the client fetches them, so the two\n * halves must agree byte for byte. They used to state the naming rule\n * separately — `plugins/static-build.ts` for the write and\n * `client/rsc-fetch.ts` for the read — which is a duplicate representation with\n * nothing checking it. One function now, called by both.\n *\n * ## The shape\n *\n * The payload path mirrors the HTML path: every route gets a directory and the\n * artifact is the `index` inside it.\n *\n * / → /_rsc/index.rsc (cf. index.html)\n * /about → /_rsc/about/index.rsc (cf. about/index.html)\n * /blog/hello → /_rsc/blog/hello/index.rsc\n *\n * That shape is **injective**, which the previous one (`/about` →\n * `/_rsc/about.rsc`, root → `/_rsc/index.rsc`) was not: a site with both `/`\n * and `/index` produced two payloads at `_rsc/index.rsc`, and the second write\n * won silently — a client navigation to one route was served the other route's\n * payload, same content type and same shape, undetectable downstream\n * (TIM-1266).\n *\n * Injectivity holds because two distinct non-root routes give distinct\n * directories, and the root's `/_rsc/index.rsc` has no directory component for\n * a non-root route to occupy — reaching it would need an empty route path,\n * which is not a route.\n *\n * Isomorphic: no server, client, or build-time imports.\n */\n\n/**\n * The route-space path of a route's static Flight payload.\n *\n * @param routePath A route-space pathname: leading slash, no deployment base,\n * no query or fragment, no trailing slash (except the root `/`). Callers\n * normalize; this function does not, because the two callers normalize from\n * different sources (a browser URL, a build-time route entry).\n */\nexport function rscPayloadPath(routePath: string): string {\n const prefix = routePath === '/' ? '' : routePath;\n return `/_rsc${prefix}/index.rsc`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,eAAe,WAA2B;CAExD,OAAO,QADQ,cAAc,MAAM,KAAK,UAClB;AACxB"}
|
|
@@ -146,6 +146,27 @@ function classifySegment(dirName) {
|
|
|
146
146
|
}
|
|
147
147
|
return { type: "static" };
|
|
148
148
|
}
|
|
149
|
+
function effectiveUrlSegment(node) {
|
|
150
|
+
if (node.segmentType !== "intercepting" || !node.interceptedSegmentName) return {
|
|
151
|
+
segmentName: node.segmentName,
|
|
152
|
+
segmentType: node.segmentType,
|
|
153
|
+
paramName: node.paramName,
|
|
154
|
+
paramPrefix: node.paramPrefix,
|
|
155
|
+
paramSuffix: node.paramSuffix
|
|
156
|
+
};
|
|
157
|
+
const seg = classifyUrlSegment(node.interceptedSegmentName);
|
|
158
|
+
if (seg.kind === "static") return {
|
|
159
|
+
segmentName: seg.value,
|
|
160
|
+
segmentType: "static"
|
|
161
|
+
};
|
|
162
|
+
return {
|
|
163
|
+
segmentName: node.interceptedSegmentName,
|
|
164
|
+
segmentType: seg.kind,
|
|
165
|
+
paramName: seg.name,
|
|
166
|
+
paramPrefix: seg.kind === "dynamic" ? seg.prefix : void 0,
|
|
167
|
+
paramSuffix: seg.kind === "dynamic" ? seg.suffix : void 0
|
|
168
|
+
};
|
|
169
|
+
}
|
|
149
170
|
/**
|
|
150
171
|
* Parse an interception marker from a directory name.
|
|
151
172
|
*
|
|
@@ -171,6 +192,6 @@ function parseInterceptionMarker(dirName) {
|
|
|
171
192
|
return null;
|
|
172
193
|
}
|
|
173
194
|
//#endregion
|
|
174
|
-
export { INTERCEPTION_MARKERS as i, classifyUrlSegment as n,
|
|
195
|
+
export { INTERCEPTION_MARKERS as a, DEFAULT_PAGE_EXTENSIONS as i, classifyUrlSegment as n, effectiveUrlSegment as r, classifySegment as t };
|
|
175
196
|
|
|
176
|
-
//# sourceMappingURL=segment-classify-
|
|
197
|
+
//# sourceMappingURL=segment-classify-C539Pa2O.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segment-classify-C539Pa2O.js","names":[],"sources":["../../src/routing/types.ts","../../src/routing/segment-classify.ts"],"sourcesContent":["/**\n * Route tree types for timber.js file-system routing.\n *\n * The route tree is built by scanning the app/ directory and recognizing\n * file conventions (page.*, layout.*, middleware.ts, access.ts, route.ts, etc.).\n *\n * **Single shape, two specializations** (TIM-848):\n *\n * `SegmentNode<TFile>` is the one canonical in-memory shape for the\n * timber route tree. The same interface is used at build time (with\n * `TFile = RouteFile`) and at request time (with `TFile = ManifestFile`,\n * see `server/route-matcher.ts`). Walkers parameterized over `TFile`\n * work on either, eliminating the previous duplication between\n * `SegmentNode` (Map-based) and `ManifestSegmentNode` (object-based).\n *\n * Keyed groups (`slots`, `statusFiles`, `jsonStatusFiles`,\n * `metadataRoutes`) are plain `Record<string, …>`\n * objects rather than `Map`s so that the build-time tree can be\n * serialized into the virtual route manifest with no shape transform.\n *\n * See design/07-routing.md §\"Route Tree Shape\" and design/18-build-system.md\n * §\"Route Manifest Shape\".\n */\n\n/** Segment type classification */\nexport type SegmentType =\n | 'static' // e.g. \"dashboard\"\n | 'dynamic' // e.g. \"[id]\"\n | 'catch-all' // e.g. \"[...slug]\"\n | 'optional-catch-all' // e.g. \"[[...slug]]\"\n | 'group' // e.g. \"(marketing)\"\n | 'slot' // e.g. \"@sidebar\"\n | 'intercepting' // e.g. \"(.)photo\", \"(..)photo\", \"(...)photo\"\n | 'private'; // e.g. \"_components\", \"_lib\" — excluded from routing\n\n/**\n * Intercepting route marker — indicates how many levels up to resolve the\n * intercepted route from the intercepting route's location.\n *\n * See design/07-routing.md §\"Intercepting Routes\"\n */\nexport type InterceptionMarker = '(.)' | '(..)' | '(...)' | '(..)(..)';\n\n/** All recognized interception markers, ordered longest-first for parsing. */\nexport const INTERCEPTION_MARKERS: InterceptionMarker[] = ['(..)(..)', '(.)', '(..)', '(...)'];\n\n/**\n * A single file discovered in a route segment at build time.\n *\n * The runtime equivalent (`ManifestFile`, defined in\n * `server/route-matcher.ts`) replaces `extension` with a lazy `load`\n * function. Walkers that only need `filePath` are parameterized over\n * `TFile` and accept either.\n */\nexport interface RouteFile {\n /** Absolute path to the file */\n filePath: string;\n /** File extension without leading dot (e.g. \"tsx\", \"ts\", \"mdx\") */\n extension: string;\n}\n\n/**\n * A node in the segment tree.\n *\n * Generic over `TFile` so the same interface describes both the\n * build-time tree (`SegmentNode<RouteFile>`, the default) and the\n * runtime manifest tree (`SegmentNode<ManifestFile>`, aliased as\n * `ManifestSegmentNode`). All keyed groups use `Record` (not `Map`)\n * so the build-time tree serializes to the virtual route manifest\n * with no shape transform.\n */\nexport interface SegmentNode<TFile = RouteFile> {\n /** The raw directory name (e.g. \"dashboard\", \"[id]\", \"(auth)\", \"@sidebar\") */\n segmentName: string;\n /** Classified segment type */\n segmentType: SegmentType;\n /** The dynamic param name, if dynamic (e.g. \"id\" for \"[id]\", \"slug\" for \"[...slug]\") */\n paramName?: string;\n /** Literal prefix before the dynamic bracket (e.g. \"img-\" for \"img-[id].png\") */\n paramPrefix?: string;\n /** Literal suffix after the dynamic bracket (e.g. \".png\" for \"img-[id].png\") */\n paramSuffix?: string;\n /** The URL path prefix at this segment level (e.g. \"/dashboard\") */\n urlPath: string;\n /** For intercepting segments: the marker used, e.g. \"(.)\". */\n interceptionMarker?: InterceptionMarker;\n /**\n * For intercepting segments: the segment name after stripping the marker.\n * E.g., for \"(.)photo\" this is \"photo\".\n */\n interceptedSegmentName?: string;\n\n // --- File conventions ---\n page?: TFile;\n layout?: TFile;\n middleware?: TFile;\n access?: TFile;\n route?: TFile;\n /**\n * params.ts — isomorphic convention file exporting segmentParams and/or searchParams.\n * Discovered by the scanner like middleware.ts and access.ts.\n * See design/07-routing.md §\"params.ts Convention File\"\n */\n params?: TFile;\n error?: TFile;\n default?: TFile;\n /** Status-code files: 4xx.tsx, 5xx.tsx, {status}.tsx (component format) */\n statusFiles?: Record<string, TFile>;\n /** JSON status-code files: 4xx.json, 5xx.json, {status}.json */\n jsonStatusFiles?: Record<string, TFile>;\n /** denied.tsx — slot-only denial rendering */\n denied?: TFile;\n\n /** Metadata route files (sitemap.ts, robots.ts, icon.tsx, etc.) keyed by base name */\n metadataRoutes?: Record<string, TFile>;\n\n // --- Children ---\n children: SegmentNode<TFile>[];\n /** Parallel route slots (keyed by slot name without @) */\n slots: Record<string, SegmentNode<TFile>>;\n}\n\n/**\n * The full route tree output from the scanner (or the root of the\n * runtime route manifest, when `TFile = ManifestFile`).\n *\n * Generic so the same wrapper carries app-root metadata for both\n * shapes. The runtime manifest extends this with `viteRoot` (see\n * `ManifestRoot` in `server/route-matcher.ts`).\n */\nexport interface RouteTree<TFile = RouteFile> {\n /** The root segment node (representing app/) */\n root: SegmentNode<TFile>;\n /** All discovered proxy.ts files (should be at most one, in app/) */\n proxy?: TFile;\n /**\n * Global error page: app/global-error.{tsx,ts,jsx,js}\n *\n * Rendered as a standalone full-page replacement (no layout wrapping)\n * when no segment-level error file is found. SSR-only render path.\n * Must provide its own <html> and <body>.\n *\n * See design/10-error-handling.md §\"Tier 2 — Global Error Page\"\n */\n globalError?: TFile;\n}\n\n/** Configuration passed to the scanner */\nexport interface ScannerConfig {\n /** Recognized page/layout extensions (without dots). Default: ['tsx', 'ts', 'jsx', 'js'] */\n pageExtensions?: string[];\n}\n\n/** Default page extensions */\nexport const DEFAULT_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js'];\n","/**\n * Shared segment classifier — both URL tokens and filesystem directory names.\n *\n * `classifyUrlSegment(token)` is a pure single-pass character parser that\n * classifies a route segment token (e.g. \"dashboard\", \"[id]\", \"[...slug]\",\n * \"[[...path]]\") into a typed discriminated union. NO regex, NO Node.js-only\n * APIs — safe to import from browser code (used by `Link` interpolation).\n *\n * `classifySegment(dirName)` is the build-time directory-name classifier\n * used by the scanner. It recognizes timber-only conventions (private\n * `_*`, parallel `@*`, route groups `(name)`, intercepting routes\n * `(.)`/`(..)`/`(...)`/`(..)(..)`) and delegates bracket syntax to\n * `classifyUrlSegment`. It is the **single source of truth** for what\n * counts as a routing segment — there is no separate copy in the\n * scanner. (TIM-848.)\n *\n * Malformed input falls through to `{ kind: 'static' }` — the safe default.\n *\n * If you change the bracket syntax, update ONLY this file. Every\n * consumer imports from here.\n *\n * See design/07-routing.md §\"Route Segments\"\n */\n\nimport type { InterceptionMarker, SegmentType } from './types.js';\nimport { INTERCEPTION_MARKERS } from './types.js';\n\nexport type UrlSegment =\n | { kind: 'static'; value: string }\n | { kind: 'dynamic'; name: string; prefix?: string; suffix?: string }\n | { kind: 'catch-all'; name: string }\n | { kind: 'optional-catch-all'; name: string };\n\n/**\n * Classify a URL path segment token.\n *\n * Walks the string left-to-right in one pass:\n * 1. Find the first '[' — characters before it are the prefix.\n * 2. Count opening brackets (1 or 2) to detect optional.\n * 3. Check for '...' to detect catch-all.\n * 4. Read the param name up to the closing bracket.\n * 5. Validate the expected closing sequence (']' or ']]').\n * 6. Characters after the close are the suffix.\n * 7. Reject affixes on catch-all/optional-catch-all.\n * 8. Reject affixes that contain '[' or ']' (no multi-param segments).\n *\n * Any structural violation → static (safe default).\n */\nexport function classifyUrlSegment(token: string): UrlSegment {\n const len = token.length;\n if (len === 0) return { kind: 'static', value: token };\n\n // Find first '[' — characters before it are the prefix\n const bracketStart = token.indexOf('[');\n if (bracketStart === -1) return { kind: 'static', value: token };\n\n const prefix = token.slice(0, bracketStart);\n\n let i = bracketStart + 1;\n\n // Check for optional: '[[...'\n const optional = i < len && token[i] === '[';\n if (optional) i++;\n\n // Check for catch-all: '...'\n const catchAll = i + 2 < len && token[i] === '.' && token[i + 1] === '.' && token[i + 2] === '.';\n if (catchAll) i += 3;\n\n // Read param name — everything up to ']'\n const nameStart = i;\n while (i < len && token[i] !== ']') i++;\n\n // Must have found a ']' and name must be non-empty\n if (i >= len || i === nameStart) {\n return { kind: 'static', value: token };\n }\n\n const name = token.slice(nameStart, i);\n i++; // skip first ']'\n\n // Optional requires a second ']'\n if (optional) {\n if (i >= len || token[i] !== ']') {\n return { kind: 'static', value: token };\n }\n i++;\n }\n\n const suffix = token.slice(i);\n\n // Reject affixes containing brackets (no multi-param segments like [foo]-[bar])\n if (suffix.includes('[') || suffix.includes(']')) {\n return { kind: 'static', value: token };\n }\n if (prefix.includes(']')) {\n return { kind: 'static', value: token };\n }\n\n const hasAffixes = prefix.length > 0 || suffix.length > 0;\n\n if (optional && catchAll) {\n // No affixes on optional catch-all\n if (hasAffixes) return { kind: 'static', value: token };\n return { kind: 'optional-catch-all', name };\n }\n if (catchAll) {\n // No affixes on catch-all\n if (hasAffixes) return { kind: 'static', value: token };\n return { kind: 'catch-all', name };\n }\n if (optional) {\n // '[[name]]' without '...' is malformed — not a valid segment syntax\n return { kind: 'static', value: token };\n }\n\n if (hasAffixes) {\n return {\n kind: 'dynamic',\n name,\n prefix: prefix || undefined,\n suffix: suffix || undefined,\n };\n }\n return { kind: 'dynamic', name };\n}\n\n// ─── Directory-name classifier (build-time scanner) ─────────────────────────\n\n/** Result of classifying a filesystem directory name. */\nexport interface SegmentClassification {\n type: SegmentType;\n paramName?: string;\n paramPrefix?: string;\n paramSuffix?: string;\n interceptionMarker?: InterceptionMarker;\n interceptedSegmentName?: string;\n}\n\n/**\n * Classify a directory name into its segment type.\n *\n * Recognizes all timber file-system conventions in priority order:\n * 1. Private folders: `_name` (excluded from routing)\n * 2. Parallel route slots: `@name`\n * 3. Intercepting routes: `(.)name`, `(..)name`, `(...)name`, `(..)(..)name`\n * 4. Route groups: `(name)`\n * 5. Bracket syntax: `[id]`, `[...slug]`, `[[...path]]` (delegated to\n * `classifyUrlSegment`)\n * 6. Static: anything else\n *\n * If you change the bracket syntax, update only `classifyUrlSegment`.\n * If you change the directory-prefix conventions, update this function.\n */\nexport function classifySegment(dirName: string): SegmentClassification {\n // Private folder: _name (excluded from routing)\n if (dirName.startsWith('_')) {\n return { type: 'private' };\n }\n\n // Parallel route slot: @name\n if (dirName.startsWith('@')) {\n return { type: 'slot' };\n }\n\n // Intercepting routes: (.)name, (..)name, (...)name, (..)(..)name\n // Check before route groups since intercepting markers also start with (\n const interception = parseInterceptionMarker(dirName);\n if (interception) {\n return {\n type: 'intercepting',\n interceptionMarker: interception.marker,\n interceptedSegmentName: interception.segmentName,\n };\n }\n\n // Route group: (name)\n if (dirName.startsWith('(') && dirName.endsWith(')')) {\n return { type: 'group' };\n }\n\n // Bracket-syntax segments: [param], [...param], [[...param]]\n const urlSeg = classifyUrlSegment(dirName);\n if (urlSeg.kind !== 'static') {\n const result: SegmentClassification = { type: urlSeg.kind, paramName: urlSeg.name };\n if (urlSeg.kind === 'dynamic') {\n if (urlSeg.prefix) result.paramPrefix = urlSeg.prefix;\n if (urlSeg.suffix) result.paramSuffix = urlSeg.suffix;\n }\n return result;\n }\n\n return { type: 'static' };\n}\n\n/**\n * The URL-matching identity of a segment node.\n *\n * For every node except an intercepting one this is the node itself. An\n * intercepting node is different: its directory name carries a marker\n * (`(.)photo`, `(.)[id]`), so the node's own `segmentType` is\n * `'intercepting'` and the bracket syntax of the segment it intercepts was\n * never classified. Everything that has to reason about *the URL part this\n * node stands for* — matching it, and keying its param for codec coercion —\n * needs that classification.\n *\n * Deriving it here, from `interceptedSegmentName`, keeps interception out\n * of `classifySegment`'s output and out of the serialized manifest: there\n * is no second copy of the classification to drift from this one. The\n * returned `segmentName` is the intercepted name (`photo`, `[id]`), NOT the\n * node's directory name — callers that need the directory name for tree\n * paths must keep reading the node. See TIM-1281.\n */\nexport interface UrlSegmentIdentity {\n segmentName: string;\n segmentType: SegmentType;\n paramName?: string;\n paramPrefix?: string;\n paramSuffix?: string;\n}\n\nexport function effectiveUrlSegment(node: {\n segmentName: string;\n segmentType: SegmentType;\n paramName?: string;\n paramPrefix?: string;\n paramSuffix?: string;\n interceptedSegmentName?: string;\n}): UrlSegmentIdentity {\n if (node.segmentType !== 'intercepting' || !node.interceptedSegmentName) {\n return {\n segmentName: node.segmentName,\n segmentType: node.segmentType,\n paramName: node.paramName,\n paramPrefix: node.paramPrefix,\n paramSuffix: node.paramSuffix,\n };\n }\n\n const seg = classifyUrlSegment(node.interceptedSegmentName);\n if (seg.kind === 'static') {\n return { segmentName: seg.value, segmentType: 'static' };\n }\n return {\n segmentName: node.interceptedSegmentName,\n segmentType: seg.kind,\n paramName: seg.name,\n paramPrefix: seg.kind === 'dynamic' ? seg.prefix : undefined,\n paramSuffix: seg.kind === 'dynamic' ? seg.suffix : undefined,\n };\n}\n\n/**\n * Parse an interception marker from a directory name.\n *\n * Returns the marker and the remaining segment name, or null if not an\n * intercepting route. Markers are checked longest-first to avoid `(..)`\n * matching before `(..)(..)`.\n *\n * Examples:\n * \"(.)photo\" → { marker: \"(.)\", segmentName: \"photo\" }\n * \"(..)feed\" → { marker: \"(..)\", segmentName: \"feed\" }\n * \"(...)photos\" → { marker: \"(...)\", segmentName: \"photos\" }\n * \"(..)(..)admin\" → { marker: \"(..)(..)\", segmentName: \"admin\" }\n * \"(marketing)\" → null (route group, not interception)\n */\nfunction parseInterceptionMarker(\n dirName: string\n): { marker: InterceptionMarker; segmentName: string } | null {\n for (const marker of INTERCEPTION_MARKERS) {\n if (dirName.startsWith(marker)) {\n const rest = dirName.slice(marker.length);\n // Must have a segment name after the marker, and the rest must not\n // be empty or end with ) (which would be a route group like \"(auth)\")\n if (rest.length > 0 && !rest.endsWith(')')) {\n return { marker, segmentName: rest };\n }\n }\n }\n return null;\n}\n"],"mappings":"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"}
|
|
@@ -243,6 +243,65 @@ function canonicalize(rawPathname, stripTrailingSlash = true) {
|
|
|
243
243
|
};
|
|
244
244
|
}
|
|
245
245
|
//#endregion
|
|
246
|
-
|
|
246
|
+
//#region src/routing/segment-keys.ts
|
|
247
|
+
/**
|
|
248
|
+
* Compute tree paths for a segment chain — the directory path of each segment
|
|
249
|
+
* with slots elided, e.g. `app/(browse)/feed` → `/(browse)/feed`.
|
|
250
|
+
*
|
|
251
|
+
* This is `computeSegmentKeys` with the group branch applied to *every*
|
|
252
|
+
* segment rather than only to groups. That one difference is the whole point:
|
|
253
|
+
* `computeSegmentKeys` resets to `urlPath` at each URL-visible segment, so
|
|
254
|
+
* `(browse)/feed` and `(landing)/feed` both key as `/feed`. Two directories
|
|
255
|
+
* that render different layouts must not share an identity.
|
|
256
|
+
*
|
|
257
|
+
* Used for interception scoping (`routing/interception.ts`), where the
|
|
258
|
+
* question is "does this route pass through *that* directory?" — not "does it
|
|
259
|
+
* render this URL?". Not interchangeable with `computeSegmentKeys`: the state
|
|
260
|
+
* tree is keyed by URL on purpose, because the client caches by URL.
|
|
261
|
+
*/
|
|
262
|
+
function computeSegmentTreePaths(segments) {
|
|
263
|
+
const paths = [];
|
|
264
|
+
let prev = "";
|
|
265
|
+
for (const segment of segments) {
|
|
266
|
+
const name = segment.segmentName ?? "";
|
|
267
|
+
if (!name) {
|
|
268
|
+
paths.push("/");
|
|
269
|
+
prev = "/";
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const path = `${prev === "/" ? "" : prev}/${name}`;
|
|
273
|
+
paths.push(path);
|
|
274
|
+
prev = path;
|
|
275
|
+
}
|
|
276
|
+
return paths;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* The directory names a tree path is built from — `/` is none, `/feed/(a)` is
|
|
280
|
+
* `['feed', '(a)']`.
|
|
281
|
+
*
|
|
282
|
+
* The inverse of the walk above, and it lives beside it so the format is
|
|
283
|
+
* stated once: `findChainByTreePath` (`server/children-interception.ts`) walks
|
|
284
|
+
* these names back down the tree to the directory the path addresses, and
|
|
285
|
+
* `treePathDepth` counts them. Both are readings of the same string, and a
|
|
286
|
+
* reader that disagreed with the producer about what separates two names
|
|
287
|
+
* resolves an interception to the wrong node or to none.
|
|
288
|
+
*/
|
|
289
|
+
function treePathNames(treePath) {
|
|
290
|
+
return treePath === "/" ? [] : treePath.slice(1).split("/");
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* How many directories a tree path names — `/` is 0, `/feed` is 1,
|
|
294
|
+
* `/feed/(a)` is 2.
|
|
295
|
+
*
|
|
296
|
+
* Two tree paths on a single rendered chain are strictly nested, so on that
|
|
297
|
+
* chain depth orders them totally — which is what makes it usable as "the
|
|
298
|
+
* deeper of these two directories" wherever a route passes through both
|
|
299
|
+
* (`routing/interception.ts`).
|
|
300
|
+
*/
|
|
301
|
+
function treePathDepth(treePath) {
|
|
302
|
+
return treePathNames(treePath).length;
|
|
303
|
+
}
|
|
304
|
+
//#endregion
|
|
305
|
+
export { canonicalize as a, getMetadataRouteAutoLink as c, isMetadataRouteServePath as d, NULL_BYTE_RE as i, getMetadataRouteServePath as l, treePathDepth as n, METADATA_ROUTE_CONVENTIONS as o, ENCODED_SEPARATOR_RE as r, classifyMetadataRoute as s, computeSegmentTreePaths as t, isDynamicMetadataExtension as u };
|
|
247
306
|
|
|
248
|
-
//# sourceMappingURL=
|
|
307
|
+
//# sourceMappingURL=segment-keys-BawYuNFO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segment-keys-BawYuNFO.js","names":[],"sources":["../../src/server/metadata-routes.ts","../../src/server/canonicalize.ts","../../src/routing/segment-keys.ts"],"sourcesContent":["/**\n * Metadata route classification for timber.js.\n *\n * Metadata routes are file-based endpoints that generate well-known URLs for\n * crawlers and browsers (sitemap.xml, robots.txt, OG images, etc.).\n *\n * These routes run through proxy.ts but NOT through middleware.ts or access.ts —\n * they are public endpoints by nature.\n *\n * See design/16-metadata.md §\"Metadata Routes\"\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { HeadElement } from './metadata.js';\nimport type { Metadata } from './types.js';\nimport type { ManifestSegmentNode } from './route-matcher.js';\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Classification of a metadata route file. */\nexport interface MetadataRouteInfo {\n /** The metadata route type. */\n type: MetadataRouteType;\n /** The content type to serve this route with. */\n contentType: string;\n /** Whether this route can appear in nested segments (not just app root). */\n nestable: boolean;\n}\n\nexport type MetadataRouteType =\n | 'sitemap'\n | 'robots'\n | 'manifest'\n | 'favicon'\n | 'icon'\n | 'opengraph-image'\n | 'apple-icon';\n\n// ─── Convention Table ────────────────────────────────────────────────────────\n\n/**\n * All recognized metadata route file conventions.\n *\n * Each entry maps a base file name (without extension) to its route info.\n * The extensions determine whether the file is static or dynamic.\n *\n * Static extensions: .xml, .txt, .json, .png, .jpg, .ico, .svg\n * Dynamic extensions: .ts, .tsx\n */\nexport const METADATA_ROUTE_CONVENTIONS: Record<\n string,\n {\n type: MetadataRouteType;\n contentType: string;\n nestable: boolean;\n staticExtensions: string[];\n dynamicExtensions: string[];\n /**\n * The URL path basename this file serves at (relative to segment).\n * For image routes, the full serve path includes an extension via\n * `resolveServePathForFile()`.\n */\n servePath: string;\n /**\n * When set, image routes append `.{serveExtension}` to the serve path.\n * Dynamic handlers (`.ts`/`.tsx`) use this as the default. Static files\n * use their own extension instead. Non-image routes leave this undefined.\n */\n serveExtension?: string;\n }\n> = {\n 'sitemap': {\n type: 'sitemap',\n contentType: 'application/xml',\n nestable: true,\n staticExtensions: ['xml'],\n dynamicExtensions: ['ts'],\n servePath: 'sitemap.xml',\n },\n 'robots': {\n type: 'robots',\n contentType: 'text/plain',\n nestable: false,\n staticExtensions: ['txt'],\n dynamicExtensions: ['ts'],\n servePath: 'robots.txt',\n },\n 'manifest': {\n type: 'manifest',\n contentType: 'application/manifest+json',\n nestable: false,\n staticExtensions: ['json'],\n dynamicExtensions: ['ts'],\n servePath: 'manifest.webmanifest',\n },\n 'favicon': {\n type: 'favicon',\n contentType: 'image/x-icon',\n nestable: false,\n staticExtensions: ['ico'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'favicon.ico',\n },\n 'icon': {\n type: 'icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg', 'svg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'icon',\n serveExtension: 'png',\n },\n 'opengraph-image': {\n type: 'opengraph-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'opengraph-image',\n serveExtension: 'png',\n },\n\n 'apple-icon': {\n type: 'apple-icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'apple-icon',\n serveExtension: 'png',\n },\n};\n\n// ─── MIME Type Resolution ─────────────────────────────────────────────────────\n\n/**\n * Map of file extensions to MIME types for static metadata route files.\n * Used to resolve the generic `image/*` content type for static image files.\n */\nconst EXTENSION_MIME_TYPES: Record<string, string> = {\n xml: 'application/xml',\n txt: 'text/plain',\n json: 'application/json',\n ico: 'image/x-icon',\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n svg: 'image/svg+xml',\n webp: 'image/webp',\n};\n\n/**\n * Resolve the concrete MIME type for a static metadata route file.\n *\n * For generic content types like `image/*`, this resolves to the actual\n * MIME type based on the file extension (e.g. `image/png` for `.png`).\n *\n * @param conventionContentType - The content type from the convention table (may be generic like `image/*`)\n * @param extension - The file extension without leading dot (e.g. \"png\", \"xml\")\n * @returns The resolved MIME type\n */\nexport function resolveStaticContentType(conventionContentType: string, extension: string): string {\n if (conventionContentType.includes('*')) {\n return EXTENSION_MIME_TYPES[extension] ?? 'application/octet-stream';\n }\n return conventionContentType;\n}\n\n/**\n * Check if a file extension represents a static (non-code) metadata route file.\n *\n * @param baseName - The base file name without extension (e.g. \"sitemap\", \"icon\")\n * @param extension - The file extension without leading dot (e.g. \"xml\", \"png\", \"ts\")\n * @returns true if this is a static file, false if dynamic or unrecognized\n */\nexport function isStaticMetadataExtension(baseName: string, extension: string): boolean {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return false;\n return convention.staticExtensions.includes(extension);\n}\n\n/**\n * Check if a file extension represents a dynamic (code) metadata route file.\n *\n * @param baseName - The base file name without extension (e.g. \"sitemap\", \"icon\")\n * @param extension - The file extension without leading dot (e.g. \"ts\", \"tsx\")\n * @returns true if this is a dynamic file, false if static or unrecognized\n */\nexport function isDynamicMetadataExtension(baseName: string, extension: string): boolean {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return false;\n return convention.dynamicExtensions.includes(extension);\n}\n\n// ─── Classification ──────────────────────────────────────────────────────────\n\n/**\n * Classify a file name as a metadata route, or return null if it's not one.\n *\n * @param fileName - The full file name including extension (e.g. \"sitemap.xml\", \"icon.tsx\")\n * @returns Classification info, or null if not a metadata route\n */\nexport function classifyMetadataRoute(fileName: string): MetadataRouteInfo | null {\n const dotIndex = fileName.lastIndexOf('.');\n if (dotIndex === -1) return null;\n\n const baseName = fileName.slice(0, dotIndex);\n const ext = fileName.slice(dotIndex + 1);\n\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return null;\n\n const isStatic = convention.staticExtensions.includes(ext);\n const isDynamic = convention.dynamicExtensions.includes(ext);\n\n if (!isStatic && !isDynamic) return null;\n\n return {\n type: convention.type,\n contentType: convention.contentType,\n nestable: convention.nestable,\n };\n}\n\n/**\n * Resolve the serve path for a metadata route file.\n *\n * For image routes (icon, opengraph-image, apple-icon), the serve path includes\n * a file extension so CDNs cache correctly:\n * - Dynamic handlers (.ts/.tsx) use the convention's `serveExtension` (default: .png)\n * - Static files use their own extension (e.g., icon.svg → icon.svg)\n *\n * Non-image routes return the convention's `servePath` as-is (already includes\n * extension: sitemap.xml, robots.txt, etc.).\n */\nexport function resolveServePathForFile(baseName: string, filePath: string): string {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return baseName;\n\n if (!convention.serveExtension) return convention.servePath;\n\n const ext = filePath.slice(filePath.lastIndexOf('.') + 1);\n if (convention.staticExtensions.includes(ext)) {\n return `${convention.servePath}.${ext}`;\n }\n return `${convention.servePath}.${convention.serveExtension}`;\n}\n\n/**\n * Get the default serve path for a metadata route type (using default extension\n * for image routes). Used for auto-link generation when only the type is known.\n */\nexport function getMetadataRouteServePath(type: MetadataRouteType): string {\n for (const convention of Object.values(METADATA_ROUTE_CONVENTIONS)) {\n if (convention.type === type) {\n if (convention.serveExtension) {\n return `${convention.servePath}.${convention.serveExtension}`;\n }\n return convention.servePath;\n }\n }\n throw new Error(`[timber] Unknown metadata route type: ${type}`);\n}\n\n/**\n * All possible serve path segments for metadata routes (includes extension\n * variants for image routes).\n */\nconst METADATA_SERVE_PATHS = new Set<string>();\nfor (const convention of Object.values(METADATA_ROUTE_CONVENTIONS)) {\n if (convention.serveExtension) {\n for (const ext of [...convention.staticExtensions, convention.serveExtension]) {\n METADATA_SERVE_PATHS.add(`${convention.servePath}.${ext}`);\n }\n } else {\n METADATA_SERVE_PATHS.add(convention.servePath);\n }\n}\n\nexport function isMetadataRouteServePath(pathname: string): boolean {\n let lastSegment = pathname.slice(pathname.lastIndexOf('/') + 1);\n const qIdx = lastSegment.indexOf('?');\n let query = '';\n if (qIdx !== -1) {\n query = lastSegment.slice(qIdx + 1);\n lastSegment = lastSegment.slice(0, qIdx);\n }\n if (!METADATA_SERVE_PATHS.has(lastSegment)) return false;\n // Vite module requests (e.g., /src/icon.svg?import) use special query params.\n // These are source assets, not metadata routes.\n if (/(?:^|&)(?:import|url|raw|worker|inline)(?:&|$)/.test(query)) return false;\n return true;\n}\n\n/** A <link> auto-link tag. */\nexport interface AutoLinkLink {\n tag: 'link';\n rel: string;\n href: string;\n type?: string;\n}\n\n/** A <meta> auto-link tag. */\nexport interface AutoLinkMeta {\n tag: 'meta';\n property?: string;\n name?: string;\n content: string;\n}\n\nexport type AutoLinkTag = AutoLinkLink | AutoLinkMeta;\n\n/**\n * Get the auto-link tags to inject into <head> for metadata route files\n * discovered in a segment.\n *\n * Returns link tags for icon/apple-icon/manifest, and meta tags for\n * opengraph-image (emits both og:image and twitter:image). Returns null\n * for types that don't auto-link (favicon, sitemap, robots).\n *\n * @param type - The metadata route type\n * @param href - The resolved URL path to the metadata route\n * @returns Tag descriptor(s) for the <head>, or null if no auto-link\n */\nexport function getMetadataRouteAutoLink(type: MetadataRouteType, href: string): AutoLinkTag[] {\n switch (type) {\n case 'icon':\n return [{ tag: 'link', rel: 'icon', href }];\n case 'apple-icon':\n return [{ tag: 'link', rel: 'apple-touch-icon', href }];\n case 'manifest':\n return [{ tag: 'link', rel: 'manifest', href }];\n case 'opengraph-image':\n return [\n { tag: 'meta', property: 'og:image', content: href },\n { tag: 'meta', name: 'twitter:image', content: href },\n ];\n default:\n return [];\n }\n}\n\n// ─── Auto-Linking ──────────────────────────────────────────────────────────\n\n// In dev mode, use a per-startup nonce for metadata route cache busting\n// instead of per-file content hashes (avoids rehashing on every request).\nlet _devNonce: string | undefined;\nfunction getDevNonce(): string {\n _devNonce ??= randomUUID().slice(0, 8);\n return _devNonce;\n}\n\n/**\n * Collect auto-linked head elements from metadata route files in the segment chain.\n *\n * Walks each segment's metadataRoutes, resolves serve paths and URLs, and\n * emits HeadElement descriptors for <link> and <meta> tags that React Float\n * hoists into <head>.\n *\n * See design/16-metadata.md §\"Auto-Linking\"\n */\nexport function collectMetadataRouteHeadElements(\n segments: ManifestSegmentNode[],\n firstDeniedIndex: number,\n resolvedMetadata: Metadata,\n requestUrl: URL,\n metadataRouteHashes?: Record<string, string>\n): HeadElement[] {\n const elements: HeadElement[] = [];\n const hasUserOgImage = Boolean(resolvedMetadata.openGraph?.images);\n const hasUserTwitterImage = Boolean(resolvedMetadata.twitter?.images);\n const requestPathname = requestUrl.pathname;\n // In dev mode, use the request origin so OG URLs resolve to localhost.\n // In production, use metadataBase (the canonical domain).\n const ogBase =\n process.env.NODE_ENV !== 'production'\n ? new URL(requestUrl.origin)\n : resolvedMetadata.metadataBase;\n\n for (let si = 0; si < segments.length; si++) {\n const segment = segments[si];\n if (!segment.metadataRoutes) continue;\n if (si >= firstDeniedIndex) continue;\n for (const baseName of Object.keys(segment.metadataRoutes)) {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) continue;\n if (!convention.nestable && segment.urlPath !== '/') continue;\n if (convention.type === 'opengraph-image' && hasUserOgImage) continue;\n const resolvedPrefix = convention.nestable\n ? requestPathname === '/'\n ? ''\n : requestPathname\n : '';\n const metaFile = segment.metadataRoutes[baseName];\n const fileServePath = metaFile?.filePath\n ? resolveServePathForFile(baseName, metaFile.filePath)\n : convention.serveExtension\n ? `${convention.servePath}.${convention.serveExtension}`\n : convention.servePath;\n let href = `${resolvedPrefix}/${fileServePath}`;\n if (convention.type === 'opengraph-image') {\n const fileHash = metaFile?.filePath ? metadataRouteHashes?.[metaFile.filePath] : undefined;\n const cacheBust = fileHash ?? getDevNonce();\n href = `${href}?${cacheBust}`;\n }\n if (ogBase && convention.type === 'opengraph-image') {\n href = new URL(href, ogBase).toString();\n }\n for (const autoLink of getMetadataRouteAutoLink(convention.type, href)) {\n if (\n hasUserTwitterImage &&\n autoLink.tag === 'meta' &&\n 'name' in autoLink &&\n autoLink.name === 'twitter:image'\n )\n continue;\n if (autoLink.tag === 'link') {\n const attrs: Record<string, string> = { rel: autoLink.rel, href: autoLink.href };\n if (autoLink.type) attrs.type = autoLink.type;\n elements.push({ tag: 'link', attrs });\n } else {\n const attrs: Record<string, string> = { content: autoLink.content };\n if (autoLink.property) attrs.property = autoLink.property;\n if (autoLink.name) attrs.name = autoLink.name;\n elements.push({ tag: 'meta', attrs });\n }\n }\n }\n }\n return elements;\n}\n","/**\n * URL canonicalization — runs once at the request boundary.\n *\n * Every layer (proxy.ts, middleware.ts, access.ts, components) sees the same\n * canonical path. No re-decoding occurs at any later stage.\n *\n * See design/07-routing.md §\"URL Canonicalization & Security\"\n */\n\n/** Result of canonicalization — either a clean path or a rejection. */\nexport type CanonicalizeResult = { ok: true; pathname: string } | { ok: false; status: 400 };\n\n/**\n * Encoded separators that produce a 400 rejection.\n * %2f (/) and %5c (\\) cause path-confusion attacks.\n *\n * Shared between the runtime canonicalizer and the build-time route scanner\n * to ensure both enforce identical security rules. See design/13-security.md.\n */\nexport const ENCODED_SEPARATOR_RE = /%2f|%5c/i;\n\n/** Null byte — rejected. Shared with the route scanner. */\nexport const NULL_BYTE_RE = /%00/i;\n\n/**\n * Canonicalize a URL pathname.\n *\n * 1. Reject encoded separators (%2f, %5c) and null bytes (%00)\n * 2. Single percent-decode\n * 3. Collapse // → /\n * 4. Resolve .. segments (reject if escaping root)\n * 5. Strip trailing slash (except root \"/\")\n *\n * @param rawPathname - The raw pathname from the request URL (percent-encoded)\n * @param stripTrailingSlash - Whether to strip trailing slashes. Default: true.\n */\nexport function canonicalize(rawPathname: string, stripTrailingSlash = true): CanonicalizeResult {\n // Step 1: Reject dangerous encoded sequences BEFORE decoding.\n // This must happen on the raw input so %252f doesn't bypass after a single decode.\n if (ENCODED_SEPARATOR_RE.test(rawPathname)) {\n return { ok: false, status: 400 };\n }\n if (NULL_BYTE_RE.test(rawPathname)) {\n return { ok: false, status: 400 };\n }\n\n // Step 2: Single percent-decode.\n // Double-encoded input (%2561 → %61) stays as %61 — not decoded again.\n let decoded: string;\n try {\n decoded = decodeURIComponent(rawPathname);\n } catch {\n // Malformed percent-encoding → 400\n return { ok: false, status: 400 };\n }\n\n // Reject null bytes that appeared after decoding (from valid %00-like sequences\n // that weren't caught above — belt and suspenders).\n if (decoded.includes('\\0')) {\n return { ok: false, status: 400 };\n }\n\n // Backslash is NOT a path separator — keep as literal character.\n // But reject if it would create // after normalization (e.g., /\\evil.com).\n // We do NOT convert \\ to / — it stays as a literal.\n\n // Step 3: Collapse consecutive slashes.\n let pathname = decoded.replace(/\\/\\/+/g, '/');\n\n // Step 4: Resolve .. and . segments.\n const segments = pathname.split('/');\n const resolved: string[] = [];\n for (const seg of segments) {\n if (seg === '..') {\n if (resolved.length <= 1) {\n // Trying to escape root — 400\n return { ok: false, status: 400 };\n }\n resolved.pop();\n } else if (seg !== '.') {\n resolved.push(seg);\n }\n }\n\n pathname = resolved.join('/') || '/';\n\n // Step 5: Strip trailing slash (except root \"/\").\n if (stripTrailingSlash && pathname.length > 1 && pathname.endsWith('/')) {\n pathname = pathname.slice(0, -1);\n }\n\n return { ok: true, pathname };\n}\n","/**\n * Segment key computation — the stable identity of a segment in the route tree.\n *\n * A segment key is the string that names a segment across the RSC/SSR/client\n * boundary: it appears in the X-Timber-State-Tree header, the X-Timber-Segments\n * header, SegmentOutlet props, the client segment cache, and (since TIM-1279)\n * interception scoping.\n *\n * This module lives in `routing/` rather than `server/` because the keys are a\n * property of the route tree itself — both the build-time scanner tree and the\n * runtime manifest tree produce identical keys for the same directory, which is\n * what lets a build-time value (an interception scope) be compared against a\n * request-time matched chain.\n *\n * See design/19-client-navigation.md §\"X-Timber-State-Tree Header\"\n */\n\n/**\n * Segment node shape expected by computeSegmentKeys.\n *\n * Structurally satisfied by both `SegmentNode` (build time) and\n * `ManifestSegmentNode` (request time) — keys depend only on the\n * URL path and the segment classification, never on file payloads.\n */\nexport interface SegmentKeyInput {\n urlPath: string;\n segmentName?: string;\n segmentType?: string;\n}\n\n/**\n * Compute state-tree keys for a segment chain.\n *\n * Non-group segments use their urlPath as-is. Route groups accumulate\n * ancestor group names to produce globally unique keys:\n * app/(a)/(shared)/dashboard → keys: ['/', '/(a)', '/(a)/(shared)', '/dashboard']\n *\n * **An intercepting segment switches the chain to name accumulation for good.**\n * A children-path interception (TIM-1280) puts an intercepting node and its\n * descendants into the *main* rendered chain, and their `urlPath`s are not\n * URLs: interception adds no URL depth, so `app/(browse)/(...)[artistSlug]/[year]`\n * carries `/` and `/[year]`. Keyed by urlPath the intercepting node would\n * collide with its own owner, and `[year]` with any top-level `[year]` route —\n * so a client that had one mounted would reuse the wrong cached layout. The\n * switch is sticky rather than per-node because the *descendants* are where\n * the false URLs are; the intercepting node alone is not the problem.\n *\n * This is the single source of truth for segment keys — used by the\n * element builder (skip decisions, SegmentOutlet props), segment info\n * (X-Timber-Segments header), the client cache/state tree, and\n * interception scoping (`routing/interception.ts`).\n */\nexport function computeSegmentKeys(segments: SegmentKeyInput[]): string[] {\n const keys: string[] = [];\n let prevKey = '';\n let insideIntercepting = false;\n\n for (const segment of segments) {\n if (segment.segmentType === 'intercepting') insideIntercepting = true;\n if (segment.segmentType === 'group' || insideIntercepting) {\n const base = prevKey === '/' ? '' : prevKey;\n const key = `${base}/${segment.segmentName}`;\n keys.push(key);\n prevKey = key;\n } else {\n keys.push(segment.urlPath);\n prevKey = segment.urlPath;\n }\n }\n\n return keys;\n}\n\n/**\n * Compute tree paths for a segment chain — the directory path of each segment\n * with slots elided, e.g. `app/(browse)/feed` → `/(browse)/feed`.\n *\n * This is `computeSegmentKeys` with the group branch applied to *every*\n * segment rather than only to groups. That one difference is the whole point:\n * `computeSegmentKeys` resets to `urlPath` at each URL-visible segment, so\n * `(browse)/feed` and `(landing)/feed` both key as `/feed`. Two directories\n * that render different layouts must not share an identity.\n *\n * Used for interception scoping (`routing/interception.ts`), where the\n * question is \"does this route pass through *that* directory?\" — not \"does it\n * render this URL?\". Not interchangeable with `computeSegmentKeys`: the state\n * tree is keyed by URL on purpose, because the client caches by URL.\n */\nexport function computeSegmentTreePaths(segments: SegmentKeyInput[]): string[] {\n const paths: string[] = [];\n let prev = '';\n\n for (const segment of segments) {\n const name = segment.segmentName ?? '';\n if (!name) {\n // The app root — no directory name of its own.\n paths.push('/');\n prev = '/';\n continue;\n }\n const base = prev === '/' ? '' : prev;\n const path = `${base}/${name}`;\n paths.push(path);\n prev = path;\n }\n\n return paths;\n}\n\n/**\n * The directory names a tree path is built from — `/` is none, `/feed/(a)` is\n * `['feed', '(a)']`.\n *\n * The inverse of the walk above, and it lives beside it so the format is\n * stated once: `findChainByTreePath` (`server/children-interception.ts`) walks\n * these names back down the tree to the directory the path addresses, and\n * `treePathDepth` counts them. Both are readings of the same string, and a\n * reader that disagreed with the producer about what separates two names\n * resolves an interception to the wrong node or to none.\n */\nexport function treePathNames(treePath: string): string[] {\n return treePath === '/' ? [] : treePath.slice(1).split('/');\n}\n\n/**\n * How many directories a tree path names — `/` is 0, `/feed` is 1,\n * `/feed/(a)` is 2.\n *\n * Two tree paths on a single rendered chain are strictly nested, so on that\n * chain depth orders them totally — which is what makes it usable as \"the\n * deeper of these two directories\" wherever a route passes through both\n * (`routing/interception.ts`).\n */\nexport function treePathDepth(treePath: string): number {\n return treePathNames(treePath).length;\n}\n\n/**\n * Compute a unique key for a parallel route slot.\n * Format: `{parentSegmentId}/@{slotName}`, e.g. `/@sidebar` or `/dashboard/@modal`.\n */\nexport function computeSlotKey(parentSegmentId: string, slotName: string): string {\n const name = slotName.startsWith('@') ? slotName : `@${slotName}`;\n const prefix = parentSegmentId === '/' ? '' : parentSegmentId;\n return `${prefix}/${name}`;\n}\n"],"mappings":"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"}
|