@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
package/dist/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { a as __esmMin, o as __exportAll, s as __toCommonJS } from "./_chunks/dist-BA3u1z3W.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { a as
|
|
6
|
-
import { a as resolveAppDir, c as createNoopTimer, n as loadTimberConfigFile, o as resolveBuildDir, r as mergeFileConfig, s as resolveClientJavascript, t as createPluginContext } from "./_chunks/plugin-context
|
|
7
|
-
import { t as
|
|
2
|
+
import { a as canonicalize, d as isMetadataRouteServePath, u as isDynamicMetadataExtension } from "./_chunks/segment-keys-BawYuNFO.js";
|
|
3
|
+
import { a as collectInterceptionRewrites, i as scanRoutes, o as matchUrlParts } from "./_chunks/cli-schema-sync-CKgHC2MB.js";
|
|
4
|
+
import { h as swallow } from "./_chunks/logger-N7e5auP0.js";
|
|
5
|
+
import { a as fileHasAnyExport, c as fileHasExport, i as generateRouteMap, l as fileHasStarExport, o as fileHasDefaultExport, r as validateSchemaAgainstRoutes, s as fileHasDirective, t as collectLeafRoutes, u as getPrerenderExport } from "./_chunks/walkers-BhhwI9TD.js";
|
|
6
|
+
import { a as resolveAppDir, c as createNoopTimer, n as loadTimberConfigFile, o as resolveBuildDir, r as mergeFileConfig, s as resolveClientJavascript, t as createPluginContext } from "./_chunks/plugin-context-DEGLSJs3.js";
|
|
7
|
+
import { i as withBasePath, n as normalizeBasePath, t as isRelativeBase } from "./_chunks/base-path-DaQrzbez.js";
|
|
8
|
+
import { t as rscPayloadPath } from "./_chunks/rsc-payload-path-B_LBodc2.js";
|
|
9
|
+
import { a as REDIRECTS_FILE, n as detectFsIdentity, o as RESERVED_OUTPUT_PATHS, r as fsKey, t as EXACT_FS_IDENTITY } from "./_chunks/fs-identity-D7vb6GdI.js";
|
|
10
|
+
import { t as EMPTY_BUILD_MANIFEST } from "./_chunks/build-manifest-DWppEdLB.js";
|
|
11
|
+
import { t as fnv1aHash } from "./_chunks/fast-hash-C-tVYrLb.js";
|
|
8
12
|
import { t as formatSize } from "./_chunks/format-CfwjgPz9.js";
|
|
9
13
|
import { createLogger, parseAst } from "vite";
|
|
10
|
-
import { basename, dirname, extname, join, normalize, relative, resolve, win32 } from "node:path";
|
|
14
|
+
import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, win32 } from "node:path";
|
|
11
15
|
import { builtinModules, createRequire } from "node:module";
|
|
12
16
|
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
|
|
13
17
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
@@ -1479,125 +1483,6 @@ function compressWithGzip(body) {
|
|
|
1479
1483
|
return Readable.toWeb(gzip);
|
|
1480
1484
|
}
|
|
1481
1485
|
//#endregion
|
|
1482
|
-
//#region src/server/tree-match.ts
|
|
1483
|
-
/**
|
|
1484
|
-
* Match URL path parts against a segment tree.
|
|
1485
|
-
*
|
|
1486
|
-
* Walks the tree depth-first with backtracking, applying priority
|
|
1487
|
-
* ordering globally across group boundaries. Returns the matched
|
|
1488
|
-
* segment chain and collected params, or null if no route matches.
|
|
1489
|
-
*
|
|
1490
|
-
* By default, a match requires a terminal node (one with `page` or
|
|
1491
|
-
* `route`) at the leaf. Pass `{ requireTerminal: false }` to match
|
|
1492
|
-
* any segment (used by metadata route matching).
|
|
1493
|
-
*
|
|
1494
|
-
* @param root The root segment node to start matching from.
|
|
1495
|
-
* @param parts URL path parts (e.g. ['blog', 'hello-world']).
|
|
1496
|
-
* Empty array matches the root itself (if terminal).
|
|
1497
|
-
*/
|
|
1498
|
-
function matchUrlParts(root, parts, options) {
|
|
1499
|
-
const chain = [];
|
|
1500
|
-
const params = {};
|
|
1501
|
-
if (matchNode(root, parts, 0, chain, params, options?.requireTerminal ?? true)) return {
|
|
1502
|
-
chain,
|
|
1503
|
-
params
|
|
1504
|
-
};
|
|
1505
|
-
return null;
|
|
1506
|
-
}
|
|
1507
|
-
/**
|
|
1508
|
-
* Flatten children through group segments.
|
|
1509
|
-
*
|
|
1510
|
-
* Groups are transparent for URL matching — their non-group descendants
|
|
1511
|
-
* are returned with the chain of group nodes leading to them. This lets
|
|
1512
|
-
* the caller apply priority ordering (static > dynamic > catch-all >
|
|
1513
|
-
* optional-catch-all) across all groups uniformly instead of per-group.
|
|
1514
|
-
*/
|
|
1515
|
-
function collectEffectiveChildren(node, groupChain = []) {
|
|
1516
|
-
const result = [];
|
|
1517
|
-
for (const child of node.children) if (child.segmentType === "group") {
|
|
1518
|
-
const nested = collectEffectiveChildren(child, [...groupChain, child]);
|
|
1519
|
-
result.push(...nested);
|
|
1520
|
-
} else result.push({
|
|
1521
|
-
child,
|
|
1522
|
-
groupChain
|
|
1523
|
-
});
|
|
1524
|
-
return result;
|
|
1525
|
-
}
|
|
1526
|
-
/** A node is terminal if it can serve a response (has a page or route handler). */
|
|
1527
|
-
function isTerminal(node) {
|
|
1528
|
-
return !!(node.page || node.route);
|
|
1529
|
-
}
|
|
1530
|
-
/** Combined affix length for specificity ordering. */
|
|
1531
|
-
function affixLength(node) {
|
|
1532
|
-
return (node.paramPrefix?.length ?? 0) + (node.paramSuffix?.length ?? 0);
|
|
1533
|
-
}
|
|
1534
|
-
/**
|
|
1535
|
-
* Recursive depth-first matcher with backtracking.
|
|
1536
|
-
*
|
|
1537
|
-
* Mutates `chain` and `params` in place for performance; backtracks
|
|
1538
|
-
* (restores previous state) when a candidate doesn't lead to a match.
|
|
1539
|
-
*/
|
|
1540
|
-
function matchNode(node, parts, index, chain, params, requireTerminal) {
|
|
1541
|
-
chain.push(node);
|
|
1542
|
-
if (index >= parts.length) {
|
|
1543
|
-
if (!requireTerminal || isTerminal(node)) return true;
|
|
1544
|
-
for (const child of node.children) if (child.segmentType === "group") {
|
|
1545
|
-
if (matchNode(child, parts, index, chain, params, requireTerminal)) return true;
|
|
1546
|
-
}
|
|
1547
|
-
const effective = collectEffectiveChildren(node);
|
|
1548
|
-
for (const { child, groupChain } of effective) if (child.segmentType === "optional-catch-all" && (!requireTerminal || isTerminal(child))) {
|
|
1549
|
-
chain.push(...groupChain, child);
|
|
1550
|
-
return true;
|
|
1551
|
-
}
|
|
1552
|
-
chain.pop();
|
|
1553
|
-
return false;
|
|
1554
|
-
}
|
|
1555
|
-
const part = parts[index];
|
|
1556
|
-
const effective = collectEffectiveChildren(node);
|
|
1557
|
-
for (const { child, groupChain } of effective) if (child.segmentType === "static" && child.segmentName === part) {
|
|
1558
|
-
chain.push(...groupChain);
|
|
1559
|
-
if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) return true;
|
|
1560
|
-
chain.length -= groupChain.length;
|
|
1561
|
-
}
|
|
1562
|
-
const affixed = effective.filter(({ child }) => child.segmentType === "dynamic" && child.paramName && (child.paramPrefix || child.paramSuffix));
|
|
1563
|
-
affixed.sort((a, b) => affixLength(b.child) - affixLength(a.child));
|
|
1564
|
-
for (const { child, groupChain } of affixed) {
|
|
1565
|
-
const prefix = child.paramPrefix ?? "";
|
|
1566
|
-
const suffix = child.paramSuffix ?? "";
|
|
1567
|
-
if (part.length > prefix.length + suffix.length && (!prefix || part.startsWith(prefix)) && (!suffix || part.endsWith(suffix))) {
|
|
1568
|
-
const value = part.slice(prefix.length, part.length - (suffix.length || 0));
|
|
1569
|
-
chain.push(...groupChain);
|
|
1570
|
-
const prev = params[child.paramName];
|
|
1571
|
-
params[child.paramName] = value;
|
|
1572
|
-
if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) return true;
|
|
1573
|
-
chain.length -= groupChain.length;
|
|
1574
|
-
if (prev !== void 0) params[child.paramName] = prev;
|
|
1575
|
-
else delete params[child.paramName];
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
for (const { child, groupChain } of effective) if (child.segmentType === "dynamic" && child.paramName && !child.paramPrefix && !child.paramSuffix) {
|
|
1579
|
-
chain.push(...groupChain);
|
|
1580
|
-
const prev = params[child.paramName];
|
|
1581
|
-
params[child.paramName] = part;
|
|
1582
|
-
if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) return true;
|
|
1583
|
-
chain.length -= groupChain.length;
|
|
1584
|
-
if (prev !== void 0) params[child.paramName] = prev;
|
|
1585
|
-
else delete params[child.paramName];
|
|
1586
|
-
}
|
|
1587
|
-
for (const { child, groupChain } of effective) if (child.segmentType === "catch-all" && child.paramName && (!requireTerminal || isTerminal(child))) {
|
|
1588
|
-
chain.push(...groupChain, child);
|
|
1589
|
-
params[child.paramName] = parts.slice(index);
|
|
1590
|
-
return true;
|
|
1591
|
-
}
|
|
1592
|
-
for (const { child, groupChain } of effective) if (child.segmentType === "optional-catch-all" && child.paramName && (!requireTerminal || isTerminal(child))) {
|
|
1593
|
-
chain.push(...groupChain, child);
|
|
1594
|
-
params[child.paramName] = parts.slice(index);
|
|
1595
|
-
return true;
|
|
1596
|
-
}
|
|
1597
|
-
chain.pop();
|
|
1598
|
-
return false;
|
|
1599
|
-
}
|
|
1600
|
-
//#endregion
|
|
1601
1486
|
//#region src/plugins/dev-server.ts
|
|
1602
1487
|
var RSC_ENTRY_ID = "virtual:timber-rsc-entry";
|
|
1603
1488
|
/**
|
|
@@ -2014,6 +1899,7 @@ function generateConfigModule(ctx) {
|
|
|
2014
1899
|
csrf: ctx.config.csrf ?? true,
|
|
2015
1900
|
allowedOrigins: ctx.config.allowedOrigins,
|
|
2016
1901
|
clientJavascript: ctx.clientJavascript,
|
|
1902
|
+
base: ctx.base,
|
|
2017
1903
|
dev: ctx.dev ?? false,
|
|
2018
1904
|
slowPhaseMs: ctx.config.dev?.slowPhaseMs ?? 200,
|
|
2019
1905
|
slowRequestMs: ctx.config.slowRequestMs ?? 3e3,
|
|
@@ -2401,7 +2287,6 @@ var SINGLE_FILE_KEYS = [
|
|
|
2401
2287
|
var RECORD_FILE_KEYS = [
|
|
2402
2288
|
"statusFiles",
|
|
2403
2289
|
"jsonStatusFiles",
|
|
2404
|
-
"legacyStatusFiles",
|
|
2405
2290
|
"metadataRoutes"
|
|
2406
2291
|
];
|
|
2407
2292
|
var SCHEMA_EXTENSIONS = [
|
|
@@ -2478,10 +2363,7 @@ function generateManifestModule(tree, viteRoot) {
|
|
|
2478
2363
|
let globalErrorLine = "";
|
|
2479
2364
|
if (tree.globalError) globalErrorLine = ` globalError: { load: ${addImport(tree.globalError)}, filePath: ${JSON.stringify(tree.globalError.filePath)} },`;
|
|
2480
2365
|
const rewrites = collectInterceptionRewrites(tree.root);
|
|
2481
|
-
const rewritesLine = rewrites.length > 0 ? ` interceptionRewrites: ${JSON.stringify(rewrites
|
|
2482
|
-
interceptedPattern: r.interceptedPattern,
|
|
2483
|
-
interceptingPrefix: r.interceptingPrefix
|
|
2484
|
-
})))},` : "";
|
|
2366
|
+
const rewritesLine = rewrites.length > 0 ? ` interceptionRewrites: ${JSON.stringify(rewrites)},` : "";
|
|
2485
2367
|
return [
|
|
2486
2368
|
"// Auto-generated route manifest — do not edit.",
|
|
2487
2369
|
"// Generated by timber-routing plugin.",
|
|
@@ -2829,7 +2711,7 @@ var SEGMENT_PREFIX = "\0$segment:";
|
|
|
2829
2711
|
/**
|
|
2830
2712
|
* File convention names we track for changes that require manifest regeneration.
|
|
2831
2713
|
*/
|
|
2832
|
-
var ROUTE_FILE_PATTERNS = /\/(page|layout|middleware|access|route|error|global-error|default|denied|params|schema|\d{3}|[45]xx|
|
|
2714
|
+
var ROUTE_FILE_PATTERNS = /\/(page|layout|middleware|access|route|error|global-error|default|denied|params|schema|\d{3}|[45]xx|sitemap|robots|manifest|favicon|icon|opengraph-image|apple-icon)\./;
|
|
2833
2715
|
/**
|
|
2834
2716
|
* Create the timber-routing Vite plugin.
|
|
2835
2717
|
*
|
|
@@ -2865,9 +2747,38 @@ var TIMBER_ENV_DTS = [
|
|
|
2865
2747
|
""
|
|
2866
2748
|
].join("\n");
|
|
2867
2749
|
/**
|
|
2750
|
+
* Codegen writes that have been started but have not yet landed on disk.
|
|
2751
|
+
*
|
|
2752
|
+
* `writeCodegen` is deliberately unawaited on the hot path, which leaves a
|
|
2753
|
+
* caller no way to know the files exist yet. Anything that tears down the
|
|
2754
|
+
* project root — a build process exiting, a test removing its temp root —
|
|
2755
|
+
* then races the writes: `rmSync` empties a directory that a pending `mkdir`
|
|
2756
|
+
* immediately recreates, and the removal fails with ENOTEMPTY.
|
|
2757
|
+
*
|
|
2758
|
+
* Every started write registers here and deregisters when it settles, so
|
|
2759
|
+
* `flushCodegenWrites()` can await them instead of guessing at a delay.
|
|
2760
|
+
*/
|
|
2761
|
+
var pendingCodegenWrites = /* @__PURE__ */ new Set();
|
|
2762
|
+
/**
|
|
2763
|
+
* Resolve once every codegen write started so far has landed on disk.
|
|
2764
|
+
*
|
|
2765
|
+
* Safe to call at any time and from anywhere — writes are tracked per process,
|
|
2766
|
+
* not per plugin instance, so a caller does not need a handle on the plugin
|
|
2767
|
+
* that started them. Never rejects: individual write failures are already
|
|
2768
|
+
* swallowed and logged by `writeCodegen`.
|
|
2769
|
+
*/
|
|
2770
|
+
async function flushCodegenWrites() {
|
|
2771
|
+
while (pendingCodegenWrites.size > 0) {
|
|
2772
|
+
const inFlight = [...pendingCodegenWrites];
|
|
2773
|
+
await Promise.all(inFlight);
|
|
2774
|
+
for (const write of inFlight) pendingCodegenWrites.delete(write);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2868
2778
|
* Write the generated route map and the timber-env.d.ts reference file.
|
|
2869
2779
|
*
|
|
2870
|
-
*
|
|
2780
|
+
* Not awaited by callers, to avoid blocking the hot path — use
|
|
2781
|
+
* `flushCodegenWrites()` to wait for the writes to land.
|
|
2871
2782
|
* Errors are swallowed; a missing .d.ts is a developer UX issue, not a runtime failure.
|
|
2872
2783
|
*/
|
|
2873
2784
|
function writeCodegen(ctx) {
|
|
@@ -2879,13 +2790,15 @@ function writeCodegen(ctx) {
|
|
|
2879
2790
|
});
|
|
2880
2791
|
const routesPath = join(ctx.root, CODEGEN_OUTPUT);
|
|
2881
2792
|
const envPath = join(ctx.root, "timber-env.d.ts");
|
|
2882
|
-
mkdir(timberDir, { recursive: true }).then(() => Promise.all([
|
|
2793
|
+
const done = mkdir(timberDir, { recursive: true }).then(() => Promise.all([
|
|
2883
2794
|
writeFile(routesPath, content, "utf-8"),
|
|
2884
2795
|
writeFile(envPath, TIMBER_ENV_DTS, "utf-8"),
|
|
2885
2796
|
writeSegmentTypes(ctx.routeTree, ctx.appDir, ctx.root)
|
|
2886
|
-
])).catch((err) => {
|
|
2797
|
+
])).then(() => void 0).catch((err) => {
|
|
2887
2798
|
console.warn("[timber] Failed to write codegen output:", err);
|
|
2888
2799
|
});
|
|
2800
|
+
pendingCodegenWrites.add(done);
|
|
2801
|
+
done.finally(() => pendingCodegenWrites.delete(done));
|
|
2889
2802
|
}
|
|
2890
2803
|
function timberRouting(ctx) {
|
|
2891
2804
|
/** Track warned files to avoid repeating warnings on rescan. */
|
|
@@ -2980,6 +2893,18 @@ function timberRouting(ctx) {
|
|
|
2980
2893
|
rescan();
|
|
2981
2894
|
},
|
|
2982
2895
|
/**
|
|
2896
|
+
* Wait for the codegen writes started by `buildStart` before the build
|
|
2897
|
+
* reports done.
|
|
2898
|
+
*
|
|
2899
|
+
* `writeCodegen` runs unawaited so route scanning never blocks on disk.
|
|
2900
|
+
* Without this the build can finish — and the CLI can exit — while the
|
|
2901
|
+
* generated `.d.ts` files are still being written, leaving a project root
|
|
2902
|
+
* with no route types after an apparently successful build.
|
|
2903
|
+
*/
|
|
2904
|
+
closeBundle() {
|
|
2905
|
+
return flushCodegenWrites();
|
|
2906
|
+
},
|
|
2907
|
+
/**
|
|
2983
2908
|
* Set up file watcher in dev mode.
|
|
2984
2909
|
*
|
|
2985
2910
|
* Watches app/ for file additions/deletions/renames that affect
|
|
@@ -6195,16 +6120,19 @@ function writeFontManifest(pipeline, buildManifest, base = "/") {
|
|
|
6195
6120
|
*/
|
|
6196
6121
|
function timberFonts(ctx) {
|
|
6197
6122
|
const pipeline = new FontPipeline({ hashOnRegister: !ctx.dev });
|
|
6198
|
-
let resolvedBase = "/";
|
|
6199
6123
|
return {
|
|
6200
6124
|
name: "timber-fonts",
|
|
6201
6125
|
/**
|
|
6202
|
-
*
|
|
6203
|
-
* (@font-face src, manifest hrefs, dev middleware matching)
|
|
6204
|
-
* the prefix. Emitted file locations are unaffected — only URLs.
|
|
6126
|
+
* Push Vite's resolved `base` into the pipeline so every font URL it
|
|
6127
|
+
* emits (@font-face src, manifest hrefs, dev middleware matching)
|
|
6128
|
+
* carries the prefix. Emitted file locations are unaffected — only URLs.
|
|
6129
|
+
*
|
|
6130
|
+
* `config.base` is read here rather than `ctx.base`: this plugin is
|
|
6131
|
+
* `enforce: 'pre'`, so its configResolved runs before timber-root-sync
|
|
6132
|
+
* populates `ctx.base`. Every *later* hook reads `ctx.base` — the single
|
|
6133
|
+
* copy (TIM-1261).
|
|
6205
6134
|
*/
|
|
6206
6135
|
configResolved(config) {
|
|
6207
|
-
resolvedBase = config.base;
|
|
6208
6136
|
pipeline.setBase(config.base);
|
|
6209
6137
|
const originalWarnOnce = config.logger.warnOnce.bind(config.logger);
|
|
6210
6138
|
config.logger.warnOnce = (msg, options) => {
|
|
@@ -6293,7 +6221,7 @@ function timberFonts(ctx) {
|
|
|
6293
6221
|
} else {
|
|
6294
6222
|
const familyCached = groupCachedFontsByFamily(await downloadAndCacheFonts([font], ctx.root)).get(font.family.toLowerCase()) ?? [];
|
|
6295
6223
|
pipeline.attachCachedFiles(font.id, familyCached);
|
|
6296
|
-
pipeline.attachFaces(font.id, generateProductionFontFaces(familyCached, font.display, fontUrlPrefix(
|
|
6224
|
+
pipeline.attachFaces(font.id, generateProductionFontFaces(familyCached, font.display, fontUrlPrefix(ctx.base)));
|
|
6297
6225
|
}
|
|
6298
6226
|
} catch (e) {
|
|
6299
6227
|
if (!ctx.dev) throw e;
|
|
@@ -6311,7 +6239,7 @@ function timberFonts(ctx) {
|
|
|
6311
6239
|
* this middleware.
|
|
6312
6240
|
*/
|
|
6313
6241
|
configureServer(server) {
|
|
6314
|
-
installFontDevMiddleware(server, pipeline,
|
|
6242
|
+
installFontDevMiddleware(server, pipeline, ctx.base);
|
|
6315
6243
|
},
|
|
6316
6244
|
/**
|
|
6317
6245
|
* No-op. Font downloading and face attachment happen lazily in `load()`
|
|
@@ -6381,7 +6309,7 @@ function timberFonts(ctx) {
|
|
|
6381
6309
|
generateBundle() {
|
|
6382
6310
|
emitFontAssets(pipeline, (asset) => this.emitFile(asset), (msg) => this.warn(msg));
|
|
6383
6311
|
if (!ctx.buildManifest) return;
|
|
6384
|
-
writeFontManifest(pipeline, ctx.buildManifest,
|
|
6312
|
+
writeFontManifest(pipeline, ctx.buildManifest, ctx.base);
|
|
6385
6313
|
}
|
|
6386
6314
|
};
|
|
6387
6315
|
}
|
|
@@ -6411,6 +6339,7 @@ function timberAdapterBuild(ctx) {
|
|
|
6411
6339
|
if (ctx.clientJavascript.disabled) await stripJsFromRscAssetsManifests(buildDir);
|
|
6412
6340
|
const adapterConfig = {
|
|
6413
6341
|
root: ctx.root,
|
|
6342
|
+
base: normalizeBasePath(ctx.base),
|
|
6414
6343
|
output: ctx.config.output ?? "server",
|
|
6415
6344
|
clientJavascriptDisabled: ctx.clientJavascript.disabled,
|
|
6416
6345
|
manifestInit,
|
|
@@ -6655,10 +6584,22 @@ function detectDynamicApis(code, fileId) {
|
|
|
6655
6584
|
* Generate a `_redirects` file from collected redirect entries.
|
|
6656
6585
|
* Uses the Cloudflare Pages / Netlify format: `/from /to statusCode`
|
|
6657
6586
|
*
|
|
6587
|
+
* Both sides are base-prefixed (TIM-1274). `from` is matched by the host
|
|
6588
|
+
* against the request path and `to` is served to the browser as a `Location`,
|
|
6589
|
+
* so both are deployed-URL space while `StaticRedirectEntry` carries route
|
|
6590
|
+
* space — under `base: '/repo/'`, a rule `/old /new 301` would match nothing
|
|
6591
|
+
* and, if it did, would send the browser off the deployment.
|
|
6592
|
+
*
|
|
6593
|
+
* An off-origin `to` is left alone: `redirectExternal()` produces an absolute
|
|
6594
|
+
* URL, and a protocol-relative one addresses another host too. Neither is a
|
|
6595
|
+
* path on this deployment, so neither takes the prefix.
|
|
6596
|
+
*
|
|
6597
|
+
* @param base Normalized deployment base (see `shared/base-path.ts`).
|
|
6658
6598
|
* @internal Exported for testing.
|
|
6659
6599
|
*/
|
|
6660
|
-
function generateRedirectsFile(redirects) {
|
|
6661
|
-
|
|
6600
|
+
function generateRedirectsFile(redirects, base) {
|
|
6601
|
+
const target = (to) => to.startsWith("/") && !to.startsWith("//") ? withBasePath(base, to) : to;
|
|
6602
|
+
return redirects.map((r) => `${withBasePath(base, r.from)} ${target(r.to)} ${r.status}`).join("\n") + "\n";
|
|
6662
6603
|
}
|
|
6663
6604
|
/** Write a file atomically: temp sibling + rename. */
|
|
6664
6605
|
async function writeFileAtomic$1(path, data) {
|
|
@@ -6670,18 +6611,164 @@ async function writeFileAtomic$1(path, data) {
|
|
|
6670
6611
|
* Determine the output file path for a static page entry.
|
|
6671
6612
|
*
|
|
6672
6613
|
* Pages: /about → about/index.html, / → index.html
|
|
6673
|
-
* RSC: /about → _rsc/about.rsc, / → _rsc/index.rsc
|
|
6614
|
+
* RSC: /about → _rsc/about/index.rsc, / → _rsc/index.rsc
|
|
6674
6615
|
* API: /feed.xml → feed.xml, /api/data → api/data
|
|
6616
|
+
*
|
|
6617
|
+
* The RSC shape mirrors the HTML shape for the reason the HTML shape has it:
|
|
6618
|
+
* a route gets a directory and the artifact is the `index` inside it, so the
|
|
6619
|
+
* mapping is injective. See `shared/rsc-payload-path.ts` (TIM-1266).
|
|
6675
6620
|
*/
|
|
6676
6621
|
function staticOutputPath(entry) {
|
|
6677
6622
|
const urlPath = entry.urlPath === "/" ? "" : entry.urlPath;
|
|
6678
|
-
if (entry.kind === "rsc") return
|
|
6623
|
+
if (entry.kind === "rsc") return rscPayloadPath(entry.urlPath).slice(1);
|
|
6679
6624
|
if (entry.kind === "api") return urlPath.startsWith("/") ? urlPath.slice(1) : urlPath || "index";
|
|
6680
6625
|
if (!urlPath) return "index.html";
|
|
6681
6626
|
if (urlPath === "/_timber_not_found") return "404.html";
|
|
6682
6627
|
return `${urlPath.startsWith("/") ? urlPath.slice(1) : urlPath}/index.html`;
|
|
6683
6628
|
}
|
|
6684
6629
|
/**
|
|
6630
|
+
* Records which entry claimed each output path and reports second claims.
|
|
6631
|
+
*
|
|
6632
|
+
* Two entries landing on one path is not a rendering failure — both render
|
|
6633
|
+
* fine — so nothing downstream notices. The file is written twice, the second
|
|
6634
|
+
* write wins, and the deployment serves one route's bytes at another route's
|
|
6635
|
+
* URL with the same content type and the same shape (TIM-1266).
|
|
6636
|
+
*
|
|
6637
|
+
* The naming rules are injective by construction (a route path is a directory
|
|
6638
|
+
* and the artifact is the `index` inside it), so a collision here means either
|
|
6639
|
+
* two entries were enumerated for one URL or a route reached a path the
|
|
6640
|
+
* framework reserves for itself (`404.html`). Both are build errors: there is
|
|
6641
|
+
* no correct file to write.
|
|
6642
|
+
*
|
|
6643
|
+
* Two entries can also be unwritable together without claiming the same
|
|
6644
|
+
* string: one wants a file where the other wants a directory (`about` from an
|
|
6645
|
+
* API route, `about/index.html` from a page). The filesystem does refuse that
|
|
6646
|
+
* one — `EISDIR` on the rename, `ENOTDIR` on the mkdir, depending on which
|
|
6647
|
+
* lands second — but an errno names a path, and in the rename case the path
|
|
6648
|
+
* it names is `about.tmp`. Reported here instead, so the message says which
|
|
6649
|
+
* two routes.
|
|
6650
|
+
*
|
|
6651
|
+
* Collisions are **collected, not thrown**. `onEntry` runs inside the
|
|
6652
|
+
* generator, whose RSC branch treats a throw as "this payload is unavailable,
|
|
6653
|
+
* SPA navigation falls back to a full load" and swallows it — which for a
|
|
6654
|
+
* collision is the wrong end state, since the two payloads are still
|
|
6655
|
+
* ambiguous. The caller raises them once generation is done.
|
|
6656
|
+
*
|
|
6657
|
+
* @internal Exported for testing.
|
|
6658
|
+
*/
|
|
6659
|
+
var StaticOutputPaths = class {
|
|
6660
|
+
identity;
|
|
6661
|
+
claimed = /* @__PURE__ */ new Map();
|
|
6662
|
+
/** Every ancestor directory of a claimed path, mapped to its claimant. */
|
|
6663
|
+
directories = /* @__PURE__ */ new Map();
|
|
6664
|
+
collisions = [];
|
|
6665
|
+
/**
|
|
6666
|
+
* @param identity How the output filesystem decides two paths name one
|
|
6667
|
+
* file, from `detectFsIdentity`. The guard predicts whether two writes
|
|
6668
|
+
* land on one file, which is a property of the filesystem — so it is
|
|
6669
|
+
* asked, not assumed. Folding unconditionally would reject `/Foo` beside
|
|
6670
|
+
* `/foo` on Linux, where both are perfectly servable files.
|
|
6671
|
+
*/
|
|
6672
|
+
constructor(identity = EXACT_FS_IDENTITY) {
|
|
6673
|
+
this.identity = identity;
|
|
6674
|
+
}
|
|
6675
|
+
/** The identity under which the filesystem will compare this path. */
|
|
6676
|
+
key(relPath) {
|
|
6677
|
+
return fsKey(relPath, this.identity);
|
|
6678
|
+
}
|
|
6679
|
+
/**
|
|
6680
|
+
* Claim `relPath` for `entry`. Returns false when the path cannot be
|
|
6681
|
+
* written alongside an earlier claim, in which case the caller must not
|
|
6682
|
+
* write the file — the losing entry is recorded and the build fails at
|
|
6683
|
+
* the end.
|
|
6684
|
+
*/
|
|
6685
|
+
claim(relPath, entry) {
|
|
6686
|
+
if (RESERVED_OUTPUT_PATHS.some((name) => this.key(name) === this.key(relPath))) {
|
|
6687
|
+
this.collisions.push(` - ${relPath}: reserved — the framework writes this file itself, and ${entry.kind} for ${entry.urlPath} would be overwritten by it`);
|
|
6688
|
+
return false;
|
|
6689
|
+
}
|
|
6690
|
+
const key = this.key(relPath);
|
|
6691
|
+
const conflict = this.claimed.get(key) ?? this.directories.get(key) ?? ancestors(relPath).reduce((found, dir) => found ?? this.claimed.get(this.key(dir)), void 0);
|
|
6692
|
+
if (conflict) {
|
|
6693
|
+
this.collisions.push(` - ${relPath}: ${conflict.kind} for ${conflict.urlPath} and ${entry.kind} for ${entry.urlPath}`);
|
|
6694
|
+
return false;
|
|
6695
|
+
}
|
|
6696
|
+
this.claimed.set(key, entry);
|
|
6697
|
+
for (const dir of ancestors(relPath)) {
|
|
6698
|
+
const dirKey = this.key(dir);
|
|
6699
|
+
if (!this.directories.has(dirKey)) this.directories.set(dirKey, entry);
|
|
6700
|
+
}
|
|
6701
|
+
return true;
|
|
6702
|
+
}
|
|
6703
|
+
/** Throw if any two entries could not both be written. */
|
|
6704
|
+
assertNoCollisions() {
|
|
6705
|
+
if (this.collisions.length === 0) return;
|
|
6706
|
+
throw new Error(`[timber] Static generation: ${this.collisions.length} output path collision(s) — two entries write the same path and one would silently overwrite the other. Rename or remove one route in each pair:\n${this.collisions.join("\n")}`);
|
|
6707
|
+
}
|
|
6708
|
+
};
|
|
6709
|
+
/** Every directory an output path sits under, outermost first. */
|
|
6710
|
+
function ancestors(relPath) {
|
|
6711
|
+
const parts = relPath.split("/");
|
|
6712
|
+
parts.pop();
|
|
6713
|
+
return parts.map((_, i) => parts.slice(0, i + 1).join("/"));
|
|
6714
|
+
}
|
|
6715
|
+
/**
|
|
6716
|
+
* Writes generated entries into the static output directory, one file per
|
|
6717
|
+
* entry, refusing any write that would land on a path another entry holds.
|
|
6718
|
+
*
|
|
6719
|
+
* @internal Exported for testing.
|
|
6720
|
+
*/
|
|
6721
|
+
var StaticSiteWriter = class {
|
|
6722
|
+
staticDir;
|
|
6723
|
+
/** Content-Type per URL path for extensionless API routes (TIM-1241). */
|
|
6724
|
+
contentTypes = {};
|
|
6725
|
+
paths;
|
|
6726
|
+
constructor(staticDir, identity = EXACT_FS_IDENTITY) {
|
|
6727
|
+
this.staticDir = staticDir;
|
|
6728
|
+
this.paths = new StaticOutputPaths(identity);
|
|
6729
|
+
}
|
|
6730
|
+
async write(entry) {
|
|
6731
|
+
const relPath = staticOutputPath(entry);
|
|
6732
|
+
const fullPath = resolve(this.staticDir, relPath);
|
|
6733
|
+
const rel = relative(this.staticDir, fullPath);
|
|
6734
|
+
if (rel.split(/[\\/]/)[0] === ".." || isAbsolute(rel) || resolve(this.staticDir, rel) !== fullPath) throw new Error(`[timber] Static generation: output path "${relPath}" escapes the static output directory — check generateStaticSegmentParams for path traversal characters in ${entry.urlPath}`);
|
|
6735
|
+
if (!this.paths.claim(rel.replace(/\\/g, "/"), entry)) return;
|
|
6736
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
6737
|
+
await writeFileAtomic$1(fullPath, entry.body);
|
|
6738
|
+
if (entry.kind === "api" && !extname(relPath) && entry.contentType) this.contentTypes[entry.urlPath] = entry.contentType;
|
|
6739
|
+
}
|
|
6740
|
+
/** Throw if any two entries could not both be written. */
|
|
6741
|
+
assertNoCollisions() {
|
|
6742
|
+
this.paths.assertNoCollisions();
|
|
6743
|
+
}
|
|
6744
|
+
};
|
|
6745
|
+
/**
|
|
6746
|
+
* Remove artifacts a generation pass produces only *conditionally*, so none
|
|
6747
|
+
* of them can survive from an earlier build into this one.
|
|
6748
|
+
*
|
|
6749
|
+
* Pages are not in this set: every page is rewritten each pass, and a page
|
|
6750
|
+
* whose route was deleted is a different problem (a stale file nothing links
|
|
6751
|
+
* to) from the one here.
|
|
6752
|
+
*
|
|
6753
|
+
* - `_rsc/` because the files are content-hashed. Left in place, a previous
|
|
6754
|
+
* build's already-hashed payload gets hashed again and accumulates.
|
|
6755
|
+
* - `_redirects` because it is written only when this pass found redirects.
|
|
6756
|
+
* A build whose redirects were removed would otherwise ship the previous
|
|
6757
|
+
* build's rules — and would break the merge exemption in
|
|
6758
|
+
* `shared/static-platform-files.ts`, which lets the pre-rendered copy
|
|
6759
|
+
* overwrite `public/_redirects` on the grounds that it *contains* it. That
|
|
6760
|
+
* holds only for a file the current pass produced (TIM-1266).
|
|
6761
|
+
*
|
|
6762
|
+
* @internal Exported for testing.
|
|
6763
|
+
*/
|
|
6764
|
+
async function cleanConditionalArtifacts(staticDir) {
|
|
6765
|
+
await rm(join(staticDir, "_rsc"), {
|
|
6766
|
+
recursive: true,
|
|
6767
|
+
force: true
|
|
6768
|
+
}).catch(() => {});
|
|
6769
|
+
await rm(join(staticDir, REDIRECTS_FILE), { force: true }).catch(() => {});
|
|
6770
|
+
}
|
|
6771
|
+
/**
|
|
6685
6772
|
* Load the built RSC entry and run the static generation pass.
|
|
6686
6773
|
*
|
|
6687
6774
|
* Unlike prebuilt capture, this needs the FULL pipeline handler, so
|
|
@@ -6708,35 +6795,25 @@ async function runStaticGeneration(ctx) {
|
|
|
6708
6795
|
const generate = mod.__timber_generateStaticSite;
|
|
6709
6796
|
if (typeof generate !== "function") throw new Error("[timber] Static generation: built RSC entry does not export __timber_generateStaticSite — rebuild from a clean build directory");
|
|
6710
6797
|
const staticDir = join(ctx.buildDir, "static-pages");
|
|
6711
|
-
await
|
|
6712
|
-
recursive: true,
|
|
6713
|
-
force: true
|
|
6714
|
-
}).catch(() => {});
|
|
6798
|
+
await cleanConditionalArtifacts(staticDir);
|
|
6715
6799
|
await mkdir(staticDir, { recursive: true });
|
|
6716
6800
|
const sitemapConfig = ctx.config.sitemap;
|
|
6717
6801
|
const autoSitemapEnabled = Boolean(sitemapConfig?.enabled && sitemapConfig?.baseUrl);
|
|
6718
|
-
const
|
|
6802
|
+
const writer = new StaticSiteWriter(staticDir, await detectFsIdentity(staticDir));
|
|
6719
6803
|
const summary = await generate({
|
|
6720
6804
|
autoSitemapEnabled,
|
|
6721
6805
|
renderTimeoutMs: ctx.config.renderTimeoutMs,
|
|
6722
|
-
onEntry:
|
|
6723
|
-
const relPath = staticOutputPath(entry);
|
|
6724
|
-
const fullPath = resolve(staticDir, relPath);
|
|
6725
|
-
const rel = relative(staticDir, fullPath);
|
|
6726
|
-
if (rel.startsWith("..") || resolve(staticDir, rel) !== fullPath) throw new Error(`[timber] Static generation: output path "${relPath}" escapes the static output directory — check generateStaticSegmentParams for path traversal characters in ${entry.urlPath}`);
|
|
6727
|
-
await mkdir(dirname(fullPath), { recursive: true });
|
|
6728
|
-
await writeFileAtomic$1(fullPath, entry.body);
|
|
6729
|
-
if (entry.kind === "rsc" && entry.headers["x-timber-params"]) await writeFileAtomic$1(fullPath.replace(/\.rsc$/, ".params.json"), entry.headers["x-timber-params"]);
|
|
6730
|
-
if (entry.kind === "api" && !extname(relPath) && entry.contentType) contentTypes[entry.urlPath] = entry.contentType;
|
|
6731
|
-
}
|
|
6806
|
+
onEntry: (entry) => writer.write(entry)
|
|
6732
6807
|
});
|
|
6733
6808
|
const totalUrls = summary.rendered + summary.errors.length;
|
|
6734
6809
|
console.log(`[timber] Static generation: rendered ${summary.rendered}/${totalUrls} entries.` + (summary.errors.length > 0 ? ` ${summary.errors.length} failed.` : "") + (summary.redirects.length > 0 ? ` ${summary.redirects.length} redirect(s).` : ""));
|
|
6810
|
+
for (const warning of summary.warnings) console.warn(`[timber] Static generation warning: ${warning}`);
|
|
6735
6811
|
if (summary.errors.length > 0) {
|
|
6736
6812
|
const details = summary.errors.map((e) => ` - ${e.urlPath}: ${e.message}`).join("\n");
|
|
6737
6813
|
throw new Error(`[timber] Static generation failed:\n${details}`);
|
|
6738
6814
|
}
|
|
6739
|
-
|
|
6815
|
+
writer.assertNoCollisions();
|
|
6816
|
+
if (Object.keys(writer.contentTypes).length > 0) ctx.staticContentTypes = writer.contentTypes;
|
|
6740
6817
|
if (!ctx.clientJavascript.disabled) {
|
|
6741
6818
|
const rscManifest = await hashRscPayloads(staticDir);
|
|
6742
6819
|
if (Object.keys(rscManifest).length > 0) {
|
|
@@ -6746,8 +6823,8 @@ async function runStaticGeneration(ctx) {
|
|
|
6746
6823
|
}
|
|
6747
6824
|
}
|
|
6748
6825
|
if (summary.redirects.length > 0) {
|
|
6749
|
-
const generated = generateRedirectsFile(summary.redirects);
|
|
6750
|
-
const redirectsPath = join(staticDir,
|
|
6826
|
+
const generated = generateRedirectsFile(summary.redirects, normalizeBasePath(ctx.base));
|
|
6827
|
+
const redirectsPath = join(staticDir, REDIRECTS_FILE);
|
|
6751
6828
|
const userRedirects = await readUserRedirects(ctx.root);
|
|
6752
6829
|
if (userRedirects !== null) {
|
|
6753
6830
|
await writeFileAtomic$1(redirectsPath, generated + "\n" + userRedirects);
|
|
@@ -6760,6 +6837,27 @@ async function runStaticGeneration(ctx) {
|
|
|
6760
6837
|
return summary;
|
|
6761
6838
|
}
|
|
6762
6839
|
/**
|
|
6840
|
+
* Warn when Vite's `base` is document-relative (`'./'`) in static mode.
|
|
6841
|
+
*
|
|
6842
|
+
* Payload URLs are addressed from the deployment root plus the base prefix.
|
|
6843
|
+
* A relative base gives no prefix to compute (see shared/base-path.ts), so
|
|
6844
|
+
* `/_rsc/about.rsc` is requested at the domain root — correct only when the
|
|
6845
|
+
* site is served from there. The build still succeeds: relative bases work
|
|
6846
|
+
* fine for assets, and for a root-served site nothing is wrong. The warning
|
|
6847
|
+
* exists so a sub-path deployment does not silently degrade to a hard
|
|
6848
|
+
* navigation on every link.
|
|
6849
|
+
*
|
|
6850
|
+
* Skipped when client JS is disabled — there is no SPA navigation and no
|
|
6851
|
+
* payload fetch to mis-address.
|
|
6852
|
+
*
|
|
6853
|
+
* @internal Exported for testing.
|
|
6854
|
+
*/
|
|
6855
|
+
function warnUnusableBase(ctx) {
|
|
6856
|
+
if (ctx.clientJavascript.disabled) return;
|
|
6857
|
+
if (!isRelativeBase(ctx.base)) return;
|
|
6858
|
+
console.warn(`[timber] Static mode: base is "${ctx.base}", which is document-relative. RSC payload URLs are addressed from the domain root, so client-side navigation will fall back to a full page load if the site is served from a sub-path. Use an absolute base (e.g. base: '/repo/') to deploy under a sub-path. See design/11-platform.md.`);
|
|
6859
|
+
}
|
|
6860
|
+
/**
|
|
6763
6861
|
* Compute a short content hash for immutable caching.
|
|
6764
6862
|
* Uses 8 chars of base64url-encoded SHA-256, matching Vite's asset hash style.
|
|
6765
6863
|
*/
|
|
@@ -6785,14 +6883,23 @@ async function walkDir(dir) {
|
|
|
6785
6883
|
return results;
|
|
6786
6884
|
}
|
|
6787
6885
|
/**
|
|
6788
|
-
* Hash all RSC flight payloads
|
|
6789
|
-
*
|
|
6886
|
+
* Hash all RSC flight payloads in `_rsc/`, rename them with content
|
|
6887
|
+
* hashes, and build a manifest with optional inlined route params.
|
|
6790
6888
|
*
|
|
6791
6889
|
* Before: `_rsc/index.rsc`, `_rsc/about.rsc`
|
|
6792
6890
|
* After: `_rsc/index-B7YxEKdN.rsc`, `_rsc/about-C8ZzFLfO.rsc`
|
|
6793
6891
|
*
|
|
6794
|
-
* The manifest maps the original `_rsc/` URL to
|
|
6795
|
-
* `{"/_rsc/index.rsc":"/_rsc/index-B7YxEKdN.rsc", ...}`
|
|
6892
|
+
* The manifest maps the original `_rsc/` URL to an entry object:
|
|
6893
|
+
* `{"/_rsc/index.rsc":{"url":"/_rsc/index-B7YxEKdN.rsc"}, ...}`
|
|
6894
|
+
*
|
|
6895
|
+
* Keys and URLs are **route-space**: root-relative, never carrying Vite's
|
|
6896
|
+
* `base`. The client applies the base once, at fetch time
|
|
6897
|
+
* (`client/rsc-fetch.ts` → `toStaticRscUrl`), so the generated site stays
|
|
6898
|
+
* relocatable and the base has exactly one expression at runtime rather
|
|
6899
|
+
* than two that must agree. See TIM-1261.
|
|
6900
|
+
*
|
|
6901
|
+
* Route params (from `routeParams`) are inlined into the manifest
|
|
6902
|
+
* entry, eliminating the need for .params.json sidecar files. TIM-1255.
|
|
6796
6903
|
*
|
|
6797
6904
|
* Injected into HTML as:
|
|
6798
6905
|
* `<script>window.__TIMBER_RSC_MANIFEST__={...}<\/script>`
|
|
@@ -6803,14 +6910,14 @@ async function hashRscPayloads(staticDir) {
|
|
|
6803
6910
|
const files = await walkDir(join(staticDir, "_rsc"));
|
|
6804
6911
|
const manifest = {};
|
|
6805
6912
|
for (const filePath of files) {
|
|
6806
|
-
if (!filePath.endsWith(".rsc")
|
|
6913
|
+
if (!filePath.endsWith(".rsc")) continue;
|
|
6807
6914
|
const hash = contentHash(await readFile(filePath));
|
|
6808
6915
|
const rel = relative(staticDir, filePath).replace(/\\/g, "/");
|
|
6809
|
-
const
|
|
6810
|
-
const hashedPath = `${filePath.slice(0, -ext.length)}-${hash}${ext}`;
|
|
6916
|
+
const hashedPath = `${filePath.slice(0, -4)}-${hash}.rsc`;
|
|
6811
6917
|
await rename(filePath, hashedPath);
|
|
6812
6918
|
const hashedRel = relative(staticDir, hashedPath).replace(/\\/g, "/");
|
|
6813
|
-
|
|
6919
|
+
const key = `/${rel}`;
|
|
6920
|
+
manifest[key] = { url: `/${hashedRel}` };
|
|
6814
6921
|
}
|
|
6815
6922
|
return manifest;
|
|
6816
6923
|
}
|
|
@@ -6839,7 +6946,7 @@ async function injectRscManifestIntoHtml(staticDir, manifest) {
|
|
|
6839
6946
|
* @internal Exported for testing.
|
|
6840
6947
|
*/
|
|
6841
6948
|
async function readUserRedirects(root) {
|
|
6842
|
-
const candidates = [join(root, "public",
|
|
6949
|
+
const candidates = [join(root, "public", REDIRECTS_FILE)];
|
|
6843
6950
|
for (const path of candidates) try {
|
|
6844
6951
|
return await readFile(path, "utf-8");
|
|
6845
6952
|
} catch {}
|
|
@@ -6887,6 +6994,7 @@ function timberStaticBuild(ctx) {
|
|
|
6887
6994
|
async handler() {
|
|
6888
6995
|
if (ctx.dev) return;
|
|
6889
6996
|
if (!(ctx.config.output === "static")) return;
|
|
6997
|
+
warnUnusableBase(ctx);
|
|
6890
6998
|
await runStaticGeneration(ctx);
|
|
6891
6999
|
}
|
|
6892
7000
|
}
|
|
@@ -7040,12 +7148,10 @@ function walkModuleImports(moduleId, clientRefIndex, visited, result, getModuleI
|
|
|
7040
7148
|
* Hooks: configResolved, resolveId, load, generateBundle (client and RSC envs)
|
|
7041
7149
|
*/
|
|
7042
7150
|
function timberBuildManifest(ctx) {
|
|
7043
|
-
let resolvedBase = "/";
|
|
7044
7151
|
let isDev = false;
|
|
7045
7152
|
return {
|
|
7046
7153
|
name: "timber-build-manifest",
|
|
7047
7154
|
configResolved(config) {
|
|
7048
|
-
resolvedBase = config.base;
|
|
7049
7155
|
isDev = config.command === "serve";
|
|
7050
7156
|
if (!isDev && !ctx.deploymentId) ctx.deploymentId = randomUUID().replace(/-/g, "").slice(0, 16);
|
|
7051
7157
|
},
|
|
@@ -7057,19 +7163,22 @@ function timberBuildManifest(ctx) {
|
|
|
7057
7163
|
},
|
|
7058
7164
|
load(id) {
|
|
7059
7165
|
if (id !== RESOLVED_VIRTUAL_ID) return null;
|
|
7166
|
+
const empty = JSON.stringify(EMPTY_BUILD_MANIFEST);
|
|
7060
7167
|
if (isDev) return [
|
|
7061
7168
|
"// Auto-generated build manifest — do not edit.",
|
|
7062
7169
|
"// Dev mode: empty manifest (Vite HMR handles CSS/JS).",
|
|
7063
7170
|
"",
|
|
7064
|
-
|
|
7171
|
+
`const manifest = ${empty};`,
|
|
7065
7172
|
"",
|
|
7066
7173
|
"export default manifest;"
|
|
7067
7174
|
].join("\n");
|
|
7068
7175
|
return [
|
|
7069
7176
|
"// Auto-generated build manifest — do not edit.",
|
|
7070
7177
|
"// Production: reads manifest from globalThis (set by _timber-manifest-init.js).",
|
|
7178
|
+
"// The object below is only the fallback for when that global is unset —",
|
|
7179
|
+
"// the real manifest, including the requestDep map, is in that init module.",
|
|
7071
7180
|
"",
|
|
7072
|
-
|
|
7181
|
+
`const manifest = globalThis.__TIMBER_BUILD_MANIFEST__ ?? ${empty};`,
|
|
7073
7182
|
"",
|
|
7074
7183
|
"export default manifest;"
|
|
7075
7184
|
].join("\n");
|
|
@@ -7092,10 +7201,10 @@ function timberBuildManifest(ctx) {
|
|
|
7092
7201
|
}
|
|
7093
7202
|
}
|
|
7094
7203
|
if (envName === "client") {
|
|
7095
|
-
ctx.buildManifest = buildManifestFromBundle(bundle,
|
|
7204
|
+
ctx.buildManifest = buildManifestFromBundle(bundle, ctx.base, ctx.root);
|
|
7096
7205
|
if (ctx.routeTree) ctx.buildManifest.metadataRouteHashes = computeMetadataRouteHashes(ctx.routeTree.root);
|
|
7097
|
-
|
|
7098
|
-
|
|
7206
|
+
if (ctx.requestDepMap === void 0) throw new Error("[timber] Build manifest: the request-dependency map was never computed. The RSC environment's buildEnd hook did not run before the client build, so no segment or slot could be skipped on client navigation. This is a plugin pipeline bug — timber-request-dep must run in the rsc environment.");
|
|
7207
|
+
ctx.buildManifest.requestDep = ctx.requestDepMap;
|
|
7099
7208
|
if (ctx.clientJavascript.disabled) {
|
|
7100
7209
|
ctx.buildManifest.js = {};
|
|
7101
7210
|
ctx.buildManifest.modulepreload = {};
|
|
@@ -8190,7 +8299,319 @@ function timberCacheTransform(ctx) {
|
|
|
8190
8299
|
};
|
|
8191
8300
|
}
|
|
8192
8301
|
//#endregion
|
|
8193
|
-
//#region src/plugins/request-dep.ts
|
|
8302
|
+
//#region src/plugins/request-dep/analysis.ts
|
|
8303
|
+
/** `export default` has no local name of its own; this stands in for one. */
|
|
8304
|
+
var DEFAULT_EXPORT = "default";
|
|
8305
|
+
function createModuleFacts() {
|
|
8306
|
+
return {
|
|
8307
|
+
importedBindings: /* @__PURE__ */ new Map(),
|
|
8308
|
+
reExports: /* @__PURE__ */ new Map(),
|
|
8309
|
+
starReExports: [],
|
|
8310
|
+
localExports: /* @__PURE__ */ new Map(),
|
|
8311
|
+
factoryCalls: /* @__PURE__ */ new Map(),
|
|
8312
|
+
calledLocals: /* @__PURE__ */ new Set(),
|
|
8313
|
+
calledMembers: /* @__PURE__ */ new Map(),
|
|
8314
|
+
getterLocals: /* @__PURE__ */ new Set(),
|
|
8315
|
+
getterMembers: /* @__PURE__ */ new Map(),
|
|
8316
|
+
referencedLocals: /* @__PURE__ */ new Set(),
|
|
8317
|
+
resolvedSources: /* @__PURE__ */ new Map(),
|
|
8318
|
+
unresolvedSources: /* @__PURE__ */ new Set()
|
|
8319
|
+
};
|
|
8320
|
+
}
|
|
8321
|
+
/**
|
|
8322
|
+
* Specifiers the origin walk can actually reach, so the plugin resolves those
|
|
8323
|
+
* and not every import in the file.
|
|
8324
|
+
*
|
|
8325
|
+
* A re-export edge is always reachable — a downstream module can ask for any
|
|
8326
|
+
* name it forwards. An `import` edge is reachable through any local name the
|
|
8327
|
+
* module body mentions, or that it re-exports.
|
|
8328
|
+
*
|
|
8329
|
+
* The name set must be **the same one the taint rules consult**. Deriving it
|
|
8330
|
+
* from the call shapes instead (called, `.get()`d, member-called, minted from)
|
|
8331
|
+
* left `read(filters)` — a definition passed as an argument — with its
|
|
8332
|
+
* specifier unresolved, so the origin walk reported it inert and the page read
|
|
8333
|
+
* as request-independent. Two hand-maintained lists of "names that matter" is
|
|
8334
|
+
* one list too many; this is the one, and `referencedLocals` is a superset of
|
|
8335
|
+
* every call shape by construction, since each of them is an identifier the
|
|
8336
|
+
* walk visits. (Codex review round 1 on PR #991.)
|
|
8337
|
+
*/
|
|
8338
|
+
function usedSources(facts) {
|
|
8339
|
+
const sources = new Set(facts.starReExports);
|
|
8340
|
+
for (const edge of facts.reExports.values()) sources.add(edge.source);
|
|
8341
|
+
for (const local of facts.referencedLocals) {
|
|
8342
|
+
const edge = facts.importedBindings.get(local);
|
|
8343
|
+
if (edge) sources.add(edge.source);
|
|
8344
|
+
}
|
|
8345
|
+
for (const local of facts.localExports.values()) {
|
|
8346
|
+
const edge = facts.importedBindings.get(local);
|
|
8347
|
+
if (edge) sources.add(edge.source);
|
|
8348
|
+
}
|
|
8349
|
+
return sources;
|
|
8350
|
+
}
|
|
8351
|
+
/**
|
|
8352
|
+
* True when the module cannot affect any answer, so its facts need not be kept.
|
|
8353
|
+
*
|
|
8354
|
+
* Every judgement bottoms out at a module edge: a call site resolves through
|
|
8355
|
+
* `importedBindings`, a chain link through `reExports` / `starReExports`. A
|
|
8356
|
+
* module with none of those can only ever resolve to `none` — its exports lead
|
|
8357
|
+
* to locals that lead nowhere — which is exactly what a *missing* entry
|
|
8358
|
+
* produces. Dropping it is behaviour-preserving and keeps the dev-time map from
|
|
8359
|
+
* holding an entry for every leaf module in the graph.
|
|
8360
|
+
*/
|
|
8361
|
+
function contributesNothing(facts) {
|
|
8362
|
+
return facts.importedBindings.size === 0 && facts.reExports.size === 0 && facts.starReExports.length === 0;
|
|
8363
|
+
}
|
|
8364
|
+
function addToMapSet(map, key, value) {
|
|
8365
|
+
const existing = map.get(key);
|
|
8366
|
+
if (existing) existing.add(value);
|
|
8367
|
+
else map.set(key, /* @__PURE__ */ new Set([value]));
|
|
8368
|
+
}
|
|
8369
|
+
function nameOf(node) {
|
|
8370
|
+
if (!node) return null;
|
|
8371
|
+
if (node.type === "Identifier") return node.name;
|
|
8372
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
8373
|
+
return null;
|
|
8374
|
+
}
|
|
8375
|
+
/**
|
|
8376
|
+
* `import type { … }` needs no handling here. Vite's TypeScript transform is a
|
|
8377
|
+
* core plugin and runs before this one, so types are already erased by the time
|
|
8378
|
+
* the analysis parses — and the parser it runs on is JavaScript-only, so
|
|
8379
|
+
* unerased TypeScript would throw rather than be silently misread.
|
|
8380
|
+
* `tests/request-dep-plugin.test.ts` pins the resulting property through the
|
|
8381
|
+
* real pipeline: a type-only accessor import is not request-dependent.
|
|
8382
|
+
*/
|
|
8383
|
+
function collectImportDeclaration(stmt, facts) {
|
|
8384
|
+
const source = stmt.source?.value;
|
|
8385
|
+
if (typeof source !== "string") return;
|
|
8386
|
+
for (const spec of stmt.specifiers ?? []) {
|
|
8387
|
+
const local = nameOf(spec.local);
|
|
8388
|
+
if (!local) continue;
|
|
8389
|
+
if (spec.type === "ImportNamespaceSpecifier") facts.importedBindings.set(local, {
|
|
8390
|
+
source,
|
|
8391
|
+
imported: "*"
|
|
8392
|
+
});
|
|
8393
|
+
else if (spec.type === "ImportDefaultSpecifier") facts.importedBindings.set(local, {
|
|
8394
|
+
source,
|
|
8395
|
+
imported: DEFAULT_EXPORT
|
|
8396
|
+
});
|
|
8397
|
+
else if (spec.type === "ImportSpecifier") facts.importedBindings.set(local, {
|
|
8398
|
+
source,
|
|
8399
|
+
imported: nameOf(spec.imported) ?? local
|
|
8400
|
+
});
|
|
8401
|
+
}
|
|
8402
|
+
}
|
|
8403
|
+
function collectDeclaredNames(decl, out) {
|
|
8404
|
+
if (decl.type === "VariableDeclaration") for (const d of decl.declarations ?? []) {
|
|
8405
|
+
const id = d.id;
|
|
8406
|
+
if (id?.type === "Identifier") out.add(id.name);
|
|
8407
|
+
}
|
|
8408
|
+
else if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") {
|
|
8409
|
+
const id = decl.id;
|
|
8410
|
+
if (id?.type === "Identifier") out.add(id.name);
|
|
8411
|
+
}
|
|
8412
|
+
}
|
|
8413
|
+
function collectExportNamed(stmt, facts) {
|
|
8414
|
+
const source = stmt.source?.value;
|
|
8415
|
+
const specifiers = stmt.specifiers ?? [];
|
|
8416
|
+
if (typeof source === "string") {
|
|
8417
|
+
for (const spec of specifiers) {
|
|
8418
|
+
const imported = nameOf(spec.imported) ?? nameOf(spec.local);
|
|
8419
|
+
const exported = nameOf(spec.exported);
|
|
8420
|
+
if (imported && exported) facts.reExports.set(exported, {
|
|
8421
|
+
source,
|
|
8422
|
+
imported
|
|
8423
|
+
});
|
|
8424
|
+
}
|
|
8425
|
+
return;
|
|
8426
|
+
}
|
|
8427
|
+
for (const spec of specifiers) {
|
|
8428
|
+
const local = nameOf(spec.local);
|
|
8429
|
+
const exported = nameOf(spec.exported);
|
|
8430
|
+
if (local && exported) facts.localExports.set(exported, local);
|
|
8431
|
+
}
|
|
8432
|
+
const declaration = stmt.declaration;
|
|
8433
|
+
if (declaration) {
|
|
8434
|
+
const names = /* @__PURE__ */ new Set();
|
|
8435
|
+
collectDeclaredNames(declaration, names);
|
|
8436
|
+
for (const name of names) facts.localExports.set(name, name);
|
|
8437
|
+
}
|
|
8438
|
+
}
|
|
8439
|
+
function collectExportAll(stmt, facts) {
|
|
8440
|
+
const source = stmt.source?.value;
|
|
8441
|
+
if (typeof source !== "string") return;
|
|
8442
|
+
const exported = nameOf(stmt.exported);
|
|
8443
|
+
if (exported) facts.reExports.set(exported, {
|
|
8444
|
+
source,
|
|
8445
|
+
imported: "*"
|
|
8446
|
+
});
|
|
8447
|
+
else facts.starReExports.push(source);
|
|
8448
|
+
}
|
|
8449
|
+
function collectExportDefault(stmt, facts) {
|
|
8450
|
+
const declaration = stmt.declaration;
|
|
8451
|
+
if (!declaration) return;
|
|
8452
|
+
facts.localExports.set(DEFAULT_EXPORT, DEFAULT_EXPORT);
|
|
8453
|
+
if (declaration.type === "Identifier") facts.localExports.set(DEFAULT_EXPORT, declaration.name);
|
|
8454
|
+
else if (declaration.type === "CallExpression") {
|
|
8455
|
+
const ref = calleeRef(declaration);
|
|
8456
|
+
if (ref) facts.factoryCalls.set(DEFAULT_EXPORT, ref);
|
|
8457
|
+
}
|
|
8458
|
+
}
|
|
8459
|
+
function calleeRef(call) {
|
|
8460
|
+
const callee = call.callee;
|
|
8461
|
+
if (callee.type === "Identifier") return {
|
|
8462
|
+
kind: "local",
|
|
8463
|
+
name: callee.name
|
|
8464
|
+
};
|
|
8465
|
+
if (callee.type === "MemberExpression") {
|
|
8466
|
+
const member = callee;
|
|
8467
|
+
if (!member.computed && member.object.type === "Identifier" && member.property.type === "Identifier") return {
|
|
8468
|
+
kind: "member",
|
|
8469
|
+
object: member.object.name,
|
|
8470
|
+
property: member.property.name
|
|
8471
|
+
};
|
|
8472
|
+
}
|
|
8473
|
+
return null;
|
|
8474
|
+
}
|
|
8475
|
+
/**
|
|
8476
|
+
* Walk down a `.get()` receiver to the identifier it is ultimately rooted in,
|
|
8477
|
+
* along with the first property accessed on that identifier.
|
|
8478
|
+
*
|
|
8479
|
+
* `x.get()` → `x`, no property. `x(a).get()` → `x`. `ns.defs.get()` → `ns`
|
|
8480
|
+
* plus `defs`, which is both the namespace-member shape and the
|
|
8481
|
+
* `cookies.theme.get()` shape — the resolver tries the base binding and the
|
|
8482
|
+
* member, so one walk covers both.
|
|
8483
|
+
*/
|
|
8484
|
+
function chainRoot(node) {
|
|
8485
|
+
let current = node;
|
|
8486
|
+
let property = null;
|
|
8487
|
+
for (;;) {
|
|
8488
|
+
if (current.type === "Identifier") return {
|
|
8489
|
+
base: current.name,
|
|
8490
|
+
property
|
|
8491
|
+
};
|
|
8492
|
+
if (current.type === "MemberExpression") {
|
|
8493
|
+
const member = current;
|
|
8494
|
+
if (!member.computed && member.property.type === "Identifier") property = member.property.name;
|
|
8495
|
+
else property = null;
|
|
8496
|
+
current = member.object;
|
|
8497
|
+
continue;
|
|
8498
|
+
}
|
|
8499
|
+
if (current.type === "CallExpression") {
|
|
8500
|
+
property = null;
|
|
8501
|
+
current = current.callee;
|
|
8502
|
+
continue;
|
|
8503
|
+
}
|
|
8504
|
+
return null;
|
|
8505
|
+
}
|
|
8506
|
+
}
|
|
8507
|
+
function collectExpression(node, facts) {
|
|
8508
|
+
if (node.type === "VariableDeclarator") {
|
|
8509
|
+
const id = node.id;
|
|
8510
|
+
const init = node.init;
|
|
8511
|
+
if (id?.type === "Identifier" && init?.type === "CallExpression") {
|
|
8512
|
+
const ref = calleeRef(init);
|
|
8513
|
+
if (ref) facts.factoryCalls.set(id.name, ref);
|
|
8514
|
+
}
|
|
8515
|
+
return;
|
|
8516
|
+
}
|
|
8517
|
+
if (node.type !== "CallExpression") return;
|
|
8518
|
+
const callee = node.callee;
|
|
8519
|
+
if (callee.type === "Identifier") {
|
|
8520
|
+
facts.calledLocals.add(callee.name);
|
|
8521
|
+
return;
|
|
8522
|
+
}
|
|
8523
|
+
if (callee.type !== "MemberExpression") return;
|
|
8524
|
+
const member = callee;
|
|
8525
|
+
if (member.computed || member.property.type !== "Identifier") return;
|
|
8526
|
+
const propertyName = member.property.name;
|
|
8527
|
+
if (propertyName === "get") {
|
|
8528
|
+
const root = chainRoot(member.object);
|
|
8529
|
+
if (root) {
|
|
8530
|
+
facts.getterLocals.add(root.base);
|
|
8531
|
+
if (root.property) addToMapSet(facts.getterMembers, root.base, root.property);
|
|
8532
|
+
}
|
|
8533
|
+
return;
|
|
8534
|
+
}
|
|
8535
|
+
if (member.object.type === "Identifier") addToMapSet(facts.calledMembers, member.object.name, propertyName);
|
|
8536
|
+
}
|
|
8537
|
+
/**
|
|
8538
|
+
* Statements whose whole subtree is an import/export *clause* rather than code.
|
|
8539
|
+
*
|
|
8540
|
+
* The walk skips them so `referencedLocals` means "the module body mentions
|
|
8541
|
+
* this name". Without the skip every imported name would count as referenced,
|
|
8542
|
+
* and a pure barrel — which mentions its names only in the clauses — would be
|
|
8543
|
+
* indistinguishable from a module that uses them.
|
|
8544
|
+
*/
|
|
8545
|
+
function isClauseOnly(node) {
|
|
8546
|
+
if (node.type === "ImportDeclaration" || node.type === "ExportAllDeclaration") return true;
|
|
8547
|
+
if (node.type === "ExportNamedDeclaration") return !node.declaration;
|
|
8548
|
+
if (node.type === "ExportDefaultDeclaration") return node.declaration?.type === "Identifier";
|
|
8549
|
+
return false;
|
|
8550
|
+
}
|
|
8551
|
+
/**
|
|
8552
|
+
* Keys holding *binding* positions rather than references: the name being
|
|
8553
|
+
* declared, not a mention of one.
|
|
8554
|
+
*
|
|
8555
|
+
* `referencedLocals` must exclude these or `export const filters =
|
|
8556
|
+
* defineSearchParams({})` would count as referencing `filters`, and possession
|
|
8557
|
+
* would then taint every definition module — and through it every page that
|
|
8558
|
+
* imports one, whether or not it reads the request. Skipping only these keys
|
|
8559
|
+
* keeps a shadowing parameter's *body* uses as references, which over-reports
|
|
8560
|
+
* in the safe direction.
|
|
8561
|
+
*/
|
|
8562
|
+
var BINDING_KEYS = {
|
|
8563
|
+
VariableDeclarator: ["id"],
|
|
8564
|
+
FunctionDeclaration: ["id", "params"],
|
|
8565
|
+
FunctionExpression: ["id", "params"],
|
|
8566
|
+
ArrowFunctionExpression: ["params"],
|
|
8567
|
+
ClassDeclaration: ["id"],
|
|
8568
|
+
ClassExpression: ["id"],
|
|
8569
|
+
CatchClause: ["param"]
|
|
8570
|
+
};
|
|
8571
|
+
function walk(node, facts) {
|
|
8572
|
+
if (isClauseOnly(node)) return;
|
|
8573
|
+
if (node.type === "Identifier") facts.referencedLocals.add(node.name);
|
|
8574
|
+
collectExpression(node, facts);
|
|
8575
|
+
const bindingKeys = BINDING_KEYS[node.type];
|
|
8576
|
+
for (const key of Object.keys(node)) {
|
|
8577
|
+
if (key === "type" || key === "start" || key === "end") continue;
|
|
8578
|
+
if (bindingKeys?.includes(key)) continue;
|
|
8579
|
+
const value = node[key];
|
|
8580
|
+
if (!value || typeof value !== "object") continue;
|
|
8581
|
+
if (Array.isArray(value)) {
|
|
8582
|
+
for (const item of value) if (item && typeof item === "object" && typeof item.type === "string") walk(item, facts);
|
|
8583
|
+
} else if (typeof value.type === "string") walk(value, facts);
|
|
8584
|
+
}
|
|
8585
|
+
}
|
|
8586
|
+
/**
|
|
8587
|
+
* Record every binding, module edge, and call shape in one module.
|
|
8588
|
+
*
|
|
8589
|
+
* Names are recorded without scope analysis, so a parameter that shadows an
|
|
8590
|
+
* imported name is credited to the import. That direction is deliberate: a
|
|
8591
|
+
* needlessly request-dependent segment costs a re-render, a wrongly static one
|
|
8592
|
+
* serves stale content.
|
|
8593
|
+
*/
|
|
8594
|
+
function analyzeModule(program) {
|
|
8595
|
+
const facts = createModuleFacts();
|
|
8596
|
+
for (const stmt of program.body) switch (stmt.type) {
|
|
8597
|
+
case "ImportDeclaration":
|
|
8598
|
+
collectImportDeclaration(stmt, facts);
|
|
8599
|
+
break;
|
|
8600
|
+
case "ExportNamedDeclaration":
|
|
8601
|
+
collectExportNamed(stmt, facts);
|
|
8602
|
+
break;
|
|
8603
|
+
case "ExportAllDeclaration":
|
|
8604
|
+
collectExportAll(stmt, facts);
|
|
8605
|
+
break;
|
|
8606
|
+
case "ExportDefaultDeclaration":
|
|
8607
|
+
collectExportDefault(stmt, facts);
|
|
8608
|
+
break;
|
|
8609
|
+
}
|
|
8610
|
+
walk(program, facts);
|
|
8611
|
+
return facts;
|
|
8612
|
+
}
|
|
8613
|
+
//#endregion
|
|
8614
|
+
//#region src/plugins/request-dep/origin.ts
|
|
8194
8615
|
var ACCESSOR_SOURCES = /* @__PURE__ */ new Set([
|
|
8195
8616
|
"@timber-js/app/server",
|
|
8196
8617
|
"next/headers",
|
|
@@ -8207,165 +8628,326 @@ var ACCESSOR_NAMES = /* @__PURE__ */ new Set([
|
|
|
8207
8628
|
]);
|
|
8208
8629
|
var DEFINITION_FACTORY_SOURCES = /* @__PURE__ */ new Set(["@timber-js/app/search-params", "@timber-js/app/cookies"]);
|
|
8209
8630
|
var DEFINITION_FACTORY_NAMES = /* @__PURE__ */ new Set(["defineSearchParams", "defineCookie"]);
|
|
8210
|
-
|
|
8211
|
-
|
|
8631
|
+
/** Every specifier the classifier recognises without resolving it. */
|
|
8632
|
+
var FRAMEWORK_SOURCES = /* @__PURE__ */ new Set([...ACCESSOR_SOURCES, ...DEFINITION_FACTORY_SOURCES]);
|
|
8633
|
+
var NONE = { kind: "none" };
|
|
8634
|
+
var UNKNOWN = { kind: "unknown" };
|
|
8635
|
+
/**
|
|
8636
|
+
* Classify an edge that points at a framework module, without resolving it.
|
|
8637
|
+
*
|
|
8638
|
+
* Returns `null` when the specifier is not a framework source, which is the
|
|
8639
|
+
* signal to keep walking. Both name sets are consulted for either source so a
|
|
8640
|
+
* specifier that grows a second role does not silently stop matching.
|
|
8641
|
+
*/
|
|
8642
|
+
function classifySpecifier(source, imported) {
|
|
8643
|
+
const isAccessorSource = ACCESSOR_SOURCES.has(source);
|
|
8644
|
+
const isFactorySource = DEFINITION_FACTORY_SOURCES.has(source);
|
|
8645
|
+
if (!isAccessorSource && !isFactorySource) return null;
|
|
8646
|
+
if (imported === "*") return {
|
|
8647
|
+
kind: "framework-namespace",
|
|
8648
|
+
source
|
|
8649
|
+
};
|
|
8650
|
+
if (isAccessorSource && ACCESSOR_NAMES.has(imported)) return { kind: "accessor" };
|
|
8651
|
+
if (isFactorySource && DEFINITION_FACTORY_NAMES.has(imported)) return { kind: "factory" };
|
|
8652
|
+
return NONE;
|
|
8212
8653
|
}
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
"
|
|
8216
|
-
"getSearchParams",
|
|
8217
|
-
"getCookieJar",
|
|
8218
|
-
"getCookie",
|
|
8219
|
-
"headers",
|
|
8220
|
-
"cookies",
|
|
8221
|
-
"defineSearchParams",
|
|
8222
|
-
"defineCookie",
|
|
8223
|
-
".get("
|
|
8224
|
-
];
|
|
8225
|
-
function hasRelevantToken(code) {
|
|
8226
|
-
for (const token of ACCESSOR_TOKENS) if (code.includes(token)) return true;
|
|
8227
|
-
return false;
|
|
8654
|
+
/** An origin whose *call* reads the request. */
|
|
8655
|
+
function callReadsRequest(origin) {
|
|
8656
|
+
return origin.kind === "accessor" || origin.kind === "unknown";
|
|
8228
8657
|
}
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8658
|
+
/** An origin whose `.get()` reads the request. */
|
|
8659
|
+
function getterReadsRequest(origin) {
|
|
8660
|
+
return origin.kind === "definition" || origin.kind === "unknown";
|
|
8661
|
+
}
|
|
8662
|
+
function createOriginResolver(factsMap) {
|
|
8663
|
+
/**
|
|
8664
|
+
* Cycle guard. A `(module, export)` pair already being resolved contributes
|
|
8665
|
+
* `none`: anything it could reach is reachable on the path still unwinding.
|
|
8666
|
+
*/
|
|
8667
|
+
const resolving = /* @__PURE__ */ new Map();
|
|
8668
|
+
function enter(moduleId, name) {
|
|
8669
|
+
const names = resolving.get(moduleId);
|
|
8670
|
+
if (!names) {
|
|
8671
|
+
resolving.set(moduleId, /* @__PURE__ */ new Set([name]));
|
|
8672
|
+
return true;
|
|
8239
8673
|
}
|
|
8674
|
+
if (names.has(name)) return false;
|
|
8675
|
+
names.add(name);
|
|
8676
|
+
return true;
|
|
8240
8677
|
}
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8678
|
+
function leave(moduleId, name) {
|
|
8679
|
+
resolving.get(moduleId)?.delete(name);
|
|
8680
|
+
}
|
|
8681
|
+
/** Follow one `import`/`export … from` edge out of `moduleId`. */
|
|
8682
|
+
function resolveEdge(moduleId, edge) {
|
|
8683
|
+
const classified = classifySpecifier(edge.source, edge.imported);
|
|
8684
|
+
if (classified) return classified;
|
|
8685
|
+
const facts = factsMap.get(moduleId);
|
|
8686
|
+
const targetId = facts?.resolvedSources.get(edge.source);
|
|
8687
|
+
if (!targetId) return facts?.unresolvedSources.has(edge.source) ? UNKNOWN : NONE;
|
|
8688
|
+
if (edge.imported === "*") return {
|
|
8689
|
+
kind: "namespace",
|
|
8690
|
+
moduleId: targetId
|
|
8691
|
+
};
|
|
8692
|
+
return resolveExport(targetId, edge.imported);
|
|
8244
8693
|
}
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
const
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
if (
|
|
8261
|
-
const calleeName = callee.name;
|
|
8262
|
-
for (const factoryName of DEFINITION_FACTORY_NAMES) if (findImportBindings(program, DEFINITION_FACTORY_SOURCES, factoryName).named.has(calleeName)) {
|
|
8263
|
-
const localName = decl.id.type === "Identifier" ? decl.id.name : null;
|
|
8264
|
-
if (localName && hasGetterCall(program, localName)) facts.callsAccessor = true;
|
|
8265
|
-
}
|
|
8266
|
-
}
|
|
8694
|
+
/** What does `moduleId` export under `name`? */
|
|
8695
|
+
function resolveExport(moduleId, name) {
|
|
8696
|
+
const facts = factsMap.get(moduleId);
|
|
8697
|
+
if (!facts) return NONE;
|
|
8698
|
+
if (!enter(moduleId, `export:${name}`)) return NONE;
|
|
8699
|
+
try {
|
|
8700
|
+
const reExport = facts.reExports.get(name);
|
|
8701
|
+
if (reExport) return resolveEdge(moduleId, reExport);
|
|
8702
|
+
const local = facts.localExports.get(name);
|
|
8703
|
+
if (local !== void 0) return resolveLocal(moduleId, local);
|
|
8704
|
+
for (const source of facts.starReExports) {
|
|
8705
|
+
const viaStar = resolveEdge(moduleId, {
|
|
8706
|
+
source,
|
|
8707
|
+
imported: name
|
|
8708
|
+
});
|
|
8709
|
+
if (viaStar.kind !== "none") return viaStar;
|
|
8267
8710
|
}
|
|
8711
|
+
return NONE;
|
|
8712
|
+
} finally {
|
|
8713
|
+
leave(moduleId, `export:${name}`);
|
|
8268
8714
|
}
|
|
8269
8715
|
}
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
}
|
|
8284
|
-
}
|
|
8285
|
-
for (const key of Object.keys(node)) {
|
|
8286
|
-
if (key === "type" || key === "start" || key === "end") continue;
|
|
8287
|
-
const val = node[key];
|
|
8288
|
-
if (val && typeof val === "object") {
|
|
8289
|
-
if (Array.isArray(val)) {
|
|
8290
|
-
for (const item of val) if (item && typeof item === "object" && typeof item.type === "string") {
|
|
8291
|
-
if (walkForCall(item, named, namespaces, importedName)) return true;
|
|
8292
|
-
}
|
|
8293
|
-
} else if (typeof val.type === "string") {
|
|
8294
|
-
if (walkForCall(val, named, namespaces, importedName)) return true;
|
|
8716
|
+
/** What is the local binding `name` in `moduleId`? */
|
|
8717
|
+
function resolveLocal(moduleId, name) {
|
|
8718
|
+
const facts = factsMap.get(moduleId);
|
|
8719
|
+
if (!facts) return NONE;
|
|
8720
|
+
if (!enter(moduleId, `local:${name}`)) return NONE;
|
|
8721
|
+
try {
|
|
8722
|
+
const imported = facts.importedBindings.get(name);
|
|
8723
|
+
if (imported) return resolveEdge(moduleId, imported);
|
|
8724
|
+
const factoryCall = facts.factoryCalls.get(name);
|
|
8725
|
+
if (factoryCall) {
|
|
8726
|
+
const callee = resolveCallee(moduleId, factoryCall);
|
|
8727
|
+
if (callee.kind === "factory") return { kind: "definition" };
|
|
8728
|
+
if (callee.kind === "unknown") return UNKNOWN;
|
|
8295
8729
|
}
|
|
8730
|
+
return NONE;
|
|
8731
|
+
} finally {
|
|
8732
|
+
leave(moduleId, `local:${name}`);
|
|
8296
8733
|
}
|
|
8297
8734
|
}
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8735
|
+
function resolveCallee(moduleId, ref) {
|
|
8736
|
+
if (ref.kind === "local") return resolveLocal(moduleId, ref.name);
|
|
8737
|
+
return resolveMember(moduleId, ref.object, ref.property);
|
|
8738
|
+
}
|
|
8739
|
+
/** `ns.name` where `ns` is a local binding in `moduleId`. */
|
|
8740
|
+
function resolveMember(moduleId, object, name) {
|
|
8741
|
+
const base = resolveLocal(moduleId, object);
|
|
8742
|
+
if (base.kind === "framework-namespace") return classifySpecifier(base.source, name) ?? NONE;
|
|
8743
|
+
if (base.kind === "namespace") return resolveExport(base.moduleId, name);
|
|
8744
|
+
if (base.kind === "unknown") return UNKNOWN;
|
|
8745
|
+
return NONE;
|
|
8746
|
+
}
|
|
8747
|
+
const taintCache = /* @__PURE__ */ new Map();
|
|
8748
|
+
function computeTainted(moduleId, facts) {
|
|
8749
|
+
for (const local of facts.calledLocals) if (callReadsRequest(resolveLocal(moduleId, local))) return true;
|
|
8750
|
+
for (const [object, members] of facts.calledMembers) for (const member of members) if (callReadsRequest(resolveMember(moduleId, object, member))) return true;
|
|
8751
|
+
for (const local of facts.getterLocals) if (getterReadsRequest(resolveLocal(moduleId, local))) return true;
|
|
8752
|
+
for (const [object, members] of facts.getterMembers) for (const member of members) if (getterReadsRequest(resolveMember(moduleId, object, member))) return true;
|
|
8753
|
+
return holdsRequestBinding(moduleId, facts);
|
|
8754
|
+
}
|
|
8755
|
+
/**
|
|
8756
|
+
* Possession, not use: a module that binds a request accessor or a definition
|
|
8757
|
+
* value under a local name is request-dependent whether or not the call is
|
|
8758
|
+
* one this analysis can see.
|
|
8759
|
+
*
|
|
8760
|
+
* The call-shape rules above only find a read that happens through the
|
|
8761
|
+
* binding itself. A value can leave the binding first — `const read = flag ?
|
|
8762
|
+
* getSearchParams : noop`, `const { get } = filters`, a prop handed to a
|
|
8763
|
+
* helper — and every one of those is a fresh way to be missed, which is the
|
|
8764
|
+
* shape of bug this ticket exists to stop enumerating. Nothing distinguishes
|
|
8765
|
+
* those from an honest unused import except intent, so importing the accessor
|
|
8766
|
+
* at all is treated as reading it.
|
|
8767
|
+
*
|
|
8768
|
+
* The rule is scoped by *reference*, not by export shape, and that scoping is
|
|
8769
|
+
* what keeps it from being useless: `import { getSearchParams } from
|
|
8770
|
+
* '@timber-js/app/server'; export { getSearchParams }` is a barrel whose body
|
|
8771
|
+
* never mentions the name, and tainting it would taint every page that
|
|
8772
|
+
* imports anything from that barrel. The forwarded name stays resolvable
|
|
8773
|
+
* through `resolveExport`, so the page that actually calls it is still caught
|
|
8774
|
+
* one hop later, in the module that uses it.
|
|
8775
|
+
*
|
|
8776
|
+
* Scoping on "the module only forwards it" would have been the natural
|
|
8777
|
+
* phrasing and is wrong: a barrel can forward a name *and* use it
|
|
8778
|
+
* (`export { getSearchParams }` beside `const read = flag ? getSearchParams
|
|
8779
|
+
* : noop`), and that module is exactly the one this rule exists to catch.
|
|
8780
|
+
* `referencedLocals` excludes import/export clauses and nothing else, so
|
|
8781
|
+
* forwarding is exempt only when it is all the module does with the name.
|
|
8782
|
+
*
|
|
8783
|
+
* `unknown` is deliberately not included: an unresolvable specifier taints
|
|
8784
|
+
* only when something is actually called or read on it, or every module with
|
|
8785
|
+
* one broken import would go request-dependent.
|
|
8786
|
+
*/
|
|
8787
|
+
function holdsRequestBinding(moduleId, facts) {
|
|
8788
|
+
for (const local of facts.referencedLocals) {
|
|
8789
|
+
const origin = resolveLocal(moduleId, local);
|
|
8790
|
+
if (origin.kind === "accessor" || origin.kind === "definition") return true;
|
|
8323
8791
|
}
|
|
8792
|
+
return false;
|
|
8324
8793
|
}
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8794
|
+
return {
|
|
8795
|
+
isTainted(moduleId) {
|
|
8796
|
+
const cached = taintCache.get(moduleId);
|
|
8797
|
+
if (cached !== void 0) return cached;
|
|
8798
|
+
const facts = factsMap.get(moduleId);
|
|
8799
|
+
const result = facts ? computeTainted(moduleId, facts) : false;
|
|
8800
|
+
taintCache.set(moduleId, result);
|
|
8801
|
+
return result;
|
|
8802
|
+
},
|
|
8803
|
+
isDefinitionModule(moduleId) {
|
|
8804
|
+
const facts = factsMap.get(moduleId);
|
|
8805
|
+
if (!facts) return false;
|
|
8806
|
+
for (const edge of facts.importedBindings.values()) {
|
|
8807
|
+
const origin = resolveEdge(moduleId, edge);
|
|
8808
|
+
if (origin.kind === "factory") return true;
|
|
8809
|
+
if (origin.kind === "framework-namespace" && DEFINITION_FACTORY_SOURCES.has(origin.source)) return true;
|
|
8335
8810
|
}
|
|
8811
|
+
return false;
|
|
8812
|
+
},
|
|
8813
|
+
callsGetterOnImport(moduleId) {
|
|
8814
|
+
const facts = factsMap.get(moduleId);
|
|
8815
|
+
if (!facts) return false;
|
|
8816
|
+
for (const local of facts.getterLocals) if (facts.importedBindings.has(local)) return true;
|
|
8817
|
+
return false;
|
|
8336
8818
|
}
|
|
8337
|
-
}
|
|
8338
|
-
|
|
8819
|
+
};
|
|
8820
|
+
}
|
|
8821
|
+
//#endregion
|
|
8822
|
+
//#region src/plugins/request-dep/index.ts
|
|
8823
|
+
function isFrameworkModule(id) {
|
|
8824
|
+
return /[\\/]node_modules[\\/]@timber-js[\\/]app[\\/]/.test(id) || /[\\/]packages[\\/]timber-app[\\/]src[\\/]/.test(id);
|
|
8339
8825
|
}
|
|
8340
|
-
/**
|
|
8341
|
-
|
|
8826
|
+
/**
|
|
8827
|
+
* Cheap gate before parsing.
|
|
8828
|
+
*
|
|
8829
|
+
* This used to be a substring match on the accessor and factory names, which
|
|
8830
|
+
* was sound only for the analysis it served: when a call site had to name the
|
|
8831
|
+
* framework binding in its own import declaration, a module with no such name
|
|
8832
|
+
* could not be a call site. Resolving bindings across the graph removes that
|
|
8833
|
+
* guarantee — with
|
|
8834
|
+
*
|
|
8835
|
+
* // _lib/index.ts
|
|
8836
|
+
* export { getSearchParams as query } from '@timber-js/app/server';
|
|
8837
|
+
* // page.tsx
|
|
8838
|
+
* import { query } from './_lib';
|
|
8839
|
+
* export default function Page() { return query(); }
|
|
8840
|
+
*
|
|
8841
|
+
* the page contains no accessor name, no `.get(`, and no re-export syntax, and
|
|
8842
|
+
* skipping it would have put the rename right back where the barrel was. So the
|
|
8843
|
+
* gate is now structural: a module matters if it has any module-level binding
|
|
8844
|
+
* to follow. What it skips is source with no `import` and no `export` at all —
|
|
8845
|
+
* data blobs, generated constants, side-effect scripts — none of which can name
|
|
8846
|
+
* a framework binding or forward one.
|
|
8847
|
+
*/
|
|
8848
|
+
function hasModuleBindings(code) {
|
|
8849
|
+
return code.includes("import") || code.includes("export");
|
|
8850
|
+
}
|
|
8851
|
+
/**
|
|
8852
|
+
* Walk the module graph from an entry, returning true if any module reachable
|
|
8853
|
+
* from it reads the request.
|
|
8854
|
+
*
|
|
8855
|
+
* Two passes, and the second is a deliberately blunt backstop:
|
|
8856
|
+
*
|
|
8857
|
+
* 1. **Binding origins.** For each reachable module, resolve every call site's
|
|
8858
|
+
* binding to its origin. This is precise and indifferent to indirection.
|
|
8859
|
+
* 2. **Definition reachability.** A module that calls `.get()` on *something*
|
|
8860
|
+
* it imported, with a module holding a definition factory reachable behind
|
|
8861
|
+
* that import, counts too. The origin walk cannot see through a value that
|
|
8862
|
+
* is computed rather than bound — `getFilterCookie(key).get()`, where the
|
|
8863
|
+
* helper returns a definition — and this pass can.
|
|
8864
|
+
*
|
|
8865
|
+
* The second pass over-reports by construction, which is the safe direction: a
|
|
8866
|
+
* needlessly request-dependent segment costs a re-render, a wrongly static one
|
|
8867
|
+
* serves stale content.
|
|
8868
|
+
*
|
|
8869
|
+
* An entry the graph does not contain is an edge we failed to walk, not the
|
|
8870
|
+
* absence of one — the same rule an unresolvable specifier gets. Without the
|
|
8871
|
+
* check it reads as an entry with no imports and no facts, and both passes fall
|
|
8872
|
+
* through to `false`: confidently request-INdependent, for a module nothing
|
|
8873
|
+
* ever looked at. In the build that answer is worse than no answer, because it
|
|
8874
|
+
* is written into `requestDep` as a definite `false` and the runtime's own
|
|
8875
|
+
* "absent key → conservative" fallback never fires.
|
|
8876
|
+
*/
|
|
8877
|
+
function isReachableTaintSource(entryId, input) {
|
|
8878
|
+
const { factsMap, getImports, isKnownModule } = input;
|
|
8879
|
+
if (!isKnownModule(entryId)) return true;
|
|
8880
|
+
const resolver = createOriginResolver(factsMap);
|
|
8342
8881
|
const visited = /* @__PURE__ */ new Set();
|
|
8343
8882
|
const stack = [entryId];
|
|
8344
8883
|
while (stack.length > 0) {
|
|
8345
8884
|
const id = stack.pop();
|
|
8346
8885
|
if (visited.has(id)) continue;
|
|
8347
8886
|
visited.add(id);
|
|
8348
|
-
|
|
8349
|
-
if (
|
|
8350
|
-
if (facts.callsAccessor) return true;
|
|
8351
|
-
for (const source of facts.getterCallSources);
|
|
8352
|
-
}
|
|
8353
|
-
const imports = getImports(id);
|
|
8354
|
-
for (const imp of imports) if (!imp.startsWith("\0") && !visited.has(imp)) stack.push(imp);
|
|
8887
|
+
if (resolver.isTainted(id)) return true;
|
|
8888
|
+
for (const imp of getImports(id)) if (!imp.startsWith("\0") && !visited.has(imp)) stack.push(imp);
|
|
8355
8889
|
}
|
|
8890
|
+
const reachesDefinition = makeDefinitionReachability(resolver.isDefinitionModule, getImports);
|
|
8356
8891
|
for (const id of visited) {
|
|
8357
|
-
|
|
8358
|
-
if (
|
|
8359
|
-
if (facts.getterCallSources.length === 0) continue;
|
|
8360
|
-
const imports = getImports(id);
|
|
8361
|
-
for (const imp of imports) if (definitionModules.has(imp)) return true;
|
|
8892
|
+
if (!resolver.callsGetterOnImport(id)) continue;
|
|
8893
|
+
if (reachesDefinition(id)) return true;
|
|
8362
8894
|
}
|
|
8363
8895
|
return false;
|
|
8364
8896
|
}
|
|
8897
|
+
/**
|
|
8898
|
+
* "Can a definition module be reached from this module's imports?", memoized
|
|
8899
|
+
* across the queries one entry makes.
|
|
8900
|
+
*
|
|
8901
|
+
* Walks the graph rather than testing direct imports, because `export … from`
|
|
8902
|
+
* is an ordinary module edge and a barrel is the common way an app shares one
|
|
8903
|
+
* `defineSearchParams` across routes.
|
|
8904
|
+
*
|
|
8905
|
+
* Cycles terminate on the memo seeded with `false`; a module already being
|
|
8906
|
+
* resolved contributes nothing, which is correct — anything it could reach is
|
|
8907
|
+
* reachable on the path that is still unwinding.
|
|
8908
|
+
*/
|
|
8909
|
+
function makeDefinitionReachability(isDefinitionModule, getImports) {
|
|
8910
|
+
const cache = /* @__PURE__ */ new Map();
|
|
8911
|
+
return function reaches(id) {
|
|
8912
|
+
const cached = cache.get(id);
|
|
8913
|
+
if (cached !== void 0) return cached;
|
|
8914
|
+
cache.set(id, false);
|
|
8915
|
+
let result = false;
|
|
8916
|
+
for (const imp of getImports(id)) {
|
|
8917
|
+
if (imp.startsWith("\0")) continue;
|
|
8918
|
+
if (isDefinitionModule(imp) || reaches(imp)) {
|
|
8919
|
+
result = true;
|
|
8920
|
+
break;
|
|
8921
|
+
}
|
|
8922
|
+
}
|
|
8923
|
+
cache.set(id, result);
|
|
8924
|
+
return result;
|
|
8925
|
+
};
|
|
8926
|
+
}
|
|
8365
8927
|
var BRIDGE_KEY = Symbol.for("timber:request-dep");
|
|
8928
|
+
/**
|
|
8929
|
+
* Resolve every specifier the module has an edge to, so the origin walk can
|
|
8930
|
+
* follow them later without a module graph.
|
|
8931
|
+
*
|
|
8932
|
+
* Framework specifiers are classified by name and never resolved — that is what
|
|
8933
|
+
* keeps the terminal check independent of how `@timber-js/app` happens to be
|
|
8934
|
+
* installed. A specifier that fails to resolve is recorded as unresolved rather
|
|
8935
|
+
* than dropped, so the walk can take the conservative branch on it.
|
|
8936
|
+
*/
|
|
8937
|
+
async function resolveEdgeSources(pluginCtx, facts, importer) {
|
|
8938
|
+
for (const source of usedSources(facts)) {
|
|
8939
|
+
if (FRAMEWORK_SOURCES.has(source)) continue;
|
|
8940
|
+
try {
|
|
8941
|
+
const resolved = await pluginCtx.resolve(source, importer);
|
|
8942
|
+
if (resolved?.id) facts.resolvedSources.set(source, resolved.id);
|
|
8943
|
+
else facts.unresolvedSources.add(source);
|
|
8944
|
+
} catch {
|
|
8945
|
+
facts.unresolvedSources.add(source);
|
|
8946
|
+
}
|
|
8947
|
+
}
|
|
8948
|
+
}
|
|
8366
8949
|
function timberRequestDep(ctx) {
|
|
8367
8950
|
const factsMap = /* @__PURE__ */ new Map();
|
|
8368
|
-
const definitionModules = /* @__PURE__ */ new Set();
|
|
8369
8951
|
const cache = /* @__PURE__ */ new Map();
|
|
8370
8952
|
return {
|
|
8371
8953
|
name: "timber-request-dep",
|
|
@@ -8376,12 +8958,15 @@ function timberRequestDep(ctx) {
|
|
|
8376
8958
|
if (!/\.[mc]?[jt]sx?$/.test(id) && !/\.mdx?$/.test(id)) return;
|
|
8377
8959
|
if (id.startsWith("\0") || id.startsWith("virtual:")) return;
|
|
8378
8960
|
if (isFrameworkModule(id)) return;
|
|
8379
|
-
if (!
|
|
8380
|
-
const
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8961
|
+
if (!hasModuleBindings(code)) return;
|
|
8962
|
+
const pluginCtx = this;
|
|
8963
|
+
const facts = analyzeModule(pluginCtx.parse(code));
|
|
8964
|
+
if (contributesNothing(facts)) {
|
|
8965
|
+
factsMap.delete(id);
|
|
8966
|
+
return;
|
|
8384
8967
|
}
|
|
8968
|
+
await resolveEdgeSources(pluginCtx, facts, id);
|
|
8969
|
+
factsMap.set(id, facts);
|
|
8385
8970
|
},
|
|
8386
8971
|
configureServer(server) {
|
|
8387
8972
|
const resolver = (filePath) => {
|
|
@@ -8395,8 +8980,13 @@ function timberRequestDep(ctx) {
|
|
|
8395
8980
|
for (const imp of mod.importedModules) if (imp.id) result.push(imp.id);
|
|
8396
8981
|
return result;
|
|
8397
8982
|
};
|
|
8398
|
-
const
|
|
8399
|
-
|
|
8983
|
+
const isKnownModule = (moduleId) => rscEnv.moduleGraph.getModuleById(moduleId) !== void 0;
|
|
8984
|
+
const result = isReachableTaintSource(filePath, {
|
|
8985
|
+
factsMap,
|
|
8986
|
+
getImports,
|
|
8987
|
+
isKnownModule
|
|
8988
|
+
});
|
|
8989
|
+
if (isKnownModule(filePath)) cache.set(filePath, result);
|
|
8400
8990
|
return result;
|
|
8401
8991
|
};
|
|
8402
8992
|
globalThis[BRIDGE_KEY] = resolver;
|
|
@@ -8413,10 +9003,14 @@ function timberRequestDep(ctx) {
|
|
|
8413
9003
|
if (!info) return [];
|
|
8414
9004
|
return [...info.importedIds ?? [], ...info.dynamicallyImportedIds ?? []];
|
|
8415
9005
|
};
|
|
9006
|
+
const isKnownModule = (moduleId) => getModuleInfo(moduleId) != null;
|
|
8416
9007
|
const requestDepMap = {};
|
|
8417
9008
|
if (ctx.routeTree) walkSegmentTree(ctx.routeTree.root, (filePath) => {
|
|
8418
|
-
|
|
8419
|
-
|
|
9009
|
+
requestDepMap[filePath] = isReachableTaintSource(filePath, {
|
|
9010
|
+
factsMap,
|
|
9011
|
+
getImports,
|
|
9012
|
+
isKnownModule
|
|
9013
|
+
});
|
|
8420
9014
|
});
|
|
8421
9015
|
ctx.requestDepMap = requestDepMap;
|
|
8422
9016
|
}
|
|
@@ -10435,6 +11029,7 @@ function timber(config) {
|
|
|
10435
11029
|
ctx.root = resolved.root;
|
|
10436
11030
|
ctx.appDir = resolveAppDir(resolved.root, ctx.config.appDir);
|
|
10437
11031
|
ctx.dev = resolved.command === "serve";
|
|
11032
|
+
ctx.base = resolved.base;
|
|
10438
11033
|
for (const name of [
|
|
10439
11034
|
"timber.config.ts",
|
|
10440
11035
|
"timber.config.js",
|