@timber-js/app 0.2.0-alpha.184 → 0.2.0-alpha.186
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{actions-O_LsyCE4.js → actions-C-Rw9vPc.js} +3 -3
- package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-C-Rw9vPc.js.map} +1 -1
- package/dist/_chunks/als-slots-mFweg276.js +27 -0
- package/dist/_chunks/als-slots-mFweg276.js.map +1 -0
- package/dist/_chunks/base-path-DaQrzbez.js +80 -0
- package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
- package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
- package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
- package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-Cd0VZ_Pd.js} +3 -3
- package/dist/_chunks/{cache-api-B-lhk9p4.js.map → cache-api-Cd0VZ_Pd.js.map} +1 -1
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js +1857 -0
- package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +1 -0
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-Cs0uZXea.js} +89 -12
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-Cs0uZXea.js.map} +1 -1
- package/dist/_chunks/error-boundary-DpYRI_I1.js +330 -0
- package/dist/_chunks/error-boundary-DpYRI_I1.js.map +1 -0
- package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
- package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
- package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
- package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
- package/dist/_chunks/{logger-AWfuX-KJ.js → logger-N7e5auP0.js} +9 -13
- package/dist/_chunks/logger-N7e5auP0.js.map +1 -0
- package/dist/_chunks/navigation-root-B29qg0_T.js +274 -0
- package/dist/_chunks/navigation-root-B29qg0_T.js.map +1 -0
- package/dist/_chunks/param-value-C8TNYchQ.js +66 -0
- package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
- package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-DEGLSJs3.js} +2 -1
- package/dist/_chunks/plugin-context-DEGLSJs3.js.map +1 -0
- package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
- package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
- package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
- package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
- package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
- package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
- package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
- package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
- package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
- package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
- package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
- package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
- package/dist/_chunks/use-segment-params-ClyUNq4d.js +128 -0
- package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +1 -0
- package/dist/_chunks/{walkers-BtTlKviE.js → walkers-BhhwI9TD.js} +21 -87
- package/dist/_chunks/walkers-BhhwI9TD.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +37 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/fs-identity.d.ts +64 -0
- package/dist/adapters/fs-identity.d.ts.map +1 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +2 -3
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +57 -1
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +13 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cache/fast-hash.d.ts +5 -0
- package/dist/cache/fast-hash.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts +20 -9
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts +4 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +3 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +1 -0
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -162
- package/dist/client/history.d.ts +13 -6
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +18 -11
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +4 -3
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +872 -305
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-api.d.ts +2 -2
- package/dist/client/navigation-commit.d.ts +71 -0
- package/dist/client/navigation-commit.d.ts.map +1 -0
- package/dist/client/navigation-context.d.ts +37 -20
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +63 -23
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/params-context.d.ts +81 -0
- package/dist/client/params-context.d.ts.map +1 -0
- package/dist/client/router-effects.d.ts +129 -0
- package/dist/client/router-effects.d.ts.map +1 -0
- package/dist/client/router-lifecycle.d.ts +55 -0
- package/dist/client/router-lifecycle.d.ts.map +1 -0
- package/dist/client/router-pipeline.d.ts +84 -0
- package/dist/client/router-pipeline.d.ts.map +1 -0
- package/dist/client/router-ref.d.ts +1 -1
- package/dist/client/router-ref.d.ts.map +1 -1
- package/dist/client/router-skew.d.ts +21 -0
- package/dist/client/router-skew.d.ts.map +1 -0
- package/dist/client/router-types.d.ts +207 -0
- package/dist/client/router-types.d.ts.map +1 -0
- package/dist/client/router.d.ts +1 -182
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +39 -16
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +79 -34
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/ssr-data.d.ts +6 -0
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/stale-client.d.ts +69 -0
- package/dist/client/stale-client.d.ts.map +1 -0
- package/dist/client/state.d.ts +10 -1
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +2 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/unload-guard.d.ts +5 -0
- package/dist/client/unload-guard.d.ts.map +1 -1
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/client/use-router.d.ts +1 -1
- package/dist/client/use-segment-params.d.ts +18 -8
- package/dist/client/use-segment-params.d.ts.map +1 -1
- package/dist/cookies/index.js +3 -3
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +915 -320
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +33 -1
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/request-dep/analysis.d.ts +106 -0
- package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
- package/dist/plugins/request-dep/index.d.ts +75 -0
- package/dist/plugins/request-dep/index.d.ts.map +1 -0
- package/dist/plugins/request-dep/origin.d.ts +61 -0
- package/dist/plugins/request-dep/origin.d.ts.map +1 -0
- package/dist/plugins/routing.d.ts +9 -0
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +149 -8
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +18 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/collision-probe.d.ts +104 -0
- package/dist/routing/collision-probe.d.ts.map +1 -0
- package/dist/routing/collision-spaces.d.ts +26 -0
- package/dist/routing/collision-spaces.d.ts.map +1 -0
- package/dist/routing/index.js +3 -3
- package/dist/routing/interception.d.ts +235 -6
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +33 -0
- package/dist/routing/segment-classify.d.ts.map +1 -1
- package/dist/routing/segment-keys.d.ts +95 -0
- package/dist/routing/segment-keys.d.ts.map +1 -0
- package/dist/routing/slot-placement.d.ts +48 -0
- package/dist/routing/slot-placement.d.ts.map +1 -0
- package/dist/routing/types.d.ts +1 -3
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/routing/walkers.d.ts +6 -0
- package/dist/routing/walkers.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +0 -6
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +176 -2
- package/dist/search-params/index.js.map +1 -1
- package/dist/segment-params/define.d.ts +0 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +69 -1
- package/dist/segment-params/index.js.map +1 -0
- package/dist/server/actions.d.ts +6 -3
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +2 -1
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +13 -1
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/chain-url-parts.d.ts +82 -0
- package/dist/server/chain-url-parts.d.ts.map +1 -0
- package/dist/server/children-interception.d.ts +49 -0
- package/dist/server/children-interception.d.ts.map +1 -0
- package/dist/server/default-status-page.d.ts +45 -0
- package/dist/server/default-status-page.d.ts.map +1 -0
- package/dist/server/deny-boundary.d.ts +4 -7
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +14 -3
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/head-response.d.ts +35 -0
- package/dist/server/head-response.d.ts.map +1 -0
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +528 -373
- package/dist/server/internal.js.map +1 -1
- package/dist/server/param-coercion.d.ts +10 -1
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts +0 -18
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-interception.d.ts +59 -9
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +11 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/prebuilt-builder.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +0 -12
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/publish-params.d.ts +33 -0
- package/dist/server/publish-params.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +48 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +32 -2
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-handler.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +13 -0
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-cache-key-guard.d.ts +78 -0
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +14 -2
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +2 -10
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +3 -2
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +3 -2
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +2 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/sitemap-generator.d.ts.map +1 -1
- package/dist/server/skippable-prefix.d.ts +23 -0
- package/dist/server/skippable-prefix.d.ts.map +1 -0
- package/dist/server/slot-interception.d.ts +80 -0
- package/dist/server/slot-interception.d.ts.map +1 -0
- package/dist/server/slot-resolver.d.ts +70 -8
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +6 -0
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +24 -13
- package/dist/server/ssr-wrappers.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts +6 -27
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts +6 -0
- package/dist/server/static-generator.d.ts.map +1 -1
- package/dist/server/static-not-found.d.ts +52 -0
- package/dist/server/static-not-found.d.ts.map +1 -0
- package/dist/server/status-code-resolver.d.ts +3 -10
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/shared/als-slots.d.ts +41 -0
- package/dist/shared/als-slots.d.ts.map +1 -0
- package/dist/shared/base-path.d.ts +56 -0
- package/dist/shared/base-path.d.ts.map +1 -0
- package/dist/shared/param-value.d.ts +80 -0
- package/dist/shared/param-value.d.ts.map +1 -0
- package/dist/shared/payload-root.d.ts +108 -0
- package/dist/shared/payload-root.d.ts.map +1 -0
- package/dist/shared/rsc-cache-key.d.ts +163 -0
- package/dist/shared/rsc-cache-key.d.ts.map +1 -0
- package/dist/shared/rsc-error-envelope.d.ts +25 -0
- package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
- package/dist/shared/rsc-manifest.d.ts +28 -0
- package/dist/shared/rsc-manifest.d.ts.map +1 -0
- package/dist/shared/rsc-payload-path.d.ts +42 -0
- package/dist/shared/rsc-payload-path.d.ts.map +1 -0
- package/dist/shared/segment-info.d.ts +59 -0
- package/dist/shared/segment-info.d.ts.map +1 -0
- package/dist/shared/slot-params.d.ts +51 -0
- package/dist/shared/slot-params.d.ts.map +1 -0
- package/dist/shared/static-platform-files.d.ts +71 -0
- package/dist/shared/static-platform-files.d.ts.map +1 -0
- package/dist/shims/navigation-client.d.ts +8 -19
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +0 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/docs/api/31-api-client.mdx +4 -2
- package/docs/learn/02-pages-and-layouts.mdx +1 -1
- package/docs/learn/11-error-handling.mdx +4 -3
- package/docs/learn/12-client-navigation.mdx +2 -2
- package/docs/learn/14-deploying.mdx +23 -0
- package/docs/more/01-advanced-routing.mdx +172 -2
- package/docs/more/04-metadata-and-fonts.mdx +1 -1
- package/package.json +2 -2
- package/src/adapters/build-output-helper.ts +116 -3
- package/src/adapters/cloudflare.ts +86 -11
- package/src/adapters/fs-identity.ts +156 -0
- package/src/adapters/nitro.ts +6 -6
- package/src/adapters/shared.ts +196 -21
- package/src/adapters/types.ts +13 -1
- package/src/cache/fast-hash.ts +5 -0
- package/src/client/browser-entry/action-dispatch.ts +9 -4
- package/src/client/browser-entry/hydrate.ts +40 -29
- package/src/client/browser-entry/index.ts +15 -1
- package/src/client/browser-entry/post-hydration.ts +10 -9
- package/src/client/browser-entry/router-init.ts +104 -48
- package/src/client/browser-entry/rsc-stream.ts +11 -2
- package/src/client/error-boundary.tsx +20 -15
- package/src/client/history.ts +13 -6
- package/src/client/internal.ts +4 -3
- package/src/client/link.tsx +55 -8
- package/src/client/navigation-api.ts +3 -3
- package/src/client/navigation-commit.ts +174 -0
- package/src/client/navigation-context.ts +40 -67
- package/src/client/navigation-root.tsx +326 -101
- package/src/client/params-context.ts +192 -0
- package/src/client/router-effects.ts +231 -0
- package/src/client/router-lifecycle.ts +288 -0
- package/src/client/router-pipeline.ts +389 -0
- package/src/client/router-ref.ts +1 -1
- package/src/client/router-skew.ts +30 -0
- package/src/client/router-types.ts +230 -0
- package/src/client/router.ts +242 -690
- package/src/client/rsc-fetch.ts +180 -122
- package/src/client/segment-cache.ts +121 -44
- package/src/client/ssr-data.ts +6 -0
- package/src/client/stale-client.ts +191 -0
- package/src/client/state.ts +14 -1
- package/src/client/top-loader.tsx +2 -1
- package/src/client/unload-guard.ts +34 -2
- package/src/client/use-cookie.ts +9 -2
- package/src/client/use-router.ts +1 -1
- package/src/client/use-segment-params.ts +41 -18
- package/src/index.ts +5 -1
- package/src/plugin-context.ts +34 -1
- package/src/plugins/adapter-build.ts +4 -0
- package/src/plugins/build-manifest.ts +34 -13
- package/src/plugins/entries.ts +4 -0
- package/src/plugins/fonts.ts +11 -8
- package/src/plugins/request-dep/analysis.ts +447 -0
- package/src/plugins/request-dep/index.ts +376 -0
- package/src/plugins/request-dep/origin.ts +310 -0
- package/src/plugins/routing.ts +60 -3
- package/src/plugins/static-build.ts +333 -63
- package/src/routing/codegen-types.ts +18 -0
- package/src/routing/codegen.ts +50 -10
- package/src/routing/collision-probe.ts +438 -0
- package/src/routing/collision-spaces.ts +281 -0
- package/src/routing/interception.ts +896 -50
- package/src/routing/manifest-codegen.ts +6 -9
- package/src/routing/scanner.ts +114 -113
- package/src/routing/segment-classify.ts +57 -0
- package/src/routing/segment-keys.ts +146 -0
- package/src/routing/slot-placement.ts +97 -0
- package/src/routing/types.ts +1 -3
- package/src/routing/walkers.ts +8 -0
- package/src/search-params/define.ts +6 -29
- package/src/segment-params/define.ts +4 -27
- package/src/server/action-handler.ts +3 -1
- package/src/server/actions.ts +6 -3
- package/src/server/als-registry.ts +2 -1
- package/src/server/build-manifest.ts +14 -1
- package/src/server/chain-url-parts.ts +163 -0
- package/src/server/children-interception.ts +115 -0
- package/src/server/default-status-page.ts +127 -0
- package/src/server/deny-boundary.ts +5 -31
- package/src/server/deny-renderer.ts +83 -22
- package/src/server/head-response.ts +54 -0
- package/src/server/param-coercion.ts +48 -18
- package/src/server/pipeline-helpers.ts +0 -45
- package/src/server/pipeline-interception.ts +81 -32
- package/src/server/pipeline-outcome.ts +3 -5
- package/src/server/pipeline-phases.ts +68 -16
- package/src/server/pipeline.ts +57 -10
- package/src/server/prebuilt-builder.ts +1 -7
- package/src/server/prebuilt-runtime.ts +4 -4
- package/src/server/primitives.ts +0 -18
- package/src/server/publish-params.ts +42 -0
- package/src/server/request-context.ts +97 -18
- package/src/server/route-element-builder.ts +89 -98
- package/src/server/route-handler.ts +8 -8
- package/src/server/route-matcher.ts +21 -9
- package/src/server/rsc-cache-key-guard.ts +170 -0
- package/src/server/rsc-entry/api-handler.ts +6 -8
- package/src/server/rsc-entry/error-renderer.ts +64 -25
- package/src/server/rsc-entry/helpers.ts +31 -16
- package/src/server/rsc-entry/index.ts +36 -28
- package/src/server/rsc-entry/render-route.ts +12 -7
- package/src/server/rsc-entry/revalidate-renderer.ts +1 -1
- package/src/server/rsc-entry/rsc-payload.ts +14 -25
- package/src/server/rsc-entry/rsc-stream.ts +4 -3
- package/src/server/rsc-entry/ssr-renderer.ts +13 -9
- package/src/server/sitemap-generator.ts +1 -7
- package/src/server/skippable-prefix.ts +92 -0
- package/src/server/slot-interception.ts +168 -0
- package/src/server/slot-resolver.ts +253 -156
- package/src/server/ssr-bridge-types.ts +6 -0
- package/src/server/ssr-entry.ts +12 -4
- package/src/server/ssr-wrappers.tsx +33 -32
- package/src/server/state-tree-diff.ts +14 -55
- package/src/server/static-generator.ts +67 -45
- package/src/server/static-not-found.ts +134 -0
- package/src/server/status-code-resolver.ts +4 -47
- package/src/shared/als-slots.ts +72 -0
- package/src/shared/base-path.ts +80 -0
- package/src/shared/param-value.ts +244 -0
- package/src/shared/payload-root.ts +151 -0
- package/src/shared/rsc-cache-key.ts +269 -0
- package/src/shared/rsc-error-envelope.ts +31 -0
- package/src/shared/rsc-manifest.ts +29 -0
- package/src/shared/rsc-payload-path.ts +45 -0
- package/src/shared/segment-info.ts +60 -0
- package/src/shared/slot-params.ts +111 -0
- package/src/shared/static-platform-files.ts +75 -0
- package/src/shims/navigation-client.ts +8 -26
- package/src/shims/navigation.ts +0 -4
- package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
- package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
- package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-D3naS3eH.js +0 -688
- package/dist/_chunks/cli-schema-sync-D3naS3eH.js.map +0 -1
- package/dist/_chunks/define-COtkxMRT.js +0 -191
- package/dist/_chunks/define-COtkxMRT.js.map +0 -1
- package/dist/_chunks/define-c4au4I9R.js +0 -82
- package/dist/_chunks/define-c4au4I9R.js.map +0 -1
- package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
- package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
- package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
- package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
- package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
- package/dist/_chunks/segment-classify-Byy425ng.js.map +0 -1
- package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
- package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
- package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
- package/dist/_chunks/walkers-BtTlKviE.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/plugins/request-dep.d.ts +0 -32
- package/dist/plugins/request-dep.d.ts.map +0 -1
- package/src/plugins/request-dep.ts +0 -499
|
@@ -13,7 +13,16 @@
|
|
|
13
13
|
|
|
14
14
|
import { writeFile, mkdir, cp, rm, readdir, readFile } from 'node:fs/promises';
|
|
15
15
|
import { join, basename, relative } from 'node:path';
|
|
16
|
-
import { generateHeadersFile } from './shared.js';
|
|
16
|
+
import { adapterBase, generateHeadersFile } from './shared.js';
|
|
17
|
+
// Isomorphic, zero-import module — safe to load from an adapter, which Node's
|
|
18
|
+
// native ESM loader evaluates before Vite's resolver exists. Same precedent as
|
|
19
|
+
// `adapters/shared.ts` importing `../shared/base-path.js`.
|
|
20
|
+
import {
|
|
21
|
+
HEADERS_FILE,
|
|
22
|
+
USER_MERGED_PLATFORM_FILES,
|
|
23
|
+
USER_PLATFORM_DIR,
|
|
24
|
+
} from '../shared/static-platform-files.js';
|
|
25
|
+
import { detectFsIdentity, fsKey } from './fs-identity.js';
|
|
17
26
|
import type { TimberConfig } from './types';
|
|
18
27
|
|
|
19
28
|
/** Options for the shared build output steps. */
|
|
@@ -77,8 +86,7 @@ export async function runSharedBuildSteps(opts: BuildOutputBaseOptions): Promise
|
|
|
77
86
|
// Scan for unhashed files so they get per-file Cache-Control rules in the
|
|
78
87
|
// _headers file. On platforms like Cloudflare Workers Static Assets, the
|
|
79
88
|
// worker never sees static file requests — _headers is the only mechanism.
|
|
80
|
-
|
|
81
|
-
await writeFile(join(publicDir, '_headers'), generateHeadersFile(unhashedPaths));
|
|
89
|
+
await writeHeadersFile(publicDir, config, await findUnhashedStaticFiles(publicDir));
|
|
82
90
|
|
|
83
91
|
// 4. Copy RSC + SSR server bundles into the output directory.
|
|
84
92
|
await cp(join(buildDir, 'rsc'), join(outDir, 'rsc'), { recursive: true });
|
|
@@ -218,11 +226,116 @@ export async function buildStaticDirectoryOutput(
|
|
|
218
226
|
});
|
|
219
227
|
|
|
220
228
|
// Copy pre-rendered static pages (HTML + RSC flight + API routes).
|
|
229
|
+
//
|
|
230
|
+
// This is the one place the build merges two independently-produced trees
|
|
231
|
+
// into a single directory, and `cp` resolves an overlap by overwriting with
|
|
232
|
+
// no diagnostic — the deployment then contains one of the two artifacts and
|
|
233
|
+
// nothing recorded which. Refuse instead (TIM-1266).
|
|
221
234
|
if (config.staticPagesDir) {
|
|
235
|
+
await assertNoTreeCollision(config.staticPagesDir, publicDir);
|
|
222
236
|
await cp(config.staticPagesDir, publicDir, { recursive: true });
|
|
223
237
|
}
|
|
224
238
|
}
|
|
225
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Write the `_headers` file, folding in the user's `public/_headers`.
|
|
242
|
+
*
|
|
243
|
+
* Every adapter used to `writeFile` a generated `_headers` over whatever
|
|
244
|
+
* Vite had copied out of `public/`, so an app shipping its own CSP, CORS, or
|
|
245
|
+
* cache rules deployed without them and without a word (TIM-1326). The
|
|
246
|
+
* generated rules and the user's are not competitors — they address different
|
|
247
|
+
* paths — so this merges rather than refusing, the way `_redirects` has since
|
|
248
|
+
* TIM-1249.
|
|
249
|
+
*
|
|
250
|
+
* **Read from the project, not the output.** The generated file lands under
|
|
251
|
+
* the same name in the output directory, and `runSharedBuildSteps` does not
|
|
252
|
+
* clean `outDir`; a reader aimed there would fold the previous build's
|
|
253
|
+
* generated rules back in and compound them on every rebuild — the defect
|
|
254
|
+
* this pass fixed for `_redirects`.
|
|
255
|
+
*
|
|
256
|
+
* User rules go last. On Cloudflare and Netlify a `_headers` file applies
|
|
257
|
+
* every matching rule, so appending means the user's rules take effect; where
|
|
258
|
+
* a platform resolves a conflict in favour of the earlier rule instead, the
|
|
259
|
+
* framework keeps the paths it owns and the user keeps the rest. Under either
|
|
260
|
+
* reading this is no worse than the previous behaviour, which was to drop the
|
|
261
|
+
* user's rules entirely.
|
|
262
|
+
*
|
|
263
|
+
* @internal Exported for testing.
|
|
264
|
+
*/
|
|
265
|
+
export async function writeHeadersFile(
|
|
266
|
+
dir: string,
|
|
267
|
+
config: TimberConfig,
|
|
268
|
+
unhashedPaths: string[]
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
const userRules = await readUserPlatformFile(config.root, HEADERS_FILE);
|
|
271
|
+
await writeFile(
|
|
272
|
+
join(dir, HEADERS_FILE),
|
|
273
|
+
generateHeadersFile({
|
|
274
|
+
base: adapterBase(config),
|
|
275
|
+
unhashedPaths,
|
|
276
|
+
contentTypes: config.staticContentTypes,
|
|
277
|
+
userRules: userRules ?? undefined,
|
|
278
|
+
})
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Read a user-authored platform file from the project's `public/` directory.
|
|
284
|
+
* Returns null when the project has none, or when no root is configured —
|
|
285
|
+
* a caller without a root cannot locate the project at all, and inventing
|
|
286
|
+
* one from `process.cwd()` would read a different project's file.
|
|
287
|
+
*/
|
|
288
|
+
async function readUserPlatformFile(
|
|
289
|
+
root: string | undefined,
|
|
290
|
+
name: string
|
|
291
|
+
): Promise<string | null> {
|
|
292
|
+
if (!root) return null;
|
|
293
|
+
try {
|
|
294
|
+
return await readFile(join(root, USER_PLATFORM_DIR, name), 'utf-8');
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Throw if copying `srcDir` over `destDir` would overwrite an existing file.
|
|
302
|
+
*
|
|
303
|
+
* Compared against the destination **as it stands**, not against the other
|
|
304
|
+
* source tree: the first copy is filtered (client JS is dropped when client
|
|
305
|
+
* JavaScript is disabled), so the files that are actually there are the only
|
|
306
|
+
* ones a second copy can clobber.
|
|
307
|
+
*
|
|
308
|
+
* @internal Exported for testing.
|
|
309
|
+
*/
|
|
310
|
+
export async function assertNoTreeCollision(srcDir: string, destDir: string): Promise<void> {
|
|
311
|
+
const [srcFiles, destFiles, identity] = await Promise.all([
|
|
312
|
+
collectFiles(srcDir).catch(() => [] as string[]),
|
|
313
|
+
collectFiles(destDir).catch(() => [] as string[]),
|
|
314
|
+
// `cp` will resolve these paths the way the destination filesystem does,
|
|
315
|
+
// so the comparison predicting it has to as well: on macOS a hand-written
|
|
316
|
+
// `public/About/index.html` and a generated `about/index.html` are one
|
|
317
|
+
// file, and an exact-string set waves the overwrite through (TIM-1266).
|
|
318
|
+
detectFsIdentity(destDir),
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
const key = (p: string) => fsKey(p, identity);
|
|
322
|
+
const existing = new Set(destFiles.map((f) => key(relative(destDir, f).replace(/\\/g, '/'))));
|
|
323
|
+
const exempt = new Set(USER_MERGED_PLATFORM_FILES.map(key));
|
|
324
|
+
const collisions = srcFiles
|
|
325
|
+
.map((f) => relative(srcDir, f).replace(/\\/g, '/'))
|
|
326
|
+
.filter((rel) => existing.has(key(rel)) && !exempt.has(key(rel)))
|
|
327
|
+
.sort();
|
|
328
|
+
|
|
329
|
+
if (collisions.length === 0) return;
|
|
330
|
+
|
|
331
|
+
throw new Error(
|
|
332
|
+
`[timber] Build output: ${collisions.length} file(s) in the pre-rendered site ` +
|
|
333
|
+
`collide with files already in the output directory. Copying would silently ` +
|
|
334
|
+
`overwrite them — a generated page and a file in public/ cannot share a path:\n` +
|
|
335
|
+
collisions.map((c) => ` - ${c}`).join('\n')
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
226
339
|
async function collectFiles(dir: string): Promise<string[]> {
|
|
227
340
|
const result: string[] = [];
|
|
228
341
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -13,8 +13,13 @@ import {
|
|
|
13
13
|
runSharedBuildSteps,
|
|
14
14
|
findUnhashedStaticFiles,
|
|
15
15
|
buildStaticDirectoryOutput,
|
|
16
|
+
writeHeadersFile,
|
|
16
17
|
} from './build-output-helper.js';
|
|
17
|
-
import { generateHeadersFile } from './shared.js';
|
|
18
|
+
import { adapterBase, generateHeadersFile } from './shared.js';
|
|
19
|
+
// Isomorphic, zero-import module — safe to load from an adapter, which Node's
|
|
20
|
+
// native ESM loader evaluates before Vite's resolver exists. Same precedent as
|
|
21
|
+
// `adapters/shared.ts` importing `../shared/base-path.js`.
|
|
22
|
+
import { RSC_KEY_HEADERS } from '../shared/rsc-cache-key.js';
|
|
18
23
|
|
|
19
24
|
// ─── Bindings passthrough ─────────────────────────────────────────────────
|
|
20
25
|
// ALS stores the env object per-request so server components and middleware
|
|
@@ -409,14 +414,78 @@ function initPlatformTracer() {
|
|
|
409
414
|
return tracerInit
|
|
410
415
|
}
|
|
411
416
|
|
|
417
|
+
// The \`Vary\` tokens this cache can serve a correct answer for.
|
|
418
|
+
//
|
|
419
|
+
// Cloudflare's Cache API keys on the request URL. It does not implement
|
|
420
|
+
// RFC 9111 §4.1 secondary key selection, so a stored response is handed to
|
|
421
|
+
// every request for that URL regardless of what \`Vary\` nominates — except
|
|
422
|
+
// \`Accept-Encoding\`, which the platform segregates itself. Storing a
|
|
423
|
+
// response whose \`Vary\` says "this answer is specific to header H" under a
|
|
424
|
+
// key that ignores H is cross-client reuse (TIM-1269): a developer writing
|
|
425
|
+
// the correct \`public, s-maxage=60\` + \`Vary: Cookie\` gets their logged-in
|
|
426
|
+
// page served to anonymous visitors. Refusing to store is the conservative
|
|
427
|
+
// answer, and the only one available without rewriting the key.
|
|
428
|
+
//
|
|
429
|
+
// The framework's own tokens are on this list because the adapter already
|
|
430
|
+
// segregates them, more strictly than \`Vary\` would:
|
|
431
|
+
//
|
|
432
|
+
// - \`Accept\` — \`isRsc\` above excludes \`text/x-component\` from both lookup
|
|
433
|
+
// and store, so the HTML/Flight split \`Vary: Accept\` exists to protect
|
|
434
|
+
// never reaches this cache in the first place.
|
|
435
|
+
// - \`X-Timber-State-Tree\`, \`X-Timber-URL\`, \`X-Timber-Deployment-Id\` — the
|
|
436
|
+
// origin reads all three only on RSC requests (version skew at
|
|
437
|
+
// \`server/pipeline-phases.ts\`, interception rewrites likewise, state-tree
|
|
438
|
+
// diffing at \`server/rsc-entry/render-route.ts\`). On the non-RSC requests
|
|
439
|
+
// this cache stores, they cannot change the response.
|
|
440
|
+
//
|
|
441
|
+
// That second bullet is a coupling, not an observation: if any of those
|
|
442
|
+
// headers ever starts varying a document response, this list is wrong and
|
|
443
|
+
// the cache poisons. Because the list is derived, a header added to
|
|
444
|
+
// \`RSC_KEY_HEADERS\` inherits the exemption silently — so
|
|
445
|
+
// \`tests/adapters/cloudflare-vary.test.ts\` fails until that header is named
|
|
446
|
+
// there alongside the evidence for its gate, and the gates themselves are
|
|
447
|
+
// covered there, in \`tests/render-route.test.ts\`, and in
|
|
448
|
+
// \`tests/interception-x-timber-url.test.ts\`.
|
|
449
|
+
//
|
|
450
|
+
// Interpolated from \`RSC_KEY_HEADERS\` rather than spelled again — "which
|
|
451
|
+
// headers vary an RSC response" has one answer, and a hand-copied second
|
|
452
|
+
// list here would drift silently on the fallback path nobody exercises.
|
|
453
|
+
//
|
|
454
|
+
// Known residual: a route handler that content-negotiates on \`Accept\`
|
|
455
|
+
// (JSON vs HTML) *and* marks itself \`public\` is still mis-keyed, because the
|
|
456
|
+
// \`isRsc\` split is binary and does not separate those two. That is no worse
|
|
457
|
+
// than the pre-TIM-1269 behaviour, which stored every \`Vary\` but \`*\`.
|
|
458
|
+
const VARY_HONORED = new Set(${JSON.stringify(['accept-encoding', ...RSC_KEY_HEADERS.map((h) => h.toLowerCase())])})
|
|
459
|
+
|
|
460
|
+
// Applied to BOTH sides of the cache — what may be stored, and what a hit
|
|
461
|
+
// may serve. Refusing to store only stops new poisoning: entries a previous
|
|
462
|
+
// deployment wrote under the old predicate stay servable for whatever
|
|
463
|
+
// s-maxage the developer chose, which can be a day. Checking on the way out
|
|
464
|
+
// makes the fix take effect on deploy rather than on expiry, and costs one
|
|
465
|
+
// header read per hit. A rejected entry falls through to the origin and is
|
|
466
|
+
// then declined by the same predicate on the way back in, so it is not
|
|
467
|
+
// rewritten — it simply ages out.
|
|
468
|
+
function canHonorVary(vary) {
|
|
469
|
+
// Absent or empty Vary nominates nothing — the URL is the whole key.
|
|
470
|
+
if (!vary) return true
|
|
471
|
+
return vary.split(',').every((token) => VARY_HONORED.has(token.trim().toLowerCase()))
|
|
472
|
+
}
|
|
473
|
+
|
|
412
474
|
export default {
|
|
413
475
|
${handlersSpread} async fetch(request, env, ctx) {
|
|
414
476
|
// One-time on first request; resolved-promise await thereafter.
|
|
415
477
|
await initPlatformTracer()
|
|
416
478
|
|
|
417
479
|
// Edge-cache GET/HEAD responses via the Cache API. RSC navigation
|
|
418
|
-
// requests (Accept: text/x-component) are excluded
|
|
419
|
-
//
|
|
480
|
+
// requests (Accept: text/x-component) are excluded. Their varying
|
|
481
|
+
// inputs are now in the URL via ?_rsc= (TIM-1268), but this cache keys
|
|
482
|
+
// on the request URL alone and cannot honour Vary, so a payload request
|
|
483
|
+
// that arrives without the param — stripped query, hand-rolled client —
|
|
484
|
+
// would share a key with the document.
|
|
485
|
+
//
|
|
486
|
+
// This exclusion is load-bearing in the other direction too: the store
|
|
487
|
+
// predicate below treats "Vary: Accept" as honoured *because* of it
|
|
488
|
+
// (see VARY_HONORED). Removing it would make that allowlist unsound.
|
|
420
489
|
const isRsc = request.headers.get('Accept')?.includes('text/x-component')
|
|
421
490
|
const reqCc = request.headers.get('Cache-Control') || ''
|
|
422
491
|
const bypassCache = reqCc.includes('no-cache') || reqCc.includes('max-age=0')
|
|
@@ -426,7 +495,17 @@ ${handlersSpread} async fetch(request, env, ctx) {
|
|
|
426
495
|
const cache = cacheable && typeof caches !== 'undefined' ? caches.default : null
|
|
427
496
|
if (cache) {
|
|
428
497
|
const cached = await cache.match(request)
|
|
429
|
-
|
|
498
|
+
// A hit whose Vary this cache cannot key on is treated as a miss —
|
|
499
|
+
// see canHonorVary. Entries written before that predicate existed are
|
|
500
|
+
// exactly the ones that would be served to the wrong client.
|
|
501
|
+
if (cached) {
|
|
502
|
+
if (canHonorVary(cached.headers.get('Vary'))) return cached
|
|
503
|
+
// Declining the hit means nobody will ever read this body. Release
|
|
504
|
+
// it rather than leaving the stream dangling for the rest of the
|
|
505
|
+
// request (design/02-rendering-pipeline.md §"Cancel Readers When You
|
|
506
|
+
// No Longer Need the Stream").
|
|
507
|
+
cached.body?.cancel().catch(() => {})
|
|
508
|
+
}
|
|
430
509
|
}
|
|
431
510
|
|
|
432
511
|
// Bridge ctx.cache into the per-request ALS so WorkersCachePurge can
|
|
@@ -449,10 +528,10 @@ ${handlersSpread} async fetch(request, env, ctx) {
|
|
|
449
528
|
// See design/25-production-deployments.md §"CDN / Edge Cache".
|
|
450
529
|
if (cache && request.method === 'GET' && response.status === 200) {
|
|
451
530
|
const cc = response.headers.get('Cache-Control')
|
|
452
|
-
const vary = response.headers.get('Vary')
|
|
453
531
|
if (cc && !cc.includes('no-store') && !cc.includes('private')
|
|
454
532
|
&& (cc.includes('public') || cc.includes('s-maxage'))
|
|
455
|
-
&&
|
|
533
|
+
&& canHonorVary(response.headers.get('Vary'))
|
|
534
|
+
&& !response.headers.has('Set-Cookie')) {
|
|
456
535
|
ctx.waitUntil(cache.put(request, response.clone()))
|
|
457
536
|
}
|
|
458
537
|
}
|
|
@@ -804,11 +883,7 @@ export async function buildStaticOutput(
|
|
|
804
883
|
await buildStaticDirectoryOutput(config, buildDir, outDir, '.');
|
|
805
884
|
|
|
806
885
|
// Write _headers file for cache control + content-type rules.
|
|
807
|
-
|
|
808
|
-
await writeFile(
|
|
809
|
-
join(outDir, '_headers'),
|
|
810
|
-
generateHeadersFile(unhashedPaths, config.staticContentTypes)
|
|
811
|
-
);
|
|
886
|
+
await writeHeadersFile(outDir, config, await findUnhashedStaticFiles(outDir));
|
|
812
887
|
}
|
|
813
888
|
|
|
814
889
|
// ─── Preview ─────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the output filesystem considers "the same file".
|
|
3
|
+
*
|
|
4
|
+
* Two build steps compare paths to decide whether two artifacts would land on
|
|
5
|
+
* one file — the render pass (`plugins/static-build.ts`) and the adapter's
|
|
6
|
+
* tree merge (`build-output-helper.ts`). Both are predicting a filesystem
|
|
7
|
+
* decision, so both ask the filesystem rather than comparing raw strings
|
|
8
|
+
* (TIM-1266).
|
|
9
|
+
*
|
|
10
|
+
* Two properties matter, and they are independent:
|
|
11
|
+
*
|
|
12
|
+
* - **Case folding.** macOS APFS and Windows NTFS fold by default, ext4 does
|
|
13
|
+
* not. Dynamic params `Foo` and `foo` are one file on a Mac and two on
|
|
14
|
+
* Linux CI.
|
|
15
|
+
* - **Unicode normalization.** APFS compares normalization-insensitively, so
|
|
16
|
+
* `café` spelled NFC (`é`) and NFD (`e` + combining acute) address one
|
|
17
|
+
* file. This holds even on a *case-sensitive* APFS volume, which is why it
|
|
18
|
+
* is probed separately rather than inferred from case folding.
|
|
19
|
+
*
|
|
20
|
+
* Probed rather than read off `process.platform`: a case-sensitive volume on
|
|
21
|
+
* macOS and a case-insensitive mount on Linux are both ordinary, and the
|
|
22
|
+
* answer that matters is the one for the directory being written.
|
|
23
|
+
*
|
|
24
|
+
* Leaf module — Node built-ins only. Deliberately not in `adapters/shared.ts`,
|
|
25
|
+
* which `server/asset-headers.ts` re-exports from and which therefore must
|
|
26
|
+
* stay free of `node:fs` for the Workers bundle.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { writeFile, stat, rm } from 'node:fs/promises';
|
|
30
|
+
import { randomUUID } from 'node:crypto';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
|
|
33
|
+
/** How the output filesystem decides that two paths name one file. */
|
|
34
|
+
export interface FsIdentity {
|
|
35
|
+
/** `Foo` and `foo` are the same file. */
|
|
36
|
+
caseInsensitive: boolean;
|
|
37
|
+
/** Canonically equivalent Unicode spellings are the same file. */
|
|
38
|
+
normalizationInsensitive: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Trailing dots and spaces are stripped from each segment, so `foo.` and
|
|
41
|
+
* `foo` are the same file. Win32 does this in path resolution rather than
|
|
42
|
+
* in the filesystem, which is why it is probed like the others.
|
|
43
|
+
*/
|
|
44
|
+
stripsTrailingDotsAndSpaces: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Every path comparison is exact — the conservative answer when unprobed. */
|
|
48
|
+
export const EXACT_FS_IDENTITY: FsIdentity = {
|
|
49
|
+
caseInsensitive: false,
|
|
50
|
+
normalizationInsensitive: false,
|
|
51
|
+
stripsTrailingDotsAndSpaces: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Probe `dir` by writing one file and asking for it under other spellings.
|
|
56
|
+
*
|
|
57
|
+
* Returns {@link EXACT_FS_IDENTITY} when the probe cannot run. That
|
|
58
|
+
* under-detects, but a directory this cannot write to is one the caller is
|
|
59
|
+
* about to fail on anyway, with a clearer error than a missed collision.
|
|
60
|
+
*/
|
|
61
|
+
export async function detectFsIdentity(dir: string): Promise<FsIdentity> {
|
|
62
|
+
// `café` — NFC, with a precomposed é. The UUID contributes the
|
|
63
|
+
// hex letters the case probe needs.
|
|
64
|
+
const name = `.timber-probe-${randomUUID()}-café`;
|
|
65
|
+
const path = join(dir, name);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await writeFile(path, '');
|
|
69
|
+
const [caseInsensitive, normalizationInsensitive, stripsTrailingDotsAndSpaces] =
|
|
70
|
+
await Promise.all([
|
|
71
|
+
exists(join(dir, name.toUpperCase())),
|
|
72
|
+
// NFD spells the same string as `e` + U+0301; a filesystem that
|
|
73
|
+
// resolves it to the NFC file compares normalization-insensitively.
|
|
74
|
+
exists(join(dir, name.normalize('NFD'))),
|
|
75
|
+
// Win32 strips trailing dots and spaces during path resolution, so
|
|
76
|
+
// asking for `name.` reaches `name`.
|
|
77
|
+
exists(join(dir, `${name}.`)),
|
|
78
|
+
]);
|
|
79
|
+
return { caseInsensitive, normalizationInsensitive, stripsTrailingDotsAndSpaces };
|
|
80
|
+
} catch {
|
|
81
|
+
return EXACT_FS_IDENTITY;
|
|
82
|
+
} finally {
|
|
83
|
+
await rm(path, { force: true }).catch(() => {});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Fold a path to the identity the filesystem compares it under.
|
|
89
|
+
*
|
|
90
|
+
* Normalization first, then case: `toLowerCase()` operates per code point, so
|
|
91
|
+
* folding a decomposed string first would leave the combining marks in a
|
|
92
|
+
* different order than folding a precomposed one.
|
|
93
|
+
*
|
|
94
|
+
* An approximation of the filesystem's own algorithm — APFS and NTFS fold per
|
|
95
|
+
* their own Unicode tables, which `toLowerCase()` matches for everything short
|
|
96
|
+
* of the exotic. It errs toward *finding* a collision, and the cost of a false
|
|
97
|
+
* positive is a build error naming two routes rather than a silent overwrite.
|
|
98
|
+
*/
|
|
99
|
+
export function fsKey(path: string, identity: FsIdentity): string {
|
|
100
|
+
let key = identity.caseInsensitive ? caseFold(path) : path;
|
|
101
|
+
if (identity.normalizationInsensitive) key = key.normalize('NFC');
|
|
102
|
+
if (identity.stripsTrailingDotsAndSpaces) key = key.split('/').map(stripTrailing).join('/');
|
|
103
|
+
return key;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Approximate Unicode case folding, which is what case-insensitive
|
|
108
|
+
* filesystems actually compare under.
|
|
109
|
+
*
|
|
110
|
+
* `toLowerCase()` is not it. Greek `σ` and final `ς` are both already
|
|
111
|
+
* lowercase and stay distinct under it, yet NTFS upcases both to `Σ` and APFS
|
|
112
|
+
* folds `ς` to `σ` — so two routes differing only in that letter are one file
|
|
113
|
+
* on both, and a lowercase key admitted them as two claims (TIM-1266).
|
|
114
|
+
* Uppercasing first collapses them.
|
|
115
|
+
*
|
|
116
|
+
* The residual is over-folding, in these cases and a few like them:
|
|
117
|
+
*
|
|
118
|
+
* | pair | folded together here | NTFS / APFS |
|
|
119
|
+
* | --------- | -------------------- | ----------------- |
|
|
120
|
+
* | `σ` / `ς` | yes | yes — the fix |
|
|
121
|
+
* | `ß` / `ss`| yes | no |
|
|
122
|
+
* | `ı` / `i` | yes | no |
|
|
123
|
+
* | `fi` / `fi`| yes | no |
|
|
124
|
+
*
|
|
125
|
+
* That direction is deliberate. Over-folding costs a build error naming two
|
|
126
|
+
* routes, which a developer fixes by renaming one; under-folding costs a
|
|
127
|
+
* payload silently overwritten by another route's, which nothing downstream
|
|
128
|
+
* can detect. An exact fold would need `Intl.Collator`, which compares but
|
|
129
|
+
* emits no sort key, so using it would turn a hash lookup into a pairwise
|
|
130
|
+
* scan over every claimed path.
|
|
131
|
+
*/
|
|
132
|
+
function caseFold(value: string): string {
|
|
133
|
+
return value.toUpperCase().toLowerCase();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Drop trailing dots and spaces from one path segment, as Win32 does.
|
|
138
|
+
*
|
|
139
|
+
* A segment that is nothing but dots and spaces is left alone: `.` and `..`
|
|
140
|
+
* are traversal, which the caller rejects before reaching here, and emptying
|
|
141
|
+
* a segment would merge two unrelated paths rather than the aliases this
|
|
142
|
+
* models.
|
|
143
|
+
*/
|
|
144
|
+
function stripTrailing(segment: string): string {
|
|
145
|
+
const stripped = segment.replace(/[. ]+$/, '');
|
|
146
|
+
return stripped === '' ? segment : stripped;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function exists(path: string): Promise<boolean> {
|
|
150
|
+
try {
|
|
151
|
+
await stat(path);
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/adapters/nitro.ts
CHANGED
|
@@ -10,11 +10,12 @@ import { execFile } from 'node:child_process';
|
|
|
10
10
|
import { join, relative, dirname, basename } from 'node:path';
|
|
11
11
|
import type { TimberPlatformAdapter, TimberConfig } from './types';
|
|
12
12
|
import { generateCompressModule } from './compress-module.js';
|
|
13
|
-
import { IMMUTABLE_CACHE, generateHeadersFile } from './shared.js';
|
|
13
|
+
import { adapterBase, IMMUTABLE_CACHE, generateHeadersFile } from './shared.js';
|
|
14
14
|
import {
|
|
15
15
|
runSharedBuildSteps,
|
|
16
16
|
buildStaticDirectoryOutput,
|
|
17
17
|
findUnhashedStaticFiles,
|
|
18
|
+
writeHeadersFile,
|
|
18
19
|
} from './build-output-helper.js';
|
|
19
20
|
|
|
20
21
|
// ─── Presets ─────────────────────────────────────────────────────────────────
|
|
@@ -229,15 +230,14 @@ export function nitro(options: NitroAdapterOptions = {}): TimberPlatformAdapter
|
|
|
229
230
|
|
|
230
231
|
// Write _headers for cache control + extensionless Content-Type.
|
|
231
232
|
// Netlify supports the _headers convention just like Cloudflare.
|
|
232
|
-
|
|
233
|
-
await writeFile(
|
|
234
|
-
join(publicDir, '_headers'),
|
|
235
|
-
generateHeadersFile(unhashedPaths, config.staticContentTypes)
|
|
236
|
-
);
|
|
233
|
+
await writeHeadersFile(publicDir, config, await findUnhashedStaticFiles(publicDir));
|
|
237
234
|
|
|
238
235
|
// Platform-specific files for static deploy targets.
|
|
239
236
|
const target = options.staticTarget ?? 'generic';
|
|
240
237
|
if (target === 'github-pages') {
|
|
238
|
+
// Deploy-from-a-branch runs Jekyll, which drops `_`-prefixed paths —
|
|
239
|
+
// every `_rsc/` payload. Written unconditionally: GitHub reads only
|
|
240
|
+
// that the file exists, never its contents.
|
|
241
241
|
await writeFile(join(publicDir, '.nojekyll'), '');
|
|
242
242
|
// GitHub Pages doesn't support _redirects files. Warn if any
|
|
243
243
|
// redirects were generated — those routes will 404 on GH Pages.
|