@timber-js/app 0.2.0-alpha.185 → 0.2.0-alpha.186
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{actions-O_LsyCE4.js → actions-C-Rw9vPc.js} +3 -3
- package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-C-Rw9vPc.js.map} +1 -1
- package/dist/_chunks/als-slots-mFweg276.js +27 -0
- package/dist/_chunks/als-slots-mFweg276.js.map +1 -0
- package/dist/_chunks/base-path-DaQrzbez.js +80 -0
- package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
- package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
- package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
- package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-Cd0VZ_Pd.js} +3 -3
- package/dist/_chunks/{cache-api-B-lhk9p4.js.map → cache-api-Cd0VZ_Pd.js.map} +1 -1
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js +1857 -0
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +1 -0
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-Cs0uZXea.js} +89 -12
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-Cs0uZXea.js.map} +1 -1
- package/dist/_chunks/error-boundary-DpYRI_I1.js +330 -0
- package/dist/_chunks/error-boundary-DpYRI_I1.js.map +1 -0
- package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
- package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
- package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
- package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
- package/dist/_chunks/{logger-AWfuX-KJ.js → logger-N7e5auP0.js} +9 -13
- package/dist/_chunks/logger-N7e5auP0.js.map +1 -0
- package/dist/_chunks/navigation-root-B29qg0_T.js +274 -0
- package/dist/_chunks/navigation-root-B29qg0_T.js.map +1 -0
- package/dist/_chunks/param-value-C8TNYchQ.js +66 -0
- package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
- package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-DEGLSJs3.js} +2 -1
- package/dist/_chunks/plugin-context-DEGLSJs3.js.map +1 -0
- package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
- package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
- package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
- package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
- package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
- package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
- package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
- package/dist/_chunks/{segment-classify-Byy425ng.js.map → segment-classify-C539Pa2O.js.map} +1 -1
- package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
- package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
- package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
- package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
- package/dist/_chunks/use-segment-params-ClyUNq4d.js +128 -0
- package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +1 -0
- package/dist/_chunks/{walkers-_6zKFlch.js → walkers-BhhwI9TD.js} +21 -87
- package/dist/_chunks/walkers-BhhwI9TD.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +37 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/fs-identity.d.ts +64 -0
- package/dist/adapters/fs-identity.d.ts.map +1 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +2 -3
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +57 -1
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +13 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cache/fast-hash.d.ts +5 -0
- package/dist/cache/fast-hash.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts +20 -9
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts +4 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +3 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +1 -0
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -162
- package/dist/client/history.d.ts +13 -6
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +18 -11
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +4 -3
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +865 -306
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-api.d.ts +2 -2
- package/dist/client/navigation-commit.d.ts +71 -0
- package/dist/client/navigation-commit.d.ts.map +1 -0
- package/dist/client/navigation-context.d.ts +37 -20
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +63 -23
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/params-context.d.ts +81 -0
- package/dist/client/params-context.d.ts.map +1 -0
- package/dist/client/router-effects.d.ts +129 -0
- package/dist/client/router-effects.d.ts.map +1 -0
- package/dist/client/router-lifecycle.d.ts +55 -0
- package/dist/client/router-lifecycle.d.ts.map +1 -0
- package/dist/client/router-pipeline.d.ts +84 -0
- package/dist/client/router-pipeline.d.ts.map +1 -0
- package/dist/client/router-ref.d.ts +1 -1
- package/dist/client/router-ref.d.ts.map +1 -1
- package/dist/client/router-skew.d.ts +21 -0
- package/dist/client/router-skew.d.ts.map +1 -0
- package/dist/client/router-types.d.ts +207 -0
- package/dist/client/router-types.d.ts.map +1 -0
- package/dist/client/router.d.ts +1 -182
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +38 -25
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +79 -34
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/ssr-data.d.ts +6 -0
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/stale-client.d.ts +69 -0
- package/dist/client/stale-client.d.ts.map +1 -0
- package/dist/client/state.d.ts +10 -1
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +2 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/unload-guard.d.ts +5 -0
- package/dist/client/unload-guard.d.ts.map +1 -1
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/client/use-router.d.ts +1 -1
- package/dist/client/use-segment-params.d.ts +18 -8
- package/dist/client/use-segment-params.d.ts.map +1 -1
- package/dist/cookies/index.js +3 -3
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +906 -321
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +31 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/request-dep/analysis.d.ts +106 -0
- package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
- package/dist/plugins/request-dep/index.d.ts +75 -0
- package/dist/plugins/request-dep/index.d.ts.map +1 -0
- package/dist/plugins/request-dep/origin.d.ts +61 -0
- package/dist/plugins/request-dep/origin.d.ts.map +1 -0
- package/dist/plugins/routing.d.ts +9 -0
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +136 -8
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +18 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/collision-probe.d.ts +104 -0
- package/dist/routing/collision-probe.d.ts.map +1 -0
- package/dist/routing/collision-spaces.d.ts +26 -0
- package/dist/routing/collision-spaces.d.ts.map +1 -0
- package/dist/routing/index.js +3 -3
- package/dist/routing/interception.d.ts +235 -6
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +33 -0
- package/dist/routing/segment-classify.d.ts.map +1 -1
- package/dist/routing/segment-keys.d.ts +95 -0
- package/dist/routing/segment-keys.d.ts.map +1 -0
- package/dist/routing/slot-placement.d.ts +48 -0
- package/dist/routing/slot-placement.d.ts.map +1 -0
- package/dist/routing/walkers.d.ts +6 -0
- package/dist/routing/walkers.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +0 -6
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +176 -2
- package/dist/search-params/index.js.map +1 -1
- package/dist/segment-params/define.d.ts +0 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +69 -1
- package/dist/segment-params/index.js.map +1 -0
- package/dist/server/actions.d.ts +6 -3
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +2 -1
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +13 -1
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/chain-url-parts.d.ts +82 -0
- package/dist/server/chain-url-parts.d.ts.map +1 -0
- package/dist/server/children-interception.d.ts +49 -0
- package/dist/server/children-interception.d.ts.map +1 -0
- package/dist/server/default-status-page.d.ts +45 -0
- package/dist/server/default-status-page.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts +14 -3
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/head-response.d.ts +35 -0
- package/dist/server/head-response.d.ts.map +1 -0
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +393 -212
- package/dist/server/internal.js.map +1 -1
- package/dist/server/param-coercion.d.ts +10 -1
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts +0 -18
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-interception.d.ts +59 -9
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +11 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/publish-params.d.ts +33 -0
- package/dist/server/publish-params.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +48 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +32 -2
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-handler.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +13 -0
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-cache-key-guard.d.ts +78 -0
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +14 -2
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +2 -10
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +3 -2
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +3 -2
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +2 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/skippable-prefix.d.ts +23 -0
- package/dist/server/skippable-prefix.d.ts.map +1 -0
- package/dist/server/slot-interception.d.ts +80 -0
- package/dist/server/slot-interception.d.ts.map +1 -0
- package/dist/server/slot-resolver.d.ts +70 -8
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +6 -0
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +24 -13
- package/dist/server/ssr-wrappers.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts +6 -27
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts +6 -0
- package/dist/server/static-generator.d.ts.map +1 -1
- package/dist/server/static-not-found.d.ts +52 -0
- package/dist/server/static-not-found.d.ts.map +1 -0
- package/dist/shared/als-slots.d.ts +41 -0
- package/dist/shared/als-slots.d.ts.map +1 -0
- package/dist/shared/base-path.d.ts +56 -0
- package/dist/shared/base-path.d.ts.map +1 -0
- package/dist/shared/param-value.d.ts +80 -0
- package/dist/shared/param-value.d.ts.map +1 -0
- package/dist/shared/payload-root.d.ts +108 -0
- package/dist/shared/payload-root.d.ts.map +1 -0
- package/dist/shared/rsc-cache-key.d.ts +163 -0
- package/dist/shared/rsc-cache-key.d.ts.map +1 -0
- package/dist/shared/rsc-error-envelope.d.ts +25 -0
- package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
- package/dist/shared/rsc-manifest.d.ts +28 -0
- package/dist/shared/rsc-manifest.d.ts.map +1 -0
- package/dist/shared/rsc-payload-path.d.ts +42 -0
- package/dist/shared/rsc-payload-path.d.ts.map +1 -0
- package/dist/shared/segment-info.d.ts +59 -0
- package/dist/shared/segment-info.d.ts.map +1 -0
- package/dist/shared/slot-params.d.ts +51 -0
- package/dist/shared/slot-params.d.ts.map +1 -0
- package/dist/shared/static-platform-files.d.ts +71 -0
- package/dist/shared/static-platform-files.d.ts.map +1 -0
- package/docs/api/31-api-client.mdx +4 -2
- package/docs/learn/02-pages-and-layouts.mdx +1 -1
- package/docs/learn/11-error-handling.mdx +2 -0
- package/docs/learn/12-client-navigation.mdx +2 -2
- package/docs/learn/14-deploying.mdx +23 -0
- package/docs/more/01-advanced-routing.mdx +172 -2
- package/docs/more/04-metadata-and-fonts.mdx +1 -1
- package/package.json +2 -2
- package/src/adapters/build-output-helper.ts +116 -3
- package/src/adapters/cloudflare.ts +86 -11
- package/src/adapters/fs-identity.ts +156 -0
- package/src/adapters/nitro.ts +6 -6
- package/src/adapters/shared.ts +196 -21
- package/src/adapters/types.ts +13 -1
- package/src/cache/fast-hash.ts +5 -0
- package/src/client/browser-entry/action-dispatch.ts +9 -4
- package/src/client/browser-entry/hydrate.ts +40 -29
- package/src/client/browser-entry/index.ts +15 -1
- package/src/client/browser-entry/post-hydration.ts +10 -9
- package/src/client/browser-entry/router-init.ts +104 -48
- package/src/client/browser-entry/rsc-stream.ts +11 -2
- package/src/client/error-boundary.tsx +20 -15
- package/src/client/history.ts +13 -6
- package/src/client/internal.ts +4 -3
- package/src/client/link.tsx +55 -8
- package/src/client/navigation-api.ts +3 -3
- package/src/client/navigation-commit.ts +174 -0
- package/src/client/navigation-context.ts +40 -67
- package/src/client/navigation-root.tsx +326 -101
- package/src/client/params-context.ts +192 -0
- package/src/client/router-effects.ts +231 -0
- package/src/client/router-lifecycle.ts +288 -0
- package/src/client/router-pipeline.ts +389 -0
- package/src/client/router-ref.ts +1 -1
- package/src/client/router-skew.ts +30 -0
- package/src/client/router-types.ts +230 -0
- package/src/client/router.ts +242 -707
- package/src/client/rsc-fetch.ts +169 -102
- package/src/client/segment-cache.ts +121 -44
- package/src/client/ssr-data.ts +6 -0
- package/src/client/stale-client.ts +191 -0
- package/src/client/state.ts +14 -1
- package/src/client/top-loader.tsx +2 -1
- package/src/client/unload-guard.ts +34 -2
- package/src/client/use-cookie.ts +9 -2
- package/src/client/use-router.ts +1 -1
- package/src/client/use-segment-params.ts +41 -18
- package/src/index.ts +5 -1
- package/src/plugin-context.ts +32 -0
- package/src/plugins/adapter-build.ts +4 -0
- package/src/plugins/build-manifest.ts +34 -13
- package/src/plugins/entries.ts +4 -0
- package/src/plugins/fonts.ts +11 -8
- package/src/plugins/request-dep/analysis.ts +447 -0
- package/src/plugins/request-dep/index.ts +376 -0
- package/src/plugins/request-dep/origin.ts +310 -0
- package/src/plugins/routing.ts +59 -2
- package/src/plugins/static-build.ts +315 -73
- package/src/routing/codegen-types.ts +18 -0
- package/src/routing/codegen.ts +50 -10
- package/src/routing/collision-probe.ts +438 -0
- package/src/routing/collision-spaces.ts +281 -0
- package/src/routing/interception.ts +896 -50
- package/src/routing/manifest-codegen.ts +5 -3
- package/src/routing/scanner.ts +114 -89
- package/src/routing/segment-classify.ts +57 -0
- package/src/routing/segment-keys.ts +146 -0
- package/src/routing/slot-placement.ts +97 -0
- package/src/routing/walkers.ts +8 -0
- package/src/search-params/define.ts +6 -29
- package/src/segment-params/define.ts +4 -27
- package/src/server/action-handler.ts +3 -1
- package/src/server/actions.ts +6 -3
- package/src/server/als-registry.ts +2 -1
- package/src/server/build-manifest.ts +14 -1
- package/src/server/chain-url-parts.ts +163 -0
- package/src/server/children-interception.ts +115 -0
- package/src/server/default-status-page.ts +127 -0
- package/src/server/deny-renderer.ts +81 -20
- package/src/server/head-response.ts +54 -0
- package/src/server/param-coercion.ts +48 -18
- package/src/server/pipeline-helpers.ts +0 -45
- package/src/server/pipeline-interception.ts +81 -32
- package/src/server/pipeline-outcome.ts +2 -4
- package/src/server/pipeline-phases.ts +68 -16
- package/src/server/pipeline.ts +57 -10
- package/src/server/prebuilt-runtime.ts +4 -4
- package/src/server/publish-params.ts +42 -0
- package/src/server/request-context.ts +97 -18
- package/src/server/route-element-builder.ts +89 -98
- package/src/server/route-handler.ts +8 -8
- package/src/server/route-matcher.ts +21 -9
- package/src/server/rsc-cache-key-guard.ts +170 -0
- package/src/server/rsc-entry/api-handler.ts +6 -8
- package/src/server/rsc-entry/error-renderer.ts +64 -25
- package/src/server/rsc-entry/helpers.ts +31 -16
- package/src/server/rsc-entry/index.ts +36 -28
- package/src/server/rsc-entry/render-route.ts +9 -4
- package/src/server/rsc-entry/revalidate-renderer.ts +1 -1
- package/src/server/rsc-entry/rsc-payload.ts +14 -25
- package/src/server/rsc-entry/rsc-stream.ts +4 -3
- package/src/server/rsc-entry/ssr-renderer.ts +13 -9
- package/src/server/skippable-prefix.ts +92 -0
- package/src/server/slot-interception.ts +168 -0
- package/src/server/slot-resolver.ts +253 -156
- package/src/server/ssr-bridge-types.ts +6 -0
- package/src/server/ssr-entry.ts +12 -4
- package/src/server/ssr-wrappers.tsx +33 -32
- package/src/server/state-tree-diff.ts +14 -55
- package/src/server/static-generator.ts +65 -27
- package/src/server/static-not-found.ts +134 -0
- package/src/shared/als-slots.ts +72 -0
- package/src/shared/base-path.ts +80 -0
- package/src/shared/param-value.ts +244 -0
- package/src/shared/payload-root.ts +151 -0
- package/src/shared/rsc-cache-key.ts +269 -0
- package/src/shared/rsc-error-envelope.ts +31 -0
- package/src/shared/rsc-manifest.ts +29 -0
- package/src/shared/rsc-payload-path.ts +45 -0
- package/src/shared/segment-info.ts +60 -0
- package/src/shared/slot-params.ts +111 -0
- package/src/shared/static-platform-files.ts +75 -0
- package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
- package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
- package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-BTWEKJXo.js +0 -668
- package/dist/_chunks/cli-schema-sync-BTWEKJXo.js.map +0 -1
- package/dist/_chunks/define-COtkxMRT.js +0 -191
- package/dist/_chunks/define-COtkxMRT.js.map +0 -1
- package/dist/_chunks/define-c4au4I9R.js +0 -82
- package/dist/_chunks/define-c4au4I9R.js.map +0 -1
- package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
- package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
- package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
- package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
- package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
- package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
- package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
- package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
- package/dist/_chunks/walkers-_6zKFlch.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/plugins/request-dep.d.ts +0 -32
- package/dist/plugins/request-dep.d.ts.map +0 -1
- package/src/plugins/request-dep.ts +0 -499
|
@@ -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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segment-classify-Byy425ng.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 * 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":";;AA4CA,IAAa,uBAA6C;CAAC;CAAY;CAAO;CAAQ;AAAO;;AA8G7F,IAAa,0BAA0B;CAAC;CAAO;CAAM;CAAO;AAAI;;;;;;;;;;;;;;;;;;AC1GhE,SAAgB,mBAAmB,OAA2B;CAC5D,MAAM,MAAM,MAAM;CAClB,IAAI,QAAQ,GAAG,OAAO;EAAE,MAAM;EAAU,OAAO;CAAM;CAGrD,MAAM,eAAe,MAAM,QAAQ,GAAG;CACtC,IAAI,iBAAiB,IAAI,OAAO;EAAE,MAAM;EAAU,OAAO;CAAM;CAE/D,MAAM,SAAS,MAAM,MAAM,GAAG,YAAY;CAE1C,IAAI,IAAI,eAAe;CAGvB,MAAM,WAAW,IAAI,OAAO,MAAM,OAAO;CACzC,IAAI,UAAU;CAGd,MAAM,WAAW,IAAI,IAAI,OAAO,MAAM,OAAO,OAAO,MAAM,IAAI,OAAO,OAAO,MAAM,IAAI,OAAO;CAC7F,IAAI,UAAU,KAAK;CAGnB,MAAM,YAAY;CAClB,OAAO,IAAI,OAAO,MAAM,OAAO,KAAK;CAGpC,IAAI,KAAK,OAAO,MAAM,WACpB,OAAO;EAAE,MAAM;EAAU,OAAO;CAAM;CAGxC,MAAM,OAAO,MAAM,MAAM,WAAW,CAAC;CACrC;CAGA,IAAI,UAAU;EACZ,IAAI,KAAK,OAAO,MAAM,OAAO,KAC3B,OAAO;GAAE,MAAM;GAAU,OAAO;EAAM;EAExC;CACF;CAEA,MAAM,SAAS,MAAM,MAAM,CAAC;CAG5B,IAAI,OAAO,SAAS,GAAG,KAAK,OAAO,SAAS,GAAG,GAC7C,OAAO;EAAE,MAAM;EAAU,OAAO;CAAM;CAExC,IAAI,OAAO,SAAS,GAAG,GACrB,OAAO;EAAE,MAAM;EAAU,OAAO;CAAM;CAGxC,MAAM,aAAa,OAAO,SAAS,KAAK,OAAO,SAAS;CAExD,IAAI,YAAY,UAAU;EAExB,IAAI,YAAY,OAAO;GAAE,MAAM;GAAU,OAAO;EAAM;EACtD,OAAO;GAAE,MAAM;GAAsB;EAAK;CAC5C;CACA,IAAI,UAAU;EAEZ,IAAI,YAAY,OAAO;GAAE,MAAM;GAAU,OAAO;EAAM;EACtD,OAAO;GAAE,MAAM;GAAa;EAAK;CACnC;CACA,IAAI,UAEF,OAAO;EAAE,MAAM;EAAU,OAAO;CAAM;CAGxC,IAAI,YACF,OAAO;EACL,MAAM;EACN;EACA,QAAQ,UAAU,KAAA;EAClB,QAAQ,UAAU,KAAA;CACpB;CAEF,OAAO;EAAE,MAAM;EAAW;CAAK;AACjC;;;;;;;;;;;;;;;;AA6BA,SAAgB,gBAAgB,SAAwC;CAEtE,IAAI,QAAQ,WAAW,GAAG,GACxB,OAAO,EAAE,MAAM,UAAU;CAI3B,IAAI,QAAQ,WAAW,GAAG,GACxB,OAAO,EAAE,MAAM,OAAO;CAKxB,MAAM,eAAe,wBAAwB,OAAO;CACpD,IAAI,cACF,OAAO;EACL,MAAM;EACN,oBAAoB,aAAa;EACjC,wBAAwB,aAAa;CACvC;CAIF,IAAI,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,GACjD,OAAO,EAAE,MAAM,QAAQ;CAIzB,MAAM,SAAS,mBAAmB,OAAO;CACzC,IAAI,OAAO,SAAS,UAAU;EAC5B,MAAM,SAAgC;GAAE,MAAM,OAAO;GAAM,WAAW,OAAO;EAAK;EAClF,IAAI,OAAO,SAAS,WAAW;GAC7B,IAAI,OAAO,QAAQ,OAAO,cAAc,OAAO;GAC/C,IAAI,OAAO,QAAQ,OAAO,cAAc,OAAO;EACjD;EACA,OAAO;CACT;CAEA,OAAO,EAAE,MAAM,SAAS;AAC1B;;;;;;;;;;;;;;;AAgBA,SAAS,wBACP,SAC4D;CAC5D,KAAK,MAAM,UAAU,sBACnB,IAAI,QAAQ,WAAW,MAAM,GAAG;EAC9B,MAAM,OAAO,QAAQ,MAAM,OAAO,MAAM;EAGxC,IAAI,KAAK,SAAS,KAAK,CAAC,KAAK,SAAS,GAAG,GACvC,OAAO;GAAE;GAAQ,aAAa;EAAK;CAEvC;CAEF,OAAO;AACT"}
|
|
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
|