@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
|
@@ -160,11 +160,13 @@ export function generateManifestModule(tree: RouteTree, viteRoot: string): strin
|
|
|
160
160
|
globalErrorLine = ` globalError: { load: ${v}, filePath: ${JSON.stringify(tree.globalError.filePath)} },`;
|
|
161
161
|
}
|
|
162
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).
|
|
163
167
|
const rewrites = collectInterceptionRewrites(tree.root);
|
|
164
168
|
const rewritesLine =
|
|
165
|
-
rewrites.length > 0
|
|
166
|
-
? ` interceptionRewrites: ${JSON.stringify(rewrites.map((r) => ({ interceptedPattern: r.interceptedPattern, interceptingPrefix: r.interceptingPrefix })))},`
|
|
167
|
-
: '';
|
|
169
|
+
rewrites.length > 0 ? ` interceptionRewrites: ${JSON.stringify(rewrites)},` : '';
|
|
168
170
|
|
|
169
171
|
const code = [
|
|
170
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';
|
|
@@ -76,6 +79,12 @@ export function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTre
|
|
|
76
79
|
// Scan children recursively
|
|
77
80
|
scanChildren(appDir, tree.root, extSet);
|
|
78
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
|
+
|
|
79
88
|
// Validate: detect route group collisions (different groups producing pages at the same URL)
|
|
80
89
|
validateRouteGroupCollisions(tree.root);
|
|
81
90
|
|
|
@@ -91,6 +100,10 @@ export function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTre
|
|
|
91
100
|
// e.g., a-[id] and [slug]-b both have prefix+suffix length = 2
|
|
92
101
|
validateAffixedSiblingConflicts(tree.root);
|
|
93
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
|
+
|
|
94
107
|
return tree;
|
|
95
108
|
}
|
|
96
109
|
|
|
@@ -355,69 +368,6 @@ function scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<stri
|
|
|
355
368
|
}
|
|
356
369
|
}
|
|
357
370
|
|
|
358
|
-
/**
|
|
359
|
-
* Validate that route groups don't produce conflicting pages/routes at the same URL path.
|
|
360
|
-
*
|
|
361
|
-
* Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim
|
|
362
|
-
* /login — the scanner must detect and reject this at build time.
|
|
363
|
-
*
|
|
364
|
-
* Each parallel slot is validated as its OWN collision space: a slot's pages
|
|
365
|
-
* intentionally share URL paths with the parent tree (that's the point of
|
|
366
|
-
* parallel routes), but two route groups INSIDE one slot claiming the same
|
|
367
|
-
* URL — e.g. @side/(a)/x/page.tsx + @side/(b)/x/page.tsx — still collide.
|
|
368
|
-
*/
|
|
369
|
-
function validateRouteGroupCollisions(root: SegmentNode): void {
|
|
370
|
-
// Map from urlPath → { filePath, source } for the first page/route seen at that path
|
|
371
|
-
collectRoutableLeaves(root, new Map(), '');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Walk the segment tree and collect all routable leaves (page or route files),
|
|
376
|
-
* throwing on collision. Each slot recurses with a FRESH collision map —
|
|
377
|
-
* slots are parallel routes that intentionally share URL paths with their
|
|
378
|
-
* parent tree, but collisions within a single slot are real errors.
|
|
379
|
-
*/
|
|
380
|
-
function collectRoutableLeaves(
|
|
381
|
-
node: SegmentNode,
|
|
382
|
-
seen: Map<string, { filePath: string; segmentPath: string }>,
|
|
383
|
-
segmentPath: string
|
|
384
|
-
): void {
|
|
385
|
-
// Intercepting routes are resolved by the slot resolver, not URL matching,
|
|
386
|
-
// and their computed urlPaths intentionally mirror other routes (e.g. two
|
|
387
|
-
// interceptors in one slot both produce /gallery/[id]). Skip them entirely.
|
|
388
|
-
if (node.segmentType === 'intercepting') return;
|
|
389
|
-
|
|
390
|
-
const currentPath = segmentPath
|
|
391
|
-
? `${segmentPath}/${node.segmentName}`
|
|
392
|
-
: node.segmentName || '(root)';
|
|
393
|
-
|
|
394
|
-
const routableFile = node.page ?? node.route;
|
|
395
|
-
if (routableFile) {
|
|
396
|
-
const existing = seen.get(node.urlPath);
|
|
397
|
-
if (existing) {
|
|
398
|
-
throw new Error(
|
|
399
|
-
`Build error: route collision — multiple route groups produce a page/route at the same URL path.\n` +
|
|
400
|
-
` URL path: ${node.urlPath}\n` +
|
|
401
|
-
` File 1: ${existing.filePath} (via ${existing.segmentPath})\n` +
|
|
402
|
-
` File 2: ${routableFile.filePath} (via ${currentPath})\n` +
|
|
403
|
-
`Each URL path must map to exactly one page or route handler. ` +
|
|
404
|
-
`Rename or move one of the conflicting files.`
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
seen.set(node.urlPath, { filePath: routableFile.filePath, segmentPath: currentPath });
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Recurse into children
|
|
411
|
-
for (const child of node.children) {
|
|
412
|
-
collectRoutableLeaves(child, seen, currentPath);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Recurse into slots — each slot gets a fresh map (its own collision space)
|
|
416
|
-
for (const slotNode of Object.values(node.slots)) {
|
|
417
|
-
collectRoutableLeaves(slotNode, new Map(), currentPath);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
371
|
/**
|
|
422
372
|
* Validate that no route chain contains duplicate dynamic param names.
|
|
423
373
|
*
|
|
@@ -438,12 +388,20 @@ function validateDuplicateParamNames(root: SegmentNode): void {
|
|
|
438
388
|
* Throws on the first duplicate found.
|
|
439
389
|
*/
|
|
440
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
|
+
|
|
441
399
|
// If this node introduces a param name, check for duplicates
|
|
442
|
-
if (
|
|
443
|
-
const existing = seen.get(
|
|
400
|
+
if (paramName) {
|
|
401
|
+
const existing = seen.get(paramName);
|
|
444
402
|
if (existing) {
|
|
445
403
|
throw new Error(
|
|
446
|
-
`[timber] Duplicate param name '${
|
|
404
|
+
`[timber] Duplicate param name '${paramName}' in route chain.\n` +
|
|
447
405
|
` First defined at: ${existing}\n` +
|
|
448
406
|
` Duplicate at: ${node.urlPath || '/'}\n` +
|
|
449
407
|
` Rename one of the segments to avoid ambiguity.`
|
|
@@ -451,7 +409,7 @@ function walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): v
|
|
|
451
409
|
}
|
|
452
410
|
// Add to seen for descendants (use a new Map to avoid polluting siblings)
|
|
453
411
|
seen = new Map(seen);
|
|
454
|
-
seen.set(
|
|
412
|
+
seen.set(paramName, node.urlPath || '/');
|
|
455
413
|
}
|
|
456
414
|
|
|
457
415
|
// Recurse into children (they inherit the accumulated params)
|
|
@@ -459,8 +417,16 @@ function walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): v
|
|
|
459
417
|
walkForDuplicateParams(child, seen);
|
|
460
418
|
}
|
|
461
419
|
|
|
462
|
-
//
|
|
463
|
-
//
|
|
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.
|
|
464
430
|
for (const slotNode of Object.values(node.slots)) {
|
|
465
431
|
walkForDuplicateParams(slotNode, new Map(seen));
|
|
466
432
|
}
|
|
@@ -488,25 +454,29 @@ function collectBracketForms(
|
|
|
488
454
|
node: SegmentNode,
|
|
489
455
|
forms: Map<string, { segmentType: SegmentType; urlPath: string }>
|
|
490
456
|
): void {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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) {
|
|
494
464
|
const bracketExamples: Record<string, string> = {
|
|
495
|
-
'dynamic': `[${
|
|
496
|
-
'catch-all': `[...${
|
|
497
|
-
'optional-catch-all': `[[...${
|
|
465
|
+
'dynamic': `[${paramName}]`,
|
|
466
|
+
'catch-all': `[...${paramName}]`,
|
|
467
|
+
'optional-catch-all': `[[...${paramName}]]`,
|
|
498
468
|
};
|
|
499
469
|
throw new Error(
|
|
500
|
-
`[timber] Param name '${
|
|
470
|
+
`[timber] Param name '${paramName}' uses multiple bracket forms.\n` +
|
|
501
471
|
` ${bracketExamples[existing.segmentType] ?? existing.segmentType} at: ${existing.urlPath}\n` +
|
|
502
|
-
` ${bracketExamples[
|
|
472
|
+
` ${bracketExamples[segmentType] ?? segmentType} at: ${node.urlPath || '/'}\n` +
|
|
503
473
|
` Each param name must use a single bracket form across all routes.\n` +
|
|
504
|
-
` Rename one of the segments to fix this (e.g., [${
|
|
474
|
+
` Rename one of the segments to fix this (e.g., [${paramName}] → [${paramName}Id]).`
|
|
505
475
|
);
|
|
506
476
|
}
|
|
507
477
|
if (!existing) {
|
|
508
|
-
forms.set(
|
|
509
|
-
segmentType
|
|
478
|
+
forms.set(paramName, {
|
|
479
|
+
segmentType,
|
|
510
480
|
urlPath: node.urlPath || '/',
|
|
511
481
|
});
|
|
512
482
|
}
|
|
@@ -530,20 +500,39 @@ function collectBracketForms(
|
|
|
530
500
|
* Route groups are URL-transparent, so affixed segments inside different groups
|
|
531
501
|
* at the same level still conflict with each other.
|
|
532
502
|
*
|
|
533
|
-
*
|
|
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".
|
|
534
519
|
*/
|
|
535
520
|
function validateAffixedSiblingConflicts(node: SegmentNode): void {
|
|
536
521
|
const affixed = collectEffectiveAffixed(node);
|
|
537
522
|
|
|
538
523
|
if (affixed.length >= 2) {
|
|
539
|
-
const byLength = new Map<
|
|
524
|
+
const byLength = new Map<string, SegmentNode[]>();
|
|
540
525
|
for (const child of affixed) {
|
|
541
|
-
|
|
542
|
-
|
|
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);
|
|
543
532
|
if (group) {
|
|
544
533
|
group.push(child);
|
|
545
534
|
} else {
|
|
546
|
-
byLength.set(
|
|
535
|
+
byLength.set(key, [child]);
|
|
547
536
|
}
|
|
548
537
|
}
|
|
549
538
|
|
|
@@ -553,7 +542,7 @@ function validateAffixedSiblingConflicts(node: SegmentNode): void {
|
|
|
553
542
|
`Build error: ambiguous affixed dynamic siblings with equal specificity.\n` +
|
|
554
543
|
` Parent: ${node.urlPath || '/'}\n` +
|
|
555
544
|
` Segments: ${group.map((s) => s.segmentName).join(', ')}\n` +
|
|
556
|
-
` Combined prefix+suffix length: ${(group[0]
|
|
545
|
+
` Combined prefix+suffix length: ${affixLengthOf(group[0]!)}\n` +
|
|
557
546
|
`The router cannot deterministically choose between these — filesystem scan order is platform-dependent.\n` +
|
|
558
547
|
`Rename one segment to give it a longer or shorter combined affix.`
|
|
559
548
|
);
|
|
@@ -569,16 +558,52 @@ function validateAffixedSiblingConflicts(node: SegmentNode): void {
|
|
|
569
558
|
}
|
|
570
559
|
}
|
|
571
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
|
+
|
|
572
588
|
/**
|
|
573
589
|
* Collect affixed dynamic segments that are effective children of a node.
|
|
574
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).
|
|
575
599
|
*/
|
|
576
600
|
function collectEffectiveAffixed(node: SegmentNode): SegmentNode[] {
|
|
577
601
|
const result: SegmentNode[] = [];
|
|
578
602
|
for (const child of node.children) {
|
|
579
|
-
|
|
603
|
+
const { segmentType, paramPrefix, paramSuffix } = effectiveUrlSegment(child);
|
|
604
|
+
if (segmentType === 'dynamic' && (paramPrefix || paramSuffix)) {
|
|
580
605
|
result.push(child);
|
|
581
|
-
} else if (
|
|
606
|
+
} else if (segmentType === 'group') {
|
|
582
607
|
result.push(...collectEffectiveAffixed(child));
|
|
583
608
|
}
|
|
584
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
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel slot placement.
|
|
3
|
+
*
|
|
4
|
+
* One rule: a slot is resolved only for a segment on the **matched route
|
|
5
|
+
* chain**, and a slot's own subtree is never on that chain. So a slot declared
|
|
6
|
+
* anywhere inside another slot's subtree can never render, and is rejected at
|
|
7
|
+
* build time rather than left as a directory the author will keep editing.
|
|
8
|
+
*
|
|
9
|
+
* See design/07-routing.md §"Parallel Routes".
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { SegmentNode } from './types.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reject every parallel slot that no renderer reaches.
|
|
16
|
+
*
|
|
17
|
+
* **Where slots are resolved.** Two places read `segment.slots`, and both walk
|
|
18
|
+
* the main route chain: `route-element-builder.ts` (the RSC render) and
|
|
19
|
+
* `tree-builder.ts`. Neither is reachable from inside a slot. The slot's own
|
|
20
|
+
* subtree is rendered by `slot-resolver.ts`, which walks the slot's matched
|
|
21
|
+
* chain to apply that chain's layouts, error boundaries and access gates
|
|
22
|
+
* (`wrapWithIntermediateSegments`, `wrapWithLayout`) and never reads `.slots`
|
|
23
|
+
* on any of those nodes. So `app/feed/@modal/photo/@side/page.tsx` and
|
|
24
|
+
* everything below it is dead: the file is scanned, typed, code-split and
|
|
25
|
+
* shipped, and no request can render it.
|
|
26
|
+
*
|
|
27
|
+
* That is the TIM-1278 failure — a directory that silently never renders —
|
|
28
|
+
* and the reason it is a build error rather than a documented limit is that
|
|
29
|
+
* nothing at request time can report it. A slot that does not match renders
|
|
30
|
+
* `default.tsx` or `null`, which is exactly what a slot the resolver never
|
|
31
|
+
* looked at also produces, so the author sees the *designed* behaviour of a
|
|
32
|
+
* non-matching slot and has no signal to distinguish it from a slot that
|
|
33
|
+
* cannot match at all.
|
|
34
|
+
*
|
|
35
|
+
* **Interception is incidental.** TIM-1311 arrived as
|
|
36
|
+
* `feed/@modal/(.)photo/@side`, and the interception contributes nothing: the
|
|
37
|
+
* identical `feed/@modal/photo/@side` never renders either, for the same
|
|
38
|
+
* reason and with no marker anywhere. Rejecting only the intercepting spelling
|
|
39
|
+
* would have drawn a line the author cannot see from the directory names.
|
|
40
|
+
*
|
|
41
|
+
* The children-path interception shape is **not** affected and must not be:
|
|
42
|
+
* its intercepting node joins the main chain (`server/children-interception.ts`
|
|
43
|
+
* splices `[…owner ancestors, interceptingNode, …descendants]` into an ordinary
|
|
44
|
+
* `RouteMatch`), so `route-element-builder.ts` resolves slots on it and below
|
|
45
|
+
* it exactly as it does for any other chain segment. `(browse)/(...)photo/[id]/@side`
|
|
46
|
+
* renders; `feed/@modal/(.)photo/@side` does not. The discriminator is the slot
|
|
47
|
+
* boundary, which is what this walk tracks — not the marker.
|
|
48
|
+
*/
|
|
49
|
+
export function validateSlotPlacement(root: SegmentNode): void {
|
|
50
|
+
walkForSlotPlacement(root, '', null);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `enclosingSlotPath` is the `app/`-relative path of the nearest slot at or
|
|
55
|
+
* above `node`, or null on the main chain. It is carried rather than recomputed
|
|
56
|
+
* so the error can name the slot that swallows the offender — "this is inside
|
|
57
|
+
* @modal" is the part of the diagnosis the author cannot see from the offending
|
|
58
|
+
* path alone.
|
|
59
|
+
*/
|
|
60
|
+
function walkForSlotPlacement(
|
|
61
|
+
node: SegmentNode,
|
|
62
|
+
path: string,
|
|
63
|
+
enclosingSlotPath: string | null
|
|
64
|
+
): void {
|
|
65
|
+
for (const [slotName, slotNode] of Object.entries(node.slots)) {
|
|
66
|
+
const slotPath = path ? `${path}/@${slotName}` : `@${slotName}`;
|
|
67
|
+
if (enclosingSlotPath !== null) {
|
|
68
|
+
throwNestedSlot(slotPath, enclosingSlotPath);
|
|
69
|
+
}
|
|
70
|
+
walkForSlotPlacement(slotNode, slotPath, slotPath);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const child of node.children) {
|
|
74
|
+
walkForSlotPlacement(
|
|
75
|
+
child,
|
|
76
|
+
path ? `${path}/${child.segmentName}` : child.segmentName,
|
|
77
|
+
enclosingSlotPath
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function throwNestedSlot(slotPath: string, enclosingSlotPath: string): never {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Build error: parallel slot nested inside another parallel slot.\n` +
|
|
85
|
+
` Directory: app/${slotPath}\n` +
|
|
86
|
+
` Inside: app/${enclosingSlotPath}\n` +
|
|
87
|
+
`A parallel slot is resolved only for a segment on the matched route chain, and a ` +
|
|
88
|
+
`slot's own subtree is never on that chain — the slot resolver walks the slot's ` +
|
|
89
|
+
`matched chain to apply its layouts, error boundaries and access gates, and never ` +
|
|
90
|
+
`looks at slots declared inside it. So app/${slotPath} and everything below it can ` +
|
|
91
|
+
`never render, and no request-time signal can tell you: an unreachable slot ` +
|
|
92
|
+
`produces exactly what a slot that simply did not match produces.\n` +
|
|
93
|
+
`Declare the slot on a segment of the main route chain instead, or move its pages ` +
|
|
94
|
+
`into app/${enclosingSlotPath}'s own subtree.\n` +
|
|
95
|
+
`See design/07-routing.md §"Parallel Routes".`
|
|
96
|
+
);
|
|
97
|
+
}
|
package/src/routing/walkers.ts
CHANGED
|
@@ -49,6 +49,12 @@ export interface CollectLeafRoutesOptions {
|
|
|
49
49
|
* entries (the build report deduplicates by URL afterward).
|
|
50
50
|
* - Parallel slots are skipped unless `includeSlots: true` (slots
|
|
51
51
|
* share their parent's URL and are not addressable on their own).
|
|
52
|
+
* - Intercepting subtrees are skipped unconditionally. They render only
|
|
53
|
+
* on soft navigation and add no URL depth, so their computed `urlPath`s
|
|
54
|
+
* are not addresses — listing them would advertise routes (`/[year]`)
|
|
55
|
+
* that no request can reach. This matches every other consumer of the
|
|
56
|
+
* tree that produces URLs: the sitemap generator, the static generator,
|
|
57
|
+
* prerendering and the prebuilt-payload builder all skip them too.
|
|
52
58
|
* - Result is sorted by `urlPath` for deterministic output.
|
|
53
59
|
*/
|
|
54
60
|
export function collectLeafRoutes<TFile>(
|
|
@@ -68,6 +74,8 @@ function walk<TFile>(
|
|
|
68
74
|
result: LeafRoute<TFile>[],
|
|
69
75
|
includeSlots: boolean
|
|
70
76
|
): void {
|
|
77
|
+
if (node.segmentType === 'intercepting') return;
|
|
78
|
+
|
|
71
79
|
const currentChain = [...chain, node];
|
|
72
80
|
const path = node.urlPath || '/';
|
|
73
81
|
|