@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
package/src/plugins/routing.ts
CHANGED
|
@@ -107,10 +107,49 @@ const TIMBER_ENV_DTS = [
|
|
|
107
107
|
'',
|
|
108
108
|
].join('\n');
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Codegen writes that have been started but have not yet landed on disk.
|
|
112
|
+
*
|
|
113
|
+
* `writeCodegen` is deliberately unawaited on the hot path, which leaves a
|
|
114
|
+
* caller no way to know the files exist yet. Anything that tears down the
|
|
115
|
+
* project root — a build process exiting, a test removing its temp root —
|
|
116
|
+
* then races the writes: `rmSync` empties a directory that a pending `mkdir`
|
|
117
|
+
* immediately recreates, and the removal fails with ENOTEMPTY.
|
|
118
|
+
*
|
|
119
|
+
* Every started write registers here and deregisters when it settles, so
|
|
120
|
+
* `flushCodegenWrites()` can await them instead of guessing at a delay.
|
|
121
|
+
*/
|
|
122
|
+
const pendingCodegenWrites = new Set<Promise<void>>();
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolve once every codegen write started so far has landed on disk.
|
|
126
|
+
*
|
|
127
|
+
* Safe to call at any time and from anywhere — writes are tracked per process,
|
|
128
|
+
* not per plugin instance, so a caller does not need a handle on the plugin
|
|
129
|
+
* that started them. Never rejects: individual write failures are already
|
|
130
|
+
* swallowed and logged by `writeCodegen`.
|
|
131
|
+
*/
|
|
132
|
+
export async function flushCodegenWrites(): Promise<void> {
|
|
133
|
+
// Loop rather than a single Promise.all: a write started while we were
|
|
134
|
+
// awaiting would otherwise be missed, and the set would still be non-empty
|
|
135
|
+
// on return.
|
|
136
|
+
//
|
|
137
|
+
// Each round drops what it awaited rather than trusting the per-write
|
|
138
|
+
// `finally` to have run, so the two removal paths are independent — a
|
|
139
|
+
// deregistration that stopped firing would leak memory in dev, but it can
|
|
140
|
+
// never turn this into a spin.
|
|
141
|
+
while (pendingCodegenWrites.size > 0) {
|
|
142
|
+
const inFlight = [...pendingCodegenWrites];
|
|
143
|
+
await Promise.all(inFlight);
|
|
144
|
+
for (const write of inFlight) pendingCodegenWrites.delete(write);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
110
148
|
/**
|
|
111
149
|
* Write the generated route map and the timber-env.d.ts reference file.
|
|
112
150
|
*
|
|
113
|
-
*
|
|
151
|
+
* Not awaited by callers, to avoid blocking the hot path — use
|
|
152
|
+
* `flushCodegenWrites()` to wait for the writes to land.
|
|
114
153
|
* Errors are swallowed; a missing .d.ts is a developer UX issue, not a runtime failure.
|
|
115
154
|
*/
|
|
116
155
|
function writeCodegen(ctx: PluginContext): void {
|
|
@@ -119,7 +158,7 @@ function writeCodegen(ctx: PluginContext): void {
|
|
|
119
158
|
const content = generateRouteMap(ctx.routeTree, { appDir: ctx.appDir, outputDir: timberDir });
|
|
120
159
|
const routesPath = join(ctx.root, CODEGEN_OUTPUT);
|
|
121
160
|
const envPath = join(ctx.root, 'timber-env.d.ts');
|
|
122
|
-
mkdir(timberDir, { recursive: true })
|
|
161
|
+
const done = mkdir(timberDir, { recursive: true })
|
|
123
162
|
.then(() =>
|
|
124
163
|
Promise.all([
|
|
125
164
|
writeFile(routesPath, content, 'utf-8'),
|
|
@@ -127,10 +166,15 @@ function writeCodegen(ctx: PluginContext): void {
|
|
|
127
166
|
writeSegmentTypes(ctx.routeTree!, ctx.appDir, ctx.root),
|
|
128
167
|
])
|
|
129
168
|
)
|
|
169
|
+
.then(() => undefined)
|
|
130
170
|
.catch((err) => {
|
|
131
171
|
// Non-fatal — types are a dev convenience, but log so issues are visible
|
|
132
172
|
console.warn('[timber] Failed to write codegen output:', err);
|
|
133
173
|
});
|
|
174
|
+
|
|
175
|
+
pendingCodegenWrites.add(done);
|
|
176
|
+
// `done` has a catch attached, so it never rejects and this stays quiet.
|
|
177
|
+
void done.finally(() => pendingCodegenWrites.delete(done));
|
|
134
178
|
}
|
|
135
179
|
|
|
136
180
|
export function timberRouting(ctx: PluginContext): Plugin {
|
|
@@ -288,6 +332,19 @@ export function timberRouting(ctx: PluginContext): Plugin {
|
|
|
288
332
|
rescan();
|
|
289
333
|
},
|
|
290
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Wait for the codegen writes started by `buildStart` before the build
|
|
337
|
+
* reports done.
|
|
338
|
+
*
|
|
339
|
+
* `writeCodegen` runs unawaited so route scanning never blocks on disk.
|
|
340
|
+
* Without this the build can finish — and the CLI can exit — while the
|
|
341
|
+
* generated `.d.ts` files are still being written, leaving a project root
|
|
342
|
+
* with no route types after an apparently successful build.
|
|
343
|
+
*/
|
|
344
|
+
closeBundle() {
|
|
345
|
+
return flushCodegenWrites();
|
|
346
|
+
},
|
|
347
|
+
|
|
291
348
|
/**
|
|
292
349
|
* Set up file watcher in dev mode.
|
|
293
350
|
*
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Plugin } from 'vite';
|
|
12
12
|
import { existsSync } from 'node:fs';
|
|
13
|
-
import { mkdir, readdir, readFile, writeFile, rename, rm } from 'node:fs/promises';
|
|
14
|
-
import { createHash } from 'node:crypto';
|
|
15
|
-
import { dirname, extname, join, resolve, relative } from 'node:path';
|
|
13
|
+
import { mkdir, readdir, readFile, writeFile, rename, rm, stat } from 'node:fs/promises';
|
|
14
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
15
|
+
import { dirname, extname, isAbsolute, join, resolve, relative } from 'node:path';
|
|
16
16
|
import { pathToFileURL } from 'node:url';
|
|
17
17
|
import type { PluginContext } from '../plugin-context.js';
|
|
18
18
|
import type {
|
|
@@ -22,7 +22,17 @@ import type {
|
|
|
22
22
|
StaticRedirectEntry,
|
|
23
23
|
} from '../server/static-generator.js';
|
|
24
24
|
import { mergeRscAssetsManifest } from './adapter-build.js';
|
|
25
|
+
import { isRelativeBase, normalizeBasePath, withBasePath } from '../shared/base-path.js';
|
|
26
|
+
import type { RscManifest, RscManifestEntry } from '../shared/rsc-manifest.js';
|
|
25
27
|
import { htmlEscapeJsonString } from '../server/flight-scripts.js';
|
|
28
|
+
import { rscPayloadPath } from '../shared/rsc-payload-path.js';
|
|
29
|
+
import { REDIRECTS_FILE, RESERVED_OUTPUT_PATHS } from '../shared/static-platform-files.js';
|
|
30
|
+
import {
|
|
31
|
+
detectFsIdentity,
|
|
32
|
+
fsKey,
|
|
33
|
+
EXACT_FS_IDENTITY,
|
|
34
|
+
type FsIdentity,
|
|
35
|
+
} from '../adapters/fs-identity.js';
|
|
26
36
|
|
|
27
37
|
// ---------------------------------------------------------------------------
|
|
28
38
|
// Types
|
|
@@ -33,12 +43,9 @@ import { htmlEscapeJsonString } from '../server/flight-scripts.js';
|
|
|
33
43
|
* Maps an unhashed `/_rsc/*.rsc` path to its hashed URL, with
|
|
34
44
|
* optional route params inlined to avoid a sidecar fetch.
|
|
35
45
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export type RscManifest = Record<string, RscManifestEntry>;
|
|
46
|
+
// Declared in shared/rsc-manifest.ts so the client that reads this manifest and
|
|
47
|
+
// the build that writes it cannot drift. Re-exported for existing importers.
|
|
48
|
+
export type { RscManifestEntry, RscManifest } from '../shared/rsc-manifest.js';
|
|
42
49
|
|
|
43
50
|
export interface StaticValidationError {
|
|
44
51
|
type: 'dynamic-api';
|
|
@@ -107,10 +114,23 @@ export function detectDynamicApis(code: string, fileId: string): StaticValidatio
|
|
|
107
114
|
* Generate a `_redirects` file from collected redirect entries.
|
|
108
115
|
* Uses the Cloudflare Pages / Netlify format: `/from /to statusCode`
|
|
109
116
|
*
|
|
117
|
+
* Both sides are base-prefixed (TIM-1274). `from` is matched by the host
|
|
118
|
+
* against the request path and `to` is served to the browser as a `Location`,
|
|
119
|
+
* so both are deployed-URL space while `StaticRedirectEntry` carries route
|
|
120
|
+
* space — under `base: '/repo/'`, a rule `/old /new 301` would match nothing
|
|
121
|
+
* and, if it did, would send the browser off the deployment.
|
|
122
|
+
*
|
|
123
|
+
* An off-origin `to` is left alone: `redirectExternal()` produces an absolute
|
|
124
|
+
* URL, and a protocol-relative one addresses another host too. Neither is a
|
|
125
|
+
* path on this deployment, so neither takes the prefix.
|
|
126
|
+
*
|
|
127
|
+
* @param base Normalized deployment base (see `shared/base-path.ts`).
|
|
110
128
|
* @internal Exported for testing.
|
|
111
129
|
*/
|
|
112
|
-
export function generateRedirectsFile(redirects: StaticRedirectEntry[]): string {
|
|
113
|
-
const
|
|
130
|
+
export function generateRedirectsFile(redirects: StaticRedirectEntry[], base: string): string {
|
|
131
|
+
const target = (to: string) =>
|
|
132
|
+
to.startsWith('/') && !to.startsWith('//') ? withBasePath(base, to) : to;
|
|
133
|
+
const lines = redirects.map((r) => `${withBasePath(base, r.from)} ${target(r.to)} ${r.status}`);
|
|
114
134
|
return lines.join('\n') + '\n';
|
|
115
135
|
}
|
|
116
136
|
|
|
@@ -129,15 +149,20 @@ async function writeFileAtomic(path: string, data: Uint8Array | string): Promise
|
|
|
129
149
|
* Determine the output file path for a static page entry.
|
|
130
150
|
*
|
|
131
151
|
* Pages: /about → about/index.html, / → index.html
|
|
132
|
-
* RSC: /about → _rsc/about.rsc, / → _rsc/index.rsc
|
|
152
|
+
* RSC: /about → _rsc/about/index.rsc, / → _rsc/index.rsc
|
|
133
153
|
* API: /feed.xml → feed.xml, /api/data → api/data
|
|
154
|
+
*
|
|
155
|
+
* The RSC shape mirrors the HTML shape for the reason the HTML shape has it:
|
|
156
|
+
* a route gets a directory and the artifact is the `index` inside it, so the
|
|
157
|
+
* mapping is injective. See `shared/rsc-payload-path.ts` (TIM-1266).
|
|
134
158
|
*/
|
|
135
159
|
export function staticOutputPath(entry: StaticPageEntry): string {
|
|
136
160
|
const urlPath = entry.urlPath === '/' ? '' : entry.urlPath;
|
|
137
161
|
|
|
138
162
|
if (entry.kind === 'rsc') {
|
|
139
|
-
|
|
140
|
-
|
|
163
|
+
// Route space is `/…`; the static output path is relative to the output
|
|
164
|
+
// directory, so drop the leading slash.
|
|
165
|
+
return rscPayloadPath(entry.urlPath).slice(1);
|
|
141
166
|
}
|
|
142
167
|
|
|
143
168
|
if (entry.kind === 'api') {
|
|
@@ -156,6 +181,216 @@ export function staticOutputPath(entry: StaticPageEntry): string {
|
|
|
156
181
|
return `${urlPath.startsWith('/') ? urlPath.slice(1) : urlPath}/index.html`;
|
|
157
182
|
}
|
|
158
183
|
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Output path collisions (TIM-1266)
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Records which entry claimed each output path and reports second claims.
|
|
190
|
+
*
|
|
191
|
+
* Two entries landing on one path is not a rendering failure — both render
|
|
192
|
+
* fine — so nothing downstream notices. The file is written twice, the second
|
|
193
|
+
* write wins, and the deployment serves one route's bytes at another route's
|
|
194
|
+
* URL with the same content type and the same shape (TIM-1266).
|
|
195
|
+
*
|
|
196
|
+
* The naming rules are injective by construction (a route path is a directory
|
|
197
|
+
* and the artifact is the `index` inside it), so a collision here means either
|
|
198
|
+
* two entries were enumerated for one URL or a route reached a path the
|
|
199
|
+
* framework reserves for itself (`404.html`). Both are build errors: there is
|
|
200
|
+
* no correct file to write.
|
|
201
|
+
*
|
|
202
|
+
* Two entries can also be unwritable together without claiming the same
|
|
203
|
+
* string: one wants a file where the other wants a directory (`about` from an
|
|
204
|
+
* API route, `about/index.html` from a page). The filesystem does refuse that
|
|
205
|
+
* one — `EISDIR` on the rename, `ENOTDIR` on the mkdir, depending on which
|
|
206
|
+
* lands second — but an errno names a path, and in the rename case the path
|
|
207
|
+
* it names is `about.tmp`. Reported here instead, so the message says which
|
|
208
|
+
* two routes.
|
|
209
|
+
*
|
|
210
|
+
* Collisions are **collected, not thrown**. `onEntry` runs inside the
|
|
211
|
+
* generator, whose RSC branch treats a throw as "this payload is unavailable,
|
|
212
|
+
* SPA navigation falls back to a full load" and swallows it — which for a
|
|
213
|
+
* collision is the wrong end state, since the two payloads are still
|
|
214
|
+
* ambiguous. The caller raises them once generation is done.
|
|
215
|
+
*
|
|
216
|
+
* @internal Exported for testing.
|
|
217
|
+
*/
|
|
218
|
+
export class StaticOutputPaths {
|
|
219
|
+
private readonly claimed = new Map<string, StaticPageEntry>();
|
|
220
|
+
/** Every ancestor directory of a claimed path, mapped to its claimant. */
|
|
221
|
+
private readonly directories = new Map<string, StaticPageEntry>();
|
|
222
|
+
private readonly collisions: string[] = [];
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param identity How the output filesystem decides two paths name one
|
|
226
|
+
* file, from `detectFsIdentity`. The guard predicts whether two writes
|
|
227
|
+
* land on one file, which is a property of the filesystem — so it is
|
|
228
|
+
* asked, not assumed. Folding unconditionally would reject `/Foo` beside
|
|
229
|
+
* `/foo` on Linux, where both are perfectly servable files.
|
|
230
|
+
*/
|
|
231
|
+
constructor(private readonly identity: FsIdentity = EXACT_FS_IDENTITY) {}
|
|
232
|
+
|
|
233
|
+
/** The identity under which the filesystem will compare this path. */
|
|
234
|
+
private key(relPath: string): string {
|
|
235
|
+
return fsKey(relPath, this.identity);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Claim `relPath` for `entry`. Returns false when the path cannot be
|
|
240
|
+
* written alongside an earlier claim, in which case the caller must not
|
|
241
|
+
* write the file — the losing entry is recorded and the build fails at
|
|
242
|
+
* the end.
|
|
243
|
+
*/
|
|
244
|
+
claim(relPath: string, entry: StaticPageEntry): boolean {
|
|
245
|
+
// Folded like every other comparison here: on a case-insensitive
|
|
246
|
+
// filesystem a route rendering to `_HEADERS` is the same file the
|
|
247
|
+
// adapter is about to write, so a case-sensitive match would wave it
|
|
248
|
+
// through into the overwrite this reservation exists to stop.
|
|
249
|
+
if (RESERVED_OUTPUT_PATHS.some((name) => this.key(name) === this.key(relPath))) {
|
|
250
|
+
this.collisions.push(
|
|
251
|
+
` - ${relPath}: reserved — the framework writes this file itself, ` +
|
|
252
|
+
`and ${entry.kind} for ${entry.urlPath} would be overwritten by it`
|
|
253
|
+
);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const key = this.key(relPath);
|
|
258
|
+
const conflict =
|
|
259
|
+
this.claimed.get(key) ??
|
|
260
|
+
// This entry wants a file where an earlier one wants a directory…
|
|
261
|
+
this.directories.get(key) ??
|
|
262
|
+
// …or a directory where an earlier one wants a file.
|
|
263
|
+
ancestors(relPath).reduce<StaticPageEntry | undefined>(
|
|
264
|
+
(found, dir) => found ?? this.claimed.get(this.key(dir)),
|
|
265
|
+
undefined
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (conflict) {
|
|
269
|
+
this.collisions.push(
|
|
270
|
+
` - ${relPath}: ${conflict.kind} for ${conflict.urlPath} and ${entry.kind} for ${entry.urlPath}`
|
|
271
|
+
);
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.claimed.set(key, entry);
|
|
276
|
+
for (const dir of ancestors(relPath)) {
|
|
277
|
+
const dirKey = this.key(dir);
|
|
278
|
+
if (!this.directories.has(dirKey)) this.directories.set(dirKey, entry);
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Throw if any two entries could not both be written. */
|
|
284
|
+
assertNoCollisions(): void {
|
|
285
|
+
if (this.collisions.length === 0) return;
|
|
286
|
+
throw new Error(
|
|
287
|
+
`[timber] Static generation: ${this.collisions.length} output path collision(s) — ` +
|
|
288
|
+
`two entries write the same path and one would silently overwrite the other. ` +
|
|
289
|
+
`Rename or remove one route in each pair:\n${this.collisions.join('\n')}`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Every directory an output path sits under, outermost first. */
|
|
295
|
+
function ancestors(relPath: string): string[] {
|
|
296
|
+
const parts = relPath.split('/');
|
|
297
|
+
parts.pop();
|
|
298
|
+
return parts.map((_, i) => parts.slice(0, i + 1).join('/'));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Writes generated entries into the static output directory, one file per
|
|
303
|
+
* entry, refusing any write that would land on a path another entry holds.
|
|
304
|
+
*
|
|
305
|
+
* @internal Exported for testing.
|
|
306
|
+
*/
|
|
307
|
+
export class StaticSiteWriter {
|
|
308
|
+
/** Content-Type per URL path for extensionless API routes (TIM-1241). */
|
|
309
|
+
readonly contentTypes: Record<string, string> = {};
|
|
310
|
+
private readonly paths: StaticOutputPaths;
|
|
311
|
+
|
|
312
|
+
constructor(
|
|
313
|
+
private readonly staticDir: string,
|
|
314
|
+
identity: FsIdentity = EXACT_FS_IDENTITY
|
|
315
|
+
) {
|
|
316
|
+
this.paths = new StaticOutputPaths(identity);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async write(entry: StaticPageEntry): Promise<void> {
|
|
320
|
+
const relPath = staticOutputPath(entry);
|
|
321
|
+
const fullPath = resolve(this.staticDir, relPath);
|
|
322
|
+
|
|
323
|
+
// Reject path traversal: generated params with "../" could escape
|
|
324
|
+
// the output directory and overwrite build artifacts.
|
|
325
|
+
//
|
|
326
|
+
// Tested on the first *segment*, not on the prefix. A slug is arbitrary
|
|
327
|
+
// application data, and `..draft` is an ordinary directory name whose
|
|
328
|
+
// relative path merely begins with two dots — a prefix test failed the
|
|
329
|
+
// build on it. `isAbsolute` covers the case `relative()` gives up on and
|
|
330
|
+
// returns an absolute path for: a different Windows drive, where the
|
|
331
|
+
// round-trip check below compares equal and would let it through.
|
|
332
|
+
const rel = relative(this.staticDir, fullPath);
|
|
333
|
+
const firstSegment = rel.split(/[\\/]/)[0];
|
|
334
|
+
if (firstSegment === '..' || isAbsolute(rel) || resolve(this.staticDir, rel) !== fullPath) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`[timber] Static generation: output path "${relPath}" escapes the ` +
|
|
337
|
+
`static output directory — check generateStaticSegmentParams for ` +
|
|
338
|
+
`path traversal characters in ${entry.urlPath}`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Claim the *normalized* path, not the one `staticOutputPath` built. Two
|
|
343
|
+
// distinct strings can name one file — a generated param carrying `..`
|
|
344
|
+
// makes `_rsc/a/../b/index.rsc` and `_rsc/b/index.rsc` different keys for
|
|
345
|
+
// the same write, which is the collision this check exists to stop. The
|
|
346
|
+
// traversal guard above only rejects escapes; it lets an in-bounds detour
|
|
347
|
+
// through.
|
|
348
|
+
//
|
|
349
|
+
// A losing claim is not written: the build is about to fail, and
|
|
350
|
+
// clobbering the winner would make the failure state worse than the one
|
|
351
|
+
// being reported.
|
|
352
|
+
if (!this.paths.claim(rel.replace(/\\/g, '/'), entry)) return;
|
|
353
|
+
|
|
354
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
355
|
+
await writeFileAtomic(fullPath, entry.body);
|
|
356
|
+
|
|
357
|
+
// Track Content-Type for extensionless API routes so adapters
|
|
358
|
+
// can write per-path Content-Type rules in _headers. TIM-1241.
|
|
359
|
+
if (entry.kind === 'api' && !extname(relPath) && entry.contentType) {
|
|
360
|
+
this.contentTypes[entry.urlPath] = entry.contentType;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Throw if any two entries could not both be written. */
|
|
365
|
+
assertNoCollisions(): void {
|
|
366
|
+
this.paths.assertNoCollisions();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Remove artifacts a generation pass produces only *conditionally*, so none
|
|
372
|
+
* of them can survive from an earlier build into this one.
|
|
373
|
+
*
|
|
374
|
+
* Pages are not in this set: every page is rewritten each pass, and a page
|
|
375
|
+
* whose route was deleted is a different problem (a stale file nothing links
|
|
376
|
+
* to) from the one here.
|
|
377
|
+
*
|
|
378
|
+
* - `_rsc/` because the files are content-hashed. Left in place, a previous
|
|
379
|
+
* build's already-hashed payload gets hashed again and accumulates.
|
|
380
|
+
* - `_redirects` because it is written only when this pass found redirects.
|
|
381
|
+
* A build whose redirects were removed would otherwise ship the previous
|
|
382
|
+
* build's rules — and would break the merge exemption in
|
|
383
|
+
* `shared/static-platform-files.ts`, which lets the pre-rendered copy
|
|
384
|
+
* overwrite `public/_redirects` on the grounds that it *contains* it. That
|
|
385
|
+
* holds only for a file the current pass produced (TIM-1266).
|
|
386
|
+
*
|
|
387
|
+
* @internal Exported for testing.
|
|
388
|
+
*/
|
|
389
|
+
export async function cleanConditionalArtifacts(staticDir: string): Promise<void> {
|
|
390
|
+
await rm(join(staticDir, '_rsc'), { recursive: true, force: true }).catch(() => {});
|
|
391
|
+
await rm(join(staticDir, REDIRECTS_FILE), { force: true }).catch(() => {});
|
|
392
|
+
}
|
|
393
|
+
|
|
159
394
|
// ---------------------------------------------------------------------------
|
|
160
395
|
// Static generation orchestration (runs after Vite build)
|
|
161
396
|
// ---------------------------------------------------------------------------
|
|
@@ -221,59 +456,24 @@ async function runStaticGeneration(ctx: PluginContext): Promise<StaticGeneration
|
|
|
221
456
|
}
|
|
222
457
|
|
|
223
458
|
const staticDir = join(ctx.buildDir, 'static-pages');
|
|
224
|
-
|
|
225
|
-
// new ones. Without this, already-hashed files from a prior build
|
|
226
|
-
// would get re-hashed and accumulate in the output.
|
|
227
|
-
await rm(join(staticDir, '_rsc'), { recursive: true, force: true }).catch(() => {});
|
|
459
|
+
await cleanConditionalArtifacts(staticDir);
|
|
228
460
|
await mkdir(staticDir, { recursive: true });
|
|
229
461
|
|
|
230
462
|
const sitemapConfig = ctx.config.sitemap;
|
|
231
463
|
const autoSitemapEnabled = Boolean(sitemapConfig?.enabled && sitemapConfig?.baseUrl);
|
|
232
464
|
|
|
233
|
-
const
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
|
|
465
|
+
const writer = new StaticSiteWriter(staticDir, await detectFsIdentity(staticDir));
|
|
466
|
+
// Route params are not collected here any more. They used to be lifted off
|
|
467
|
+
// the generated entry's response headers and inlined into the manifest,
|
|
468
|
+
// because a `.rsc` file carries no headers and the manifest was the only
|
|
469
|
+
// channel a static SPA navigation had (TIM-1255, TIM-1285). The params are
|
|
470
|
+
// now inside the payload itself — a field on its root row — so the `.rsc`
|
|
471
|
+
// file carries them and there is nothing to lift (TIM-1294).
|
|
238
472
|
|
|
239
473
|
const summary = await generate({
|
|
240
474
|
autoSitemapEnabled,
|
|
241
475
|
renderTimeoutMs: ctx.config.renderTimeoutMs,
|
|
242
|
-
onEntry:
|
|
243
|
-
const relPath = staticOutputPath(entry);
|
|
244
|
-
const fullPath = resolve(staticDir, relPath);
|
|
245
|
-
|
|
246
|
-
// Reject path traversal: generated params with "../" could escape
|
|
247
|
-
// the output directory and overwrite build artifacts.
|
|
248
|
-
const rel = relative(staticDir, fullPath);
|
|
249
|
-
if (rel.startsWith('..') || resolve(staticDir, rel) !== fullPath) {
|
|
250
|
-
throw new Error(
|
|
251
|
-
`[timber] Static generation: output path "${relPath}" escapes the ` +
|
|
252
|
-
`static output directory — check generateStaticSegmentParams for ` +
|
|
253
|
-
`path traversal characters in ${entry.urlPath}`
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
await mkdir(dirname(fullPath), { recursive: true });
|
|
258
|
-
await writeFileAtomic(fullPath, entry.body);
|
|
259
|
-
|
|
260
|
-
// Capture route params for RSC entries. These are inlined into
|
|
261
|
-
// the RSC manifest (TIM-1255) instead of written as sidecars.
|
|
262
|
-
if (entry.kind === 'rsc' && entry.headers['x-timber-params']) {
|
|
263
|
-
const rscUrlKey = entry.urlPath === '/' ? '/_rsc/index.rsc' : `/_rsc${entry.urlPath}.rsc`;
|
|
264
|
-
try {
|
|
265
|
-
routeParams[rscUrlKey] = JSON.parse(entry.headers['x-timber-params']);
|
|
266
|
-
} catch {
|
|
267
|
-
// Malformed params header — skip
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Track Content-Type for extensionless API routes so adapters
|
|
272
|
-
// can write per-path Content-Type rules in _headers. TIM-1241.
|
|
273
|
-
if (entry.kind === 'api' && !extname(relPath) && entry.contentType) {
|
|
274
|
-
contentTypes[entry.urlPath] = entry.contentType;
|
|
275
|
-
}
|
|
276
|
-
},
|
|
476
|
+
onEntry: (entry: StaticPageEntry) => writer.write(entry),
|
|
277
477
|
});
|
|
278
478
|
|
|
279
479
|
const totalUrls = summary.rendered + summary.errors.length;
|
|
@@ -283,15 +483,25 @@ async function runStaticGeneration(ctx: PluginContext): Promise<StaticGeneration
|
|
|
283
483
|
(summary.redirects.length > 0 ? ` ${summary.redirects.length} redirect(s).` : '')
|
|
284
484
|
);
|
|
285
485
|
|
|
486
|
+
// Warnings are non-fatal but must not be buried in the per-entry log stream —
|
|
487
|
+
// they name a degraded artifact the developer is about to deploy (TIM-1258).
|
|
488
|
+
for (const warning of summary.warnings) {
|
|
489
|
+
console.warn(`[timber] Static generation warning: ${warning}`);
|
|
490
|
+
}
|
|
491
|
+
|
|
286
492
|
if (summary.errors.length > 0) {
|
|
287
493
|
const details = summary.errors.map((e) => ` - ${e.urlPath}: ${e.message}`).join('\n');
|
|
288
494
|
throw new Error(`[timber] Static generation failed:\n${details}`);
|
|
289
495
|
}
|
|
290
496
|
|
|
497
|
+
// Raised after the render errors above: a collision means the site is
|
|
498
|
+
// ambiguous, not that a page failed, and the message names both routes.
|
|
499
|
+
writer.assertNoCollisions();
|
|
500
|
+
|
|
291
501
|
// Store content-type mappings on the plugin context so adapters
|
|
292
502
|
// can include them in the _headers file. TIM-1241.
|
|
293
|
-
if (Object.keys(contentTypes).length > 0) {
|
|
294
|
-
ctx.staticContentTypes = contentTypes;
|
|
503
|
+
if (Object.keys(writer.contentTypes).length > 0) {
|
|
504
|
+
ctx.staticContentTypes = writer.contentTypes;
|
|
295
505
|
}
|
|
296
506
|
|
|
297
507
|
// Hash RSC flight payloads for immutable caching (TIM-1254).
|
|
@@ -300,7 +510,7 @@ async function runStaticGeneration(ctx: PluginContext): Promise<StaticGeneration
|
|
|
300
510
|
// Skip when client JS is disabled — no SPA navigation, no RSC fetches,
|
|
301
511
|
// and injecting a <script> tag would violate the no-JS contract.
|
|
302
512
|
if (!ctx.clientJavascript.disabled) {
|
|
303
|
-
const rscManifest = await hashRscPayloads(staticDir
|
|
513
|
+
const rscManifest = await hashRscPayloads(staticDir);
|
|
304
514
|
if (Object.keys(rscManifest).length > 0) {
|
|
305
515
|
await injectRscManifestIntoHtml(staticDir, rscManifest);
|
|
306
516
|
ctx.rscManifest = rscManifest;
|
|
@@ -313,8 +523,8 @@ async function runStaticGeneration(ctx: PluginContext): Promise<StaticGeneration
|
|
|
313
523
|
// Write _redirects file for platform redirect rules (Cloudflare Pages,
|
|
314
524
|
// Netlify, etc.). Format: /from /to status. See TIM-1245.
|
|
315
525
|
if (summary.redirects.length > 0) {
|
|
316
|
-
const generated = generateRedirectsFile(summary.redirects);
|
|
317
|
-
const redirectsPath = join(staticDir,
|
|
526
|
+
const generated = generateRedirectsFile(summary.redirects, normalizeBasePath(ctx.base));
|
|
527
|
+
const redirectsPath = join(staticDir, REDIRECTS_FILE);
|
|
318
528
|
|
|
319
529
|
// Merge with user-authored _redirects if present. TIM-1249.
|
|
320
530
|
const userRedirects = await readUserRedirects(ctx.root);
|
|
@@ -335,6 +545,38 @@ async function runStaticGeneration(ctx: PluginContext): Promise<StaticGeneration
|
|
|
335
545
|
return summary;
|
|
336
546
|
}
|
|
337
547
|
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
// Base path validation (TIM-1261)
|
|
550
|
+
// ---------------------------------------------------------------------------
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Warn when Vite's `base` is document-relative (`'./'`) in static mode.
|
|
554
|
+
*
|
|
555
|
+
* Payload URLs are addressed from the deployment root plus the base prefix.
|
|
556
|
+
* A relative base gives no prefix to compute (see shared/base-path.ts), so
|
|
557
|
+
* `/_rsc/about.rsc` is requested at the domain root — correct only when the
|
|
558
|
+
* site is served from there. The build still succeeds: relative bases work
|
|
559
|
+
* fine for assets, and for a root-served site nothing is wrong. The warning
|
|
560
|
+
* exists so a sub-path deployment does not silently degrade to a hard
|
|
561
|
+
* navigation on every link.
|
|
562
|
+
*
|
|
563
|
+
* Skipped when client JS is disabled — there is no SPA navigation and no
|
|
564
|
+
* payload fetch to mis-address.
|
|
565
|
+
*
|
|
566
|
+
* @internal Exported for testing.
|
|
567
|
+
*/
|
|
568
|
+
export function warnUnusableBase(ctx: PluginContext): void {
|
|
569
|
+
if (ctx.clientJavascript.disabled) return;
|
|
570
|
+
if (!isRelativeBase(ctx.base)) return;
|
|
571
|
+
console.warn(
|
|
572
|
+
`[timber] Static mode: base is "${ctx.base}", which is document-relative. ` +
|
|
573
|
+
`RSC payload URLs are addressed from the domain root, so client-side ` +
|
|
574
|
+
`navigation will fall back to a full page load if the site is served ` +
|
|
575
|
+
`from a sub-path. Use an absolute base (e.g. base: '/repo/') to deploy ` +
|
|
576
|
+
`under a sub-path. See design/11-platform.md.`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
338
580
|
// ---------------------------------------------------------------------------
|
|
339
581
|
// RSC flight payload hashing (TIM-1254)
|
|
340
582
|
// ---------------------------------------------------------------------------
|
|
@@ -379,6 +621,12 @@ async function walkDir(dir: string): Promise<string[]> {
|
|
|
379
621
|
* The manifest maps the original `_rsc/` URL to an entry object:
|
|
380
622
|
* `{"/_rsc/index.rsc":{"url":"/_rsc/index-B7YxEKdN.rsc"}, ...}`
|
|
381
623
|
*
|
|
624
|
+
* Keys and URLs are **route-space**: root-relative, never carrying Vite's
|
|
625
|
+
* `base`. The client applies the base once, at fetch time
|
|
626
|
+
* (`client/rsc-fetch.ts` → `toStaticRscUrl`), so the generated site stays
|
|
627
|
+
* relocatable and the base has exactly one expression at runtime rather
|
|
628
|
+
* than two that must agree. See TIM-1261.
|
|
629
|
+
*
|
|
382
630
|
* Route params (from `routeParams`) are inlined into the manifest
|
|
383
631
|
* entry, eliminating the need for .params.json sidecar files. TIM-1255.
|
|
384
632
|
*
|
|
@@ -387,10 +635,7 @@ async function walkDir(dir: string): Promise<string[]> {
|
|
|
387
635
|
*
|
|
388
636
|
* @internal Exported for testing.
|
|
389
637
|
*/
|
|
390
|
-
export async function hashRscPayloads(
|
|
391
|
-
staticDir: string,
|
|
392
|
-
routeParams?: Record<string, Record<string, string | string[]>>
|
|
393
|
-
): Promise<RscManifest> {
|
|
638
|
+
export async function hashRscPayloads(staticDir: string): Promise<RscManifest> {
|
|
394
639
|
const rscDir = join(staticDir, '_rsc');
|
|
395
640
|
const files = await walkDir(rscDir);
|
|
396
641
|
const manifest: RscManifest = {};
|
|
@@ -409,11 +654,7 @@ export async function hashRscPayloads(
|
|
|
409
654
|
|
|
410
655
|
const hashedRel = relative(staticDir, hashedPath).replace(/\\/g, '/');
|
|
411
656
|
const key = `/${rel}`;
|
|
412
|
-
|
|
413
|
-
if (routeParams?.[key]) {
|
|
414
|
-
entry.params = routeParams[key];
|
|
415
|
-
}
|
|
416
|
-
manifest[key] = entry;
|
|
657
|
+
manifest[key] = { url: `/${hashedRel}` };
|
|
417
658
|
}
|
|
418
659
|
|
|
419
660
|
return manifest;
|
|
@@ -460,7 +701,7 @@ export async function injectRscManifestIntoHtml(
|
|
|
460
701
|
* @internal Exported for testing.
|
|
461
702
|
*/
|
|
462
703
|
export async function readUserRedirects(root: string): Promise<string | null> {
|
|
463
|
-
const candidates = [join(root, 'public',
|
|
704
|
+
const candidates = [join(root, 'public', REDIRECTS_FILE)];
|
|
464
705
|
for (const path of candidates) {
|
|
465
706
|
try {
|
|
466
707
|
return await readFile(path, 'utf-8');
|
|
@@ -542,6 +783,7 @@ export function timberStaticBuild(ctx: PluginContext): Plugin {
|
|
|
542
783
|
const isStatic = ctx.config.output === 'static';
|
|
543
784
|
if (!isStatic) return;
|
|
544
785
|
|
|
786
|
+
warnUnusableBase(ctx);
|
|
545
787
|
await runStaticGeneration(ctx);
|
|
546
788
|
},
|
|
547
789
|
},
|
|
@@ -25,6 +25,24 @@ export interface RouteEntry {
|
|
|
25
25
|
* share their parent's urlPath — urlPath-keyed emissions dedupe them,
|
|
26
26
|
* preferring the non-slot entry. segmentPath-keyed emissions keep all. */
|
|
27
27
|
isSlotRoute?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Whether this entry lives inside an intercepting subtree.
|
|
30
|
+
*
|
|
31
|
+
* Such an entry has **no URL of its own**. Interception adds no URL depth,
|
|
32
|
+
* so the scanner's `urlPath` for the subtree is the owner's path with the
|
|
33
|
+
* descendants' names appended — `(browse)/(...)[artistSlug]/[year]` computes
|
|
34
|
+
* `/[year]`, which is not the route it renders at (that is
|
|
35
|
+
* `/[artistSlug]/[year]`) and is not reachable by any request. It is also
|
|
36
|
+
* soft-navigation-only.
|
|
37
|
+
*
|
|
38
|
+
* So these entries are dropped from every urlPath-keyed emission (`Routes`,
|
|
39
|
+
* `useQueryStates`, typed `<Link>`) rather than deduped against the
|
|
40
|
+
* canonical route: dedupe only helps where the paths happen to collide, and
|
|
41
|
+
* a non-colliding computed path would be published as a real route.
|
|
42
|
+
* segmentPath-keyed emissions keep them, so the intercepting page's own
|
|
43
|
+
* params stay typed.
|
|
44
|
+
*/
|
|
45
|
+
isInterceptingRoute?: boolean;
|
|
28
46
|
/** Whether a global schema (app/schema.ts) exists for this app */
|
|
29
47
|
hasSchema?: boolean;
|
|
30
48
|
}
|