@timber-js/app 0.2.0-alpha.184 → 0.2.0-alpha.186
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{actions-O_LsyCE4.js → actions-C-Rw9vPc.js} +3 -3
- package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-C-Rw9vPc.js.map} +1 -1
- package/dist/_chunks/als-slots-mFweg276.js +27 -0
- package/dist/_chunks/als-slots-mFweg276.js.map +1 -0
- package/dist/_chunks/base-path-DaQrzbez.js +80 -0
- package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
- package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
- package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
- package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-Cd0VZ_Pd.js} +3 -3
- package/dist/_chunks/{cache-api-B-lhk9p4.js.map → cache-api-Cd0VZ_Pd.js.map} +1 -1
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js +1857 -0
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +1 -0
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-Cs0uZXea.js} +89 -12
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-Cs0uZXea.js.map} +1 -1
- package/dist/_chunks/error-boundary-DpYRI_I1.js +330 -0
- package/dist/_chunks/error-boundary-DpYRI_I1.js.map +1 -0
- package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
- package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
- package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
- package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
- package/dist/_chunks/{logger-AWfuX-KJ.js → logger-N7e5auP0.js} +9 -13
- package/dist/_chunks/logger-N7e5auP0.js.map +1 -0
- package/dist/_chunks/navigation-root-B29qg0_T.js +274 -0
- package/dist/_chunks/navigation-root-B29qg0_T.js.map +1 -0
- package/dist/_chunks/param-value-C8TNYchQ.js +66 -0
- package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
- package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-DEGLSJs3.js} +2 -1
- package/dist/_chunks/plugin-context-DEGLSJs3.js.map +1 -0
- package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
- package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
- package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
- package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
- package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
- package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
- package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
- package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
- package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
- package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
- package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
- package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
- package/dist/_chunks/use-segment-params-ClyUNq4d.js +128 -0
- package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +1 -0
- package/dist/_chunks/{walkers-BtTlKviE.js → walkers-BhhwI9TD.js} +21 -87
- package/dist/_chunks/walkers-BhhwI9TD.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +37 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/fs-identity.d.ts +64 -0
- package/dist/adapters/fs-identity.d.ts.map +1 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +2 -3
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +57 -1
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +13 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cache/fast-hash.d.ts +5 -0
- package/dist/cache/fast-hash.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts +20 -9
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts +4 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +3 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +1 -0
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -162
- package/dist/client/history.d.ts +13 -6
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +18 -11
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +4 -3
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +872 -305
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-api.d.ts +2 -2
- package/dist/client/navigation-commit.d.ts +71 -0
- package/dist/client/navigation-commit.d.ts.map +1 -0
- package/dist/client/navigation-context.d.ts +37 -20
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +63 -23
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/params-context.d.ts +81 -0
- package/dist/client/params-context.d.ts.map +1 -0
- package/dist/client/router-effects.d.ts +129 -0
- package/dist/client/router-effects.d.ts.map +1 -0
- package/dist/client/router-lifecycle.d.ts +55 -0
- package/dist/client/router-lifecycle.d.ts.map +1 -0
- package/dist/client/router-pipeline.d.ts +84 -0
- package/dist/client/router-pipeline.d.ts.map +1 -0
- package/dist/client/router-ref.d.ts +1 -1
- package/dist/client/router-ref.d.ts.map +1 -1
- package/dist/client/router-skew.d.ts +21 -0
- package/dist/client/router-skew.d.ts.map +1 -0
- package/dist/client/router-types.d.ts +207 -0
- package/dist/client/router-types.d.ts.map +1 -0
- package/dist/client/router.d.ts +1 -182
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +39 -16
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +79 -34
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/ssr-data.d.ts +6 -0
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/stale-client.d.ts +69 -0
- package/dist/client/stale-client.d.ts.map +1 -0
- package/dist/client/state.d.ts +10 -1
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +2 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/unload-guard.d.ts +5 -0
- package/dist/client/unload-guard.d.ts.map +1 -1
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/client/use-router.d.ts +1 -1
- package/dist/client/use-segment-params.d.ts +18 -8
- package/dist/client/use-segment-params.d.ts.map +1 -1
- package/dist/cookies/index.js +3 -3
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +915 -320
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +33 -1
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/request-dep/analysis.d.ts +106 -0
- package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
- package/dist/plugins/request-dep/index.d.ts +75 -0
- package/dist/plugins/request-dep/index.d.ts.map +1 -0
- package/dist/plugins/request-dep/origin.d.ts +61 -0
- package/dist/plugins/request-dep/origin.d.ts.map +1 -0
- package/dist/plugins/routing.d.ts +9 -0
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +149 -8
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +18 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/collision-probe.d.ts +104 -0
- package/dist/routing/collision-probe.d.ts.map +1 -0
- package/dist/routing/collision-spaces.d.ts +26 -0
- package/dist/routing/collision-spaces.d.ts.map +1 -0
- package/dist/routing/index.js +3 -3
- package/dist/routing/interception.d.ts +235 -6
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +33 -0
- package/dist/routing/segment-classify.d.ts.map +1 -1
- package/dist/routing/segment-keys.d.ts +95 -0
- package/dist/routing/segment-keys.d.ts.map +1 -0
- package/dist/routing/slot-placement.d.ts +48 -0
- package/dist/routing/slot-placement.d.ts.map +1 -0
- package/dist/routing/types.d.ts +1 -3
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/routing/walkers.d.ts +6 -0
- package/dist/routing/walkers.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +0 -6
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +176 -2
- package/dist/search-params/index.js.map +1 -1
- package/dist/segment-params/define.d.ts +0 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +69 -1
- package/dist/segment-params/index.js.map +1 -0
- package/dist/server/actions.d.ts +6 -3
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +2 -1
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +13 -1
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/chain-url-parts.d.ts +82 -0
- package/dist/server/chain-url-parts.d.ts.map +1 -0
- package/dist/server/children-interception.d.ts +49 -0
- package/dist/server/children-interception.d.ts.map +1 -0
- package/dist/server/default-status-page.d.ts +45 -0
- package/dist/server/default-status-page.d.ts.map +1 -0
- package/dist/server/deny-boundary.d.ts +4 -7
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +14 -3
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/head-response.d.ts +35 -0
- package/dist/server/head-response.d.ts.map +1 -0
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +528 -373
- package/dist/server/internal.js.map +1 -1
- package/dist/server/param-coercion.d.ts +10 -1
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts +0 -18
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-interception.d.ts +59 -9
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +11 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/prebuilt-builder.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +0 -12
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/publish-params.d.ts +33 -0
- package/dist/server/publish-params.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +48 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +32 -2
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-handler.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +13 -0
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-cache-key-guard.d.ts +78 -0
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +14 -2
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +2 -10
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +3 -2
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +3 -2
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +2 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/sitemap-generator.d.ts.map +1 -1
- package/dist/server/skippable-prefix.d.ts +23 -0
- package/dist/server/skippable-prefix.d.ts.map +1 -0
- package/dist/server/slot-interception.d.ts +80 -0
- package/dist/server/slot-interception.d.ts.map +1 -0
- package/dist/server/slot-resolver.d.ts +70 -8
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +6 -0
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +24 -13
- package/dist/server/ssr-wrappers.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts +6 -27
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts +6 -0
- package/dist/server/static-generator.d.ts.map +1 -1
- package/dist/server/static-not-found.d.ts +52 -0
- package/dist/server/static-not-found.d.ts.map +1 -0
- package/dist/server/status-code-resolver.d.ts +3 -10
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/shared/als-slots.d.ts +41 -0
- package/dist/shared/als-slots.d.ts.map +1 -0
- package/dist/shared/base-path.d.ts +56 -0
- package/dist/shared/base-path.d.ts.map +1 -0
- package/dist/shared/param-value.d.ts +80 -0
- package/dist/shared/param-value.d.ts.map +1 -0
- package/dist/shared/payload-root.d.ts +108 -0
- package/dist/shared/payload-root.d.ts.map +1 -0
- package/dist/shared/rsc-cache-key.d.ts +163 -0
- package/dist/shared/rsc-cache-key.d.ts.map +1 -0
- package/dist/shared/rsc-error-envelope.d.ts +25 -0
- package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
- package/dist/shared/rsc-manifest.d.ts +28 -0
- package/dist/shared/rsc-manifest.d.ts.map +1 -0
- package/dist/shared/rsc-payload-path.d.ts +42 -0
- package/dist/shared/rsc-payload-path.d.ts.map +1 -0
- package/dist/shared/segment-info.d.ts +59 -0
- package/dist/shared/segment-info.d.ts.map +1 -0
- package/dist/shared/slot-params.d.ts +51 -0
- package/dist/shared/slot-params.d.ts.map +1 -0
- package/dist/shared/static-platform-files.d.ts +71 -0
- package/dist/shared/static-platform-files.d.ts.map +1 -0
- package/dist/shims/navigation-client.d.ts +8 -19
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +0 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/docs/api/31-api-client.mdx +4 -2
- package/docs/learn/02-pages-and-layouts.mdx +1 -1
- package/docs/learn/11-error-handling.mdx +4 -3
- package/docs/learn/12-client-navigation.mdx +2 -2
- package/docs/learn/14-deploying.mdx +23 -0
- package/docs/more/01-advanced-routing.mdx +172 -2
- package/docs/more/04-metadata-and-fonts.mdx +1 -1
- package/package.json +2 -2
- package/src/adapters/build-output-helper.ts +116 -3
- package/src/adapters/cloudflare.ts +86 -11
- package/src/adapters/fs-identity.ts +156 -0
- package/src/adapters/nitro.ts +6 -6
- package/src/adapters/shared.ts +196 -21
- package/src/adapters/types.ts +13 -1
- package/src/cache/fast-hash.ts +5 -0
- package/src/client/browser-entry/action-dispatch.ts +9 -4
- package/src/client/browser-entry/hydrate.ts +40 -29
- package/src/client/browser-entry/index.ts +15 -1
- package/src/client/browser-entry/post-hydration.ts +10 -9
- package/src/client/browser-entry/router-init.ts +104 -48
- package/src/client/browser-entry/rsc-stream.ts +11 -2
- package/src/client/error-boundary.tsx +20 -15
- package/src/client/history.ts +13 -6
- package/src/client/internal.ts +4 -3
- package/src/client/link.tsx +55 -8
- package/src/client/navigation-api.ts +3 -3
- package/src/client/navigation-commit.ts +174 -0
- package/src/client/navigation-context.ts +40 -67
- package/src/client/navigation-root.tsx +326 -101
- package/src/client/params-context.ts +192 -0
- package/src/client/router-effects.ts +231 -0
- package/src/client/router-lifecycle.ts +288 -0
- package/src/client/router-pipeline.ts +389 -0
- package/src/client/router-ref.ts +1 -1
- package/src/client/router-skew.ts +30 -0
- package/src/client/router-types.ts +230 -0
- package/src/client/router.ts +242 -690
- package/src/client/rsc-fetch.ts +180 -122
- package/src/client/segment-cache.ts +121 -44
- package/src/client/ssr-data.ts +6 -0
- package/src/client/stale-client.ts +191 -0
- package/src/client/state.ts +14 -1
- package/src/client/top-loader.tsx +2 -1
- package/src/client/unload-guard.ts +34 -2
- package/src/client/use-cookie.ts +9 -2
- package/src/client/use-router.ts +1 -1
- package/src/client/use-segment-params.ts +41 -18
- package/src/index.ts +5 -1
- package/src/plugin-context.ts +34 -1
- package/src/plugins/adapter-build.ts +4 -0
- package/src/plugins/build-manifest.ts +34 -13
- package/src/plugins/entries.ts +4 -0
- package/src/plugins/fonts.ts +11 -8
- package/src/plugins/request-dep/analysis.ts +447 -0
- package/src/plugins/request-dep/index.ts +376 -0
- package/src/plugins/request-dep/origin.ts +310 -0
- package/src/plugins/routing.ts +60 -3
- package/src/plugins/static-build.ts +333 -63
- package/src/routing/codegen-types.ts +18 -0
- package/src/routing/codegen.ts +50 -10
- package/src/routing/collision-probe.ts +438 -0
- package/src/routing/collision-spaces.ts +281 -0
- package/src/routing/interception.ts +896 -50
- package/src/routing/manifest-codegen.ts +6 -9
- package/src/routing/scanner.ts +114 -113
- package/src/routing/segment-classify.ts +57 -0
- package/src/routing/segment-keys.ts +146 -0
- package/src/routing/slot-placement.ts +97 -0
- package/src/routing/types.ts +1 -3
- package/src/routing/walkers.ts +8 -0
- package/src/search-params/define.ts +6 -29
- package/src/segment-params/define.ts +4 -27
- package/src/server/action-handler.ts +3 -1
- package/src/server/actions.ts +6 -3
- package/src/server/als-registry.ts +2 -1
- package/src/server/build-manifest.ts +14 -1
- package/src/server/chain-url-parts.ts +163 -0
- package/src/server/children-interception.ts +115 -0
- package/src/server/default-status-page.ts +127 -0
- package/src/server/deny-boundary.ts +5 -31
- package/src/server/deny-renderer.ts +83 -22
- package/src/server/head-response.ts +54 -0
- package/src/server/param-coercion.ts +48 -18
- package/src/server/pipeline-helpers.ts +0 -45
- package/src/server/pipeline-interception.ts +81 -32
- package/src/server/pipeline-outcome.ts +3 -5
- package/src/server/pipeline-phases.ts +68 -16
- package/src/server/pipeline.ts +57 -10
- package/src/server/prebuilt-builder.ts +1 -7
- package/src/server/prebuilt-runtime.ts +4 -4
- package/src/server/primitives.ts +0 -18
- package/src/server/publish-params.ts +42 -0
- package/src/server/request-context.ts +97 -18
- package/src/server/route-element-builder.ts +89 -98
- package/src/server/route-handler.ts +8 -8
- package/src/server/route-matcher.ts +21 -9
- package/src/server/rsc-cache-key-guard.ts +170 -0
- package/src/server/rsc-entry/api-handler.ts +6 -8
- package/src/server/rsc-entry/error-renderer.ts +64 -25
- package/src/server/rsc-entry/helpers.ts +31 -16
- package/src/server/rsc-entry/index.ts +36 -28
- package/src/server/rsc-entry/render-route.ts +12 -7
- package/src/server/rsc-entry/revalidate-renderer.ts +1 -1
- package/src/server/rsc-entry/rsc-payload.ts +14 -25
- package/src/server/rsc-entry/rsc-stream.ts +4 -3
- package/src/server/rsc-entry/ssr-renderer.ts +13 -9
- package/src/server/sitemap-generator.ts +1 -7
- package/src/server/skippable-prefix.ts +92 -0
- package/src/server/slot-interception.ts +168 -0
- package/src/server/slot-resolver.ts +253 -156
- package/src/server/ssr-bridge-types.ts +6 -0
- package/src/server/ssr-entry.ts +12 -4
- package/src/server/ssr-wrappers.tsx +33 -32
- package/src/server/state-tree-diff.ts +14 -55
- package/src/server/static-generator.ts +67 -45
- package/src/server/static-not-found.ts +134 -0
- package/src/server/status-code-resolver.ts +4 -47
- package/src/shared/als-slots.ts +72 -0
- package/src/shared/base-path.ts +80 -0
- package/src/shared/param-value.ts +244 -0
- package/src/shared/payload-root.ts +151 -0
- package/src/shared/rsc-cache-key.ts +269 -0
- package/src/shared/rsc-error-envelope.ts +31 -0
- package/src/shared/rsc-manifest.ts +29 -0
- package/src/shared/rsc-payload-path.ts +45 -0
- package/src/shared/segment-info.ts +60 -0
- package/src/shared/slot-params.ts +111 -0
- package/src/shared/static-platform-files.ts +75 -0
- package/src/shims/navigation-client.ts +8 -26
- package/src/shims/navigation.ts +0 -4
- package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
- package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
- package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-D3naS3eH.js +0 -688
- package/dist/_chunks/cli-schema-sync-D3naS3eH.js.map +0 -1
- package/dist/_chunks/define-COtkxMRT.js +0 -191
- package/dist/_chunks/define-COtkxMRT.js.map +0 -1
- package/dist/_chunks/define-c4au4I9R.js +0 -82
- package/dist/_chunks/define-c4au4I9R.js.map +0 -1
- package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
- package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
- package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
- package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
- package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
- package/dist/_chunks/segment-classify-Byy425ng.js.map +0 -1
- package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
- package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
- package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
- package/dist/_chunks/walkers-BtTlKviE.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/plugins/request-dep.d.ts +0 -32
- package/dist/plugins/request-dep.d.ts.map +0 -1
- package/src/plugins/request-dep.ts +0 -499
|
@@ -31,12 +31,7 @@ const SINGLE_FILE_KEYS = [
|
|
|
31
31
|
|
|
32
32
|
// ── Record-shaped conventions ───────────────────────────────────────────
|
|
33
33
|
// Each entry maps to a node property that holds Record<string, RouteFile>.
|
|
34
|
-
const RECORD_FILE_KEYS = [
|
|
35
|
-
'statusFiles',
|
|
36
|
-
'jsonStatusFiles',
|
|
37
|
-
'legacyStatusFiles',
|
|
38
|
-
'metadataRoutes',
|
|
39
|
-
] as const;
|
|
34
|
+
const RECORD_FILE_KEYS = ['statusFiles', 'jsonStatusFiles', 'metadataRoutes'] as const;
|
|
40
35
|
|
|
41
36
|
const SCHEMA_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
|
|
42
37
|
|
|
@@ -165,11 +160,13 @@ export function generateManifestModule(tree: RouteTree, viteRoot: string): strin
|
|
|
165
160
|
globalErrorLine = ` globalError: { load: ${v}, filePath: ${JSON.stringify(tree.globalError.filePath)} },`;
|
|
166
161
|
}
|
|
167
162
|
|
|
163
|
+
// Serialize the rewrite objects wholesale — an explicit field list here
|
|
164
|
+
// silently drops any field added to InterceptionRewrite, and the guard that
|
|
165
|
+
// reads it would then fall back to matching too broadly. InterceptionRewrite
|
|
166
|
+
// is plain JSON data by construction (no file references, no functions).
|
|
168
167
|
const rewrites = collectInterceptionRewrites(tree.root);
|
|
169
168
|
const rewritesLine =
|
|
170
|
-
rewrites.length > 0
|
|
171
|
-
? ` interceptionRewrites: ${JSON.stringify(rewrites.map((r) => ({ interceptedPattern: r.interceptedPattern, interceptingPrefix: r.interceptingPrefix })))},`
|
|
172
|
-
: '';
|
|
169
|
+
rewrites.length > 0 ? ` interceptionRewrites: ${JSON.stringify(rewrites)},` : '';
|
|
173
170
|
|
|
174
171
|
const code = [
|
|
175
172
|
'// Auto-generated route manifest — do not edit.',
|
package/src/routing/scanner.ts
CHANGED
|
@@ -18,7 +18,10 @@ import type {
|
|
|
18
18
|
ScannerConfig,
|
|
19
19
|
InterceptionMarker,
|
|
20
20
|
} from './types.js';
|
|
21
|
-
import { classifySegment } from './segment-classify.js';
|
|
21
|
+
import { classifySegment, effectiveUrlSegment } from './segment-classify.js';
|
|
22
|
+
import { validateInterceptionPlacement, computeInterceptedBase } from './interception.js';
|
|
23
|
+
import { validateRouteGroupCollisions } from './collision-spaces.js';
|
|
24
|
+
import { validateSlotPlacement } from './slot-placement.js';
|
|
22
25
|
import { DEFAULT_PAGE_EXTENSIONS } from './types.js';
|
|
23
26
|
import { classifyMetadataRoute, isDynamicMetadataExtension } from '../server/metadata-routes.js';
|
|
24
27
|
import { swallow } from '../server/logger.js';
|
|
@@ -29,17 +32,6 @@ import { ENCODED_SEPARATOR_RE, NULL_BYTE_RE } from '../server/canonicalize.js';
|
|
|
29
32
|
*/
|
|
30
33
|
const PAGE_EXT_CONVENTIONS = new Set(['page', 'layout', 'error', 'default', 'denied']);
|
|
31
34
|
|
|
32
|
-
/**
|
|
33
|
-
* Legacy compat status-code files.
|
|
34
|
-
* Maps legacy file name → HTTP status code for the fallback chain.
|
|
35
|
-
* See design/10-error-handling.md §"Fallback Chain".
|
|
36
|
-
*/
|
|
37
|
-
const LEGACY_STATUS_FILES: Record<string, number> = {
|
|
38
|
-
'not-found': 404,
|
|
39
|
-
'forbidden': 403,
|
|
40
|
-
'unauthorized': 401,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
35
|
/**
|
|
44
36
|
* File convention names that are always .ts/.tsx (never .mdx etc.)
|
|
45
37
|
*/
|
|
@@ -87,6 +79,12 @@ export function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTre
|
|
|
87
79
|
// Scan children recursively
|
|
88
80
|
scanChildren(appDir, tree.root, extSet);
|
|
89
81
|
|
|
82
|
+
// Validate: every parallel slot sits where a renderer reaches it. First,
|
|
83
|
+
// because a slot nested inside another slot never renders, and a collision
|
|
84
|
+
// reported from inside one would send the author to fix an ambiguity between
|
|
85
|
+
// two pages that were both already dead.
|
|
86
|
+
validateSlotPlacement(tree.root);
|
|
87
|
+
|
|
90
88
|
// Validate: detect route group collisions (different groups producing pages at the same URL)
|
|
91
89
|
validateRouteGroupCollisions(tree.root);
|
|
92
90
|
|
|
@@ -102,6 +100,10 @@ export function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTre
|
|
|
102
100
|
// e.g., a-[id] and [slug]-b both have prefix+suffix length = 2
|
|
103
101
|
validateAffixedSiblingConflicts(tree.root);
|
|
104
102
|
|
|
103
|
+
// Validate: intercepting routes must be direct children of a parallel slot —
|
|
104
|
+
// the slot resolver is the only thing that can render one.
|
|
105
|
+
validateInterceptionPlacement(tree.root);
|
|
106
|
+
|
|
105
107
|
return tree;
|
|
106
108
|
}
|
|
107
109
|
|
|
@@ -219,19 +221,6 @@ function scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string
|
|
|
219
221
|
continue;
|
|
220
222
|
}
|
|
221
223
|
|
|
222
|
-
// Legacy compat files (not-found.tsx, forbidden.tsx, unauthorized.tsx)
|
|
223
|
-
if (name in LEGACY_STATUS_FILES && extSet.has(ext)) {
|
|
224
|
-
if (!node.legacyStatusFiles) {
|
|
225
|
-
node.legacyStatusFiles = {};
|
|
226
|
-
}
|
|
227
|
-
const existing = node.legacyStatusFiles[name];
|
|
228
|
-
if (existing) {
|
|
229
|
-
throwDuplicateConvention(name, existing.filePath, fullPath);
|
|
230
|
-
}
|
|
231
|
-
node.legacyStatusFiles[name] = { filePath: fullPath, extension: ext };
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
224
|
// Metadata route files (sitemap.ts, robots.ts, icon.tsx, opengraph-image.tsx, etc.)
|
|
236
225
|
// Both static (.xml, .txt, .png, .ico, etc.) and dynamic (.ts, .tsx) files are recognized.
|
|
237
226
|
// When both exist for the same base name, dynamic takes precedence.
|
|
@@ -379,69 +368,6 @@ function scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<stri
|
|
|
379
368
|
}
|
|
380
369
|
}
|
|
381
370
|
|
|
382
|
-
/**
|
|
383
|
-
* Validate that route groups don't produce conflicting pages/routes at the same URL path.
|
|
384
|
-
*
|
|
385
|
-
* Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim
|
|
386
|
-
* /login — the scanner must detect and reject this at build time.
|
|
387
|
-
*
|
|
388
|
-
* Each parallel slot is validated as its OWN collision space: a slot's pages
|
|
389
|
-
* intentionally share URL paths with the parent tree (that's the point of
|
|
390
|
-
* parallel routes), but two route groups INSIDE one slot claiming the same
|
|
391
|
-
* URL — e.g. @side/(a)/x/page.tsx + @side/(b)/x/page.tsx — still collide.
|
|
392
|
-
*/
|
|
393
|
-
function validateRouteGroupCollisions(root: SegmentNode): void {
|
|
394
|
-
// Map from urlPath → { filePath, source } for the first page/route seen at that path
|
|
395
|
-
collectRoutableLeaves(root, new Map(), '');
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Walk the segment tree and collect all routable leaves (page or route files),
|
|
400
|
-
* throwing on collision. Each slot recurses with a FRESH collision map —
|
|
401
|
-
* slots are parallel routes that intentionally share URL paths with their
|
|
402
|
-
* parent tree, but collisions within a single slot are real errors.
|
|
403
|
-
*/
|
|
404
|
-
function collectRoutableLeaves(
|
|
405
|
-
node: SegmentNode,
|
|
406
|
-
seen: Map<string, { filePath: string; segmentPath: string }>,
|
|
407
|
-
segmentPath: string
|
|
408
|
-
): void {
|
|
409
|
-
// Intercepting routes are resolved by the slot resolver, not URL matching,
|
|
410
|
-
// and their computed urlPaths intentionally mirror other routes (e.g. two
|
|
411
|
-
// interceptors in one slot both produce /gallery/[id]). Skip them entirely.
|
|
412
|
-
if (node.segmentType === 'intercepting') return;
|
|
413
|
-
|
|
414
|
-
const currentPath = segmentPath
|
|
415
|
-
? `${segmentPath}/${node.segmentName}`
|
|
416
|
-
: node.segmentName || '(root)';
|
|
417
|
-
|
|
418
|
-
const routableFile = node.page ?? node.route;
|
|
419
|
-
if (routableFile) {
|
|
420
|
-
const existing = seen.get(node.urlPath);
|
|
421
|
-
if (existing) {
|
|
422
|
-
throw new Error(
|
|
423
|
-
`Build error: route collision — multiple route groups produce a page/route at the same URL path.\n` +
|
|
424
|
-
` URL path: ${node.urlPath}\n` +
|
|
425
|
-
` File 1: ${existing.filePath} (via ${existing.segmentPath})\n` +
|
|
426
|
-
` File 2: ${routableFile.filePath} (via ${currentPath})\n` +
|
|
427
|
-
`Each URL path must map to exactly one page or route handler. ` +
|
|
428
|
-
`Rename or move one of the conflicting files.`
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
seen.set(node.urlPath, { filePath: routableFile.filePath, segmentPath: currentPath });
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Recurse into children
|
|
435
|
-
for (const child of node.children) {
|
|
436
|
-
collectRoutableLeaves(child, seen, currentPath);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Recurse into slots — each slot gets a fresh map (its own collision space)
|
|
440
|
-
for (const slotNode of Object.values(node.slots)) {
|
|
441
|
-
collectRoutableLeaves(slotNode, new Map(), currentPath);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
371
|
/**
|
|
446
372
|
* Validate that no route chain contains duplicate dynamic param names.
|
|
447
373
|
*
|
|
@@ -462,12 +388,20 @@ function validateDuplicateParamNames(root: SegmentNode): void {
|
|
|
462
388
|
* Throws on the first duplicate found.
|
|
463
389
|
*/
|
|
464
390
|
function walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): void {
|
|
391
|
+
// Read through the effective identity so an intercepting directory is seen
|
|
392
|
+
// as the segment it intercepts: the scanner leaves `paramName` unset on
|
|
393
|
+
// `(.)[id]`, so a validator reading the node directly is blind to a param
|
|
394
|
+
// the resolver goes on to bind — `@modal/(.)[id]/[id]` would pass here and
|
|
395
|
+
// then silently overwrite the outer `id` at match time, where the ordinary
|
|
396
|
+
// `/[id]/[id]` is rejected. See TIM-1288.
|
|
397
|
+
const { paramName } = effectiveUrlSegment(node);
|
|
398
|
+
|
|
465
399
|
// If this node introduces a param name, check for duplicates
|
|
466
|
-
if (
|
|
467
|
-
const existing = seen.get(
|
|
400
|
+
if (paramName) {
|
|
401
|
+
const existing = seen.get(paramName);
|
|
468
402
|
if (existing) {
|
|
469
403
|
throw new Error(
|
|
470
|
-
`[timber] Duplicate param name '${
|
|
404
|
+
`[timber] Duplicate param name '${paramName}' in route chain.\n` +
|
|
471
405
|
` First defined at: ${existing}\n` +
|
|
472
406
|
` Duplicate at: ${node.urlPath || '/'}\n` +
|
|
473
407
|
` Rename one of the segments to avoid ambiguity.`
|
|
@@ -475,7 +409,7 @@ function walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): v
|
|
|
475
409
|
}
|
|
476
410
|
// Add to seen for descendants (use a new Map to avoid polluting siblings)
|
|
477
411
|
seen = new Map(seen);
|
|
478
|
-
seen.set(
|
|
412
|
+
seen.set(paramName, node.urlPath || '/');
|
|
479
413
|
}
|
|
480
414
|
|
|
481
415
|
// Recurse into children (they inherit the accumulated params)
|
|
@@ -483,8 +417,16 @@ function walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): v
|
|
|
483
417
|
walkForDuplicateParams(child, seen);
|
|
484
418
|
}
|
|
485
419
|
|
|
486
|
-
//
|
|
487
|
-
//
|
|
420
|
+
// A slot INHERITS the ancestor params rather than starting fresh: a slot
|
|
421
|
+
// child may not reuse a name its owner already bound
|
|
422
|
+
// (`app/gallery/[id]/@modal/[id]` is rejected). The copy is so sibling
|
|
423
|
+
// slots don't see each other's names, not so the chain is reset.
|
|
424
|
+
//
|
|
425
|
+
// Whether that rule is right is a separate question — slot params are
|
|
426
|
+
// merged over the main route's at read time, so a collision is defined
|
|
427
|
+
// rather than ambiguous — but changing it would change every slot.
|
|
428
|
+
// Tracked as TIM-1287; do not "fix" this by making the map fresh without
|
|
429
|
+
// deciding that.
|
|
488
430
|
for (const slotNode of Object.values(node.slots)) {
|
|
489
431
|
walkForDuplicateParams(slotNode, new Map(seen));
|
|
490
432
|
}
|
|
@@ -512,25 +454,29 @@ function collectBracketForms(
|
|
|
512
454
|
node: SegmentNode,
|
|
513
455
|
forms: Map<string, { segmentType: SegmentType; urlPath: string }>
|
|
514
456
|
): void {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
457
|
+
// Effective identity, so `(.)[...rest]` is compared as the catch-all it
|
|
458
|
+
// intercepts rather than skipped as an 'intercepting' node (TIM-1288).
|
|
459
|
+
const { paramName, segmentType } = effectiveUrlSegment(node);
|
|
460
|
+
|
|
461
|
+
if (paramName && segmentType !== 'static' && segmentType !== 'group') {
|
|
462
|
+
const existing = forms.get(paramName);
|
|
463
|
+
if (existing && existing.segmentType !== segmentType) {
|
|
518
464
|
const bracketExamples: Record<string, string> = {
|
|
519
|
-
'dynamic': `[${
|
|
520
|
-
'catch-all': `[...${
|
|
521
|
-
'optional-catch-all': `[[...${
|
|
465
|
+
'dynamic': `[${paramName}]`,
|
|
466
|
+
'catch-all': `[...${paramName}]`,
|
|
467
|
+
'optional-catch-all': `[[...${paramName}]]`,
|
|
522
468
|
};
|
|
523
469
|
throw new Error(
|
|
524
|
-
`[timber] Param name '${
|
|
470
|
+
`[timber] Param name '${paramName}' uses multiple bracket forms.\n` +
|
|
525
471
|
` ${bracketExamples[existing.segmentType] ?? existing.segmentType} at: ${existing.urlPath}\n` +
|
|
526
|
-
` ${bracketExamples[
|
|
472
|
+
` ${bracketExamples[segmentType] ?? segmentType} at: ${node.urlPath || '/'}\n` +
|
|
527
473
|
` Each param name must use a single bracket form across all routes.\n` +
|
|
528
|
-
` Rename one of the segments to fix this (e.g., [${
|
|
474
|
+
` Rename one of the segments to fix this (e.g., [${paramName}] → [${paramName}Id]).`
|
|
529
475
|
);
|
|
530
476
|
}
|
|
531
477
|
if (!existing) {
|
|
532
|
-
forms.set(
|
|
533
|
-
segmentType
|
|
478
|
+
forms.set(paramName, {
|
|
479
|
+
segmentType,
|
|
534
480
|
urlPath: node.urlPath || '/',
|
|
535
481
|
});
|
|
536
482
|
}
|
|
@@ -554,20 +500,39 @@ function collectBracketForms(
|
|
|
554
500
|
* Route groups are URL-transparent, so affixed segments inside different groups
|
|
555
501
|
* at the same level still conflict with each other.
|
|
556
502
|
*
|
|
557
|
-
*
|
|
503
|
+
* Ambiguity requires the two segments to be candidates for the *same* URL
|
|
504
|
+
* part, which is narrower than "same parent" once interception is involved:
|
|
505
|
+
*
|
|
506
|
+
* - Plain children and intercepting children never compete. They are resolved
|
|
507
|
+
* by different walks — `findSlotMatch` matches the slot subtree (where
|
|
508
|
+
* `tree-match.ts` ignores intercepting nodes outright) and
|
|
509
|
+
* `findInterceptingMatch` builds a synthetic parent holding only
|
|
510
|
+
* interceptors.
|
|
511
|
+
* - Two interceptors compete only at the same anchor. `(.)a-[id]` and
|
|
512
|
+
* `(..)[slug]-b` in one slot are based at different URL depths, so no
|
|
513
|
+
* pathname reaches both, and `anchorIndex` rejects the one whose base does
|
|
514
|
+
* not match the target. Grouping them together would fail the build for a
|
|
515
|
+
* pair that can never be ambiguous.
|
|
516
|
+
*
|
|
517
|
+
* So candidates are keyed by the position they contend for, then by affix
|
|
518
|
+
* length. See design/07-routing.md §"Priority" and §"Intercepting Routes".
|
|
558
519
|
*/
|
|
559
520
|
function validateAffixedSiblingConflicts(node: SegmentNode): void {
|
|
560
521
|
const affixed = collectEffectiveAffixed(node);
|
|
561
522
|
|
|
562
523
|
if (affixed.length >= 2) {
|
|
563
|
-
const byLength = new Map<
|
|
524
|
+
const byLength = new Map<string, SegmentNode[]>();
|
|
564
525
|
for (const child of affixed) {
|
|
565
|
-
|
|
566
|
-
|
|
526
|
+
// `\0` as the separator, written as the escape rather than a literal
|
|
527
|
+
// byte: a raw NUL in the source makes git and grep treat this file as
|
|
528
|
+
// binary. A contention key is a URL path, which cannot contain one, so
|
|
529
|
+
// the two halves of the key can never run together.
|
|
530
|
+
const key = `${contentionKey(node, child)}\0${affixLengthOf(child)}`;
|
|
531
|
+
const group = byLength.get(key);
|
|
567
532
|
if (group) {
|
|
568
533
|
group.push(child);
|
|
569
534
|
} else {
|
|
570
|
-
byLength.set(
|
|
535
|
+
byLength.set(key, [child]);
|
|
571
536
|
}
|
|
572
537
|
}
|
|
573
538
|
|
|
@@ -577,7 +542,7 @@ function validateAffixedSiblingConflicts(node: SegmentNode): void {
|
|
|
577
542
|
`Build error: ambiguous affixed dynamic siblings with equal specificity.\n` +
|
|
578
543
|
` Parent: ${node.urlPath || '/'}\n` +
|
|
579
544
|
` Segments: ${group.map((s) => s.segmentName).join(', ')}\n` +
|
|
580
|
-
` Combined prefix+suffix length: ${(group[0]
|
|
545
|
+
` Combined prefix+suffix length: ${affixLengthOf(group[0]!)}\n` +
|
|
581
546
|
`The router cannot deterministically choose between these — filesystem scan order is platform-dependent.\n` +
|
|
582
547
|
`Rename one segment to give it a longer or shorter combined affix.`
|
|
583
548
|
);
|
|
@@ -593,16 +558,52 @@ function validateAffixedSiblingConflicts(node: SegmentNode): void {
|
|
|
593
558
|
}
|
|
594
559
|
}
|
|
595
560
|
|
|
561
|
+
/**
|
|
562
|
+
* The URL position a child contends for, as a comparable key.
|
|
563
|
+
*
|
|
564
|
+
* Plain children contend for the parent's next part. An intercepting child
|
|
565
|
+
* contends for a part determined by its marker's intercepted base, so two
|
|
566
|
+
* markers of different depth are simply different positions — and two markers
|
|
567
|
+
* that compute the *same* base do contend, which is why the key is the base
|
|
568
|
+
* rather than the marker (`(..)` and `(...)` under `/feed` both base at `/`).
|
|
569
|
+
*
|
|
570
|
+
* `groupByInterceptedAnchor` (`routing/interception.ts`) is the canonical
|
|
571
|
+
* statement of that grouping — it is what the resolver walks and what the
|
|
572
|
+
* collision validator checks. This computes the same key from the same
|
|
573
|
+
* `computeInterceptedBase` rather than calling it, because affix ambiguity is
|
|
574
|
+
* decided per *child* and `collectEffectiveAffixed` looks through route groups,
|
|
575
|
+
* so the two do not partition the same set of nodes.
|
|
576
|
+
*/
|
|
577
|
+
function contentionKey(parent: SegmentNode, child: SegmentNode): string {
|
|
578
|
+
if (child.segmentType !== 'intercepting') return 'child';
|
|
579
|
+
return `intercept:${computeInterceptedBase(parent.urlPath, child.interceptionMarker ?? '(.)')}`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** Combined affix length of a node, read through its effective identity. */
|
|
583
|
+
function affixLengthOf(node: SegmentNode): number {
|
|
584
|
+
const { paramPrefix, paramSuffix } = effectiveUrlSegment(node);
|
|
585
|
+
return (paramPrefix?.length ?? 0) + (paramSuffix?.length ?? 0);
|
|
586
|
+
}
|
|
587
|
+
|
|
596
588
|
/**
|
|
597
589
|
* Collect affixed dynamic segments that are effective children of a node.
|
|
598
590
|
* Route groups are URL-transparent, so we look through them recursively.
|
|
591
|
+
*
|
|
592
|
+
* Segments are read through `effectiveUrlSegment`, so an intercepting
|
|
593
|
+
* directory counts as the affixed segment it intercepts: `(.)a-[id]` is
|
|
594
|
+
* `a-[id]`. The proxy the slot resolver builds hands those affixes to
|
|
595
|
+
* `matchUrlParts`, whose equal-length sort is not stable across equal
|
|
596
|
+
* specificity — so two intercepting siblings with the same combined affix
|
|
597
|
+
* length would resolve by filesystem enumeration order, which is precisely
|
|
598
|
+
* what this validator exists to prevent for ordinary siblings (TIM-1288).
|
|
599
599
|
*/
|
|
600
600
|
function collectEffectiveAffixed(node: SegmentNode): SegmentNode[] {
|
|
601
601
|
const result: SegmentNode[] = [];
|
|
602
602
|
for (const child of node.children) {
|
|
603
|
-
|
|
603
|
+
const { segmentType, paramPrefix, paramSuffix } = effectiveUrlSegment(child);
|
|
604
|
+
if (segmentType === 'dynamic' && (paramPrefix || paramSuffix)) {
|
|
604
605
|
result.push(child);
|
|
605
|
-
} else if (
|
|
606
|
+
} else if (segmentType === 'group') {
|
|
606
607
|
result.push(...collectEffectiveAffixed(child));
|
|
607
608
|
}
|
|
608
609
|
}
|
|
@@ -192,6 +192,63 @@ export function classifySegment(dirName: string): SegmentClassification {
|
|
|
192
192
|
return { type: 'static' };
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* The URL-matching identity of a segment node.
|
|
197
|
+
*
|
|
198
|
+
* For every node except an intercepting one this is the node itself. An
|
|
199
|
+
* intercepting node is different: its directory name carries a marker
|
|
200
|
+
* (`(.)photo`, `(.)[id]`), so the node's own `segmentType` is
|
|
201
|
+
* `'intercepting'` and the bracket syntax of the segment it intercepts was
|
|
202
|
+
* never classified. Everything that has to reason about *the URL part this
|
|
203
|
+
* node stands for* — matching it, and keying its param for codec coercion —
|
|
204
|
+
* needs that classification.
|
|
205
|
+
*
|
|
206
|
+
* Deriving it here, from `interceptedSegmentName`, keeps interception out
|
|
207
|
+
* of `classifySegment`'s output and out of the serialized manifest: there
|
|
208
|
+
* is no second copy of the classification to drift from this one. The
|
|
209
|
+
* returned `segmentName` is the intercepted name (`photo`, `[id]`), NOT the
|
|
210
|
+
* node's directory name — callers that need the directory name for tree
|
|
211
|
+
* paths must keep reading the node. See TIM-1281.
|
|
212
|
+
*/
|
|
213
|
+
export interface UrlSegmentIdentity {
|
|
214
|
+
segmentName: string;
|
|
215
|
+
segmentType: SegmentType;
|
|
216
|
+
paramName?: string;
|
|
217
|
+
paramPrefix?: string;
|
|
218
|
+
paramSuffix?: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function effectiveUrlSegment(node: {
|
|
222
|
+
segmentName: string;
|
|
223
|
+
segmentType: SegmentType;
|
|
224
|
+
paramName?: string;
|
|
225
|
+
paramPrefix?: string;
|
|
226
|
+
paramSuffix?: string;
|
|
227
|
+
interceptedSegmentName?: string;
|
|
228
|
+
}): UrlSegmentIdentity {
|
|
229
|
+
if (node.segmentType !== 'intercepting' || !node.interceptedSegmentName) {
|
|
230
|
+
return {
|
|
231
|
+
segmentName: node.segmentName,
|
|
232
|
+
segmentType: node.segmentType,
|
|
233
|
+
paramName: node.paramName,
|
|
234
|
+
paramPrefix: node.paramPrefix,
|
|
235
|
+
paramSuffix: node.paramSuffix,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const seg = classifyUrlSegment(node.interceptedSegmentName);
|
|
240
|
+
if (seg.kind === 'static') {
|
|
241
|
+
return { segmentName: seg.value, segmentType: 'static' };
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
segmentName: node.interceptedSegmentName,
|
|
245
|
+
segmentType: seg.kind,
|
|
246
|
+
paramName: seg.name,
|
|
247
|
+
paramPrefix: seg.kind === 'dynamic' ? seg.prefix : undefined,
|
|
248
|
+
paramSuffix: seg.kind === 'dynamic' ? seg.suffix : undefined,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
195
252
|
/**
|
|
196
253
|
* Parse an interception marker from a directory name.
|
|
197
254
|
*
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Segment key computation — the stable identity of a segment in the route tree.
|
|
3
|
+
*
|
|
4
|
+
* A segment key is the string that names a segment across the RSC/SSR/client
|
|
5
|
+
* boundary: it appears in the X-Timber-State-Tree header, the X-Timber-Segments
|
|
6
|
+
* header, SegmentOutlet props, the client segment cache, and (since TIM-1279)
|
|
7
|
+
* interception scoping.
|
|
8
|
+
*
|
|
9
|
+
* This module lives in `routing/` rather than `server/` because the keys are a
|
|
10
|
+
* property of the route tree itself — both the build-time scanner tree and the
|
|
11
|
+
* runtime manifest tree produce identical keys for the same directory, which is
|
|
12
|
+
* what lets a build-time value (an interception scope) be compared against a
|
|
13
|
+
* request-time matched chain.
|
|
14
|
+
*
|
|
15
|
+
* See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Segment node shape expected by computeSegmentKeys.
|
|
20
|
+
*
|
|
21
|
+
* Structurally satisfied by both `SegmentNode` (build time) and
|
|
22
|
+
* `ManifestSegmentNode` (request time) — keys depend only on the
|
|
23
|
+
* URL path and the segment classification, never on file payloads.
|
|
24
|
+
*/
|
|
25
|
+
export interface SegmentKeyInput {
|
|
26
|
+
urlPath: string;
|
|
27
|
+
segmentName?: string;
|
|
28
|
+
segmentType?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compute state-tree keys for a segment chain.
|
|
33
|
+
*
|
|
34
|
+
* Non-group segments use their urlPath as-is. Route groups accumulate
|
|
35
|
+
* ancestor group names to produce globally unique keys:
|
|
36
|
+
* app/(a)/(shared)/dashboard → keys: ['/', '/(a)', '/(a)/(shared)', '/dashboard']
|
|
37
|
+
*
|
|
38
|
+
* **An intercepting segment switches the chain to name accumulation for good.**
|
|
39
|
+
* A children-path interception (TIM-1280) puts an intercepting node and its
|
|
40
|
+
* descendants into the *main* rendered chain, and their `urlPath`s are not
|
|
41
|
+
* URLs: interception adds no URL depth, so `app/(browse)/(...)[artistSlug]/[year]`
|
|
42
|
+
* carries `/` and `/[year]`. Keyed by urlPath the intercepting node would
|
|
43
|
+
* collide with its own owner, and `[year]` with any top-level `[year]` route —
|
|
44
|
+
* so a client that had one mounted would reuse the wrong cached layout. The
|
|
45
|
+
* switch is sticky rather than per-node because the *descendants* are where
|
|
46
|
+
* the false URLs are; the intercepting node alone is not the problem.
|
|
47
|
+
*
|
|
48
|
+
* This is the single source of truth for segment keys — used by the
|
|
49
|
+
* element builder (skip decisions, SegmentOutlet props), segment info
|
|
50
|
+
* (X-Timber-Segments header), the client cache/state tree, and
|
|
51
|
+
* interception scoping (`routing/interception.ts`).
|
|
52
|
+
*/
|
|
53
|
+
export function computeSegmentKeys(segments: SegmentKeyInput[]): string[] {
|
|
54
|
+
const keys: string[] = [];
|
|
55
|
+
let prevKey = '';
|
|
56
|
+
let insideIntercepting = false;
|
|
57
|
+
|
|
58
|
+
for (const segment of segments) {
|
|
59
|
+
if (segment.segmentType === 'intercepting') insideIntercepting = true;
|
|
60
|
+
if (segment.segmentType === 'group' || insideIntercepting) {
|
|
61
|
+
const base = prevKey === '/' ? '' : prevKey;
|
|
62
|
+
const key = `${base}/${segment.segmentName}`;
|
|
63
|
+
keys.push(key);
|
|
64
|
+
prevKey = key;
|
|
65
|
+
} else {
|
|
66
|
+
keys.push(segment.urlPath);
|
|
67
|
+
prevKey = segment.urlPath;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return keys;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compute tree paths for a segment chain — the directory path of each segment
|
|
76
|
+
* with slots elided, e.g. `app/(browse)/feed` → `/(browse)/feed`.
|
|
77
|
+
*
|
|
78
|
+
* This is `computeSegmentKeys` with the group branch applied to *every*
|
|
79
|
+
* segment rather than only to groups. That one difference is the whole point:
|
|
80
|
+
* `computeSegmentKeys` resets to `urlPath` at each URL-visible segment, so
|
|
81
|
+
* `(browse)/feed` and `(landing)/feed` both key as `/feed`. Two directories
|
|
82
|
+
* that render different layouts must not share an identity.
|
|
83
|
+
*
|
|
84
|
+
* Used for interception scoping (`routing/interception.ts`), where the
|
|
85
|
+
* question is "does this route pass through *that* directory?" — not "does it
|
|
86
|
+
* render this URL?". Not interchangeable with `computeSegmentKeys`: the state
|
|
87
|
+
* tree is keyed by URL on purpose, because the client caches by URL.
|
|
88
|
+
*/
|
|
89
|
+
export function computeSegmentTreePaths(segments: SegmentKeyInput[]): string[] {
|
|
90
|
+
const paths: string[] = [];
|
|
91
|
+
let prev = '';
|
|
92
|
+
|
|
93
|
+
for (const segment of segments) {
|
|
94
|
+
const name = segment.segmentName ?? '';
|
|
95
|
+
if (!name) {
|
|
96
|
+
// The app root — no directory name of its own.
|
|
97
|
+
paths.push('/');
|
|
98
|
+
prev = '/';
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const base = prev === '/' ? '' : prev;
|
|
102
|
+
const path = `${base}/${name}`;
|
|
103
|
+
paths.push(path);
|
|
104
|
+
prev = path;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return paths;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The directory names a tree path is built from — `/` is none, `/feed/(a)` is
|
|
112
|
+
* `['feed', '(a)']`.
|
|
113
|
+
*
|
|
114
|
+
* The inverse of the walk above, and it lives beside it so the format is
|
|
115
|
+
* stated once: `findChainByTreePath` (`server/children-interception.ts`) walks
|
|
116
|
+
* these names back down the tree to the directory the path addresses, and
|
|
117
|
+
* `treePathDepth` counts them. Both are readings of the same string, and a
|
|
118
|
+
* reader that disagreed with the producer about what separates two names
|
|
119
|
+
* resolves an interception to the wrong node or to none.
|
|
120
|
+
*/
|
|
121
|
+
export function treePathNames(treePath: string): string[] {
|
|
122
|
+
return treePath === '/' ? [] : treePath.slice(1).split('/');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* How many directories a tree path names — `/` is 0, `/feed` is 1,
|
|
127
|
+
* `/feed/(a)` is 2.
|
|
128
|
+
*
|
|
129
|
+
* Two tree paths on a single rendered chain are strictly nested, so on that
|
|
130
|
+
* chain depth orders them totally — which is what makes it usable as "the
|
|
131
|
+
* deeper of these two directories" wherever a route passes through both
|
|
132
|
+
* (`routing/interception.ts`).
|
|
133
|
+
*/
|
|
134
|
+
export function treePathDepth(treePath: string): number {
|
|
135
|
+
return treePathNames(treePath).length;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Compute a unique key for a parallel route slot.
|
|
140
|
+
* Format: `{parentSegmentId}/@{slotName}`, e.g. `/@sidebar` or `/dashboard/@modal`.
|
|
141
|
+
*/
|
|
142
|
+
export function computeSlotKey(parentSegmentId: string, slotName: string): string {
|
|
143
|
+
const name = slotName.startsWith('@') ? slotName : `@${slotName}`;
|
|
144
|
+
const prefix = parentSegmentId === '/' ? '' : parentSegmentId;
|
|
145
|
+
return `${prefix}/${name}`;
|
|
146
|
+
}
|