@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 @@
|
|
|
1
|
+
{"version":3,"file":"segment-keys-BawYuNFO.js","names":[],"sources":["../../src/server/metadata-routes.ts","../../src/server/canonicalize.ts","../../src/routing/segment-keys.ts"],"sourcesContent":["/**\n * Metadata route classification for timber.js.\n *\n * Metadata routes are file-based endpoints that generate well-known URLs for\n * crawlers and browsers (sitemap.xml, robots.txt, OG images, etc.).\n *\n * These routes run through proxy.ts but NOT through middleware.ts or access.ts —\n * they are public endpoints by nature.\n *\n * See design/16-metadata.md §\"Metadata Routes\"\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { HeadElement } from './metadata.js';\nimport type { Metadata } from './types.js';\nimport type { ManifestSegmentNode } from './route-matcher.js';\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Classification of a metadata route file. */\nexport interface MetadataRouteInfo {\n /** The metadata route type. */\n type: MetadataRouteType;\n /** The content type to serve this route with. */\n contentType: string;\n /** Whether this route can appear in nested segments (not just app root). */\n nestable: boolean;\n}\n\nexport type MetadataRouteType =\n | 'sitemap'\n | 'robots'\n | 'manifest'\n | 'favicon'\n | 'icon'\n | 'opengraph-image'\n | 'apple-icon';\n\n// ─── Convention Table ────────────────────────────────────────────────────────\n\n/**\n * All recognized metadata route file conventions.\n *\n * Each entry maps a base file name (without extension) to its route info.\n * The extensions determine whether the file is static or dynamic.\n *\n * Static extensions: .xml, .txt, .json, .png, .jpg, .ico, .svg\n * Dynamic extensions: .ts, .tsx\n */\nexport const METADATA_ROUTE_CONVENTIONS: Record<\n string,\n {\n type: MetadataRouteType;\n contentType: string;\n nestable: boolean;\n staticExtensions: string[];\n dynamicExtensions: string[];\n /**\n * The URL path basename this file serves at (relative to segment).\n * For image routes, the full serve path includes an extension via\n * `resolveServePathForFile()`.\n */\n servePath: string;\n /**\n * When set, image routes append `.{serveExtension}` to the serve path.\n * Dynamic handlers (`.ts`/`.tsx`) use this as the default. Static files\n * use their own extension instead. Non-image routes leave this undefined.\n */\n serveExtension?: string;\n }\n> = {\n 'sitemap': {\n type: 'sitemap',\n contentType: 'application/xml',\n nestable: true,\n staticExtensions: ['xml'],\n dynamicExtensions: ['ts'],\n servePath: 'sitemap.xml',\n },\n 'robots': {\n type: 'robots',\n contentType: 'text/plain',\n nestable: false,\n staticExtensions: ['txt'],\n dynamicExtensions: ['ts'],\n servePath: 'robots.txt',\n },\n 'manifest': {\n type: 'manifest',\n contentType: 'application/manifest+json',\n nestable: false,\n staticExtensions: ['json'],\n dynamicExtensions: ['ts'],\n servePath: 'manifest.webmanifest',\n },\n 'favicon': {\n type: 'favicon',\n contentType: 'image/x-icon',\n nestable: false,\n staticExtensions: ['ico'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'favicon.ico',\n },\n 'icon': {\n type: 'icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg', 'svg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'icon',\n serveExtension: 'png',\n },\n 'opengraph-image': {\n type: 'opengraph-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'opengraph-image',\n serveExtension: 'png',\n },\n\n 'apple-icon': {\n type: 'apple-icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'apple-icon',\n serveExtension: 'png',\n },\n};\n\n// ─── MIME Type Resolution ─────────────────────────────────────────────────────\n\n/**\n * Map of file extensions to MIME types for static metadata route files.\n * Used to resolve the generic `image/*` content type for static image files.\n */\nconst EXTENSION_MIME_TYPES: Record<string, string> = {\n xml: 'application/xml',\n txt: 'text/plain',\n json: 'application/json',\n ico: 'image/x-icon',\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n svg: 'image/svg+xml',\n webp: 'image/webp',\n};\n\n/**\n * Resolve the concrete MIME type for a static metadata route file.\n *\n * For generic content types like `image/*`, this resolves to the actual\n * MIME type based on the file extension (e.g. `image/png` for `.png`).\n *\n * @param conventionContentType - The content type from the convention table (may be generic like `image/*`)\n * @param extension - The file extension without leading dot (e.g. \"png\", \"xml\")\n * @returns The resolved MIME type\n */\nexport function resolveStaticContentType(conventionContentType: string, extension: string): string {\n if (conventionContentType.includes('*')) {\n return EXTENSION_MIME_TYPES[extension] ?? 'application/octet-stream';\n }\n return conventionContentType;\n}\n\n/**\n * Check if a file extension represents a static (non-code) metadata route file.\n *\n * @param baseName - The base file name without extension (e.g. \"sitemap\", \"icon\")\n * @param extension - The file extension without leading dot (e.g. \"xml\", \"png\", \"ts\")\n * @returns true if this is a static file, false if dynamic or unrecognized\n */\nexport function isStaticMetadataExtension(baseName: string, extension: string): boolean {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return false;\n return convention.staticExtensions.includes(extension);\n}\n\n/**\n * Check if a file extension represents a dynamic (code) metadata route file.\n *\n * @param baseName - The base file name without extension (e.g. \"sitemap\", \"icon\")\n * @param extension - The file extension without leading dot (e.g. \"ts\", \"tsx\")\n * @returns true if this is a dynamic file, false if static or unrecognized\n */\nexport function isDynamicMetadataExtension(baseName: string, extension: string): boolean {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return false;\n return convention.dynamicExtensions.includes(extension);\n}\n\n// ─── Classification ──────────────────────────────────────────────────────────\n\n/**\n * Classify a file name as a metadata route, or return null if it's not one.\n *\n * @param fileName - The full file name including extension (e.g. \"sitemap.xml\", \"icon.tsx\")\n * @returns Classification info, or null if not a metadata route\n */\nexport function classifyMetadataRoute(fileName: string): MetadataRouteInfo | null {\n const dotIndex = fileName.lastIndexOf('.');\n if (dotIndex === -1) return null;\n\n const baseName = fileName.slice(0, dotIndex);\n const ext = fileName.slice(dotIndex + 1);\n\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return null;\n\n const isStatic = convention.staticExtensions.includes(ext);\n const isDynamic = convention.dynamicExtensions.includes(ext);\n\n if (!isStatic && !isDynamic) return null;\n\n return {\n type: convention.type,\n contentType: convention.contentType,\n nestable: convention.nestable,\n };\n}\n\n/**\n * Resolve the serve path for a metadata route file.\n *\n * For image routes (icon, opengraph-image, apple-icon), the serve path includes\n * a file extension so CDNs cache correctly:\n * - Dynamic handlers (.ts/.tsx) use the convention's `serveExtension` (default: .png)\n * - Static files use their own extension (e.g., icon.svg → icon.svg)\n *\n * Non-image routes return the convention's `servePath` as-is (already includes\n * extension: sitemap.xml, robots.txt, etc.).\n */\nexport function resolveServePathForFile(baseName: string, filePath: string): string {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return baseName;\n\n if (!convention.serveExtension) return convention.servePath;\n\n const ext = filePath.slice(filePath.lastIndexOf('.') + 1);\n if (convention.staticExtensions.includes(ext)) {\n return `${convention.servePath}.${ext}`;\n }\n return `${convention.servePath}.${convention.serveExtension}`;\n}\n\n/**\n * Get the default serve path for a metadata route type (using default extension\n * for image routes). Used for auto-link generation when only the type is known.\n */\nexport function getMetadataRouteServePath(type: MetadataRouteType): string {\n for (const convention of Object.values(METADATA_ROUTE_CONVENTIONS)) {\n if (convention.type === type) {\n if (convention.serveExtension) {\n return `${convention.servePath}.${convention.serveExtension}`;\n }\n return convention.servePath;\n }\n }\n throw new Error(`[timber] Unknown metadata route type: ${type}`);\n}\n\n/**\n * All possible serve path segments for metadata routes (includes extension\n * variants for image routes).\n */\nconst METADATA_SERVE_PATHS = new Set<string>();\nfor (const convention of Object.values(METADATA_ROUTE_CONVENTIONS)) {\n if (convention.serveExtension) {\n for (const ext of [...convention.staticExtensions, convention.serveExtension]) {\n METADATA_SERVE_PATHS.add(`${convention.servePath}.${ext}`);\n }\n } else {\n METADATA_SERVE_PATHS.add(convention.servePath);\n }\n}\n\nexport function isMetadataRouteServePath(pathname: string): boolean {\n let lastSegment = pathname.slice(pathname.lastIndexOf('/') + 1);\n const qIdx = lastSegment.indexOf('?');\n let query = '';\n if (qIdx !== -1) {\n query = lastSegment.slice(qIdx + 1);\n lastSegment = lastSegment.slice(0, qIdx);\n }\n if (!METADATA_SERVE_PATHS.has(lastSegment)) return false;\n // Vite module requests (e.g., /src/icon.svg?import) use special query params.\n // These are source assets, not metadata routes.\n if (/(?:^|&)(?:import|url|raw|worker|inline)(?:&|$)/.test(query)) return false;\n return true;\n}\n\n/** A <link> auto-link tag. */\nexport interface AutoLinkLink {\n tag: 'link';\n rel: string;\n href: string;\n type?: string;\n}\n\n/** A <meta> auto-link tag. */\nexport interface AutoLinkMeta {\n tag: 'meta';\n property?: string;\n name?: string;\n content: string;\n}\n\nexport type AutoLinkTag = AutoLinkLink | AutoLinkMeta;\n\n/**\n * Get the auto-link tags to inject into <head> for metadata route files\n * discovered in a segment.\n *\n * Returns link tags for icon/apple-icon/manifest, and meta tags for\n * opengraph-image (emits both og:image and twitter:image). Returns null\n * for types that don't auto-link (favicon, sitemap, robots).\n *\n * @param type - The metadata route type\n * @param href - The resolved URL path to the metadata route\n * @returns Tag descriptor(s) for the <head>, or null if no auto-link\n */\nexport function getMetadataRouteAutoLink(type: MetadataRouteType, href: string): AutoLinkTag[] {\n switch (type) {\n case 'icon':\n return [{ tag: 'link', rel: 'icon', href }];\n case 'apple-icon':\n return [{ tag: 'link', rel: 'apple-touch-icon', href }];\n case 'manifest':\n return [{ tag: 'link', rel: 'manifest', href }];\n case 'opengraph-image':\n return [\n { tag: 'meta', property: 'og:image', content: href },\n { tag: 'meta', name: 'twitter:image', content: href },\n ];\n default:\n return [];\n }\n}\n\n// ─── Auto-Linking ──────────────────────────────────────────────────────────\n\n// In dev mode, use a per-startup nonce for metadata route cache busting\n// instead of per-file content hashes (avoids rehashing on every request).\nlet _devNonce: string | undefined;\nfunction getDevNonce(): string {\n _devNonce ??= randomUUID().slice(0, 8);\n return _devNonce;\n}\n\n/**\n * Collect auto-linked head elements from metadata route files in the segment chain.\n *\n * Walks each segment's metadataRoutes, resolves serve paths and URLs, and\n * emits HeadElement descriptors for <link> and <meta> tags that React Float\n * hoists into <head>.\n *\n * See design/16-metadata.md §\"Auto-Linking\"\n */\nexport function collectMetadataRouteHeadElements(\n segments: ManifestSegmentNode[],\n firstDeniedIndex: number,\n resolvedMetadata: Metadata,\n requestUrl: URL,\n metadataRouteHashes?: Record<string, string>\n): HeadElement[] {\n const elements: HeadElement[] = [];\n const hasUserOgImage = Boolean(resolvedMetadata.openGraph?.images);\n const hasUserTwitterImage = Boolean(resolvedMetadata.twitter?.images);\n const requestPathname = requestUrl.pathname;\n // In dev mode, use the request origin so OG URLs resolve to localhost.\n // In production, use metadataBase (the canonical domain).\n const ogBase =\n process.env.NODE_ENV !== 'production'\n ? new URL(requestUrl.origin)\n : resolvedMetadata.metadataBase;\n\n for (let si = 0; si < segments.length; si++) {\n const segment = segments[si];\n if (!segment.metadataRoutes) continue;\n if (si >= firstDeniedIndex) continue;\n for (const baseName of Object.keys(segment.metadataRoutes)) {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) continue;\n if (!convention.nestable && segment.urlPath !== '/') continue;\n if (convention.type === 'opengraph-image' && hasUserOgImage) continue;\n const resolvedPrefix = convention.nestable\n ? requestPathname === '/'\n ? ''\n : requestPathname\n : '';\n const metaFile = segment.metadataRoutes[baseName];\n const fileServePath = metaFile?.filePath\n ? resolveServePathForFile(baseName, metaFile.filePath)\n : convention.serveExtension\n ? `${convention.servePath}.${convention.serveExtension}`\n : convention.servePath;\n let href = `${resolvedPrefix}/${fileServePath}`;\n if (convention.type === 'opengraph-image') {\n const fileHash = metaFile?.filePath ? metadataRouteHashes?.[metaFile.filePath] : undefined;\n const cacheBust = fileHash ?? getDevNonce();\n href = `${href}?${cacheBust}`;\n }\n if (ogBase && convention.type === 'opengraph-image') {\n href = new URL(href, ogBase).toString();\n }\n for (const autoLink of getMetadataRouteAutoLink(convention.type, href)) {\n if (\n hasUserTwitterImage &&\n autoLink.tag === 'meta' &&\n 'name' in autoLink &&\n autoLink.name === 'twitter:image'\n )\n continue;\n if (autoLink.tag === 'link') {\n const attrs: Record<string, string> = { rel: autoLink.rel, href: autoLink.href };\n if (autoLink.type) attrs.type = autoLink.type;\n elements.push({ tag: 'link', attrs });\n } else {\n const attrs: Record<string, string> = { content: autoLink.content };\n if (autoLink.property) attrs.property = autoLink.property;\n if (autoLink.name) attrs.name = autoLink.name;\n elements.push({ tag: 'meta', attrs });\n }\n }\n }\n }\n return elements;\n}\n","/**\n * URL canonicalization — runs once at the request boundary.\n *\n * Every layer (proxy.ts, middleware.ts, access.ts, components) sees the same\n * canonical path. No re-decoding occurs at any later stage.\n *\n * See design/07-routing.md §\"URL Canonicalization & Security\"\n */\n\n/** Result of canonicalization — either a clean path or a rejection. */\nexport type CanonicalizeResult = { ok: true; pathname: string } | { ok: false; status: 400 };\n\n/**\n * Encoded separators that produce a 400 rejection.\n * %2f (/) and %5c (\\) cause path-confusion attacks.\n *\n * Shared between the runtime canonicalizer and the build-time route scanner\n * to ensure both enforce identical security rules. See design/13-security.md.\n */\nexport const ENCODED_SEPARATOR_RE = /%2f|%5c/i;\n\n/** Null byte — rejected. Shared with the route scanner. */\nexport const NULL_BYTE_RE = /%00/i;\n\n/**\n * Canonicalize a URL pathname.\n *\n * 1. Reject encoded separators (%2f, %5c) and null bytes (%00)\n * 2. Single percent-decode\n * 3. Collapse // → /\n * 4. Resolve .. segments (reject if escaping root)\n * 5. Strip trailing slash (except root \"/\")\n *\n * @param rawPathname - The raw pathname from the request URL (percent-encoded)\n * @param stripTrailingSlash - Whether to strip trailing slashes. Default: true.\n */\nexport function canonicalize(rawPathname: string, stripTrailingSlash = true): CanonicalizeResult {\n // Step 1: Reject dangerous encoded sequences BEFORE decoding.\n // This must happen on the raw input so %252f doesn't bypass after a single decode.\n if (ENCODED_SEPARATOR_RE.test(rawPathname)) {\n return { ok: false, status: 400 };\n }\n if (NULL_BYTE_RE.test(rawPathname)) {\n return { ok: false, status: 400 };\n }\n\n // Step 2: Single percent-decode.\n // Double-encoded input (%2561 → %61) stays as %61 — not decoded again.\n let decoded: string;\n try {\n decoded = decodeURIComponent(rawPathname);\n } catch {\n // Malformed percent-encoding → 400\n return { ok: false, status: 400 };\n }\n\n // Reject null bytes that appeared after decoding (from valid %00-like sequences\n // that weren't caught above — belt and suspenders).\n if (decoded.includes('\\0')) {\n return { ok: false, status: 400 };\n }\n\n // Backslash is NOT a path separator — keep as literal character.\n // But reject if it would create // after normalization (e.g., /\\evil.com).\n // We do NOT convert \\ to / — it stays as a literal.\n\n // Step 3: Collapse consecutive slashes.\n let pathname = decoded.replace(/\\/\\/+/g, '/');\n\n // Step 4: Resolve .. and . segments.\n const segments = pathname.split('/');\n const resolved: string[] = [];\n for (const seg of segments) {\n if (seg === '..') {\n if (resolved.length <= 1) {\n // Trying to escape root — 400\n return { ok: false, status: 400 };\n }\n resolved.pop();\n } else if (seg !== '.') {\n resolved.push(seg);\n }\n }\n\n pathname = resolved.join('/') || '/';\n\n // Step 5: Strip trailing slash (except root \"/\").\n if (stripTrailingSlash && pathname.length > 1 && pathname.endsWith('/')) {\n pathname = pathname.slice(0, -1);\n }\n\n return { ok: true, pathname };\n}\n","/**\n * Segment key computation — the stable identity of a segment in the route tree.\n *\n * A segment key is the string that names a segment across the RSC/SSR/client\n * boundary: it appears in the X-Timber-State-Tree header, the X-Timber-Segments\n * header, SegmentOutlet props, the client segment cache, and (since TIM-1279)\n * interception scoping.\n *\n * This module lives in `routing/` rather than `server/` because the keys are a\n * property of the route tree itself — both the build-time scanner tree and the\n * runtime manifest tree produce identical keys for the same directory, which is\n * what lets a build-time value (an interception scope) be compared against a\n * request-time matched chain.\n *\n * See design/19-client-navigation.md §\"X-Timber-State-Tree Header\"\n */\n\n/**\n * Segment node shape expected by computeSegmentKeys.\n *\n * Structurally satisfied by both `SegmentNode` (build time) and\n * `ManifestSegmentNode` (request time) — keys depend only on the\n * URL path and the segment classification, never on file payloads.\n */\nexport interface SegmentKeyInput {\n urlPath: string;\n segmentName?: string;\n segmentType?: string;\n}\n\n/**\n * Compute state-tree keys for a segment chain.\n *\n * Non-group segments use their urlPath as-is. Route groups accumulate\n * ancestor group names to produce globally unique keys:\n * app/(a)/(shared)/dashboard → keys: ['/', '/(a)', '/(a)/(shared)', '/dashboard']\n *\n * **An intercepting segment switches the chain to name accumulation for good.**\n * A children-path interception (TIM-1280) puts an intercepting node and its\n * descendants into the *main* rendered chain, and their `urlPath`s are not\n * URLs: interception adds no URL depth, so `app/(browse)/(...)[artistSlug]/[year]`\n * carries `/` and `/[year]`. Keyed by urlPath the intercepting node would\n * collide with its own owner, and `[year]` with any top-level `[year]` route —\n * so a client that had one mounted would reuse the wrong cached layout. The\n * switch is sticky rather than per-node because the *descendants* are where\n * the false URLs are; the intercepting node alone is not the problem.\n *\n * This is the single source of truth for segment keys — used by the\n * element builder (skip decisions, SegmentOutlet props), segment info\n * (X-Timber-Segments header), the client cache/state tree, and\n * interception scoping (`routing/interception.ts`).\n */\nexport function computeSegmentKeys(segments: SegmentKeyInput[]): string[] {\n const keys: string[] = [];\n let prevKey = '';\n let insideIntercepting = false;\n\n for (const segment of segments) {\n if (segment.segmentType === 'intercepting') insideIntercepting = true;\n if (segment.segmentType === 'group' || insideIntercepting) {\n const base = prevKey === '/' ? '' : prevKey;\n const key = `${base}/${segment.segmentName}`;\n keys.push(key);\n prevKey = key;\n } else {\n keys.push(segment.urlPath);\n prevKey = segment.urlPath;\n }\n }\n\n return keys;\n}\n\n/**\n * Compute tree paths for a segment chain — the directory path of each segment\n * with slots elided, e.g. `app/(browse)/feed` → `/(browse)/feed`.\n *\n * This is `computeSegmentKeys` with the group branch applied to *every*\n * segment rather than only to groups. That one difference is the whole point:\n * `computeSegmentKeys` resets to `urlPath` at each URL-visible segment, so\n * `(browse)/feed` and `(landing)/feed` both key as `/feed`. Two directories\n * that render different layouts must not share an identity.\n *\n * Used for interception scoping (`routing/interception.ts`), where the\n * question is \"does this route pass through *that* directory?\" — not \"does it\n * render this URL?\". Not interchangeable with `computeSegmentKeys`: the state\n * tree is keyed by URL on purpose, because the client caches by URL.\n */\nexport function computeSegmentTreePaths(segments: SegmentKeyInput[]): string[] {\n const paths: string[] = [];\n let prev = '';\n\n for (const segment of segments) {\n const name = segment.segmentName ?? '';\n if (!name) {\n // The app root — no directory name of its own.\n paths.push('/');\n prev = '/';\n continue;\n }\n const base = prev === '/' ? '' : prev;\n const path = `${base}/${name}`;\n paths.push(path);\n prev = path;\n }\n\n return paths;\n}\n\n/**\n * The directory names a tree path is built from — `/` is none, `/feed/(a)` is\n * `['feed', '(a)']`.\n *\n * The inverse of the walk above, and it lives beside it so the format is\n * stated once: `findChainByTreePath` (`server/children-interception.ts`) walks\n * these names back down the tree to the directory the path addresses, and\n * `treePathDepth` counts them. Both are readings of the same string, and a\n * reader that disagreed with the producer about what separates two names\n * resolves an interception to the wrong node or to none.\n */\nexport function treePathNames(treePath: string): string[] {\n return treePath === '/' ? [] : treePath.slice(1).split('/');\n}\n\n/**\n * How many directories a tree path names — `/` is 0, `/feed` is 1,\n * `/feed/(a)` is 2.\n *\n * Two tree paths on a single rendered chain are strictly nested, so on that\n * chain depth orders them totally — which is what makes it usable as \"the\n * deeper of these two directories\" wherever a route passes through both\n * (`routing/interception.ts`).\n */\nexport function treePathDepth(treePath: string): number {\n return treePathNames(treePath).length;\n}\n\n/**\n * Compute a unique key for a parallel route slot.\n * Format: `{parentSegmentId}/@{slotName}`, e.g. `/@sidebar` or `/dashboard/@modal`.\n */\nexport function computeSlotKey(parentSegmentId: string, slotName: string): string {\n const name = slotName.startsWith('@') ? slotName : `@${slotName}`;\n const prefix = parentSegmentId === '/' ? '' : parentSegmentId;\n return `${prefix}/${name}`;\n}\n"],"mappings":";;;;;;;;;;;AAiDA,IAAa,6BAqBT;CACF,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,KAAK;EACxB,mBAAmB,CAAC,IAAI;EACxB,WAAW;CACb;CACA,UAAU;EACR,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,KAAK;EACxB,mBAAmB,CAAC,IAAI;EACxB,WAAW;CACb;CACA,YAAY;EACV,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,MAAM;EACzB,mBAAmB,CAAC,IAAI;EACxB,WAAW;CACb;CACA,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,KAAK;EACxB,mBAAmB,CAAC,MAAM,KAAK;EAC/B,WAAW;CACb;CACA,QAAQ;EACN,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB;GAAC;GAAO;GAAO;EAAK;EACtC,mBAAmB,CAAC,MAAM,KAAK;EAC/B,WAAW;EACX,gBAAgB;CAClB;CACA,mBAAmB;EACjB,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,OAAO,KAAK;EAC/B,mBAAmB,CAAC,MAAM,KAAK;EAC/B,WAAW;EACX,gBAAgB;CAClB;CAEA,cAAc;EACZ,MAAM;EACN,aAAa;EACb,UAAU;EACV,kBAAkB,CAAC,KAAK;EACxB,mBAAmB,CAAC,MAAM,KAAK;EAC/B,WAAW;EACX,gBAAgB;CAClB;AACF;;;;;;;;AAyDA,SAAgB,2BAA2B,UAAkB,WAA4B;CACvF,MAAM,aAAa,2BAA2B;CAC9C,IAAI,CAAC,YAAY,OAAO;CACxB,OAAO,WAAW,kBAAkB,SAAS,SAAS;AACxD;;;;;;;AAUA,SAAgB,sBAAsB,UAA4C;CAChF,MAAM,WAAW,SAAS,YAAY,GAAG;CACzC,IAAI,aAAa,IAAI,OAAO;CAE5B,MAAM,WAAW,SAAS,MAAM,GAAG,QAAQ;CAC3C,MAAM,MAAM,SAAS,MAAM,WAAW,CAAC;CAEvC,MAAM,aAAa,2BAA2B;CAC9C,IAAI,CAAC,YAAY,OAAO;CAExB,MAAM,WAAW,WAAW,iBAAiB,SAAS,GAAG;CACzD,MAAM,YAAY,WAAW,kBAAkB,SAAS,GAAG;CAE3D,IAAI,CAAC,YAAY,CAAC,WAAW,OAAO;CAEpC,OAAO;EACL,MAAM,WAAW;EACjB,aAAa,WAAW;EACxB,UAAU,WAAW;CACvB;AACF;;;;;AA8BA,SAAgB,0BAA0B,MAAiC;CACzE,KAAK,MAAM,cAAc,OAAO,OAAO,0BAA0B,GAC/D,IAAI,WAAW,SAAS,MAAM;EAC5B,IAAI,WAAW,gBACb,OAAO,GAAG,WAAW,UAAU,GAAG,WAAW;EAE/C,OAAO,WAAW;CACpB;CAEF,MAAM,IAAI,MAAM,yCAAyC,MAAM;AACjE;;;;;AAMA,IAAM,uCAAuB,IAAI,IAAY;AAC7C,KAAK,MAAM,cAAc,OAAO,OAAO,0BAA0B,GAC/D,IAAI,WAAW,gBACb,KAAK,MAAM,OAAO,CAAC,GAAG,WAAW,kBAAkB,WAAW,cAAc,GAC1E,qBAAqB,IAAI,GAAG,WAAW,UAAU,GAAG,KAAK;KAG3D,qBAAqB,IAAI,WAAW,SAAS;AAIjD,SAAgB,yBAAyB,UAA2B;CAClE,IAAI,cAAc,SAAS,MAAM,SAAS,YAAY,GAAG,IAAI,CAAC;CAC9D,MAAM,OAAO,YAAY,QAAQ,GAAG;CACpC,IAAI,QAAQ;CACZ,IAAI,SAAS,IAAI;EACf,QAAQ,YAAY,MAAM,OAAO,CAAC;EAClC,cAAc,YAAY,MAAM,GAAG,IAAI;CACzC;CACA,IAAI,CAAC,qBAAqB,IAAI,WAAW,GAAG,OAAO;CAGnD,IAAI,iDAAiD,KAAK,KAAK,GAAG,OAAO;CACzE,OAAO;AACT;;;;;;;;;;;;;AAgCA,SAAgB,yBAAyB,MAAyB,MAA6B;CAC7F,QAAQ,MAAR;EACE,KAAK,QACH,OAAO,CAAC;GAAE,KAAK;GAAQ,KAAK;GAAQ;EAAK,CAAC;EAC5C,KAAK,cACH,OAAO,CAAC;GAAE,KAAK;GAAQ,KAAK;GAAoB;EAAK,CAAC;EACxD,KAAK,YACH,OAAO,CAAC;GAAE,KAAK;GAAQ,KAAK;GAAY;EAAK,CAAC;EAChD,KAAK,mBACH,OAAO,CACL;GAAE,KAAK;GAAQ,UAAU;GAAY,SAAS;EAAK,GACnD;GAAE,KAAK;GAAQ,MAAM;GAAiB,SAAS;EAAK,CACtD;EACF,SACE,OAAO,CAAC;CACZ;AACF;;;;;;;;;;ACjUA,IAAa,uBAAuB;;AAGpC,IAAa,eAAe;;;;;;;;;;;;;AAc5B,SAAgB,aAAa,aAAqB,qBAAqB,MAA0B;CAG/F,IAAI,qBAAqB,KAAK,WAAW,GACvC,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAI;CAElC,IAAI,aAAa,KAAK,WAAW,GAC/B,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAI;CAKlC,IAAI;CACJ,IAAI;EACF,UAAU,mBAAmB,WAAW;CAC1C,QAAQ;EAEN,OAAO;GAAE,IAAI;GAAO,QAAQ;EAAI;CAClC;CAIA,IAAI,QAAQ,SAAS,IAAI,GACvB,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAI;CAQlC,IAAI,WAAW,QAAQ,QAAQ,UAAU,GAAG;CAG5C,MAAM,WAAW,SAAS,MAAM,GAAG;CACnC,MAAM,WAAqB,CAAC;CAC5B,KAAK,MAAM,OAAO,UAChB,IAAI,QAAQ,MAAM;EAChB,IAAI,SAAS,UAAU,GAErB,OAAO;GAAE,IAAI;GAAO,QAAQ;EAAI;EAElC,SAAS,IAAI;CACf,OAAO,IAAI,QAAQ,KACjB,SAAS,KAAK,GAAG;CAIrB,WAAW,SAAS,KAAK,GAAG,KAAK;CAGjC,IAAI,sBAAsB,SAAS,SAAS,KAAK,SAAS,SAAS,GAAG,GACpE,WAAW,SAAS,MAAM,GAAG,EAAE;CAGjC,OAAO;EAAE,IAAI;EAAM;CAAS;AAC9B;;;;;;;;;;;;;;;;;;ACJA,SAAgB,wBAAwB,UAAuC;CAC7E,MAAM,QAAkB,CAAC;CACzB,IAAI,OAAO;CAEX,KAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,OAAO,QAAQ,eAAe;EACpC,IAAI,CAAC,MAAM;GAET,MAAM,KAAK,GAAG;GACd,OAAO;GACP;EACF;EAEA,MAAM,OAAO,GADA,SAAS,MAAM,KAAK,KACZ,GAAG;EACxB,MAAM,KAAK,IAAI;EACf,OAAO;CACT;CAEA,OAAO;AACT;;;;;;;;;;;;AAaA,SAAgB,cAAc,UAA4B;CACxD,OAAO,aAAa,MAAM,CAAC,IAAI,SAAS,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG;AAC5D;;;;;;;;;;AAWA,SAAgB,cAAc,UAA0B;CACtD,OAAO,cAAc,QAAQ,CAAC,CAAC;AACjC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//#region src/shared/slot-params.ts
|
|
2
|
+
/**
|
|
3
|
+
* The params a slot sees: the main route's as a base, the slot's own on top.
|
|
4
|
+
*
|
|
5
|
+
* Params from segments *above* the slot are not re-derived by the slot's own
|
|
6
|
+
* chain, so they have to come from the main route; params the slot does
|
|
7
|
+
* re-derive may have a different type than the main route gave them, so the
|
|
8
|
+
* slot's win.
|
|
9
|
+
*
|
|
10
|
+
* Null-prototype, like every other param record the framework hands out
|
|
11
|
+
* (`coerceSegmentParams` installs one unconditionally). A spread literal would
|
|
12
|
+
* inherit `Object.prototype`, so a lookup for a param the route does not
|
|
13
|
+
* define would resolve to an inherited member — `params.constructor` returning
|
|
14
|
+
* a function instead of `undefined` — in the one param record that was built
|
|
15
|
+
* by merging. See design/13-security.md #36c.
|
|
16
|
+
*/
|
|
17
|
+
function mergeSlotParams(mainParams, slotParams) {
|
|
18
|
+
return Object.assign(Object.create(null), mainParams, slotParams);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the params for a segment path against a published slot map.
|
|
22
|
+
*
|
|
23
|
+
* Returns the merge when `segmentPath` names a slot that published params,
|
|
24
|
+
* and the main route's record otherwise — an ordinary (non-slot) segment
|
|
25
|
+
* path, an unknown path, or a request with no slots at all. Mirrors the
|
|
26
|
+
* server's `getSegmentParams(segmentPath)` branch exactly.
|
|
27
|
+
*
|
|
28
|
+
* `Object.hasOwn` rather than `in` or a truthiness test: the map arrives from
|
|
29
|
+
* JSON, so a segment path of `constructor` or `toString` would otherwise
|
|
30
|
+
* resolve to an inherited member of `Object.prototype`.
|
|
31
|
+
*/
|
|
32
|
+
function resolveSegmentParams(mainParams, slotParams, segmentPath) {
|
|
33
|
+
if (!segmentPath || !slotParams || !Object.hasOwn(slotParams, segmentPath)) return mainParams;
|
|
34
|
+
return cachedMerge(mainParams, slotParams, segmentPath);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The merged record for one (main record, slot map, path) triple, reused until
|
|
38
|
+
* one of the three changes.
|
|
39
|
+
*
|
|
40
|
+
* `useSegmentParams(slotPath)` calls this on every render. Returning a fresh
|
|
41
|
+
* object each time makes the hook's result change by reference on renders where
|
|
42
|
+
* nothing navigated, so a component using it as a `useEffect` dependency
|
|
43
|
+
* re-runs the effect — and re-renders in a loop if that effect sets state —
|
|
44
|
+
* while memoized children below it lose their memoization.
|
|
45
|
+
*
|
|
46
|
+
* Keyed on the *identities* of the two records, not their contents: both are
|
|
47
|
+
* replaced rather than mutated on every navigation (`setCurrentParams`,
|
|
48
|
+
* `setCurrentSlotParams`, and the server's per-request store), so identity is
|
|
49
|
+
* the correct equality here and a deep comparison would be slower and no more
|
|
50
|
+
* accurate.
|
|
51
|
+
*
|
|
52
|
+
* A `WeakMap` chain rather than a `Map`, so a superseded navigation's records
|
|
53
|
+
* and their merges become collectable as soon as nothing else holds them.
|
|
54
|
+
*/
|
|
55
|
+
var mergeCache = /* @__PURE__ */ new WeakMap();
|
|
56
|
+
function cachedMerge(mainParams, slotParams, segmentPath) {
|
|
57
|
+
let bySlotMap = mergeCache.get(mainParams);
|
|
58
|
+
if (!bySlotMap) {
|
|
59
|
+
bySlotMap = /* @__PURE__ */ new WeakMap();
|
|
60
|
+
mergeCache.set(mainParams, bySlotMap);
|
|
61
|
+
}
|
|
62
|
+
let byPath = bySlotMap.get(slotParams);
|
|
63
|
+
if (!byPath) {
|
|
64
|
+
byPath = /* @__PURE__ */ new Map();
|
|
65
|
+
bySlotMap.set(slotParams, byPath);
|
|
66
|
+
}
|
|
67
|
+
const cached = byPath.get(segmentPath);
|
|
68
|
+
if (cached) return cached;
|
|
69
|
+
const merged = mergeSlotParams(mainParams, slotParams[segmentPath]);
|
|
70
|
+
byPath.set(segmentPath, merged);
|
|
71
|
+
return merged;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { resolveSegmentParams as t };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=slot-params-BCTmZkQB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot-params-BCTmZkQB.js","names":[],"sources":["../../src/shared/slot-params.ts"],"sourcesContent":["/**\n * Per-slot segment params — the one definition both halves of the API read.\n *\n * A parallel slot matches the URL through its *own* sub-tree, so it can derive\n * a param the main route never had, or derive the same name with a different\n * type (`[...year]` → `string[]` where the main route's `[year]` gave\n * `string`). The server answers `getSegmentParams(slotPath)` from a per-slot\n * map; the client answers `useSegmentParams(slotPath)` from the same map,\n * published on the RSC payload (TIM-1285).\n *\n * The merge lives here rather than in either half because the two must agree\n * about what \"the slot's params\" are — a divergence is invisible until\n * somebody reads a param of the wrong type, which is exactly the class of bug\n * this module closes.\n *\n * Isomorphic: no server or client imports.\n *\n * See design/41-global-params.md §\"Params in a parallel slot\".\n */\n\n/** Slot tree path → that slot's own coerced params. */\nexport type SlotParamsRecord = Record<string, Record<string, string | string[]>>;\n\n/**\n * The params a slot sees: the main route's as a base, the slot's own on top.\n *\n * Params from segments *above* the slot are not re-derived by the slot's own\n * chain, so they have to come from the main route; params the slot does\n * re-derive may have a different type than the main route gave them, so the\n * slot's win.\n *\n * Null-prototype, like every other param record the framework hands out\n * (`coerceSegmentParams` installs one unconditionally). A spread literal would\n * inherit `Object.prototype`, so a lookup for a param the route does not\n * define would resolve to an inherited member — `params.constructor` returning\n * a function instead of `undefined` — in the one param record that was built\n * by merging. See design/13-security.md #36c.\n */\nexport function mergeSlotParams(\n mainParams: Record<string, string | string[]>,\n slotParams: Record<string, string | string[]>\n): Record<string, string | string[]> {\n return Object.assign(Object.create(null), mainParams, slotParams);\n}\n\n/**\n * Resolve the params for a segment path against a published slot map.\n *\n * Returns the merge when `segmentPath` names a slot that published params,\n * and the main route's record otherwise — an ordinary (non-slot) segment\n * path, an unknown path, or a request with no slots at all. Mirrors the\n * server's `getSegmentParams(segmentPath)` branch exactly.\n *\n * `Object.hasOwn` rather than `in` or a truthiness test: the map arrives from\n * JSON, so a segment path of `constructor` or `toString` would otherwise\n * resolve to an inherited member of `Object.prototype`.\n */\nexport function resolveSegmentParams(\n mainParams: Record<string, string | string[]>,\n slotParams: SlotParamsRecord | null | undefined,\n segmentPath: string | undefined\n): Record<string, string | string[]> {\n if (!segmentPath || !slotParams || !Object.hasOwn(slotParams, segmentPath)) return mainParams;\n return cachedMerge(mainParams, slotParams, segmentPath);\n}\n\n/**\n * The merged record for one (main record, slot map, path) triple, reused until\n * one of the three changes.\n *\n * `useSegmentParams(slotPath)` calls this on every render. Returning a fresh\n * object each time makes the hook's result change by reference on renders where\n * nothing navigated, so a component using it as a `useEffect` dependency\n * re-runs the effect — and re-renders in a loop if that effect sets state —\n * while memoized children below it lose their memoization.\n *\n * Keyed on the *identities* of the two records, not their contents: both are\n * replaced rather than mutated on every navigation (`setCurrentParams`,\n * `setCurrentSlotParams`, and the server's per-request store), so identity is\n * the correct equality here and a deep comparison would be slower and no more\n * accurate.\n *\n * A `WeakMap` chain rather than a `Map`, so a superseded navigation's records\n * and their merges become collectable as soon as nothing else holds them.\n */\nconst mergeCache = new WeakMap<\n object,\n WeakMap<object, Map<string, Record<string, string | string[]>>>\n>();\n\nfunction cachedMerge(\n mainParams: Record<string, string | string[]>,\n slotParams: SlotParamsRecord,\n segmentPath: string\n): Record<string, string | string[]> {\n let bySlotMap = mergeCache.get(mainParams);\n if (!bySlotMap) {\n bySlotMap = new WeakMap();\n mergeCache.set(mainParams, bySlotMap);\n }\n let byPath = bySlotMap.get(slotParams);\n if (!byPath) {\n byPath = new Map();\n bySlotMap.set(slotParams, byPath);\n }\n const cached = byPath.get(segmentPath);\n if (cached) return cached;\n const merged = mergeSlotParams(mainParams, slotParams[segmentPath]);\n byPath.set(segmentPath, merged);\n return merged;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAsCA,SAAgB,gBACd,YACA,YACmC;CACnC,OAAO,OAAO,OAAO,OAAO,OAAO,IAAI,GAAG,YAAY,UAAU;AAClE;;;;;;;;;;;;;AAcA,SAAgB,qBACd,YACA,YACA,aACmC;CACnC,IAAI,CAAC,eAAe,CAAC,cAAc,CAAC,OAAO,OAAO,YAAY,WAAW,GAAG,OAAO;CACnF,OAAO,YAAY,YAAY,YAAY,WAAW;AACxD;;;;;;;;;;;;;;;;;;;;AAqBA,IAAM,6BAAa,IAAI,QAGrB;AAEF,SAAS,YACP,YACA,YACA,aACmC;CACnC,IAAI,YAAY,WAAW,IAAI,UAAU;CACzC,IAAI,CAAC,WAAW;EACd,4BAAY,IAAI,QAAQ;EACxB,WAAW,IAAI,YAAY,SAAS;CACtC;CACA,IAAI,SAAS,UAAU,IAAI,UAAU;CACrC,IAAI,CAAC,QAAQ;EACX,yBAAS,IAAI,IAAI;EACjB,UAAU,IAAI,YAAY,MAAM;CAClC;CACA,MAAM,SAAS,OAAO,IAAI,WAAW;CACrC,IAAI,QAAQ,OAAO;CACnB,MAAM,SAAS,gBAAgB,YAAY,WAAW,YAAY;CAClE,OAAO,IAAI,aAAa,MAAM;CAC9B,OAAO;AACT"}
|
|
@@ -19,6 +19,16 @@ var currentParams = {};
|
|
|
19
19
|
function _setCurrentParams(params) {
|
|
20
20
|
currentParams = params;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Per-slot params snapshot, keyed by slot tree path — the module-level
|
|
24
|
+
* fallback behind `useSegmentParams(slotPath)` when no NavigationContext is
|
|
25
|
+
* mounted (tests, calls outside a component). Replaced, never mutated, on
|
|
26
|
+
* each navigation. See TIM-1285.
|
|
27
|
+
*/
|
|
28
|
+
var currentSlotParams = null;
|
|
29
|
+
function _setCurrentSlotParams(slotParams) {
|
|
30
|
+
currentSlotParams = slotParams;
|
|
31
|
+
}
|
|
22
32
|
/** Cached search string — avoids reparsing when URL hasn't changed. */
|
|
23
33
|
var cachedSearch = "";
|
|
24
34
|
var cachedSearchParams = new URLSearchParams();
|
|
@@ -26,6 +36,11 @@ function _setCachedSearch(search, params) {
|
|
|
26
36
|
cachedSearch = search;
|
|
27
37
|
cachedSearchParams = params;
|
|
28
38
|
}
|
|
39
|
+
/** Whether the page is currently being unloaded. */
|
|
40
|
+
var unloading = false;
|
|
41
|
+
function _setUnloading(value) {
|
|
42
|
+
unloading = value;
|
|
43
|
+
}
|
|
29
44
|
//#endregion
|
|
30
45
|
//#region src/client/ssr-data.ts
|
|
31
46
|
/**
|
|
@@ -83,6 +98,6 @@ function getSsrData() {
|
|
|
83
98
|
return currentSsrData;
|
|
84
99
|
}
|
|
85
100
|
//#endregion
|
|
86
|
-
export { _setCurrentParams as a,
|
|
101
|
+
export { _setCurrentParams as a, _setUnloading as c, currentParams as d, currentSlotParams as f, _setCachedSearch as i, cachedSearch as l, unloading as m, getSsrData as n, _setCurrentSlotParams as o, globalRouter as p, setSsrData as r, _setGlobalRouter as s, clearSsrData as t, cachedSearchParams as u };
|
|
87
102
|
|
|
88
|
-
//# sourceMappingURL=ssr-data-
|
|
103
|
+
//# sourceMappingURL=ssr-data-14MXm7Pj.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssr-data-14MXm7Pj.js","names":[],"sources":["../../src/client/state.ts","../../src/client/ssr-data.ts"],"sourcesContent":["/**\n * Centralized client singleton state registry.\n *\n * ALL mutable module-level state that must have singleton semantics across\n * the client bundle lives here. Individual modules (router-ref.ts, ssr-data.ts,\n * use-segment-params.ts, use-search-params.ts, unload-guard.ts) import from this file\n * and re-export thin wrapper functions.\n *\n * Why: In Vite dev, a module is instantiated separately if reached via different\n * import paths (e.g., relative `./foo.js` vs barrel `@timber-js/app/client`).\n * By centralizing all mutable state in a single module that is always reached\n * through the same dependency chain (barrel → wrapper → state.ts), we guarantee\n * a single instance of every piece of shared state.\n *\n * DO NOT import this file from outside client/. Server code must never depend\n * on client state. The barrel (client/index.ts) is the public entry point.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\".\n */\n\nimport type { SlotParamsRecord } from '../shared/slot-params.js';\nimport type { RouterInstance } from './router-types.js';\nimport type { SsrData } from './ssr-data.js';\n\n// ─── Router (from router-ref.ts) ──────────────────────────────────────────\n\n/** The global router singleton — set once during bootstrap. */\nexport let globalRouter: RouterInstance | null = null;\n\nexport function _setGlobalRouter(router: RouterInstance | null): void {\n globalRouter = router;\n}\n\n// ─── SSR Data Provider (from ssr-data.ts) ──────────────────────────────────\n\n/**\n * ALS-backed SSR data provider. When registered, getSsrData() reads from\n * this function (ALS store) instead of module-level currentSsrData.\n */\nexport let ssrDataProvider: (() => SsrData | undefined) | undefined;\n\nexport function _setSsrDataProvider(provider: (() => SsrData | undefined) | undefined): void {\n ssrDataProvider = provider;\n}\n\n/** Fallback SSR data for tests and environments without ALS. */\nexport let currentSsrData: SsrData | undefined;\n\nexport function _setCurrentSsrData(data: SsrData | undefined): void {\n currentSsrData = data;\n}\n\n// ─── Route Params (from use-segment-params.ts) ──────────────────────────────────────\n\n/** Current route params snapshot — replaced (not mutated) on each navigation. */\nexport let currentParams: Record<string, string | string[]> = {};\n\nexport function _setCurrentParams(params: Record<string, string | string[]>): void {\n currentParams = params;\n}\n\n/**\n * Per-slot params snapshot, keyed by slot tree path — the module-level\n * fallback behind `useSegmentParams(slotPath)` when no NavigationContext is\n * mounted (tests, calls outside a component). Replaced, never mutated, on\n * each navigation. See TIM-1285.\n */\nexport let currentSlotParams: SlotParamsRecord | null = null;\n\nexport function _setCurrentSlotParams(slotParams: SlotParamsRecord | null): void {\n currentSlotParams = slotParams;\n}\n\n/** Listeners notified when currentParams changes. */\nexport const paramsListeners = new Set<() => void>();\n\n// ─── Search Params Cache (from use-search-params.ts) ────────────────────────\n\n/** Cached search string — avoids reparsing when URL hasn't changed. */\nexport let cachedSearch = '';\nexport let cachedSearchParams = new URLSearchParams();\n\nexport function _setCachedSearch(search: string, params: URLSearchParams): void {\n cachedSearch = search;\n cachedSearchParams = params;\n}\n\n// ─── Unload Guard (from unload-guard.ts) ─────────────────────────────────────\n\n/** Whether the page is currently being unloaded. */\nexport let unloading = false;\n\nexport function _setUnloading(value: boolean): void {\n unloading = value;\n}\n","/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n */\n\nimport {\n ssrDataProvider,\n currentSsrData,\n _setSsrDataProvider,\n _setCurrentSsrData,\n} from './state.js';\nimport type { SlotParamsRecord } from '../shared/slot-params.js';\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: Record<string, string | string[]>;\n /**\n * Per-slot params keyed by slot tree path, absent when the request rendered\n * no slot with params of its own (TIM-1285).\n */\n slotParams?: SlotParamsRecord;\n /**\n * Mutable reference to NavContext for error boundary → pipeline communication.\n *\n * When TimberErrorBoundary catches a DenySignal during SSR, it:\n * 1. Sets `statusCode` to the deny status (e.g., 403) — so the HTTP\n * Response has the correct status code without a re-render.\n * 2. Sets `_denyHandledByBoundary = true` — so the pipeline skips\n * the redundant renderDenyPage() re-render.\n *\n * This runs synchronously during Fizz rendering, BEFORE onShellReady,\n * so the status code is committed before any bytes are sent.\n *\n * See TIM-664, design/04-authorization.md §\"React.cache Scope in Deny/Error Re-renders\"\n */\n _navContext?: { statusCode?: number; _denyHandledByBoundary?: boolean };\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n//\n// Module singleton guarantee: In Vite's SSR environment, both\n// ssr-entry.ts (via #/client/ssr-data.js) and client component hooks\n// (via @timber-js/app/client) must resolve to the SAME module instance\n// of this file. The timber-shims plugin ensures this by remapping\n// @timber-js/app/client → src/client/index.ts in the SSR environment.\n// Without this remap, @timber-js/app/client resolves to dist/ (via\n// package.json exports), creating a split where registerSsrDataProvider\n// writes to one instance but getSsrData reads from another.\n// See timber-shims plugin resolveId for details.\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _setSsrDataProvider(provider);\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n _setCurrentSsrData(data);\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n _setCurrentSsrData(undefined);\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (ssrDataProvider) {\n return ssrDataProvider();\n }\n return currentSsrData;\n}\n"],"mappings":";;AA4BA,IAAW,eAAsC;AAEjD,SAAgB,iBAAiB,QAAqC;CACpE,eAAe;AACjB;;;;;AAQA,IAAW;;AAOX,IAAW;AAEX,SAAgB,mBAAmB,MAAiC;CAClE,iBAAiB;AACnB;;AAKA,IAAW,gBAAmD,CAAC;AAE/D,SAAgB,kBAAkB,QAAiD;CACjF,gBAAgB;AAClB;;;;;;;AAQA,IAAW,oBAA6C;AAExD,SAAgB,sBAAsB,YAA2C;CAC/E,oBAAoB;AACtB;;AAQA,IAAW,eAAe;AAC1B,IAAW,qBAAqB,IAAI,gBAAgB;AAEpD,SAAgB,iBAAiB,QAAgB,QAA+B;CAC9E,eAAe;CACf,qBAAqB;AACvB;;AAKA,IAAW,YAAY;AAEvB,SAAgB,cAAc,OAAsB;CAClD,YAAY;AACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQA,SAAgB,WAAW,MAAqB;CAC9C,mBAAmB,IAAI;AACzB;;;;;;;AAQA,SAAgB,eAAqB;CACnC,mBAAmB,KAAA,CAAS;AAC9B;;;;;;;;;;AAWA,SAAgB,aAAkC;CAChD,IAAI,iBACF,OAAO,gBAAgB;CAEzB,OAAO;AACT"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { t as resolveSegmentParams } from "./slot-params-BCTmZkQB.js";
|
|
2
|
+
import { a as _setCurrentParams, d as currentParams, f as currentSlotParams, n as getSsrData, o as _setCurrentSlotParams } from "./ssr-data-14MXm7Pj.js";
|
|
3
|
+
import "./param-value-C8TNYchQ.js";
|
|
4
|
+
import React from "react";
|
|
5
|
+
//#region src/client/params-context.ts
|
|
6
|
+
/**
|
|
7
|
+
* Segment params context — the one channel params use to reach the browser.
|
|
8
|
+
*
|
|
9
|
+
* Params ride the RSC payload's root row as a sibling of the tree
|
|
10
|
+
* (`{ tree, params, slotParams }`), rather than in four side channels that
|
|
11
|
+
* raced to seed them: a response header, an inline script, and two build-time
|
|
12
|
+
* manifest fields all previously carried the same record, each with its own
|
|
13
|
+
* `JSON.stringify` (TIM-1294).
|
|
14
|
+
*
|
|
15
|
+
* Riding the payload is what makes them *typed*. `defineSchema` takes any
|
|
16
|
+
* `Codec<T>`, so a coerced param is whatever the codec returned — a `Date`, a
|
|
17
|
+
* `bigint` — and `JSON.stringify` either flattened it to a string or threw
|
|
18
|
+
* mid-response. React Flight carries those values natively, so the client
|
|
19
|
+
* reads the value the server produced instead of a lossy copy of it. See
|
|
20
|
+
* design/41-global-params.md §"Transport".
|
|
21
|
+
*
|
|
22
|
+
* **The client owns the provider.** There is exactly one `ParamsProvider` in
|
|
23
|
+
* the browser's tree, rendered by `PayloadRoot` above the point where a
|
|
24
|
+
* partial navigation splices the new payload into the retained tree. It has to
|
|
25
|
+
* be there and it has to be alone: a provider *inside* the payload lands below
|
|
26
|
+
* the retained region, whose own root is the departing route's provider, so
|
|
27
|
+
* every reader in a skipped layout resolves to the departing record and no
|
|
28
|
+
* amount of wrapping above it helps (TIM-1297).
|
|
29
|
+
*
|
|
30
|
+
* Ordering still holds without a bootstrap contract, for the same reason it
|
|
31
|
+
* did when the provider was in the tree: a provider renders before its own
|
|
32
|
+
* descendants by construction, so `useSegmentParams()` is correct during
|
|
33
|
+
* hydration without anything having to run before `hydrateRoot()`.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* SINGLETON GUARANTEE: globalThis + `Symbol.for`, the same pattern as
|
|
37
|
+
* `NavigationContext` and `SegmentUpdateContext`.
|
|
38
|
+
*
|
|
39
|
+
* The RSC client bundler can duplicate a module across chunks, and with ESM
|
|
40
|
+
* output each chunk gets its own module scope — so a bare `createContext` at
|
|
41
|
+
* module level yields one context per chunk. This module is now reached from
|
|
42
|
+
* *both* graphs: `PayloadRoot` is imported by the browser entry, while
|
|
43
|
+
* `useParamsContext()` arrives through the client-reference graph with the
|
|
44
|
+
* app's own components. A duplicate would put the provider on instance A and
|
|
45
|
+
* every reader on instance B, so `useContext` returns `null` and every
|
|
46
|
+
* `useSegmentParams()` call silently falls back to the module snapshot.
|
|
47
|
+
*
|
|
48
|
+
* This module was the one client context without the guard — harmless while
|
|
49
|
+
* the provider travelled inside the payload, in the same graph as its readers,
|
|
50
|
+
* and load-bearing the moment the client started rendering it (TIM-1297).
|
|
51
|
+
*
|
|
52
|
+
* The React APIs are reached through the namespace rather than named imports,
|
|
53
|
+
* for the same reason `segment-update-context.ts` and `navigation-context.ts`
|
|
54
|
+
* do it: React's `react-server` export provides neither `createContext` nor
|
|
55
|
+
* `useContext`, and a *named* ESM import of a missing export fails at module
|
|
56
|
+
* instantiation — before any feature check could run. This module is reachable
|
|
57
|
+
* from `@timber-js/app/segment-params`, which a Server Component imports for
|
|
58
|
+
* `defineSegmentParams`, so the named form crashed that entry outright
|
|
59
|
+
* (codex, PR #992; reproduced with
|
|
60
|
+
* `node --conditions react-server -e "import('./dist/segment-params/index.js')"`).
|
|
61
|
+
*
|
|
62
|
+
* See design/19-client-navigation.md §"Singleton Guarantee via globalThis"
|
|
63
|
+
*/
|
|
64
|
+
var PARAMS_CTX_KEY = Symbol.for("__timber_params_ctx");
|
|
65
|
+
function getOrCreateContext() {
|
|
66
|
+
const store = globalThis;
|
|
67
|
+
const existing = store[PARAMS_CTX_KEY];
|
|
68
|
+
if (existing !== void 0) return existing;
|
|
69
|
+
if (typeof React.createContext !== "function") return;
|
|
70
|
+
const ctx = React.createContext(null);
|
|
71
|
+
store[PARAMS_CTX_KEY] = ctx;
|
|
72
|
+
return ctx;
|
|
73
|
+
}
|
|
74
|
+
var ParamsContext = getOrCreateContext();
|
|
75
|
+
/**
|
|
76
|
+
* Read the params provided by the tree. Returns null when no provider is
|
|
77
|
+
* above the caller — a component rendered outside a timber route, a
|
|
78
|
+
* `useSegmentParams()` call from outside React entirely, or any component
|
|
79
|
+
* during SSR (where the params reach the hook through the ALS-backed SSR data
|
|
80
|
+
* context instead, and there is no client-owned tree to hold a provider).
|
|
81
|
+
*/
|
|
82
|
+
function useParamsContext() {
|
|
83
|
+
return React.useContext(ParamsContext);
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/client/use-segment-params.ts
|
|
87
|
+
/**
|
|
88
|
+
* Set the current route params in the module-level store.
|
|
89
|
+
*
|
|
90
|
+
* Called by the router on each navigation. This updates the fallback
|
|
91
|
+
* snapshot used by tests and by the hook when called outside a React
|
|
92
|
+
* component (no NavigationContext available).
|
|
93
|
+
*
|
|
94
|
+
* On the client, the primary reactivity path is NavigationContext —
|
|
95
|
+
* the router calls setNavigationState() then renderRoot() which wraps
|
|
96
|
+
* the element in NavigationProvider. setCurrentParams is still called
|
|
97
|
+
* for the module-level fallback.
|
|
98
|
+
*
|
|
99
|
+
* During SSR, params are also available via getSsrData().params
|
|
100
|
+
* (ALS-backed).
|
|
101
|
+
*/
|
|
102
|
+
function setCurrentParams(params) {
|
|
103
|
+
_setCurrentParams(params);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Set the per-slot params snapshot in the module-level store.
|
|
107
|
+
*
|
|
108
|
+
* Paired with `setCurrentParams`: the router calls both on every navigation,
|
|
109
|
+
* including with `null` when a response carries no slot params, so a slot's
|
|
110
|
+
* params from the *previous* route cannot be read on the next one. Fill and
|
|
111
|
+
* serve are paired; so are fill and clear. See TIM-1285.
|
|
112
|
+
*/
|
|
113
|
+
function setCurrentSlotParams(slotParams) {
|
|
114
|
+
_setCurrentSlotParams(slotParams);
|
|
115
|
+
}
|
|
116
|
+
function useSegmentParams(segmentPath) {
|
|
117
|
+
try {
|
|
118
|
+
const paramsContext = useParamsContext();
|
|
119
|
+
if (paramsContext !== null) return resolveSegmentParams(paramsContext.params, paramsContext.slotParams, segmentPath);
|
|
120
|
+
} catch {}
|
|
121
|
+
const ssrData = getSsrData();
|
|
122
|
+
if (ssrData) return resolveSegmentParams(ssrData.params, ssrData.slotParams, segmentPath);
|
|
123
|
+
return resolveSegmentParams(currentParams, currentSlotParams, segmentPath);
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export { setCurrentSlotParams as n, useSegmentParams as r, setCurrentParams as t };
|
|
127
|
+
|
|
128
|
+
//# sourceMappingURL=use-segment-params-ClyUNq4d.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-segment-params-ClyUNq4d.js","names":[],"sources":["../../src/client/params-context.ts","../../src/client/use-segment-params.ts"],"sourcesContent":["/**\n * Segment params context — the one channel params use to reach the browser.\n *\n * Params ride the RSC payload's root row as a sibling of the tree\n * (`{ tree, params, slotParams }`), rather than in four side channels that\n * raced to seed them: a response header, an inline script, and two build-time\n * manifest fields all previously carried the same record, each with its own\n * `JSON.stringify` (TIM-1294).\n *\n * Riding the payload is what makes them *typed*. `defineSchema` takes any\n * `Codec<T>`, so a coerced param is whatever the codec returned — a `Date`, a\n * `bigint` — and `JSON.stringify` either flattened it to a string or threw\n * mid-response. React Flight carries those values natively, so the client\n * reads the value the server produced instead of a lossy copy of it. See\n * design/41-global-params.md §\"Transport\".\n *\n * **The client owns the provider.** There is exactly one `ParamsProvider` in\n * the browser's tree, rendered by `PayloadRoot` above the point where a\n * partial navigation splices the new payload into the retained tree. It has to\n * be there and it has to be alone: a provider *inside* the payload lands below\n * the retained region, whose own root is the departing route's provider, so\n * every reader in a skipped layout resolves to the departing record and no\n * amount of wrapping above it helps (TIM-1297).\n *\n * Ordering still holds without a bootstrap contract, for the same reason it\n * did when the provider was in the tree: a provider renders before its own\n * descendants by construction, so `useSegmentParams()` is correct during\n * hydration without anything having to run before `hydrateRoot()`.\n */\n\n'use client';\n\nimport React, { createElement, useMemo, use } from 'react';\nimport { _setCurrentParams, _setCurrentSlotParams } from './state.js';\nimport { toNullProtoRecord } from '../shared/param-value.js';\nimport { readPublishedParams, type PublishedParams } from '../shared/payload-root.js';\nimport type { SlotParamsRecord } from '../shared/slot-params.js';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type ParamsContextValue = PublishedParams;\n\n// ─── Context ─────────────────────────────────────────────────────\n\n/**\n * SINGLETON GUARANTEE: globalThis + `Symbol.for`, the same pattern as\n * `NavigationContext` and `SegmentUpdateContext`.\n *\n * The RSC client bundler can duplicate a module across chunks, and with ESM\n * output each chunk gets its own module scope — so a bare `createContext` at\n * module level yields one context per chunk. This module is now reached from\n * *both* graphs: `PayloadRoot` is imported by the browser entry, while\n * `useParamsContext()` arrives through the client-reference graph with the\n * app's own components. A duplicate would put the provider on instance A and\n * every reader on instance B, so `useContext` returns `null` and every\n * `useSegmentParams()` call silently falls back to the module snapshot.\n *\n * This module was the one client context without the guard — harmless while\n * the provider travelled inside the payload, in the same graph as its readers,\n * and load-bearing the moment the client started rendering it (TIM-1297).\n *\n * The React APIs are reached through the namespace rather than named imports,\n * for the same reason `segment-update-context.ts` and `navigation-context.ts`\n * do it: React's `react-server` export provides neither `createContext` nor\n * `useContext`, and a *named* ESM import of a missing export fails at module\n * instantiation — before any feature check could run. This module is reachable\n * from `@timber-js/app/segment-params`, which a Server Component imports for\n * `defineSegmentParams`, so the named form crashed that entry outright\n * (codex, PR #992; reproduced with\n * `node --conditions react-server -e \"import('./dist/segment-params/index.js')\"`).\n *\n * See design/19-client-navigation.md §\"Singleton Guarantee via globalThis\"\n */\nconst PARAMS_CTX_KEY = Symbol.for('__timber_params_ctx');\n\nfunction getOrCreateContext(): React.Context<ParamsContextValue | null> {\n const store = globalThis as Record<symbol, unknown>;\n const existing = store[PARAMS_CTX_KEY] as React.Context<ParamsContextValue | null> | undefined;\n if (existing !== undefined) return existing;\n if (typeof React.createContext !== 'function') {\n // RSC environment — no contexts here. Nothing in this module runs on that\n // side; it only has to import cleanly.\n return undefined as unknown as React.Context<ParamsContextValue | null>;\n }\n const ctx = React.createContext<ParamsContextValue | null>(null);\n store[PARAMS_CTX_KEY] = ctx;\n return ctx;\n}\n\nconst ParamsContext = getOrCreateContext();\n\n/**\n * Read the params provided by the tree. Returns null when no provider is\n * above the caller — a component rendered outside a timber route, a\n * `useSegmentParams()` call from outside React entirely, or any component\n * during SSR (where the params reach the hook through the ALS-backed SSR data\n * context instead, and there is no client-owned tree to hold a provider).\n */\nexport function useParamsContext(): ParamsContextValue | null {\n return React.useContext(ParamsContext);\n}\n\n// ─── Provider ────────────────────────────────────────────────────\n\ninterface ParamsProviderProps {\n params: Record<string, string | string[]>;\n slotParams: SlotParamsRecord | null;\n children?: React.ReactNode;\n}\n\n/**\n * Provides the current navigation's params to everything below it.\n *\n * Rendered only by `PayloadRoot`. Not exported: a second provider anywhere in\n * the tree would shadow this one for the region below it, which is precisely\n * the defect TIM-1297 fixed.\n *\n * The module-level snapshot in `state.ts` is written during render rather\n * than in an effect. It is the fallback path for `useSegmentParams()` called\n * outside a component (tests, module scope), and an effect would leave that\n * path reading the *previous* route's params for the whole commit — the\n * window in which a navigation's components actually run. Writing during\n * render is safe here because the value is derived entirely from props: a\n * double-invoked render in StrictMode writes the same record twice.\n */\nfunction ParamsProvider({ params, slotParams, children }: ParamsProviderProps) {\n // Restore the null prototype the wire could not carry. Flight rejects a\n // null-prototype object, so `withPublishedParams` flattens the records;\n // rebuilding them here is what keeps `params.constructor` returning\n // `undefined` instead of a function for a param the route does not define\n // (design/13-security.md #36c). Memoized on the props so a re-render with\n // the same records does not rebuild — the identity of what the hook returns\n // is load-bearing for `useEffect` dependencies (TIM-1285).\n const value = useMemo(\n () => ({\n params: toNullProtoRecord(params),\n slotParams: toNullProtoRecord(slotParams),\n }),\n [params, slotParams]\n );\n\n // Keep the out-of-component fallback in step with the tree being rendered.\n _setCurrentParams(value.params);\n _setCurrentSlotParams(value.slotParams);\n\n return createElement(ParamsContext.Provider, { value }, children);\n}\n\n// ─── Payload root ────────────────────────────────────────────────\n\n/**\n * The client's root: publishes a payload's params over the tree being shown.\n *\n * Rendered at the same position in the wrapper chain on **every** render path\n * — hydration, full navigation, partial navigation, popstate replay, shallow\n * search sync, and revalidation from a server action. Being unconditional is\n * load-bearing twice over: an element type that appears on one render and not\n * the next remounts everything below it, destroying exactly the layout state a\n * partial navigation exists to preserve; and a reader in a skipped layout has\n * to have *some* provider above it on every path or it falls back to the\n * module-level snapshot.\n *\n * `children` is the tree to display, which is not always `source`'s tree:\n *\n * - Full navigation, hydration, replay — `source` is the payload being shown,\n * and `children` is its own tree.\n * - **Partial navigation** — `children` is the *retained* tree and `source` is\n * the *incoming* payload. This is the case the whole design exists for: the\n * retained tree is not re-rendered, so the destination's params can only\n * reach it from above, and this provider is above it.\n *\n * `source` may be a thenable, in which case this suspends on the payload's\n * root row. That happens on the hydration path only, where the payload\n * promise was going to be rendered at this position anyway. Every other path\n * resolves the row in the router — inside the navigation transition — and\n * hands over a settled value, so a decode rejection surfaces where React\n * renders the tree and is caught by the error boundary *around* it, rather\n * than here, above every boundary the app has.\n */\nexport function PayloadRoot({ source, children }: { source: unknown; children?: React.ReactNode }) {\n const resolved = isThenable(source) ? use(source) : source;\n const { params, slotParams } = readPublishedParams(resolved);\n return createElement(ParamsProvider, { params, slotParams }, children);\n}\n\nfunction isThenable(value: unknown): value is Promise<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { then?: unknown }).then === 'function'\n );\n}\n","/**\n * useParams() — client-side hook for accessing route params.\n *\n * Returns the dynamic route parameters for the current URL.\n * When called with a route pattern argument, TypeScript narrows\n * the return type to the exact params shape for that route.\n *\n * Two layers of type narrowing work together:\n * 1. The generic overload here uses the Routes interface directly —\n * `useParams<R>()` returns `Routes[R]['segmentParams']`.\n * 2. Build-time codegen generates per-route string-literal overloads\n * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).\n *\n * When the Routes interface is empty (no codegen yet), the generic\n * overload has `keyof Routes = never`, so only the fallback matches.\n *\n * During SSR, params are read from the ALS-backed SSR data context\n * (populated by ssr-entry.ts) to ensure correct per-request isolation\n * across concurrent requests with streaming Suspense.\n *\n * Reactivity: On the client, useParams() reads from ParamsContext, published\n * by the one provider the client renders above the merge point\n * (`PayloadRoot`). Params update atomically with the tree because they travel\n * on the same payload root — there is no separate channel that could be\n * seeded a render early or late (TIM-1294, TIM-1297).\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * Design doc: design/09-typescript.md §\"Typed Routes\"\n */\n\nimport type { Routes } from '../index.js';\nimport { getSsrData } from './ssr-data.js';\nimport {\n currentParams,\n currentSlotParams,\n _setCurrentParams,\n _setCurrentSlotParams,\n paramsListeners,\n} from './state.js';\nimport { resolveSegmentParams, type SlotParamsRecord } from '../shared/slot-params.js';\nimport { useParamsContext } from './params-context.js';\n\n// ---------------------------------------------------------------------------\n// Module-level subscribe/notify pattern — kept for backward compat and tests\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to params changes.\n * Retained for backward compatibility with tests that verify the\n * subscribe/notify contract. On the client, useParams() reads from\n * NavigationContext instead.\n */\nexport function subscribe(callback: () => void): () => void {\n paramsListeners.add(callback);\n return () => paramsListeners.delete(callback);\n}\n\n/**\n * Get the current params snapshot (module-level fallback).\n * Used by tests and by the hook when called outside a React component.\n */\nexport function getSnapshot(): Record<string, string | string[]> {\n return currentParams;\n}\n\n// ---------------------------------------------------------------------------\n// Framework API — called by the segment router on each navigation\n// ---------------------------------------------------------------------------\n\n/**\n * Set the current route params in the module-level store.\n *\n * Called by the router on each navigation. This updates the fallback\n * snapshot used by tests and by the hook when called outside a React\n * component (no NavigationContext available).\n *\n * On the client, the primary reactivity path is NavigationContext —\n * the router calls setNavigationState() then renderRoot() which wraps\n * the element in NavigationProvider. setCurrentParams is still called\n * for the module-level fallback.\n *\n * During SSR, params are also available via getSsrData().params\n * (ALS-backed).\n */\nexport function setCurrentParams(params: Record<string, string | string[]>): void {\n _setCurrentParams(params);\n}\n\n/**\n * Set the per-slot params snapshot in the module-level store.\n *\n * Paired with `setCurrentParams`: the router calls both on every navigation,\n * including with `null` when a response carries no slot params, so a slot's\n * params from the *previous* route cannot be read on the next one. Fill and\n * serve are paired; so are fill and clear. See TIM-1285.\n */\nexport function setCurrentSlotParams(slotParams: SlotParamsRecord | null): void {\n _setCurrentSlotParams(slotParams);\n}\n\n/**\n * Notify all legacy subscribers that params have changed.\n *\n * Retained for backward compatibility with tests. On the client,\n * the NavigationContext + renderRoot pattern replaces this — params\n * update atomically with the tree render, so explicit notification\n * is no longer needed.\n */\nexport function notifyParamsListeners(): void {\n for (const listener of paramsListeners) {\n listener();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public hook\n// ---------------------------------------------------------------------------\n\n/**\n * Read the current route's dynamic params.\n *\n * The optional `_route` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value.\n *\n * On the client, reads from ParamsContext, published by `PayloadRoot` above\n * everything the navigation renders. Params update atomically with the RSC\n * tree — no timing gap.\n *\n * During SSR, reads from the ALS-backed SSR data context to ensure\n * per-request isolation across concurrent requests with streaming Suspense.\n *\n * When called outside a React component (e.g., in test assertions),\n * falls back to the module-level snapshot.\n *\n * @overload Typed — when a known segment path is passed, returns the\n * exact params shape from the generated Routes interface.\n * @overload Fallback — returns the generic params record.\n */\nexport function useSegmentParams<R extends keyof Routes>(\n segmentPath: R\n): Routes[R] extends { segmentParams: infer P } ? P : Record<string, string | string[]>;\nexport function useSegmentParams(segmentPath?: string): Record<string, string | string[]>;\nexport function useSegmentParams(segmentPath?: string): Record<string, string | string[]> {\n // Try the client-owned provider first. It sits above everything a navigation\n // renders, so any component on the page — initial document, full navigation,\n // or a layout the server skipped — has one above it. Absent during SSR,\n // where the ALS path below is the answer. When called outside a React\n // component, useContext throws — caught below.\n try {\n // eslint-disable-next-line react-hooks/rules-of-hooks -- conditional on environment, not render path\n const paramsContext = useParamsContext();\n if (paramsContext !== null) {\n return resolveSegmentParams(paramsContext.params, paramsContext.slotParams, segmentPath);\n }\n } catch {\n // No React dispatcher available (called outside a component).\n // Fall through to module-level snapshot below.\n }\n\n // SSR path: read from ALS-backed SSR data context.\n // Falls back to module-level currentParams for tests.\n const ssrData = getSsrData();\n if (ssrData) return resolveSegmentParams(ssrData.params, ssrData.slotParams, segmentPath);\n return resolveSegmentParams(currentParams, currentSlotParams, segmentPath);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyEA,IAAM,iBAAiB,OAAO,IAAI,qBAAqB;AAEvD,SAAS,qBAA+D;CACtE,MAAM,QAAQ;CACd,MAAM,WAAW,MAAM;CACvB,IAAI,aAAa,KAAA,GAAW,OAAO;CACnC,IAAI,OAAO,MAAM,kBAAkB,YAGjC;CAEF,MAAM,MAAM,MAAM,cAAyC,IAAI;CAC/D,MAAM,kBAAkB;CACxB,OAAO;AACT;AAEA,IAAM,gBAAgB,mBAAmB;;;;;;;;AASzC,SAAgB,mBAA8C;CAC5D,OAAO,MAAM,WAAW,aAAa;AACvC;;;;;;;;;;;;;;;;;;ACdA,SAAgB,iBAAiB,QAAiD;CAChF,kBAAkB,MAAM;AAC1B;;;;;;;;;AAUA,SAAgB,qBAAqB,YAA2C;CAC9E,sBAAsB,UAAU;AAClC;AA4CA,SAAgB,iBAAiB,aAAyD;CAMxF,IAAI;EAEF,MAAM,gBAAgB,iBAAiB;EACvC,IAAI,kBAAkB,MACpB,OAAO,qBAAqB,cAAc,QAAQ,cAAc,YAAY,WAAW;CAE3F,QAAQ,CAGR;CAIA,MAAM,UAAU,WAAW;CAC3B,IAAI,SAAS,OAAO,qBAAqB,QAAQ,QAAQ,QAAQ,YAAY,WAAW;CACxF,OAAO,qBAAqB,eAAe,mBAAmB,WAAW;AAC3E"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { n as
|
|
1
|
+
import { n as findSchemaFile, r as parseExistingSchemaKeys } from "./cli-schema-sync-CKgHC2MB.js";
|
|
2
|
+
import { n as classifyUrlSegment, r as effectiveUrlSegment } from "./segment-classify-C539Pa2O.js";
|
|
3
3
|
import { parseAst } from "vite";
|
|
4
4
|
import { join, posix, relative } from "node:path";
|
|
5
5
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
@@ -508,16 +508,17 @@ function detectSchema(appDir) {
|
|
|
508
508
|
* A route entry is created for any segment that has a `page` or `route` file.
|
|
509
509
|
* Params accumulate from ancestor dynamic segments.
|
|
510
510
|
*/
|
|
511
|
-
function collectRoutes(node, ancestorParams, ancestorParamsFiles, parentTreePath, routes, hasSchema, insideSlot) {
|
|
511
|
+
function collectRoutes(node, ancestorParams, ancestorParamsFiles, parentTreePath, routes, hasSchema, insideSlot, insideIntercepting = false) {
|
|
512
512
|
const treePath = node.segmentName ? `${parentTreePath}/${node.segmentName}` : parentTreePath;
|
|
513
513
|
const ownParamsFile = node.params && fileHasExport(node.params.filePath, "segmentParams") ? node.params.filePath : void 0;
|
|
514
|
+
const urlSegment = effectiveUrlSegment(node);
|
|
514
515
|
const params = [...ancestorParams];
|
|
515
|
-
if (
|
|
516
|
+
if (urlSegment.paramName) {
|
|
516
517
|
const legacyFallback = ownParamsFile ? void 0 : findLegacyParamsExport(node);
|
|
517
518
|
params.push({
|
|
518
|
-
name:
|
|
519
|
-
bracketName: bracketNameForSegment(
|
|
520
|
-
type: paramTypeForSegment(
|
|
519
|
+
name: urlSegment.paramName,
|
|
520
|
+
bracketName: bracketNameForSegment(urlSegment),
|
|
521
|
+
type: paramTypeForSegment(urlSegment.segmentType),
|
|
521
522
|
legacyCodecFilePath: legacyFallback
|
|
522
523
|
});
|
|
523
524
|
}
|
|
@@ -542,6 +543,7 @@ function collectRoutes(node, ancestorParams, ancestorParamsFiles, parentTreePath
|
|
|
542
543
|
hasSearchParams: false,
|
|
543
544
|
isApiRoute,
|
|
544
545
|
isSlotRoute: insideSlot,
|
|
546
|
+
isInterceptingRoute: insideIntercepting,
|
|
545
547
|
hasSchema
|
|
546
548
|
};
|
|
547
549
|
if (isPage) {
|
|
@@ -555,8 +557,8 @@ function collectRoutes(node, ancestorParams, ancestorParamsFiles, parentTreePath
|
|
|
555
557
|
}
|
|
556
558
|
routes.push(entry);
|
|
557
559
|
}
|
|
558
|
-
for (const child of node.children) collectRoutes(child, params, nextAncestorFiles, treePath, routes, hasSchema, insideSlot);
|
|
559
|
-
for (const slot of Object.values(node.slots)) collectRoutes(slot, params, nextAncestorFiles, treePath, routes, hasSchema, true);
|
|
560
|
+
for (const child of node.children) collectRoutes(child, params, nextAncestorFiles, treePath, routes, hasSchema, insideSlot, insideIntercepting || child.segmentType === "intercepting");
|
|
561
|
+
for (const slot of Object.values(node.slots)) collectRoutes(slot, params, nextAncestorFiles, treePath, routes, hasSchema, true, insideIntercepting);
|
|
560
562
|
}
|
|
561
563
|
/**
|
|
562
564
|
* Dedupe route entries by urlPath for urlPath-keyed emissions (the Routes
|
|
@@ -701,7 +703,7 @@ function formatDeclarationFile(routes, importBase, hasSchema, appDir) {
|
|
|
701
703
|
lines.push(emitResolveSegmentFieldHelper());
|
|
702
704
|
lines.push("");
|
|
703
705
|
}
|
|
704
|
-
const uniqueByUrlPath = dedupeRoutesByUrlPath(routes);
|
|
706
|
+
const uniqueByUrlPath = dedupeRoutesByUrlPath(routes.filter((r) => !r.isInterceptingRoute));
|
|
705
707
|
lines.push("declare module '@timber-js/app' {");
|
|
706
708
|
lines.push(" interface Routes {");
|
|
707
709
|
for (const route of uniqueByUrlPath) {
|
|
@@ -886,81 +888,6 @@ function validateKeys(schemaKeys, filesystemSegments) {
|
|
|
886
888
|
return warnings;
|
|
887
889
|
}
|
|
888
890
|
//#endregion
|
|
889
|
-
//#region src/routing/interception.ts
|
|
890
|
-
/**
|
|
891
|
-
* Collect all interception rewrite rules from the route tree.
|
|
892
|
-
*
|
|
893
|
-
* Walks the tree recursively. For each intercepting segment, computes the
|
|
894
|
-
* intercepted URL based on the marker and the segment's position.
|
|
895
|
-
*/
|
|
896
|
-
function collectInterceptionRewrites(root) {
|
|
897
|
-
const rewrites = [];
|
|
898
|
-
walkForInterceptions(root, [root], rewrites);
|
|
899
|
-
return rewrites;
|
|
900
|
-
}
|
|
901
|
-
/**
|
|
902
|
-
* Recursively walk the segment tree to find intercepting routes.
|
|
903
|
-
*/
|
|
904
|
-
function walkForInterceptions(node, ancestors, rewrites) {
|
|
905
|
-
for (const child of node.children) if (child.segmentType === "intercepting" && child.interceptionMarker) collectFromInterceptingNode(child, ancestors, rewrites);
|
|
906
|
-
else walkForInterceptions(child, [...ancestors, child], rewrites);
|
|
907
|
-
for (const slot of Object.values(node.slots)) walkForInterceptions(slot, ancestors, rewrites);
|
|
908
|
-
}
|
|
909
|
-
/**
|
|
910
|
-
* For an intercepting segment, find all leaf pages in its sub-tree and
|
|
911
|
-
* generate rewrite rules for each.
|
|
912
|
-
*/
|
|
913
|
-
function collectFromInterceptingNode(interceptingNode, ancestors, rewrites) {
|
|
914
|
-
const marker = interceptingNode.interceptionMarker;
|
|
915
|
-
const segmentName = interceptingNode.interceptedSegmentName;
|
|
916
|
-
const parentUrlPath = ancestors[ancestors.length - 1].urlPath;
|
|
917
|
-
const interceptedBase = computeInterceptedBase(parentUrlPath, marker);
|
|
918
|
-
collectLeavesWithRewrites(interceptingNode, interceptedBase === "/" ? `/${segmentName}` : `${interceptedBase}/${segmentName}`, parentUrlPath, rewrites);
|
|
919
|
-
}
|
|
920
|
-
/**
|
|
921
|
-
* Recursively find leaf pages in an intercepting sub-tree and generate
|
|
922
|
-
* rewrite rules for each.
|
|
923
|
-
*/
|
|
924
|
-
function collectLeavesWithRewrites(node, interceptedUrlPath, interceptingPrefix, rewrites) {
|
|
925
|
-
if (node.page) rewrites.push({
|
|
926
|
-
interceptedPattern: interceptedUrlPath,
|
|
927
|
-
interceptingPrefix
|
|
928
|
-
});
|
|
929
|
-
for (const child of node.children) collectLeavesWithRewrites(child, child.segmentType === "group" ? interceptedUrlPath : `${interceptedUrlPath}/${child.segmentName}`, interceptingPrefix, rewrites);
|
|
930
|
-
}
|
|
931
|
-
/**
|
|
932
|
-
* Compute the base URL that an intercepting route intercepts, given the
|
|
933
|
-
* parent's URL path and the interception marker.
|
|
934
|
-
*
|
|
935
|
-
* - (.) — same level: parent's URL path
|
|
936
|
-
* - (..) — one level up: parent's parent URL path
|
|
937
|
-
* - (...) — root level: /
|
|
938
|
-
* - (..)(..) — two levels up: parent's grandparent URL path
|
|
939
|
-
*
|
|
940
|
-
* Level counting operates on URL path segments, NOT filesystem directories.
|
|
941
|
-
* Route groups and parallel slots are already excluded from urlPath (they
|
|
942
|
-
* don't add URL depth), so (..) correctly climbs visible segments. This
|
|
943
|
-
* avoids the Vinext bug where path.dirname() on filesystem paths would
|
|
944
|
-
* waste climbs on invisible route groups.
|
|
945
|
-
*/
|
|
946
|
-
function computeInterceptedBase(parentUrlPath, marker) {
|
|
947
|
-
switch (marker) {
|
|
948
|
-
case "(.)": return parentUrlPath;
|
|
949
|
-
case "(..)": {
|
|
950
|
-
const parts = parentUrlPath.split("/").filter(Boolean);
|
|
951
|
-
parts.pop();
|
|
952
|
-
return parts.length === 0 ? "/" : `/${parts.join("/")}`;
|
|
953
|
-
}
|
|
954
|
-
case "(...)": return "/";
|
|
955
|
-
case "(..)(..)": {
|
|
956
|
-
const parts = parentUrlPath.split("/").filter(Boolean);
|
|
957
|
-
parts.pop();
|
|
958
|
-
parts.pop();
|
|
959
|
-
return parts.length === 0 ? "/" : `/${parts.join("/")}`;
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
//#endregion
|
|
964
891
|
//#region src/routing/walkers.ts
|
|
965
892
|
/**
|
|
966
893
|
* Walk a segment tree and collect every leaf with a `page` or `route`
|
|
@@ -971,6 +898,12 @@ function computeInterceptedBase(parentUrlPath, marker) {
|
|
|
971
898
|
* entries (the build report deduplicates by URL afterward).
|
|
972
899
|
* - Parallel slots are skipped unless `includeSlots: true` (slots
|
|
973
900
|
* share their parent's URL and are not addressable on their own).
|
|
901
|
+
* - Intercepting subtrees are skipped unconditionally. They render only
|
|
902
|
+
* on soft navigation and add no URL depth, so their computed `urlPath`s
|
|
903
|
+
* are not addresses — listing them would advertise routes (`/[year]`)
|
|
904
|
+
* that no request can reach. This matches every other consumer of the
|
|
905
|
+
* tree that produces URLs: the sitemap generator, the static generator,
|
|
906
|
+
* prerendering and the prebuilt-payload builder all skip them too.
|
|
974
907
|
* - Result is sorted by `urlPath` for deterministic output.
|
|
975
908
|
*/
|
|
976
909
|
function collectLeafRoutes(root, options = {}) {
|
|
@@ -981,6 +914,7 @@ function collectLeafRoutes(root, options = {}) {
|
|
|
981
914
|
return result;
|
|
982
915
|
}
|
|
983
916
|
function walk(node, chain, result, includeSlots) {
|
|
917
|
+
if (node.segmentType === "intercepting") return;
|
|
984
918
|
const currentChain = [...chain, node];
|
|
985
919
|
const path = node.urlPath || "/";
|
|
986
920
|
if (node.page) result.push({
|
|
@@ -997,6 +931,6 @@ function walk(node, chain, result, includeSlots) {
|
|
|
997
931
|
if (includeSlots) for (const slotNode of Object.values(node.slots)) walk(slotNode, currentChain, result, includeSlots);
|
|
998
932
|
}
|
|
999
933
|
//#endregion
|
|
1000
|
-
export {
|
|
934
|
+
export { fileHasAnyExport as a, fileHasExport as c, generateRouteMap as i, fileHasStarExport as l, collectDynamicSegmentsFromTree as n, fileHasDefaultExport as o, validateSchemaAgainstRoutes as r, fileHasDirective as s, collectLeafRoutes as t, getPrerenderExport as u };
|
|
1001
935
|
|
|
1002
|
-
//# sourceMappingURL=walkers-
|
|
936
|
+
//# sourceMappingURL=walkers-BhhwI9TD.js.map
|