@timber-js/app 0.2.0-alpha.208 → 0.2.0-alpha.209
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/{primitives-DLAnvsrk.js → actions-BerlqoXA.js} +640 -410
- package/dist/_chunks/actions-BerlqoXA.js.map +1 -0
- package/dist/_chunks/{als-registry-C6kcfprT.js → als-registry-DaxkVjt5.js} +6 -2
- package/dist/_chunks/als-registry-DaxkVjt5.js.map +1 -0
- package/dist/_chunks/{build-manifest-DTmSGLRz.js → build-manifest-DWppEdLB.js} +2 -51
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
- package/dist/_chunks/{build-output-helper-9dqCv1pZ.js → build-output-helper-RV_BRcF1.js} +50 -53
- package/dist/_chunks/build-output-helper-RV_BRcF1.js.map +1 -0
- package/dist/_chunks/{cache-api-CUD9Ezaq.js → cache-api-CR23J_NC.js} +64 -32
- package/dist/_chunks/cache-api-CR23J_NC.js.map +1 -0
- package/dist/_chunks/{chains-B4bgOl2g.js → chains-CpFg56UB.js} +2 -2
- package/dist/_chunks/{chains-B4bgOl2g.js.map → chains-CpFg56UB.js.map} +1 -1
- package/dist/_chunks/{cli-check-G8o30ldg.js → cli-check-C6Ev6wBO.js} +11 -52
- package/dist/_chunks/cli-check-C6Ev6wBO.js.map +1 -0
- package/dist/_chunks/cli-context-By7k2xVo.js +70 -0
- package/dist/_chunks/cli-context-By7k2xVo.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync--LZKs8Zn.js → cli-schema-sync-CbT2AUUI.js} +3 -3
- package/dist/_chunks/{cli-schema-sync--LZKs8Zn.js.map → cli-schema-sync-CbT2AUUI.js.map} +1 -1
- package/dist/_chunks/{cloudflare-BFb__LYG.js → cloudflare-Byxl8FWP.js} +96 -53
- package/dist/_chunks/{cloudflare-BFb__LYG.js.map → cloudflare-Byxl8FWP.js.map} +1 -1
- package/dist/_chunks/{convention-lint-5KzoMg_A.js → convention-lint-jKTwKwPe.js} +21 -6
- package/dist/_chunks/convention-lint-jKTwKwPe.js.map +1 -0
- package/dist/_chunks/{dev-server-BzBxgYAV.js → dev-server-C4WZdB7L.js} +15 -167
- package/dist/_chunks/dev-server-C4WZdB7L.js.map +1 -0
- package/dist/_chunks/{error-boundary-BrHmG-2X.js → error-boundary-tA7kVfs4.js} +84 -104
- package/dist/_chunks/error-boundary-tA7kVfs4.js.map +1 -0
- package/dist/_chunks/{fs-identity-D7vb6GdI.js → fs-identity-BRKuL215.js} +3 -4
- package/dist/_chunks/fs-identity-BRKuL215.js.map +1 -0
- package/dist/_chunks/{graph-cache-CP4GEmf9.js → graph-cache-CYiUS2aS.js} +4 -5
- package/dist/_chunks/graph-cache-CYiUS2aS.js.map +1 -0
- package/dist/_chunks/{json-lossy-check-ClNvBM_3.js → json-lossy-check-ip0Qi0MT.js} +6 -3
- package/dist/_chunks/json-lossy-check-ip0Qi0MT.js.map +1 -0
- package/dist/_chunks/{live-graph-Bm5WTvcr.js → live-graph-Dv-JJCZw.js} +3 -3
- package/dist/_chunks/{live-graph-Bm5WTvcr.js.map → live-graph-Dv-JJCZw.js.map} +1 -1
- package/dist/_chunks/{logger-k2DQ4EUf.js → logger-DiDt5ppH.js} +192 -51
- package/dist/_chunks/logger-DiDt5ppH.js.map +1 -0
- package/dist/_chunks/{navigation-root-mHSK9psY.js → navigation-root-BQfo1-kG.js} +2 -2
- package/dist/_chunks/{navigation-root-mHSK9psY.js.map → navigation-root-BQfo1-kG.js.map} +1 -1
- package/dist/_chunks/{plugin-context-8idfR3UZ.js → plugin-context-BPBPc3lL.js} +28 -4
- package/dist/_chunks/plugin-context-BPBPc3lL.js.map +1 -0
- package/dist/_chunks/{poison-scan-DG5eI2_z.js → poison-scan-CpeT6_OJ.js} +2 -2
- package/dist/_chunks/{poison-scan-DG5eI2_z.js.map → poison-scan-CpeT6_OJ.js.map} +1 -1
- package/dist/_chunks/render-timeout-DOf_BBcO.js +46 -0
- package/dist/_chunks/render-timeout-DOf_BBcO.js.map +1 -0
- package/dist/_chunks/{rsc-payload-path-B_LBodc2.js → rsc-payload-path-dffcs4jp.js} +9 -3
- package/dist/_chunks/{rsc-payload-path-B_LBodc2.js.map → rsc-payload-path-dffcs4jp.js.map} +1 -1
- package/dist/_chunks/{scanner-BQ5sPIJ7.js → scanner-Bw0oq1HB.js} +2 -2
- package/dist/_chunks/{scanner-BQ5sPIJ7.js.map → scanner-Bw0oq1HB.js.map} +1 -1
- package/dist/_chunks/{navigation-context-D0TU0Jog.js → segment-context-xtPUGfdq.js} +36 -34
- package/dist/_chunks/segment-context-xtPUGfdq.js.map +1 -0
- package/dist/_chunks/{singleflight-2lUWfcAk.js → singleflight-BeVMMKi0.js} +4 -3
- package/dist/_chunks/{singleflight-2lUWfcAk.js.map → singleflight-BeVMMKi0.js.map} +1 -1
- package/dist/_chunks/{sizeof-lXffQ1Vr.js → sizeof-UwzwB1uM.js} +3 -2
- package/dist/_chunks/sizeof-UwzwB1uM.js.map +1 -0
- package/dist/_chunks/ssr-data-nA3I70_n.js +49 -0
- package/dist/_chunks/ssr-data-nA3I70_n.js.map +1 -0
- package/dist/_chunks/{state-FippDgxN.js → state-BhNGPsdi.js} +6 -8
- package/dist/_chunks/state-BhNGPsdi.js.map +1 -0
- package/dist/_chunks/{status-page-marker-CNbpUOPB.js → status-page-marker-gaihi0KZ.js} +53 -20
- package/dist/_chunks/status-page-marker-gaihi0KZ.js.map +1 -0
- package/dist/_chunks/{walkers-CN2N_bA-.js → walkers-BXExhzzk.js} +2 -2
- package/dist/_chunks/{walkers-CN2N_bA-.js.map → walkers-BXExhzzk.js.map} +1 -1
- package/dist/_chunks/write-file-atomic-D1qWqHb4.js +32 -0
- package/dist/_chunks/write-file-atomic-D1qWqHb4.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +7 -7
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/canonicalize-module.d.ts +8 -0
- package/dist/adapters/canonicalize-module.d.ts.map +1 -0
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +2 -2
- package/dist/adapters/cloudflare.d.ts +30 -8
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +2 -2
- package/dist/adapters/compress-module.d.ts +8 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/fs-identity.d.ts +2 -3
- package/dist/adapters/fs-identity.d.ts.map +1 -1
- package/dist/adapters/nitro-presets.d.ts +0 -6
- package/dist/adapters/nitro-presets.d.ts.map +1 -1
- package/dist/adapters/nitro-preview.d.ts +43 -11
- package/dist/adapters/nitro-preview.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +1 -13
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +291 -176
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +7 -2
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +105 -21
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/adapters/types.js +0 -0
- package/dist/analyze/crawl-entry.js +3 -3
- package/dist/analyze/graph-cache.d.ts.map +1 -1
- package/dist/analyze/graph-command.js +4 -4
- package/dist/cache/index.js +2 -2
- package/dist/cache/json-lossy-check.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/sizeof.d.ts.map +1 -1
- package/dist/cache/stores/memory.js +1 -1
- package/dist/cache/tag-aware-handler.d.ts.map +1 -1
- package/dist/cdn/workers-cache-purge.js +1 -1
- package/dist/cli-check.d.ts +7 -37
- package/dist/cli-check.d.ts.map +1 -1
- package/dist/cli-context.d.ts +54 -0
- package/dist/cli-context.d.ts.map +1 -0
- package/dist/cli.d.ts +9 -9
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -24
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-dev.d.ts +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts +19 -29
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +7 -7
- package/dist/client/browser-entry/post-hydration.d.ts +6 -9
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +6 -11
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +4 -3
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/error-reconstituter.d.ts +3 -9
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/client/history.d.ts +12 -11
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.d.ts +0 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +5 -6
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +3 -3
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +68 -104
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-commit.d.ts +16 -7
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +3 -5
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/react-root.d.ts +3 -2
- package/dist/client/react-root.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +1 -6
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +45 -39
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router-url.d.ts +24 -0
- package/dist/client/router-url.d.ts.map +1 -0
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +8 -4
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/ssr-data.d.ts +18 -64
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/state.d.ts +2 -5
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/use-pathname.d.ts +1 -1
- package/dist/config-types.d.ts +2 -1
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/dev-page-shell.d.ts +17 -0
- package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
- package/dist/dev-tools/index.d.ts +1 -1
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/dev-tools/instrumentation.d.ts +17 -2
- package/dist/dev-tools/instrumentation.d.ts.map +1 -1
- package/dist/dev-tools/overlay.d.ts +6 -27
- package/dist/dev-tools/overlay.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +531 -373
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +26 -40
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +15 -2
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +19 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/post-build.d.ts +110 -0
- package/dist/plugins/post-build.d.ts.map +1 -0
- package/dist/plugins/prebuilt-capture.d.ts +11 -4
- package/dist/plugins/prebuilt-capture.d.ts.map +1 -1
- package/dist/plugins/prebuilt.d.ts +17 -1
- package/dist/plugins/prebuilt.d.ts.map +1 -1
- package/dist/plugins/prerender-sugar.d.ts.map +1 -1
- package/dist/plugins/rsc-assets-manifest.d.ts +33 -0
- package/dist/plugins/rsc-assets-manifest.d.ts.map +1 -0
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +32 -9
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts +0 -9
- package/dist/routing/codegen-write.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/segment-codegen.d.ts.map +1 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts +5 -4
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +46 -7
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +58 -5
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/children-interception.d.ts.map +1 -1
- package/dist/server/cookie-context.d.ts +2 -23
- package/dist/server/cookie-context.d.ts.map +1 -1
- package/dist/server/csp-nonce.d.ts +0 -9
- package/dist/server/csp-nonce.d.ts.map +1 -1
- package/dist/server/csrf.d.ts +27 -4
- package/dist/server/csrf.d.ts.map +1 -1
- package/dist/server/debug.d.ts +0 -1
- package/dist/server/debug.d.ts.map +1 -1
- package/dist/server/default-logger.d.ts +2 -1
- package/dist/server/default-logger.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +12 -6
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +2 -2
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +7 -3
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/fallback-error.d.ts +9 -2
- package/dist/server/fallback-error.d.ts.map +1 -1
- package/dist/server/head-response.d.ts +4 -4
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts +10 -3
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/internal.d.ts +1 -4
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +961 -581
- package/dist/server/internal.js.map +1 -1
- package/dist/server/json-status-response.d.ts +34 -0
- package/dist/server/json-status-response.d.ts.map +1 -0
- package/dist/server/logger.d.ts +26 -0
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +11 -6
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts +95 -7
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-interception.d.ts +11 -0
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts +11 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +2 -2
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +23 -35
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/prebuilt/capture-render.d.ts +25 -0
- package/dist/server/prebuilt/capture-render.d.ts.map +1 -0
- package/dist/server/prebuilt/capture-routes.d.ts +37 -0
- package/dist/server/prebuilt/capture-routes.d.ts.map +1 -0
- package/dist/server/prebuilt/capture-state.d.ts +79 -35
- package/dist/server/prebuilt/capture-state.d.ts.map +1 -1
- package/dist/server/prebuilt/lifetime.d.ts +38 -0
- package/dist/server/prebuilt/lifetime.d.ts.map +1 -0
- package/dist/server/prebuilt/slots.d.ts +7 -0
- package/dist/server/prebuilt/slots.d.ts.map +1 -1
- package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -1
- package/dist/server/prebuilt-builder.d.ts.map +1 -1
- package/dist/server/prebuilt-runtime.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +6 -81
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/react-cache-bridge.d.ts +59 -0
- package/dist/server/react-cache-bridge.d.ts.map +1 -0
- package/dist/server/react-cache-scope.d.ts +49 -0
- package/dist/server/react-cache-scope.d.ts.map +1 -0
- package/dist/server/render-timeout.d.ts +15 -0
- package/dist/server/render-timeout.d.ts.map +1 -1
- package/dist/server/render-utils.d.ts +9 -0
- package/dist/server/render-utils.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +26 -5
- package/dist/server/request-context.d.ts.map +1 -1
- 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 +7 -0
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +22 -22
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
- package/dist/server/rsc-entry/api-handler.d.ts +7 -0
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/deny-fallback.d.ts +32 -6
- package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +11 -18
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +0 -1
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +5 -15
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +5 -6
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-signals.d.ts +65 -0
- package/dist/server/rsc-entry/render-signals.d.ts.map +1 -0
- package/dist/server/rsc-entry/revalidate-renderer.d.ts +6 -16
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +7 -3
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +8 -53
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-bridge.d.ts +2 -2
- package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +9 -4
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +1 -1
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -1
- package/dist/server/signal-identity.d.ts +43 -0
- package/dist/server/signal-identity.d.ts.map +1 -0
- package/dist/server/slot-resolver.d.ts +4 -3
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/slot-subtree-contain.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +91 -53
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +8 -5
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +26 -40
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +1 -1
- package/dist/server/static-generator.d.ts.map +1 -1
- package/dist/server/stream-utils.d.ts +12 -0
- package/dist/server/stream-utils.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +12 -16
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -1
- package/dist/server/version-skew.d.ts +3 -3
- package/dist/shared/csrf-reject.d.ts +16 -0
- package/dist/shared/csrf-reject.d.ts.map +1 -0
- package/dist/shared/error-digest.d.ts +28 -0
- package/dist/shared/error-digest.d.ts.map +1 -0
- package/dist/shared/payload-root.d.ts +11 -10
- package/dist/shared/payload-root.d.ts.map +1 -1
- package/dist/shared/rsc-payload-path.d.ts +6 -0
- package/dist/shared/rsc-payload-path.d.ts.map +1 -1
- package/dist/utils/write-file-atomic.d.ts +17 -0
- package/dist/utils/write-file-atomic.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +17 -14
- package/docs/api/32-api-cache.mdx +51 -23
- package/docs/api/34-api-config.mdx +34 -6
- package/docs/api/36-cli.mdx +11 -1
- package/docs/learn/03-fetching-data.mdx +17 -1
- package/docs/learn/04-access-control.mdx +1 -1
- package/docs/learn/10-caching.mdx +29 -11
- package/docs/learn/11-middleware.mdx +8 -2
- package/docs/learn/12-error-handling.mdx +6 -1
- package/package.json +5 -1
- package/src/adapters/build-output-helper.ts +55 -53
- package/src/adapters/canonicalize-module.ts +29 -0
- package/src/adapters/cloudflare.ts +105 -63
- package/src/adapters/compress-module.ts +16 -5
- package/src/adapters/fs-identity.ts +2 -3
- package/src/adapters/nitro-presets.ts +0 -15
- package/src/adapters/nitro-preview.ts +272 -136
- package/src/adapters/nitro.ts +119 -82
- package/src/adapters/shared.ts +23 -14
- package/src/adapters/types.ts +110 -21
- package/src/analyze/graph-cache.ts +3 -4
- package/src/cache/json-lossy-check.ts +3 -9
- package/src/cache/singleflight.ts +5 -2
- package/src/cache/sizeof.ts +3 -1
- package/src/cache/tag-aware-handler.ts +43 -26
- package/src/cli-check.ts +8 -98
- package/src/cli-context.ts +119 -0
- package/src/cli.ts +19 -28
- package/src/client/browser-dev.ts +1 -1
- package/src/client/browser-entry/action-dispatch.ts +26 -4
- package/src/client/browser-entry/hydrate.ts +53 -58
- package/src/client/browser-entry/index.ts +16 -27
- package/src/client/browser-entry/post-hydration.ts +6 -40
- package/src/client/browser-entry/router-init.ts +66 -84
- package/src/client/error-boundary.tsx +9 -41
- package/src/client/error-reconstituter.tsx +10 -5
- package/src/client/history.ts +18 -21
- package/src/client/index.ts +0 -1
- package/src/client/internal.ts +2 -7
- package/src/client/navigation-commit.ts +35 -18
- package/src/client/navigation-context.ts +3 -39
- package/src/client/react-root.ts +3 -2
- package/src/client/router-pipeline.ts +65 -128
- package/src/client/router-types.ts +46 -44
- package/src/client/router-url.ts +32 -0
- package/src/client/router.ts +43 -22
- package/src/client/rsc-fetch.ts +117 -155
- package/src/client/ssr-data.ts +19 -80
- package/src/client/state.ts +2 -9
- package/src/client/use-pathname.ts +1 -1
- package/src/config-types.ts +2 -1
- package/src/config-validation.ts +2 -1
- package/src/dev-tools/dev-page-shell.ts +18 -0
- package/src/dev-tools/index.ts +0 -2
- package/src/dev-tools/instrumentation.ts +50 -2
- package/src/dev-tools/overlay.ts +8 -238
- package/src/index.ts +24 -14
- package/src/plugin-context.ts +39 -40
- package/src/plugins/build-manifest.ts +4 -11
- package/src/plugins/build-report.ts +70 -70
- package/src/plugins/dev-server.ts +24 -44
- package/src/plugins/entries.ts +2 -1
- package/src/plugins/fonts.ts +35 -7
- package/src/plugins/post-build.ts +327 -0
- package/src/plugins/prebuilt-capture.ts +28 -32
- package/src/plugins/prebuilt.ts +54 -39
- package/src/plugins/prerender-sugar.ts +0 -4
- package/src/plugins/{adapter-build.ts → rsc-assets-manifest.ts} +20 -109
- package/src/plugins/shims.ts +1 -0
- package/src/plugins/static-build.ts +42 -74
- package/src/routing/codegen-write.ts +25 -3
- package/src/routing/segment-codegen.ts +3 -2
- package/src/rsc-runtime/vendor-types.d.ts +8 -0
- package/src/server/access-gate.tsx +2 -1
- package/src/server/action-client.ts +1 -1
- package/src/server/action-handler.ts +253 -110
- package/src/server/actions.ts +73 -25
- package/src/server/als-registry.ts +73 -5
- package/src/server/chain-url-parts.ts +1 -1
- package/src/server/children-interception.ts +2 -1
- package/src/server/cookie-context.ts +18 -48
- package/src/server/csp-nonce.ts +0 -12
- package/src/server/csrf.ts +140 -8
- package/src/server/debug.ts +0 -1
- package/src/server/default-logger.ts +2 -1
- package/src/server/deny-boundary.ts +25 -13
- package/src/server/deny-renderer.ts +30 -97
- package/src/server/error-boundary-wrapper.ts +7 -3
- package/src/server/fallback-error.ts +26 -11
- package/src/server/flight-scripts.ts +1 -1
- package/src/server/head-response.ts +4 -4
- package/src/server/html-injector-core.ts +2 -2
- package/src/server/html-injectors.ts +2 -4
- package/src/server/index.ts +0 -4
- package/src/server/instrumentation.ts +10 -3
- package/src/server/internal.ts +1 -6
- package/src/server/json-status-response.ts +105 -0
- package/src/server/logger.ts +39 -3
- package/src/server/param-coercion.ts +15 -12
- package/src/server/pipeline-helpers.ts +257 -12
- package/src/server/pipeline-interception.ts +133 -0
- package/src/server/pipeline-outcome.ts +103 -31
- package/src/server/pipeline-phases.ts +127 -237
- package/src/server/pipeline.ts +204 -207
- package/src/server/prebuilt/capture-render.ts +118 -0
- package/src/server/prebuilt/capture-routes.ts +104 -0
- package/src/server/prebuilt/capture-state.ts +155 -48
- package/src/server/prebuilt/lifetime.ts +151 -0
- package/src/server/prebuilt/slots.ts +14 -2
- package/src/server/prebuilt/synthetic-store.ts +3 -0
- package/src/server/prebuilt-builder.ts +30 -320
- package/src/server/prebuilt-runtime.ts +125 -129
- package/src/server/primitives.ts +8 -168
- package/src/server/react-cache-bridge.ts +124 -0
- package/src/server/react-cache-scope.ts +104 -0
- package/src/server/render-timeout.ts +20 -0
- package/src/server/render-utils.ts +11 -1
- package/src/server/request-context.ts +36 -9
- package/src/server/route-element-builder.ts +11 -6
- package/src/server/route-handler.ts +36 -23
- package/src/server/route-matcher.ts +15 -1
- package/src/server/rsc-entry/action-middleware-runner.ts +79 -123
- package/src/server/rsc-entry/api-handler.ts +27 -103
- package/src/server/rsc-entry/deny-fallback.ts +93 -26
- package/src/server/rsc-entry/error-renderer.ts +39 -91
- package/src/server/rsc-entry/helpers.ts +2 -4
- package/src/server/rsc-entry/index.ts +64 -58
- package/src/server/rsc-entry/render-route.ts +15 -80
- package/src/server/rsc-entry/render-signals.ts +108 -0
- package/src/server/rsc-entry/revalidate-renderer.ts +56 -57
- package/src/server/rsc-entry/rsc-payload.ts +28 -61
- package/src/server/rsc-entry/rsc-stream.ts +113 -213
- package/src/server/rsc-entry/ssr-bridge.ts +2 -2
- package/src/server/rsc-entry/ssr-renderer.ts +182 -263
- package/src/server/rsc-entry/wrap-action-dispatch.ts +12 -14
- package/src/server/signal-identity.ts +112 -0
- package/src/server/slot-resolver.ts +18 -18
- package/src/server/slot-subtree-contain.ts +2 -1
- package/src/server/ssr-bridge-types.ts +92 -56
- package/src/server/ssr-entry.ts +87 -111
- package/src/server/ssr-render.ts +55 -64
- package/src/server/ssr-wrappers.tsx +2 -2
- package/src/server/static-generator.ts +7 -42
- package/src/server/static-not-found.ts +3 -3
- package/src/server/stream-utils.ts +51 -0
- package/src/server/tracing.ts +58 -75
- package/src/server/types.ts +1 -1
- package/src/server/version-skew.ts +5 -5
- package/src/shared/csrf-reject.ts +15 -0
- package/src/shared/error-digest.ts +46 -0
- package/src/shared/payload-root.ts +11 -10
- package/src/shared/rsc-payload-path.ts +8 -1
- package/src/utils/write-file-atomic.ts +32 -0
- package/dist/_chunks/actions-jNrXdzgs.js +0 -346
- package/dist/_chunks/actions-jNrXdzgs.js.map +0 -1
- package/dist/_chunks/als-registry-C6kcfprT.js.map +0 -1
- package/dist/_chunks/build-manifest-DTmSGLRz.js.map +0 -1
- package/dist/_chunks/build-output-helper-9dqCv1pZ.js.map +0 -1
- package/dist/_chunks/cache-api-CUD9Ezaq.js.map +0 -1
- package/dist/_chunks/cli-check-G8o30ldg.js.map +0 -1
- package/dist/_chunks/convention-lint-5KzoMg_A.js.map +0 -1
- package/dist/_chunks/dev-server-BzBxgYAV.js.map +0 -1
- package/dist/_chunks/error-boundary-BrHmG-2X.js.map +0 -1
- package/dist/_chunks/fs-identity-D7vb6GdI.js.map +0 -1
- package/dist/_chunks/graph-cache-CP4GEmf9.js.map +0 -1
- package/dist/_chunks/json-lossy-check-ClNvBM_3.js.map +0 -1
- package/dist/_chunks/logger-k2DQ4EUf.js.map +0 -1
- package/dist/_chunks/navigation-context-D0TU0Jog.js.map +0 -1
- package/dist/_chunks/plugin-context-8idfR3UZ.js.map +0 -1
- package/dist/_chunks/primitives-DLAnvsrk.js.map +0 -1
- package/dist/_chunks/segment-context-D9_89u34.js +0 -34
- package/dist/_chunks/segment-context-D9_89u34.js.map +0 -1
- package/dist/_chunks/sizeof-lXffQ1Vr.js.map +0 -1
- package/dist/_chunks/ssr-data-D6T6Y3ef.js +0 -60
- package/dist/_chunks/ssr-data-D6T6Y3ef.js.map +0 -1
- package/dist/_chunks/state-FippDgxN.js.map +0 -1
- package/dist/_chunks/status-page-marker-CNbpUOPB.js.map +0 -1
- package/dist/client/types.d.ts +0 -14
- package/dist/client/types.d.ts.map +0 -1
- package/dist/plugins/adapter-build.d.ts +0 -38
- package/dist/plugins/adapter-build.d.ts.map +0 -1
- package/dist/server/asset-headers.d.ts +0 -32
- package/dist/server/asset-headers.d.ts.map +0 -1
- package/dist/server/dev-source-map.d.ts +0 -22
- package/dist/server/dev-source-map.d.ts.map +0 -1
- package/src/client/types.ts +0 -21
- package/src/server/asset-headers.ts +0 -59
- package/src/server/dev-source-map.ts +0 -31
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-api-CR23J_NC.js","names":[],"sources":["../../src/server/waituntil-bridge.ts","../../src/cache/stable-stringify.ts","../../src/cache/invalidation-epoch.ts","../../src/cache/timber-cache.ts","../../src/cache/handler-store.ts","../../src/cache/redis-handler.ts","../../src/cache/tag-aware-handler.ts","../../src/cache/index.ts","../../src/server/version-skew.ts","../../src/server/prebuilt/manifest-tag-index.ts","../../src/server/prebuilt/payload-source.ts","../../src/server/prebuilt/overlay.ts","../../src/cache/cache-api.ts"],"sourcesContent":["/**\n * Per-request waitUntil bridge — ALS bridge for platform adapters.\n *\n * The generated entry point (Nitro, Cloudflare) wraps the handler with\n * `runWithWaitUntil`, binding the platform's lifecycle extension function\n * (e.g., h3's `event.waitUntil()` or CF's `ctx.waitUntil()`) for the\n * request duration. The `waitUntil()` primitive reads from this ALS to\n * dispatch background work to the correct platform API.\n *\n * Design doc: design/11-platform.md §\"waitUntil()\"\n */\n\nimport { waitUntilAls } from './als-registry.ts';\n\n/**\n * Run a function with a per-request waitUntil handler installed.\n *\n * Called by generated entry points (Nitro node-server/bun, Cloudflare)\n * to bind the platform's lifecycle extension for the request duration.\n */\nexport function runWithWaitUntil<T>(\n waitUntilFn: (promise: Promise<unknown>) => void,\n fn: () => T\n): T {\n return waitUntilAls.run(waitUntilFn, fn);\n}\n\n/**\n * Get the current request's waitUntil function, if available.\n *\n * Returns undefined when no platform adapter has installed a waitUntil\n * handler for the current request (e.g., on platforms that don't support\n * lifecycle extension, or outside a request context).\n */\nexport function getWaitUntil(): ((promise: Promise<unknown>) => void) | undefined {\n return waitUntilAls.getStore();\n}\n","/**\n * Deterministic JSON serialization with sorted object keys.\n * Used for cache key generation — ensures { a: 1, b: 2 } and { b: 2, a: 1 }\n * produce the same string.\n *\n * Matches JSON.stringify semantics for toJSON: Date serializes to its ISO\n * string, URL to its href, and class instances with a custom toJSON use it.\n * Map and Set serialize their entries explicitly (insertion-order\n * independent). Values that would serialize lossily — functions, symbols,\n * and non-plain objects with no enumerable own state (RegExp, class\n * instances holding data behind getters or internal slots) — throw instead:\n * every such value would collapse to the same cache key, silently returning\n * one argument's cached data for another (TIM-1020).\n */\nexport function stableStringify(value: unknown): string {\n if (value === null || value === undefined) return String(value);\n\n const type = typeof value;\n if (type === 'function' || type === 'symbol') {\n throw new TypeError(\n `stableStringify: cannot serialize a ${type} into a stable cache key — ` +\n 'all such values would collide. Provide an explicit `key` option to cache().'\n );\n }\n if (type !== 'object') return JSON.stringify(value);\n\n if (Array.isArray(value)) {\n return '[' + value.map((item) => stableStringify(item)).join(',') + ']';\n }\n\n // Honor toJSON like JSON.stringify does (Date → ISO string, URL → href,\n // custom class serialization). JSON.stringify does not re-invoke toJSON\n // on the value it returns, so a toJSON that returns `this` falls through\n // to plain own-property serialization instead of recursing forever.\n const withToJSON = value as { toJSON?: () => unknown };\n if (typeof withToJSON.toJSON === 'function') {\n const json = withToJSON.toJSON();\n if (json !== value) return stableStringify(json);\n }\n\n // Map/Set have no enumerable own properties — serialize entries explicitly.\n // Entries are sorted after serialization so insertion order doesn't change\n // the key. The Map(/Set( prefixes can't collide with object ('{'), array\n // ('['), or string ('\"') output.\n if (value instanceof Map) {\n const entries = [...value.entries()].map(\n ([k, v]) => stableStringify(k) + '=>' + stableStringify(v)\n );\n return 'Map(' + entries.sort().join(',') + ')';\n }\n if (value instanceof Set) {\n const members = [...value].map((member) => stableStringify(member));\n return 'Set(' + members.sort().join(',') + ')';\n }\n\n const obj = value as Record<string, unknown>;\n const keys = Object.keys(obj).sort();\n const pairs: string[] = [];\n for (const key of keys) {\n if (obj[key] === undefined) continue;\n // A callable own toJSON only reaches this loop via the returns-`this`\n // fall-through above. It's the serialization protocol, not data —\n // JSON.stringify omits it too.\n if (key === 'toJSON' && typeof obj[key] === 'function') continue;\n pairs.push(JSON.stringify(key) + ':' + stableStringify(obj[key]));\n }\n\n // A non-plain object that serializes to '{}' carries its state behind\n // getters or internal slots (RegExp, sessions, ORM entities). Serializing\n // it would give every instance the same cache key.\n if (pairs.length === 0) {\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && proto !== null) {\n const name = (value as object).constructor?.name || 'Object';\n throw new TypeError(\n `stableStringify: ${name} instance has no serializable own state — ` +\n 'all instances would share one cache key. Provide an explicit `key` ' +\n 'option to cache(), or add a toJSON() method.'\n );\n }\n }\n\n return '{' + pairs.join(',') + '}';\n}\n","/**\n * Per-process invalidation epoch tracking for timber.cache (TIM-1028).\n *\n * handler.set for a cache miss runs after `fn()` resolves. An explicit\n * `cache.invalidate({ tag })` / `revalidateTag` that lands while fn is in\n * flight would otherwise be silently undone — the just-invalidated value\n * re-stored as fresh for the full TTL, resurrecting pre-mutation data.\n *\n * Every invalidation bumps a monotonic epoch and records it against the\n * invalidated key/tag. Before storing a result, the cache wrapper checks\n * whether its key or any of its tags were invalidated after fn started and\n * skips the write if so. Skipping is always safe: the only cost is one\n * extra cache miss on the next call.\n *\n * Scope: per-process, like the singleflight map. An invalidation issued by\n * another instance against a shared handler (Redis/KV) during fn flight is\n * not observable here — closing that window fully would require store-level\n * versioning, which no CacheHandler backend offers.\n */\n\nlet epoch = 0;\n\n/**\n * Last-invalidated epoch per `key:<k>` / `tag:<t>` entry. Insertion order is\n * epoch order (entries are re-inserted on update), so the oldest entry is\n * always first — pruning evicts lowest epochs.\n */\nconst lastInvalidated = new Map<string, number>();\n\n/**\n * Bound on tracked entries so high-cardinality tag invalidation (e.g.\n * per-user tags) cannot grow this map without limit.\n */\nconst MAX_TRACKED_ENTRIES = 10_000;\n\n/**\n * Highest epoch evicted from the map. Any execution that started at or\n * before this epoch conservatively counts as invalidated — history was\n * truncated, so we can no longer prove it wasn't.\n */\nlet evictedEpochFloor = 0;\n\n/** The current epoch. Capture before running fn(), pass to wasInvalidatedSince. */\nexport function currentInvalidationEpoch(): number {\n return epoch;\n}\n\n/**\n * Record an invalidation. Call BEFORE handler.invalidate() so an execution\n * whose set-check races the invalidation errs toward skipping the write.\n */\nexport function recordInvalidation(opts: { key?: string; tag?: string }): void {\n epoch++;\n if (opts.key) track(`key:${opts.key}`);\n if (opts.tag) track(`tag:${opts.tag}`);\n}\n\nfunction track(entry: string): void {\n lastInvalidated.delete(entry);\n lastInvalidated.set(entry, epoch);\n while (lastInvalidated.size > MAX_TRACKED_ENTRIES) {\n const oldest = lastInvalidated.entries().next().value;\n if (oldest === undefined) break;\n evictedEpochFloor = Math.max(evictedEpochFloor, oldest[1]);\n lastInvalidated.delete(oldest[0]);\n }\n}\n\n/**\n * Whether the key or any tag was invalidated after `startEpoch` (the epoch\n * captured when fn started executing).\n */\nexport function wasInvalidatedSince(startEpoch: number, key: string, tags: string[]): boolean {\n return lastInvalidationEpochFor(key, tags) > startEpoch;\n}\n\n/**\n * The highest epoch at which the key or any of the tags was invalidated\n * (0 if never). Used to scope the singleflight key: callers arriving after\n * an invalidation must not coalesce onto a flight that started before it —\n * they would receive the pre-invalidation result the invalidation was\n * issued to discard.\n */\nexport function lastInvalidationEpochFor(key: string, tags: string[]): number {\n let last = evictedEpochFloor;\n const keyEpoch = lastInvalidated.get(`key:${key}`);\n if (keyEpoch !== undefined && keyEpoch > last) last = keyEpoch;\n for (const tag of tags) {\n const tagEpoch = lastInvalidated.get(`tag:${tag}`);\n if (tagEpoch !== undefined && tagEpoch > last) last = tagEpoch;\n }\n return last;\n}\n","import type { CacheHandler, CacheOptions } from './index.ts';\nimport { stableStringify } from './stable-stringify.ts';\nimport { createSingleflight } from './singleflight.ts';\nimport {\n currentInvalidationEpoch,\n lastInvalidationEpochFor,\n wasInvalidatedSince,\n} from './invalidation-epoch.ts';\nimport { addSpanEventSync } from '../server/tracing.ts';\nimport { logSwrRefetchFailed, swallow } from '../server/logger.ts';\nimport { getWaitUntil } from '../server/waituntil-bridge.ts';\nimport { fnv1aHash } from './fast-hash.ts';\n\nlet defaultSingleflight = createSingleflight();\n\n/**\n * Per-key monotonic generation counter for CAS writes (TIM-1158).\n *\n * Unbounded, like the singleflight map — the key space is the same set of\n * cache keys the handler already tracks. Pruning was removed because it\n * desyncs from MemoryCacheHandler.generations: a pruned key restarts at 1\n * while the handler still holds the old higher generation, causing fresh\n * writes to be rejected as stale.\n */\nconst writeGenerations = new Map<string, number>();\nfunction nextGeneration(key: string): number {\n const gen = (writeGenerations.get(key) ?? 0) + 1;\n writeGenerations.set(key, gen);\n return gen;\n}\n\n/**\n * Set the timeout for the module-level default singleflight.\n * Called at framework boot with renderTimeoutMs from timber.config.ts so that\n * cache calls without a per-call timeoutMs still get deadlock protection.\n * See design/06-caching.md §\"Singleflight Timeout — Deadlock Risk\".\n */\nexport function setDefaultSingleflightTimeout(timeoutMs: number): void {\n defaultSingleflight = createSingleflight({ timeoutMs });\n}\n\n/**\n * Generate a cache key from function identity and serialized args.\n *\n * Uses FNV-1a (fast non-crypto hash) instead of SHA-256. Cache keys don't\n * need collision resistance — they need speed. The fnId prefix provides\n * namespace isolation; the hash covers the args.\n *\n * See TIM-370 for perf motivation.\n */\nfunction defaultKeyGenerator(fnId: string, args: unknown[]): string {\n const raw = fnId + ':' + stableStringify(args);\n return fnId + ':' + fnv1aHash(raw);\n}\n\n/**\n * Resolve tags from the options — supports static array or function form.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction resolveTags<Fn extends (...args: any[]) => any>(\n opts: CacheOptions<Fn>,\n args: Parameters<Fn>\n): string[] {\n if (!opts.tags) return [];\n if (Array.isArray(opts.tags)) return opts.tags;\n return opts.tags(...args);\n}\n\n/** Per-process count of byte-identical fn sources seen by deriveFallbackFnId. */\nconst fallbackIdOccurrences = new Map<string, number>();\nlet hasWarnedFallbackId = false;\n\n/**\n * Derive a fallback fnId for callsites the timber-cache-transform Vite plugin\n * could not statically detect (aliased bindings, user re-export wrappers).\n *\n * The base is fnv1a(fn.toString()) — deterministic across instances and cold\n * starts of the same build, so two *different* functions can never swap fnIds\n * by module-evaluation order (the TIM-997 poisoning scenario that the old\n * process-local counter allowed; see TIM-1054). A per-process occurrence\n * suffix disambiguates byte-identical duplicate sources. That residue is the\n * one remaining hazard: closures produced by the same factory share source\n * text but capture different values, and their occurrence order may differ\n * across instances — such callsites need an explicit `key` option.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction deriveFallbackFnId(fn: (...args: any[]) => any): string {\n const sourceHash = fnv1aHash(fn.toString());\n const occurrence = fallbackIdOccurrences.get(sourceHash) ?? 0;\n fallbackIdOccurrences.set(sourceHash, occurrence + 1);\n\n if (!hasWarnedFallbackId && typeof process !== 'undefined' && process.env?.NODE_ENV !== 'test') {\n hasWarnedFallbackId = true;\n console.warn(\n '[timber] cache() callsite has no build-time stable ID and no explicit `key` option — ' +\n 'the call is not statically visible to the timber Vite plugin (e.g. an aliased ' +\n 'binding like `const c = cache` or a re-export wrapper module). Falling back to a ' +\n 'function-source hash for the cache key. With a shared CacheHandler (Redis/KV) this ' +\n 'is safe for distinct functions, but byte-identical closures from a factory can swap ' +\n 'cache entries across instances. Provide an explicit `key` option to make the cache ' +\n 'key contract stable and auditable.'\n );\n }\n\n return `timber-cache:fn:${sourceHash}:${occurrence}`;\n}\n\n/**\n * Creates a cached wrapper around an async function.\n *\n * - FNV-1a default keys with normalized JSON args\n * - Singleflight: concurrent misses → single execution AND single\n * handler.set — the write happens inside the coalesced callback (TIM-1028)\n * - Writes are skipped when the key/tags were invalidated while fn was in\n * flight, so explicit invalidation is never silently undone (TIM-1028)\n * - SWR: serve stale immediately, background refetch\n * - Tags as string[] or function of args\n * - The handler is resolved per invocation via `getHandler`, never captured\n * at wrapper-creation time — module-scope cache() wrappers created before\n * the framework's boot wiring calls setCacheHandler() still use the\n * configured handler once it arrives (TIM-1029)\n * - No ALS dependency for correctness — cache() works outside a request\n * context. The waitUntil ALS bridge is read optionally so the SWR\n * background refetch survives handler return on Workers/Lambda.\n *\n * Cache hits/misses are recorded as OTEL span events on the enclosing\n * span (not child spans). The DevSpanProcessor reads these for dev log output.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function createCache<Fn extends (...args: any[]) => Promise<any>>(\n fn: Fn,\n opts: CacheOptions<Fn>,\n getHandler: () => CacheHandler,\n stableId?: string\n): Fn {\n // With an explicit `key` option the fnId never reaches key generation, so\n // no fallback derivation (or warning) is needed in that case.\n const fnId = stableId ?? (opts.key ? undefined : deriveFallbackFnId(fn));\n\n // Per-call timeoutMs gets a dedicated singleflight instance. Otherwise\n // resolve defaultSingleflight per invocation (not captured at creation\n // time) so wrappers created before boot pick up the timeout once\n // setDefaultSingleflightTimeout is called (TIM-1032, same rationale as\n // getCacheHandler for TIM-1029).\n const perCallSf =\n opts.timeoutMs !== undefined ? createSingleflight({ timeoutMs: opts.timeoutMs }) : undefined;\n\n // Cast to Fn to preserve the original function's generic call signature.\n // Without this, generic type parameters (e.g. <T> in apiFetch<T>) are\n // erased and callers lose type safety on the return type.\n return (async (...args: Parameters<Fn>): Promise<Awaited<ReturnType<Fn>>> => {\n const sf = perCallSf ?? defaultSingleflight;\n const key = opts.key ? opts.key(...args) : defaultKeyGenerator(fnId as string, args);\n\n const cacheStart = performance.now();\n const cached = await getHandler().get(key);\n\n if (cached && !cached.stale) {\n // Record as OTEL span event on enclosing span (not a child span).\n // Fire-and-forget — no microtask overhead on the cache hot path.\n addSpanEventSync('timber.cache.hit', {\n key,\n duration_ms: Math.round(performance.now() - cacheStart),\n });\n return cached.value as Awaited<ReturnType<Fn>>;\n }\n\n // From here fn() will execute (SWR refetch or miss). Scope the\n // singleflight key by the last invalidation epoch affecting this\n // key/tags: a caller arriving after an invalidation must not coalesce\n // onto a flight that started before it — it would receive the\n // pre-invalidation result the invalidation was issued to discard.\n // NUL separator: cannot collide with user-provided key strings.\n const tags = resolveTags(opts, args);\n const flightKey = `${key}\\u0000${lastInvalidationEpochFor(key, tags)}`;\n\n /**\n * Run fn and store the result — exactly once per coalesced execution,\n * inside the singleflight callback. The write is skipped when:\n * - the key/tags were invalidated after fn started (TIM-1028), or\n * - the singleflight timed out (signal aborted) — the timed-out\n * flight's write could overwrite a newer value from a subsequent\n * flight (TIM-1092), or\n * - the handler rejects the write because a newer generation has\n * already been stored (TIM-1158 CAS guard).\n */\n const executeAndStore = async (signal: AbortSignal): Promise<Awaited<ReturnType<Fn>>> => {\n const startEpoch = currentInvalidationEpoch();\n const generation = nextGeneration(key);\n const result = await fn(...args);\n if (!signal.aborted && !wasInvalidatedSince(startEpoch, key, tags)) {\n try {\n await getHandler().set(key, result, { ttl: opts.ttl, tags, generation });\n } catch (err) {\n // Cache-write failure must not fail the request (TIM-1046).\n // JSON-serializing handlers (Redis, KV) throw on BigInt; network\n // errors can surface from any shared handler. The result is valid\n // — serve it uncached rather than rejecting a successful fn().\n swallow(err, `cache set failed for key \"${key}\"`);\n }\n }\n return result;\n };\n\n if (cached && cached.stale && opts.staleWhileRevalidate) {\n // Record stale cache hit as OTEL span event (fire-and-forget).\n addSpanEventSync('timber.cache.hit', {\n key,\n duration_ms: Math.round(performance.now() - cacheStart),\n stale: true,\n });\n // Serve stale immediately, trigger background refetch\n const refetch = sf\n .do(`swr:${flightKey}`, async (signal) => {\n try {\n await executeAndStore(signal);\n } catch (err) {\n // Failed refetch — stale entry continues to be served.\n logSwrRefetchFailed({ cacheKey: key, error: err });\n }\n })\n .catch(() => {\n // Singleflight promise rejection handled — stale continues.\n });\n // Platforms that cancel work when the handler returns (Cloudflare\n // Workers, Lambda) would otherwise kill the refetch mid-flight —\n // stale data re-served forever. Register the (never-rejecting)\n // refetch with the request's waitUntil; undefined outside a request\n // context or on platforms without lifecycle extension (Node servers\n // keep floating promises alive, so behavior there is unchanged).\n getWaitUntil()?.(refetch);\n return cached.value as Awaited<ReturnType<Fn>>;\n }\n\n // Cache miss (or stale without SWR) — execute with singleflight.\n // The handler.set lives inside the coalesced callback, so N concurrent\n // misses produce exactly one write (TIM-1028).\n const result = await sf.do(flightKey, executeAndStore);\n\n // Record cache miss as OTEL span event (fire-and-forget).\n addSpanEventSync('timber.cache.miss', {\n key,\n duration_ms: Math.round(performance.now() - cacheStart),\n });\n\n return result;\n }) as unknown as Fn;\n}\n","/**\n * Module-level cache handler singleton.\n *\n * Lazily initialized to MemoryCacheHandler on first access. The framework\n * replaces this at boot from timber.config.ts via setCacheHandler().\n *\n * This module avoids importing from ./index to prevent circular dependencies.\n */\n\n// Inline the interface to avoid circular import with index.ts\ninterface CacheHandlerLike {\n get(key: string): Promise<{ value: unknown; stale: boolean } | null>;\n set(key: string, value: unknown, opts: { ttl: number; tags: string[] }): Promise<void>;\n invalidate(opts: { key?: string; tag?: string }): Promise<void>;\n}\n\nlet handler: CacheHandlerLike | null = null;\n\nlet explicitlyConfigured = false;\n\n/** Replace the active cache handler. Called by the framework at boot. */\nexport function setCacheHandler(h: CacheHandlerLike): void {\n handler = h;\n explicitlyConfigured = true;\n}\n\n/** Whether the handler was explicitly configured (vs the default in-memory fallback). */\nexport function isCacheHandlerConfigured(): boolean {\n return explicitlyConfigured;\n}\n\n/**\n * Get the active cache handler. Creates a default MemoryCacheHandler on\n * first access if none has been set via setCacheHandler().\n */\nexport function getCacheHandler(): CacheHandlerLike {\n if (!handler) {\n // Inline a minimal LRU cache to avoid circular dep with index.ts.\n // In production, the framework always calls setCacheHandler() at boot.\n handler = createDefaultHandler();\n }\n return handler;\n}\n\nfunction createDefaultHandler(): CacheHandlerLike {\n const store = new Map<string, { value: unknown; expiresAt: number; tags: string[] }>();\n const maxEntries = 1000;\n\n return {\n async get(key) {\n const entry = store.get(key);\n if (!entry) return null;\n store.delete(key);\n store.set(key, entry);\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n },\n async set(key, value, opts) {\n if (store.has(key)) store.delete(key);\n while (store.size >= maxEntries) {\n const oldest = store.keys().next().value;\n if (oldest !== undefined) store.delete(oldest);\n else break;\n }\n store.set(key, { value, expiresAt: Date.now() + opts.ttl * 1000, tags: opts.tags });\n },\n async invalidate(opts) {\n if (opts.key) store.delete(opts.key);\n if (opts.tag) {\n for (const [key, entry] of store) {\n if (entry.tags.includes(opts.tag)) store.delete(key);\n }\n }\n },\n };\n}\n","import type { CacheHandler } from './index.ts';\nimport { warnLossyJsonValue } from './json-lossy-check.ts';\n\n/**\n * Minimal Redis client contract for `RedisCacheHandler`.\n *\n * This is deliberately NOT the native shape of any client library. ioredis,\n * node-redis v4, and @upstash/redis all disagree on TTL argument shapes\n * (positional `'EX'` vs `{ EX }` vs `{ ex }`), method casing (`sadd` vs\n * `sAdd`), and array handling in `del` — a contract that pattern-matches one\n * client lets another silently drop the expiry (TIM-1030: node-redis ignored\n * the positional `'EX'` and wrote keys with no TTL). Instead, TTL semantics\n * are explicit in the signature and you adapt your client with a thin\n * wrapper, so a mismatch is a type error rather than silent unbounded\n * keyspace growth.\n *\n * @example ioredis\n * ```ts\n * import Redis from 'ioredis';\n * const redis = new Redis(process.env.REDIS_URL);\n * const client: RedisClient = {\n * get: (k) => redis.get(k),\n * set: (k, v, ttl) => redis.set(k, v, 'EX', ttl),\n * expireNX: (k, s) => redis.expire(k, s, 'NX'),\n * expireGT: (k, s) => redis.expire(k, s, 'GT'),\n * del: (k) => redis.del(k),\n * sadd: (k, ...m) => redis.sadd(k, ...m),\n * srem: (k, ...m) => redis.srem(k, ...m),\n * smembers: (k) => redis.smembers(k),\n * ttl: (k) => redis.ttl(k),\n * execMulti: async (ops) => {\n * const p = redis.multi();\n * for (const op of ops) {\n * switch (op.op) {\n * case 'set': p.set(op.key, op.value, 'EX', op.ttlSeconds); break;\n * case 'sadd': p.sadd(op.key, ...op.members); break;\n * case 'expireNX': p.expire(op.key, op.seconds, 'NX'); break;\n * case 'expireGT': p.expire(op.key, op.seconds, 'GT'); break;\n * case 'del': p.del(op.key); break;\n * }\n * }\n * await p.exec();\n * },\n * };\n * ```\n *\n * @example node-redis v4\n * ```ts\n * import { createClient } from 'redis';\n * const redis = createClient({ url: process.env.REDIS_URL });\n * await redis.connect();\n * const client: RedisClient = {\n * get: (k) => redis.get(k),\n * set: (k, v, ttl) => redis.set(k, v, { EX: ttl }),\n * expireNX: (k, s) => redis.expire(k, s, 'NX'),\n * expireGT: (k, s) => redis.expire(k, s, 'GT'),\n * del: (k) => redis.del(k),\n * sadd: (k, ...m) => redis.sAdd(k, m),\n * srem: (k, ...m) => redis.sRem(k, m),\n * smembers: (k) => redis.sMembers(k),\n * ttl: (k) => redis.ttl(k),\n * };\n * ```\n *\n * @example @upstash/redis\n * ```ts\n * import { Redis } from '@upstash/redis';\n * // automaticDeserialization must be off: the handler stores JSON strings\n * // and parses them itself — auto-parsing would hand back objects.\n * const redis = new Redis({ url, token, automaticDeserialization: false });\n * const client: RedisClient = {\n * get: (k) => redis.get(k),\n * set: (k, v, ttl) => redis.set(k, v, { ex: ttl }),\n * expireNX: (k, s) => redis.expire(k, s, 'NX'),\n * expireGT: (k, s) => redis.expire(k, s, 'GT'),\n * del: (k) => redis.del(k),\n * sadd: (k, ...m) => redis.sadd(k, m[0], ...m.slice(1)),\n * srem: (k, ...m) => redis.srem(k, m[0], ...m.slice(1)),\n * smembers: (k) => redis.smembers(k),\n * ttl: (k) => redis.ttl(k),\n * };\n * ```\n */\nexport type RedisWriteOp =\n | { op: 'set'; key: string; value: string; ttlSeconds: number }\n | { op: 'sadd'; key: string; members: string[] }\n | { op: 'expireNX'; key: string; seconds: number }\n | { op: 'expireGT'; key: string; seconds: number }\n | { op: 'del'; key: string };\n\nexport interface RedisClient {\n /** `GET key` — the stored string, or null when absent. */\n get(key: string): Promise<string | null>;\n /**\n * `SET key value EX ttlSeconds` — store with an expiry. The TTL is a\n * required positional argument so no adapter can forget it.\n */\n set(key: string, value: string, ttlSeconds: number): Promise<unknown>;\n /**\n * `EXPIRE key seconds NX` — set the TTL only if the key has no existing\n * expiry (Redis 7.0+). Used to set the initial TTL on new tag sets.\n */\n expireNX(key: string, seconds: number): Promise<unknown>;\n /**\n * `EXPIRE key seconds GT` — update the TTL only if the new value is greater\n * than the current one (Redis 7.0+). Used to extend tag set TTLs without\n * allowing a short-TTL writer to shrink them.\n */\n expireGT(key: string, seconds: number): Promise<unknown>;\n /** `DEL key` — delete a single key. Multi-key deletes loop per key. */\n del(key: string): Promise<unknown>;\n /** `SADD key member [member ...]` — add members to a set. */\n sadd(key: string, ...members: string[]): Promise<unknown>;\n /** `SREM key member [member ...]` — remove members from a set. */\n srem(key: string, ...members: string[]): Promise<unknown>;\n /** `SMEMBERS key` — all members of a set. */\n smembers(key: string): Promise<string[]>;\n /**\n * `TTL key` — remaining time-to-live in seconds. Returns -2 if the key does\n * not exist, -1 if the key exists but has no expiry.\n */\n ttl(key: string): Promise<number>;\n /**\n * Execute a batch of write operations atomically via `MULTI/EXEC`.\n * When provided, `set()` uses this to make tag-index writes and the data\n * write atomic — no `invalidate({tag})` can interleave. Optional: when\n * absent, operations run sequentially (correct but not race-free).\n */\n execMulti?(ops: RedisWriteOp[]): Promise<void>;\n}\n\nconst KEY_PREFIX = 'timber:cache:';\nconst TAG_PREFIX = 'timber:tag:';\n\n/**\n * Redis-backed CacheHandler for distributed caching.\n *\n * All instances sharing the same Redis see each other's cache entries and\n * invalidations. Tag-based invalidation uses Redis Sets to track which keys\n * belong to which tags.\n *\n * Bring your own Redis client wrapped to the {@link RedisClient} contract —\n * see the adapter examples on the interface for ioredis, node-redis v4, and\n * @upstash/redis.\n */\nexport class RedisCacheHandler implements CacheHandler {\n private client: RedisClient;\n private prefix: string;\n\n constructor(client: RedisClient, opts?: { prefix?: string }) {\n this.client = client;\n this.prefix = opts?.prefix ?? '';\n }\n\n private cacheKey(key: string): string {\n return `${this.prefix}${KEY_PREFIX}${key}`;\n }\n\n private tagKey(tag: string): string {\n return `${this.prefix}${TAG_PREFIX}${tag}`;\n }\n\n async get(key: string): Promise<{ value: unknown; stale: boolean } | null> {\n const raw = await this.client.get(this.cacheKey(key));\n if (raw === null) return null;\n\n const entry = JSON.parse(raw) as { value: unknown; expiresAt: number };\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n }\n\n async set(\n key: string,\n value: unknown,\n opts: { ttl: number; tags: string[]; generation?: number }\n ): Promise<void> {\n warnLossyJsonValue(value, key);\n const ck = this.cacheKey(key);\n const expiresAt = Date.now() + opts.ttl * 1000;\n\n // Serialize upfront so BigInt/non-serializable errors throw before any\n // writes — prevents tag indexes pointing at stale data from a prior set().\n const payload = JSON.stringify({ value, expiresAt, tags: opts.tags });\n\n // Redis TTL with generous margin beyond the logical TTL to allow SWR reads\n // on stale entries. The logical staleness is determined by expiresAt.\n // We use 2x TTL + 60s as the Redis expiry so stale entries remain\n // available for SWR background refetches.\n const redisTtlSeconds = Math.max(opts.ttl * 2 + 60, 120);\n\n const writeOps: RedisWriteOp[] = [];\n for (const tag of opts.tags) {\n writeOps.push({ op: 'sadd', key: this.tagKey(tag), members: [key] });\n writeOps.push({ op: 'expireNX', key: this.tagKey(tag), seconds: redisTtlSeconds });\n writeOps.push({ op: 'expireGT', key: this.tagKey(tag), seconds: redisTtlSeconds });\n }\n writeOps.push({ op: 'set', key: ck, value: payload, ttlSeconds: redisTtlSeconds });\n\n if (this.client.execMulti) {\n await this.client.execMulti(writeOps);\n } else {\n for (const op of writeOps) {\n await this.execOp(op);\n }\n }\n }\n\n private async execOp(op: RedisWriteOp): Promise<void> {\n switch (op.op) {\n case 'set':\n await this.client.set(op.key, op.value, op.ttlSeconds);\n break;\n case 'sadd':\n await this.client.sadd(op.key, ...op.members);\n break;\n case 'expireNX':\n await this.client.expireNX(op.key, op.seconds);\n break;\n case 'expireGT':\n await this.client.expireGT(op.key, op.seconds);\n break;\n case 'del':\n await this.client.del(op.key);\n break;\n }\n }\n\n async invalidate(opts: { key?: string; tag?: string }): Promise<void> {\n if (opts.key) {\n const raw = await this.client.get(this.cacheKey(opts.key));\n if (raw !== null) {\n const entry = JSON.parse(raw) as { tags?: string[] };\n if (entry.tags) {\n await Promise.all(entry.tags.map((tag) => this.client.srem(this.tagKey(tag), opts.key!)));\n }\n }\n await this.client.del(this.cacheKey(opts.key));\n }\n\n if (opts.tag) {\n const tk = this.tagKey(opts.tag);\n const keys = await this.client.smembers(tk);\n\n // Re-check each member before deleting — the tag set can contain stale\n // memberships from entries whose tags changed since they were added.\n // Mirrors TagAwareCacheHandler's eager strategy (tag-aware-handler.ts:183-198).\n await Promise.all(\n keys.map(async (k) => {\n const raw = await this.client.get(this.cacheKey(k));\n if (raw === null) {\n await this.client.srem(tk, k);\n return;\n }\n const entry = JSON.parse(raw) as { tags?: string[] };\n if (entry.tags?.includes(opts.tag!)) {\n await this.client.del(this.cacheKey(k));\n }\n await this.client.srem(tk, k);\n })\n );\n\n // Clean up the now-empty tag set so it doesn't consume memory\n const remaining = await this.client.smembers(tk);\n if (remaining.length === 0) {\n await this.client.del(tk);\n }\n }\n }\n}\n","/**\n * TagAwareCacheHandler — wraps any CacheStore and adds tag-based invalidation.\n *\n * Three strategies, selected by store capabilities (best available wins):\n *\n * 1. **Native** (store has setWithTags + invalidateTag): Delegates entirely.\n * The store owns tag indexing. Used for Vercel Runtime Cache, DO-backed\n * stores, or any platform with first-class tag support.\n *\n * 2. **Eager** (store has sadd/smembers/srem/sdel): Maintains an inverted\n * index via atomic set operations. Invalidation enumerates the set and\n * deletes entries. Used for Redis.\n *\n * 3. **Lazy** (bare get/set/del only): Version-stamps entries with tag\n * counters. Invalidation bumps a counter (O(1)). Reads validate stored\n * versions against current. Race-free, correct under eventual consistency.\n * Used for Cloudflare KV.\n *\n * Implements the CacheHandler interface consumed by timber-cache.ts.\n */\n\nimport type { CacheHandler } from './index.ts';\nimport type { CacheStore } from './store.ts';\n\nconst DATA_PREFIX = 'd:';\nconst TAG_SET_PREFIX = 'ts:';\nconst TAG_VERSION_PREFIX = 'tv:';\n\n/** A store whose optional capability methods are known to be present. */\ntype StoreWith<K extends keyof CacheStore> = CacheStore & Required<Pick<CacheStore, K>>;\ntype NativeStore = StoreWith<'setWithTags' | 'invalidateTag'>;\ntype EagerStore = StoreWith<'sadd' | 'smembers' | 'srem' | 'sdel'>;\n\n/**\n * The detected strategy carries the store narrowed to the capabilities that\n * strategy relies on, so each branch calls those methods without assertions.\n */\ntype Strategy =\n | { kind: 'native'; store: NativeStore }\n | { kind: 'eager'; store: EagerStore }\n | { kind: 'lazy' };\n\n/** Stored entry format for eager strategy. */\ninterface EagerEntry {\n value: unknown;\n expiresAt: number;\n tags: string[];\n}\n\n/** Stored entry format for lazy (version-stamp) strategy. */\ninterface LazyEntry {\n value: unknown;\n expiresAt: number;\n tags: string[];\n tagVersions: Record<string, number>;\n}\n\nfunction isNativeStore(store: CacheStore): store is NativeStore {\n return !!(store.setWithTags && store.invalidateTag);\n}\n\nfunction isEagerStore(store: CacheStore): store is EagerStore {\n return !!(store.sadd && store.smembers && store.srem && store.sdel);\n}\n\nfunction detectStrategy(store: CacheStore): Strategy {\n if (isNativeStore(store)) return { kind: 'native', store };\n if (isEagerStore(store)) return { kind: 'eager', store };\n return { kind: 'lazy' };\n}\n\nexport class TagAwareCacheHandler implements CacheHandler {\n private store: CacheStore;\n private prefix: string;\n private strategy: Strategy;\n\n constructor(store: CacheStore, opts?: { prefix?: string }) {\n this.store = store;\n this.prefix = opts?.prefix ?? 'timber:';\n this.strategy = detectStrategy(store);\n }\n\n get consistency(): 'strong' | 'eventual' | 'local' {\n return this.store.consistency;\n }\n\n private dataKey(key: string): string {\n return `${this.prefix}${DATA_PREFIX}${key}`;\n }\n\n /** Namespace tags for native stores so prefix isolates shared stores. */\n private prefixTags(tags: string[]): string[] {\n return tags.map((t) => `${this.prefix}${t}`);\n }\n\n private tagSetKey(tag: string): string {\n return `${this.prefix}${TAG_SET_PREFIX}${tag}`;\n }\n\n private tagVersionKey(tag: string): string {\n return `${this.prefix}${TAG_VERSION_PREFIX}${tag}`;\n }\n\n async get(key: string): Promise<{ value: unknown; stale: boolean } | null> {\n const raw = await this.store.get(this.dataKey(key));\n if (raw === null) return null;\n\n if (this.strategy.kind !== 'lazy') {\n const entry = JSON.parse(raw) as EagerEntry;\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n }\n\n // Lazy: validate tag versions before returning\n const entry = JSON.parse(raw) as LazyEntry;\n if (entry.tags.length > 0) {\n const currentVersions = await this.getTagVersions(entry.tags);\n for (const tag of entry.tags) {\n const stored = entry.tagVersions[tag] ?? 0;\n const current = currentVersions[tag] ?? 0;\n if (current > stored) return null;\n }\n }\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n }\n\n async set(\n key: string,\n value: unknown,\n opts: { ttl: number; tags: string[]; generation?: number }\n ): Promise<void> {\n const physicalTtl = Math.max(opts.ttl * 2 + 60, 120);\n const strategy = this.strategy;\n\n if (strategy.kind === 'native') {\n const entry: EagerEntry = {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n };\n const payload = JSON.stringify(entry);\n if (opts.tags.length > 0) {\n await strategy.store.setWithTags(\n this.dataKey(key),\n payload,\n physicalTtl,\n this.prefixTags(opts.tags)\n );\n } else {\n await this.store.set(this.dataKey(key), payload, physicalTtl);\n }\n return;\n }\n\n if (strategy.kind === 'eager') {\n const entry: EagerEntry = {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n };\n await this.store.set(this.dataKey(key), JSON.stringify(entry), physicalTtl);\n if (opts.tags.length > 0) {\n await Promise.all(\n opts.tags.map((tag) => strategy.store.sadd(this.tagSetKey(tag), [key], physicalTtl))\n );\n }\n return;\n }\n\n // Lazy: snapshot current tag versions into the entry\n const tagVersions = opts.tags.length > 0 ? await this.getTagVersions(opts.tags) : {};\n const entry: LazyEntry = {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n tagVersions,\n };\n await this.store.set(this.dataKey(key), JSON.stringify(entry), physicalTtl);\n }\n\n async invalidate(opts: { key?: string; tag?: string }): Promise<void> {\n const strategy = this.strategy;\n\n if (opts.key) {\n const key = opts.key;\n if (strategy.kind === 'eager') {\n const raw = await this.store.get(this.dataKey(key));\n if (raw !== null) {\n const entry = JSON.parse(raw) as EagerEntry;\n if (entry.tags.length > 0) {\n await Promise.all(\n entry.tags.map((tag) => strategy.store.srem(this.tagSetKey(tag), [key]))\n );\n }\n }\n }\n await this.store.del(this.dataKey(key));\n }\n\n if (opts.tag) {\n const tag = opts.tag;\n if (strategy.kind === 'native') {\n await strategy.store.invalidateTag(`${this.prefix}${tag}`);\n } else if (strategy.kind === 'eager') {\n const members = await strategy.store.smembers(this.tagSetKey(tag));\n // Verify each member still carries the tag before deleting — the set\n // can contain stale memberships from overwritten entries or LRU eviction.\n // Remove only the snapshot members (via srem, not sdel) so concurrent\n // writes that add new members between smembers() and cleanup are preserved.\n await Promise.all(\n members.map(async (k) => {\n const raw = await this.store.get(this.dataKey(k));\n if (raw !== null && (JSON.parse(raw) as EagerEntry).tags.includes(tag)) {\n await this.store.del(this.dataKey(k));\n }\n await strategy.store.srem(this.tagSetKey(tag), [k]);\n })\n );\n } else {\n await this.bumpTagVersion(opts.tag);\n }\n }\n }\n\n // --- Lazy strategy helpers ---\n\n private async getTagVersions(tags: string[]): Promise<Record<string, number>> {\n const versions: Record<string, number> = {};\n await Promise.all(\n tags.map(async (tag) => {\n const raw = await this.store.get(this.tagVersionKey(tag));\n versions[tag] = raw !== null ? parseInt(raw, 10) : 0;\n })\n );\n return versions;\n }\n\n private async bumpTagVersion(tag: string): Promise<void> {\n const key = this.tagVersionKey(tag);\n const raw = await this.store.get(key);\n const stored = raw !== null ? parseInt(raw, 10) : 0;\n // max(stored, now) + 1: strictly advancing even within the same\n // millisecond (two bumps in 1ms produce N, N+1 not N, N). On\n // eventually-consistent stores the read can be stale, but Date.now()\n // provides a floor — regression only if BOTH the read is stale AND the\n // clock is behind, bounded by the store's propagation window (~60s).\n const version = Math.max(stored, Date.now()) + 1;\n // Version keys must outlive all data entries stamped with them. Data\n // entries use physical TTL of max(ttl*2+60, 120); 365 days exceeds any\n // practical cache lifetime.\n await this.store.set(key, String(version), 365 * 86400);\n }\n}\n","// @timber-js/app/cache — Caching primitives\n\nimport { estimateByteSize } from './sizeof.ts';\n\nexport interface CacheHandler {\n get(key: string): Promise<{ value: unknown; stale: boolean } | null>;\n set(\n key: string,\n value: unknown,\n opts: { ttl: number; tags: string[]; generation?: number }\n ): Promise<void>;\n invalidate(opts: { key?: string; tag?: string }): Promise<void>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport interface CacheOptions<Fn extends (...args: any[]) => any> {\n ttl: number;\n key?: (...args: Parameters<Fn>) => string;\n staleWhileRevalidate?: boolean;\n tags?: string[] | ((...args: Parameters<Fn>) => string[]);\n /** Timeout (ms) for singleflight-coalesced calls. Prevents hung fn() from\n * permanently blocking all future callers for the same cache key. See TIM-518. */\n timeoutMs?: number;\n}\n\nexport interface MemoryCacheHandlerOptions {\n /** Maximum number of entries. Oldest accessed entries are evicted first. Default: 1000. */\n maxEntries?: number;\n /**\n * @deprecated Use `maxEntries` instead. Will be removed in a future release.\n * Alias for `maxEntries` — maximum number of entries (not bytes).\n */\n maxSize?: number;\n /** Maximum total byte budget for all cached values. Oldest entries are evicted when exceeded. Default: no limit. */\n maxBytes?: number;\n /** Maximum byte size for a single cache entry. Entries exceeding this are silently dropped. Default: no limit. */\n maxEntryBytes?: number;\n}\n\nexport class MemoryCacheHandler implements CacheHandler {\n private store = new Map<\n string,\n { value: unknown; expiresAt: number; tags: string[]; byteSize: number }\n >();\n private generations = new Map<string, number>();\n private maxEntries: number;\n private maxBytes: number | undefined;\n private maxEntryBytes: number | undefined;\n private currentBytes = 0;\n private readonly _trackBytes: boolean;\n\n constructor(opts?: MemoryCacheHandlerOptions) {\n // maxEntries takes precedence over deprecated maxSize\n this.maxEntries = opts?.maxEntries ?? opts?.maxSize ?? 1000;\n this.maxBytes = opts?.maxBytes;\n this.maxEntryBytes = opts?.maxEntryBytes;\n this._trackBytes = this.maxBytes !== undefined || this.maxEntryBytes !== undefined;\n }\n\n async get(key: string) {\n const entry = this.store.get(key);\n if (!entry) return null;\n\n // Move to end of Map (most recently used) for LRU ordering\n this.store.delete(key);\n this.store.set(key, entry);\n\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n }\n\n async set(\n key: string,\n value: unknown,\n opts: { ttl: number; tags: string[]; generation?: number }\n ) {\n // CAS guard: skip write if a newer generation has already been written\n if (opts.generation !== undefined) {\n const current = this.generations.get(key) ?? 0;\n if (opts.generation < current) return;\n }\n\n const byteSize = this._trackBytes ? estimateByteSize(value) : 0;\n\n // Reject entries exceeding per-entry byte limit\n if (this.maxEntryBytes !== undefined && byteSize > this.maxEntryBytes) {\n return;\n }\n\n // If key already exists, delete first to refresh insertion order and reclaim bytes\n if (this.store.has(key)) {\n const existing = this.store.get(key)!;\n this.currentBytes -= existing.byteSize;\n this.store.delete(key);\n }\n\n // Evict oldest entries (front of Map) if at entry count capacity\n while (this.store.size >= this.maxEntries) {\n this.evictOldest();\n }\n\n // Evict oldest entries if byte budget would be exceeded\n if (this.maxBytes !== undefined) {\n while (this.currentBytes + byteSize > this.maxBytes && this.store.size > 0) {\n this.evictOldest();\n }\n // If the single entry exceeds the total byte budget, don't store it\n if (this.currentBytes + byteSize > this.maxBytes) {\n return;\n }\n }\n\n // Record generation only after all admission checks pass\n if (opts.generation !== undefined) {\n this.generations.set(key, opts.generation);\n }\n\n this.store.set(key, {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n byteSize,\n });\n this.currentBytes += byteSize;\n }\n\n async invalidate(opts: { key?: string; tag?: string }) {\n if (opts.key) {\n const entry = this.store.get(opts.key);\n if (entry) {\n this.currentBytes -= entry.byteSize;\n this.store.delete(opts.key);\n }\n this.generations.delete(opts.key);\n }\n if (opts.tag) {\n for (const [key, entry] of this.store) {\n if (entry.tags.includes(opts.tag)) {\n this.currentBytes -= entry.byteSize;\n this.store.delete(key);\n this.generations.delete(key);\n }\n }\n }\n }\n\n /** Number of entries currently in the cache. */\n get size(): number {\n return this.store.size;\n }\n\n /** Estimated total byte size of all cached values. Only tracked when maxBytes or maxEntryBytes is configured; returns 0 otherwise. */\n get bytes(): number {\n return this.currentBytes;\n }\n\n /** Evict the oldest entry (front of Map). */\n private evictOldest(): void {\n const oldest = this.store.keys().next().value;\n if (oldest !== undefined) {\n const entry = this.store.get(oldest)!;\n this.currentBytes -= entry.byteSize;\n this.store.delete(oldest);\n this.generations.delete(oldest);\n }\n }\n}\n\nexport { RedisCacheHandler } from './redis-handler.ts';\nexport type { RedisClient, RedisWriteOp } from './redis-handler.ts';\nexport { cache } from './cache-api.ts';\nexport { setCacheHandler, getCacheHandler } from './handler-store.ts';\nexport { setDefaultSingleflightTimeout } from './timber-cache.ts';\n// NOTE: registerCachedFunction (runtime for 'use cache' directive) removed.\n// Future feature pending design doc. See design/06-caching.md.\nexport { estimateByteSize } from './sizeof.ts';\n\n// --- Cache Store layering (design/46) ---\nexport { TagAwareCacheHandler } from './tag-aware-handler.ts';\nexport type { CacheStore } from './store.ts';\n// stableStringify, createSingleflight, and SingleflightTimeoutError are\n// internal utilities used by the cache implementation. They are not\n// re-exported here — import directly from the source files if needed\n// within the package. See TIM-720.\n","/**\n * Version Skew Detection — graceful recovery when stale clients hit new deployments.\n *\n * When a new version of the app is deployed, clients with open tabs still have\n * the old JavaScript bundle. Without version skew handling, these stale clients\n * will experience:\n *\n * 1. Server action calls that crash (action IDs are content-hashed)\n * 2. Chunk load failures (old filenames gone from CDN)\n * 3. RSC payload mismatches (component references differ between builds)\n *\n * This module implements deployment ID comparison:\n * - A per-build deployment ID is generated at build time (see build-manifest.ts)\n * - The client sends it via `X-Timber-Deployment-Id` header on every RSC/action request\n * - The server compares it against the current build's ID\n * - On mismatch: signal the client to reload (not crash)\n *\n * The deployment ID is always-on in production. Dev mode skips the check\n * (HMR handles code updates without full reloads).\n *\n * See design/25-production-deployments.md, TIM-446\n */\n\n// ─── Constants ───────────────────────────────────────────────────\n\n/** Header sent by the client with every RSC/action request. */\nexport const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';\n\n/** Response header that signals the client to do a full page reload. */\nexport const RELOAD_HEADER = 'X-Timber-Reload';\n\n// ─── Deployment ID ───────────────────────────────────────────────\n\n/**\n * The current build's deployment ID. Set at startup from the runtime config\n * (`virtual:timber-config`'s `deploymentId`). Null in dev mode.\n */\nlet currentDeploymentId: string | null = null;\n\n/**\n * Set the current deployment ID. Called once at server startup by the RSC\n * entry's `createRequestHandler()`. In dev mode this is never called\n * (deployment ID checks are skipped).\n */\nexport function setDeploymentId(id: string): void {\n currentDeploymentId = id;\n}\n\n/**\n * Get the current deployment ID. Returns null in dev mode.\n */\nexport function getDeploymentId(): string | null {\n return currentDeploymentId;\n}\n\n// ─── Skew Detection ──────────────────────────────────────────────\n\n/** Result of a version skew check. */\nexport interface SkewCheckResult {\n /** Whether the client's deployment ID matches the server's. */\n ok: boolean;\n /** The client's deployment ID (null if header not sent — e.g., initial page load). */\n clientId: string | null;\n}\n\n/**\n * Check if a request's deployment ID matches the current build.\n *\n * Returns `{ ok: true }` when:\n * - Dev mode (no deployment ID set — HMR handles updates)\n * - No deployment ID header (initial page load, non-RSC request)\n * - Deployment IDs match\n *\n * Returns `{ ok: false }` when:\n * - Client sends a deployment ID that differs from the current build\n */\nexport function checkVersionSkew(req: Request): SkewCheckResult {\n // Dev mode — no deployment ID checks (HMR handles updates)\n if (!currentDeploymentId) {\n return { ok: true, clientId: null };\n }\n\n const clientId = req.headers.get(DEPLOYMENT_ID_HEADER);\n\n // No header — initial page load or non-RSC request. Always OK.\n if (!clientId) {\n return { ok: true, clientId: null };\n }\n\n // Compare deployment IDs\n if (clientId === currentDeploymentId) {\n return { ok: true, clientId };\n }\n\n return { ok: false, clientId };\n}\n\n/**\n * Apply version skew reload headers to a response.\n * Sets X-Timber-Reload: 1 to signal the client to do a full page reload.\n */\nexport function applyReloadHeaders(headers: Headers): void {\n headers.set(RELOAD_HEADER, '1');\n}\n","/**\n * Manifest tag index — maps tags to seed cache keys for tombstone writes.\n *\n * When `revalidateTag(tag)` fires, the framework must write tombstones for\n * every build-seed entry carrying that tag. The manifest records tags per\n * entry; this module inverts the index at startup so the lookup is O(1) per\n * tag instead of a full manifest scan per invalidation.\n *\n * The index is built lazily on first access and cached for the process\n * lifetime — the manifest is immutable within a deploy.\n *\n * See design/45-cache-lifetimes.md §ISR mechanics.\n */\n\nimport type { PrebuiltManifest } from './payload-source.ts';\n\nexport interface SeedKeyInfo {\n componentId: string;\n cacheKey: string;\n}\n\n/**\n * Build the inverted tag → seed key index from the manifest.\n *\n * Returns a Map where each tag maps to the set of (componentId, cacheKey)\n * pairs whose manifest entries carry that tag. Only entries with non-empty\n * `tags` arrays participate.\n */\nexport function buildTagIndex(manifest: PrebuiltManifest): Map<string, SeedKeyInfo[]> {\n const index = new Map<string, SeedKeyInfo[]>();\n for (const [componentId, component] of Object.entries(manifest)) {\n if (!component || typeof component !== 'object' || !('entries' in component)) continue;\n for (const [cacheKey, entry] of Object.entries(component.entries)) {\n if (!entry.tags || entry.tags.length === 0) continue;\n for (const tag of entry.tags) {\n let list = index.get(tag);\n if (!list) {\n list = [];\n index.set(tag, list);\n }\n list.push({ componentId, cacheKey });\n }\n }\n }\n return index;\n}\n","/**\n * Prebuilt payload source — the adapter seam through which the runtime\n * reads build-time flight artifacts (`.timber/dist/prebuilt/`).\n *\n * The runtime never touches the filesystem directly: adapters register a\n * source at startup (Node: fs reads; Cloudflare: the assets binding —\n * design/44-render-at-build-time.md §Adapter Integration). Artifacts are\n * produced by the post-build capture pass (TIM-1119, prebuilt-builder.ts);\n * adapter registration of the source is TIM-1122. Until a source is\n * registered (adapter not wired yet, dev mode, no artifacts built), every\n * lookup misses and cache.component wrappers render live — the designed\n * dynamic-fallback behavior.\n *\n * Phase 3 (design/45 runtime tier) inserts the CacheHandler overlay lookup\n * in front of `lookupPrebuiltPayload` — overlay first, then artifact. The\n * single-lookup-function shape here is that seam.\n */\n\nimport { initTagIndex, resetTagIndex } from './overlay.ts';\nimport type { CoercedParams } from '../../shared/param-value.ts';\n\nexport interface PrebuiltManifestEntry {\n /** Segment params this entry was captured under. */\n params: CoercedParams;\n /** Payload path relative to the prebuilt/ directory. */\n file: string;\n /** Payload size in bytes (build report / diagnostics). */\n bytes?: number;\n /** Runtime invalidation tags (design/45 ISR tier). */\n tags?: string[];\n /** Epoch ms when this entry was captured (seed freshness for ISR). */\n buildTimestamp?: number;\n}\n\nexport interface PrebuiltManifest {\n [componentId: string]: {\n entries: Record<string, PrebuiltManifestEntry>;\n /** When true, the component does not read segment params — the cache key uses `{}`. */\n paramIndependent?: boolean;\n };\n}\n\nexport interface PrebuiltPayloadSource {\n /** Load prebuilt/manifest.json. Null when no artifacts exist. */\n loadManifest(): Promise<PrebuiltManifest | null>;\n /** Read a `.flight` payload by manifest file path. Null when missing. */\n readPayload(file: string): Promise<Uint8Array | null>;\n}\n\nlet source: PrebuiltPayloadSource | null = null;\nlet manifestPromise: Promise<PrebuiltManifest | null> | null = null;\n\n/**\n * Ceiling on the memoized manifest load. Without it, a stalled\n * `loadManifest()` would leave `manifestPromise` pending forever and every\n * subsequent cache.component render would await the same hung promise —\n * the coalesced-promise deadlock class from design/13-security.md (A5).\n * On expiry the load memoizes as null (permanent dynamic fallback, same\n * policy as a failed load) and callers proceed immediately.\n */\nconst MANIFEST_LOAD_TIMEOUT_MS = 5_000;\n\n/**\n * Per-read ceiling on payload reads. The read is awaited inside a server\n * component render — a stalled adapter promise would otherwise hang the\n * RSC stream until the render timeout kills the whole page, when the\n * correct degradation is a miss → live render (codex review round 4 on\n * PR #835).\n */\nconst PAYLOAD_READ_TIMEOUT_MS = 5_000;\n\n/** Race a source promise against a timeout that resolves to `fallback`. */\nasync function withTimeout<T>(\n work: Promise<T>,\n timeoutMs: number,\n fallback: T,\n onTimeout: () => void\n): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n work,\n new Promise<T>((resolve) => {\n timer = setTimeout(() => {\n onTimeout();\n resolve(fallback);\n }, timeoutMs);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function loadManifestWithTimeout(\n src: PrebuiltPayloadSource\n): Promise<PrebuiltManifest | null> {\n let manifest: PrebuiltManifest | null;\n try {\n manifest = await withTimeout(src.loadManifest(), MANIFEST_LOAD_TIMEOUT_MS, null, () => {\n console.error(\n `[timber] prebuilt manifest load exceeded ${MANIFEST_LOAD_TIMEOUT_MS}ms — all cache.component lookups will render dynamically`\n );\n });\n } catch (error) {\n console.error(\n '[timber] failed to load prebuilt manifest — all cache.component lookups will miss:',\n error\n );\n return null;\n }\n if (manifest) {\n initTagIndex(manifest);\n }\n return manifest;\n}\n\n/**\n * Register the payload source. Called once at server startup by the\n * adapter/entry wiring (TIM-1122); tests inject fakes. Passing null\n * unregisters (and drops the memoized manifest).\n */\nexport function setPrebuiltPayloadSource(next: PrebuiltPayloadSource | null): void {\n source = next;\n manifestPromise = null;\n if (!next) resetTagIndex();\n}\n\n/**\n * Whether a source is registered — the gate for both marker emission and\n * splice-transform installation. False means zero prebuilt overhead.\n */\nexport function hasPrebuiltPayloadSource(): boolean {\n return source !== null;\n}\n\n/**\n * Ensure the manifest is loaded (and the tag index initialized). No-op\n * when no source is registered. Used by the tombstone path to avoid\n * skipping tombstones on cold processes where the first request is an\n * action rather than a page render.\n */\nexport async function ensureManifestLoaded(): Promise<void> {\n if (source === null) return;\n manifestPromise ??= loadManifestWithTimeout(source);\n await manifestPromise;\n}\n\n/**\n * Look up a manifest entry (metadata only, no payload read).\n * Used by the overlay to check seed existence and freshness.\n */\nexport async function isParamIndependent(componentId: string): Promise<boolean> {\n if (source === null) return false;\n manifestPromise ??= loadManifestWithTimeout(source);\n const manifest = await manifestPromise;\n return manifest?.[componentId]?.paramIndependent === true;\n}\n\nexport async function getManifestEntry(\n componentId: string,\n cacheKey: string\n): Promise<PrebuiltManifestEntry | null> {\n if (source === null) return null;\n manifestPromise ??= loadManifestWithTimeout(source);\n const manifest = await manifestPromise;\n return manifest?.[componentId]?.entries?.[cacheKey] ?? null;\n}\n\n/**\n * Look up a cached payload: manifest entry → payload bytes.\n * Null on any miss or read failure — callers fall back to live render.\n *\n * The manifest is loaded once and memoized (design/44 §manifest.json:\n * loaded at startup, held in memory; payloads read on demand). A failed\n * manifest load is memoized as null rather than retried per-request — a\n * broken artifact should degrade to dynamic rendering, not add a failing\n * read to every request.\n */\nexport async function lookupPrebuiltPayload(\n componentId: string,\n cacheKey: string\n): Promise<Uint8Array | null> {\n if (source === null) return null;\n manifestPromise ??= loadManifestWithTimeout(source);\n const manifest = await manifestPromise;\n const entry = manifest?.[componentId]?.entries?.[cacheKey];\n if (!entry) return null;\n try {\n return await withTimeout(source.readPayload(entry.file), PAYLOAD_READ_TIMEOUT_MS, null, () => {\n console.error(\n `[timber] prebuilt payload read for \"${entry.file}\" exceeded ${PAYLOAD_READ_TIMEOUT_MS}ms — rendering dynamically`\n );\n });\n } catch (error) {\n console.error(\n `[timber] failed to read prebuilt payload \"${entry.file}\" for ${componentId} — rendering dynamically:`,\n error\n );\n return null;\n }\n}\n","/**\n * Component cache overlay — runtime CacheHandler layer for ISR.\n *\n * Sits in front of the immutable build artifact (`.flight` files) and\n * stores replacement payloads and tombstones via the configured\n * CacheHandler. Overlay entries are per-deploy namespaced:\n * `timber:c:{deploymentId}:{componentId}:{hash}`\n *\n * The CacheHandler contract is untouched — tombstones and the tag index\n * live above the interface as value conventions. Redis, KV, and custom\n * handlers work unmodified.\n *\n * See design/45-cache-lifetimes.md §ISR mechanics.\n */\n\nimport { getCacheHandler, isCacheHandlerConfigured } from '../../cache/handler-store.ts';\nimport { MemoryCacheHandler } from '../../cache/index.ts';\nimport { getDeploymentId } from '../version-skew.ts';\nimport { buildTagIndex, type SeedKeyInfo } from './manifest-tag-index.ts';\nimport type { PrebuiltManifest } from './payload-source.ts';\nimport { ensureManifestLoaded } from './payload-source.ts';\n\n/** Default ceiling TTL for tag-only entries (24 hours), in seconds. */\nconst CEILING_TTL_SECONDS = 24 * 60 * 60;\n\n/** Deploy-long physical TTL for seed-backed overlay records (90 days). */\nconst DEPLOY_LONG_TTL_SECONDS = 90 * 24 * 60 * 60;\n\n// ─── Overlay record shapes (value conventions above CacheHandler) ────────\n\n/**\n * A cached component flight payload stored in the overlay.\n * The `_t` discriminant distinguishes this from a tombstone.\n */\nexport interface ComponentOverlayRecord {\n _t: 'co';\n /** Base64-encoded flight payload bytes. */\n p: string;\n /** Logical freshness deadline (Date.now() + ttl * 1000). */\n fu: number;\n /** Whether a build-artifact seed exists for this key. */\n s: boolean;\n}\n\n/**\n * A tombstone marking a build seed as invalidated.\n * Written by revalidateTag for seeds carrying the invalidated tag.\n */\nexport interface ComponentTombstoneRecord {\n _t: 'ct';\n}\n\ntype OverlayValue = ComponentOverlayRecord | ComponentTombstoneRecord;\n\nexport function isOverlayRecord(v: unknown): v is ComponentOverlayRecord {\n return v !== null && typeof v === 'object' && (v as Record<string, unknown>)._t === 'co';\n}\n\nexport function isTombstone(v: unknown): v is ComponentTombstoneRecord {\n return v !== null && typeof v === 'object' && (v as Record<string, unknown>)._t === 'ct';\n}\n\n// ─── Overlay key generation ──────────────────────────────────────────────\n\nexport function overlayKey(componentId: string, cacheKey: string): string {\n const deploymentId = getDeploymentId() ?? 'dev';\n return `timber:c:${deploymentId}:${componentId}:${cacheKey}`;\n}\n\n// ─── Overlay read ────────────────────────────────────────────────────────\n\nexport interface OverlayLookupResult {\n kind: 'fresh' | 'stale' | 'tombstone';\n /** The overlay value (null for tombstones). */\n record: ComponentOverlayRecord | null;\n /** Raw flight payload bytes (decoded from base64). Null for tombstones. */\n payload: Uint8Array | null;\n}\n\n/**\n * Look up an overlay entry in the cache handler.\n * Returns null on a clean miss (no overlay entry at all).\n */\nexport async function lookupOverlay(\n componentId: string,\n cacheKey: string\n): Promise<OverlayLookupResult | null> {\n const handler = getCacheHandler();\n const key = overlayKey(componentId, cacheKey);\n const result = await handler.get(key);\n if (!result) return null;\n\n const value = result.value as OverlayValue;\n if (isTombstone(value)) {\n return { kind: 'tombstone', record: null, payload: null };\n }\n if (isOverlayRecord(value)) {\n const logicallyStale = Date.now() > value.fu;\n return {\n kind: logicallyStale ? 'stale' : 'fresh',\n record: value,\n payload: base64ToBytes(value.p),\n };\n }\n // Unknown value shape — treat as miss (forward compatibility).\n return null;\n}\n\n// ─── Overlay write ───────────────────────────────────────────────────────\n\n/**\n * Resolve the effective TTL for a component's runtime lifetime.\n * Tag-only entries (no explicit ttl) get the ceiling TTL (design/45\n * §Tag-only entries carry a ceiling TTL).\n */\nexport function effectiveTtl(ttl: number | undefined): number {\n return ttl ?? CEILING_TTL_SECONDS;\n}\n\n/**\n * Store a component flight payload in the overlay.\n */\nexport async function storeOverlayEntry(\n componentId: string,\n cacheKey: string,\n payloadBytes: Uint8Array,\n opts: {\n ttl?: number;\n tags: string[];\n hasSeed: boolean;\n }\n): Promise<void> {\n const handler = getCacheHandler();\n const key = overlayKey(componentId, cacheKey);\n const logicalTtl = effectiveTtl(opts.ttl);\n const physicalTtl = opts.hasSeed ? DEPLOY_LONG_TTL_SECONDS : logicalTtl;\n\n const record: ComponentOverlayRecord = {\n _t: 'co',\n p: bytesToBase64(payloadBytes),\n fu: Date.now() + logicalTtl * 1000,\n s: opts.hasSeed,\n };\n\n await handler.set(key, record, { ttl: physicalTtl, tags: opts.tags });\n}\n\n// ─── Tombstone writes ────────────────────────────────────────────────────\n\nlet tagIndex: Map<string, SeedKeyInfo[]> | null = null;\n\n/**\n * Initialize the tag index from the prebuilt manifest. Called once when the\n * manifest is loaded. Without a manifest (no prebuilt components), this is\n * never called and revalidateTag skips the tombstone path entirely.\n */\nexport function initTagIndex(manifest: PrebuiltManifest): void {\n tagIndex = buildTagIndex(manifest);\n emitIsrDiagnosticIfNeeded(manifest);\n}\n\n/**\n * Get the tag index, if initialized.\n * @internal — exported for testing.\n */\nexport function getTagIndex(): Map<string, SeedKeyInfo[]> | null {\n return tagIndex;\n}\n\n/**\n * Reset the tag index (for tests).\n * @internal\n */\nexport function resetTagIndex(): void {\n tagIndex = null;\n}\n\n/**\n * Write tombstones for all seed entries carrying the given tag.\n * Called from cache.invalidate (cache-api.ts) AFTER the handler's own\n * invalidation clears overlay entries.\n *\n * The tombstone prevents the immutable seed from being served after\n * invalidation — without it, a handler.invalidate that clears the overlay\n * would expose the (now-stale) build artifact on the next request.\n *\n * Ensures the manifest is loaded first so cold-process actions that\n * invalidate before any page render still write tombstones.\n */\nexport async function writeTombstonesForTag(tag: string): Promise<void> {\n if (!tagIndex) {\n await ensureManifestLoaded();\n }\n if (!tagIndex) return;\n const seeds = tagIndex.get(tag);\n if (!seeds || seeds.length === 0) return;\n\n const handler = getCacheHandler();\n const tombstone: ComponentTombstoneRecord = { _t: 'ct' };\n\n await Promise.all(\n seeds.map((seed) => {\n const key = overlayKey(seed.componentId, seed.cacheKey);\n return handler.set(key, tombstone, {\n ttl: DEPLOY_LONG_TTL_SECONDS,\n tags: [tag],\n });\n })\n );\n}\n\n// ─── ISR build diagnostic ────────────────────────────────────────────────\n\nlet isrDiagnosticEmitted = false;\n\nfunction emitIsrDiagnosticIfNeeded(manifest: PrebuiltManifest): void {\n if (isrDiagnosticEmitted) return;\n\n const handler = getCacheHandler();\n const isMemoryBased = handler instanceof MemoryCacheHandler || !isCacheHandlerConfigured();\n if (!isMemoryBased) return;\n\n for (const [, component] of Object.entries(manifest)) {\n if (!component || typeof component !== 'object' || !('entries' in component)) continue;\n for (const [, entry] of Object.entries(component.entries)) {\n if (entry.tags && entry.tags.length > 0) {\n isrDiagnosticEmitted = true;\n console.warn(\n '[timber] tag revalidation of prerendered components requires a shared cache ' +\n 'handler in multi-instance deployments. The current MemoryCacheHandler is ' +\n 'per-process — revalidateTag on one instance will not purge the build seed ' +\n 'on others. Configure a shared handler (Redis, KV) for production ISR.'\n );\n return;\n }\n }\n }\n}\n\n/**\n * Reset the diagnostic flag (for tests).\n * @internal\n */\nexport function resetIsrDiagnostic(): void {\n isrDiagnosticEmitted = false;\n}\n\n// ─── Encoding helpers ────────────────────────────────────────────────────\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString('base64');\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n return new Uint8Array(Buffer.from(b64, 'base64'));\n}\n","import type { CacheOptions } from './index.ts';\nimport type { PrebuiltComponentOptions } from '../server/prebuilt-runtime.ts';\nimport { createCache } from './timber-cache.ts';\nimport { getCacheHandler } from './handler-store.ts';\nimport { recordInvalidation } from './invalidation-epoch.ts';\nimport { writeTombstonesForTag } from '../server/prebuilt/overlay.ts';\nimport { getCdnPurgeHandler } from '../cdn/purge-store.ts';\n\nconst componentFallbackWarned = new WeakSet<object>();\n\n/**\n * Cache namespace — `cache.data`, `cache.component`, `cache.invalidate`.\n *\n * ```ts\n * import { cache } from '@timber-js/app/cache';\n *\n * const getUser = cache.data(\n * async (id: string) => db.users.findUnique({ where: { id } }),\n * { ttl: 60, tags: (id) => [`user:${id}`] }\n * );\n * ```\n */\nexport const cache = {\n /**\n * Wrap an async function with cross-request caching.\n *\n * The configured cache handler (defaults to MemoryCacheHandler, overridable\n * via timber.config.ts) is re-resolved on every call, so module-scope\n * wrappers created before `setCacheHandler()` still pick up the configured\n * handler (TIM-1029).\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n data<Fn extends (...args: any[]) => Promise<any>>(\n fn: Fn,\n opts: CacheOptions<Fn>,\n stableId?: string\n ): Fn {\n return createCache(fn, opts, getCacheHandler, stableId);\n },\n\n /**\n * Invalidate cache entries by tag or key.\n *\n * ```ts\n * cache.invalidate({ tag: 'products' });\n * cache.invalidate({ key: 'user:abc' });\n * ```\n */\n async invalidate(opts: { key?: string; tag?: string }): Promise<void> {\n // Record before the handler delete so a cached fn in flight skips its\n // post-resolve set instead of resurrecting the invalidated value (TIM-1028).\n recordInvalidation(opts);\n await getCacheHandler().invalidate(opts);\n // Write tombstones for build-seed entries carrying the tag (design/45\n // §ISR mechanics). Runs AFTER handler.invalidate so the tombstone\n // overwrites the cleared overlay slot. This covers ALL invalidation\n // paths — actions, webhooks, route handlers — not just executeAction.\n if (opts.tag) {\n await writeTombstonesForTag(opts.tag);\n }\n // Trigger CDN purge if a handler is configured (design/25 §Layer 2).\n // Purge is best-effort: a CDN API failure or timeout must not block\n // cache invalidation. 10s ceiling prevents a hung CDN API from stalling\n // the entire revalidateTag/action flow.\n if (opts.tag) {\n const purgeHandler = getCdnPurgeHandler();\n if (purgeHandler) {\n try {\n await Promise.race([\n purgeHandler.purgeTags([opts.tag]),\n new Promise<void>((_, reject) =>\n setTimeout(() => reject(new Error('CDN purge timed out (10s)')), 10_000)\n ),\n ]);\n } catch (err) {\n console.error('[timber] CDN purge failed for tag:', err);\n }\n }\n }\n },\n\n /**\n * Runtime fallback for `cache.component(...)` callsites the timber-prebuilt\n * transform does not rewrite — nested/computed/argument-position forms (see\n * plugins/prebuilt.ts). Only module-scope `const X = cache.component(...)`\n * declarations are transformed and eligible for prebuilding; everything else\n * lands here, warns once per component, and renders dynamically. Same\n * safety-net pattern as the data cache's runtime fnId fallback (TIM-1054):\n * an untraceable callsite degrades in performance, never in correctness.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n component<C extends (props: any) => unknown>(\n componentFn: C,\n _options?: PrebuiltComponentOptions\n ): C {\n if (!componentFallbackWarned.has(componentFn)) {\n componentFallbackWarned.add(componentFn);\n const name = componentFn.name || 'anonymous component';\n console.warn(\n `[timber] cache.component: \"${name}\" was not transformed at build time — ` +\n `rendering dynamically. Only module-scope declarations ` +\n `(const X = cache.component(...)) are prebuilt.`\n );\n }\n return componentFn;\n },\n};\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
2
|
import { a as isDynamicMetadataExtension, s as isStaticMetadataExtension } from "./metadata-routes-DSDjM_hJ.js";
|
|
3
|
-
import { t as isRouteConventionFile } from "./scanner-
|
|
3
|
+
import { t as isRouteConventionFile } from "./scanner-Bw0oq1HB.js";
|
|
4
4
|
import { normalizePath, parseAst } from "vite";
|
|
5
5
|
import { basename, extname, isAbsolute, relative } from "node:path";
|
|
6
6
|
import { hasDirective } from "@vitejs/plugin-rsc/transforms";
|
|
@@ -410,4 +410,4 @@ function formatChains(chains, projectRoot) {
|
|
|
410
410
|
//#endregion
|
|
411
411
|
export { detectDirective as a, classifyModules as i, stitchImportChains as n, langForFile as o, classifyModule as r, chains_exports as t };
|
|
412
412
|
|
|
413
|
-
//# sourceMappingURL=chains-
|
|
413
|
+
//# sourceMappingURL=chains-CpFg56UB.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chains-B4bgOl2g.js","names":[],"sources":["../../src/analyze/classify.ts","../../src/analyze/chains.ts"],"sourcesContent":["/**\n * classify — environment-graph classification for project modules.\n *\n * The one classification module every frontend shares (design/47): the\n * `timber graph` CLI, the dev-server endpoint, the VS Code extension,\n * and the cold crawl must never disagree about a file's environment.\n * Classification intersects RSC/SSR module-graph membership with\n * first-directive detection to produce the decided taxonomy:\n *\n * - `server` — RSC graph only\n * - `client-boundary` — has `'use client'`\n * - `client-internal` — reachable only behind a boundary, no directive\n * - `shared` — instantiated in both environments; INFO, never a\n * warning — shared-by-design is a legitimate pattern\n * - `server-action` — has `'use server'`\n * - `unreferenced` — in no graph and no provisional signal; files with\n * a directive or filename convention get that\n * provisional kind instead, marked `pending`\n *\n * The browser (`client`) Vite graph stays empty until a real browser\n * requests modules, so membership is RSC × SSR — the SSR graph is a\n * faithful proxy for the client tree (design/47 §8, with the frontier\n * caveat below).\n *\n * Design docs: 47-module-environment-tooling.md (taxonomy, §8),\n * 43-analyze-cli.md (directive detection rules).\n */\n\nimport { basename, extname } from 'node:path';\nimport { parseAst, normalizePath } from 'vite';\nimport { hasDirective } from '../rsc-runtime/transforms.ts';\nimport { isRouteConventionFile } from '../routing/scanner.ts';\nimport {\n isDynamicMetadataExtension,\n isStaticMetadataExtension,\n} from '../server/metadata-routes.ts';\n\n/** The statement-list shape hasDirective consumes (estree Program body). */\ntype DirectiveBody = Parameters<typeof hasDirective>[0];\n\nexport type ModuleKind =\n | 'server'\n | 'client-boundary'\n | 'client-internal'\n | 'shared'\n | 'server-action'\n | 'unreferenced';\n\nexport type Directive = 'use client' | 'use server' | null;\n\n/**\n * What a frontend observed about one project module. Graph membership\n * comes from the live dev-server graphs or the cold crawl; the directive\n * comes from the module's original source (never the transformed output,\n * where the RSC plugin has already stripped or proxied it).\n */\nexport interface ModuleObservation {\n /** Absolute path of the source file. */\n file: string;\n /** Present in the RSC environment's module graph. */\n inRsc: boolean;\n /** Present in the SSR environment's module graph. */\n inSsr: boolean;\n /**\n * Present in the browser (`client`) environment's module graph. Only a\n * live dev server with a real connected browser can confirm this — the\n * client graph stays empty otherwise (design/47 §8) — so the cold\n * crawl always observes false. Defaults to false.\n */\n inClient?: boolean;\n /**\n * False when the SSR graph has a node for this file but Vite never\n * transformed it — the §8 signature of a module behind an unexecuted\n * dynamic import (`transformResult == null`). Its own membership is\n * trustworthy; its subtree is not. Defaults to true.\n */\n ssrTransformed?: boolean;\n /** First directive of the original source, or null. */\n directive: Directive;\n}\n\nexport interface ModuleClassification {\n file: string;\n kind: ModuleKind;\n /**\n * Raw per-environment graph membership, in doc 43's classification\n * vocabulary (`{ client, ssr, rsc }`). `kind` is the interpreted\n * answer; this is the evidence. `client` is confirmable only by a\n * live browser graph — static scans always report false.\n */\n envs: { rsc: boolean; ssr: boolean; client: boolean };\n /**\n * True when no graph has confirmed this module — the kind is\n * provisional, derived from directive + filename convention, and\n * frontends must visibly mark it \"pending\" (design/47 §2).\n */\n pending: boolean;\n /**\n * §8 frontier: the module is reachable (it ships to the browser) but\n * was never transformed, so modules behind it may be missing from the\n * graphs entirely. Consumers should transform-on-demand or surface\n * \"reachable, subtree unknown\".\n */\n subtreeUnknown: boolean;\n}\n\n/** File extensions eligible for directive detection. */\nconst PARSEABLE_EXTENSIONS = /\\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;\n\n/**\n * Parse language for a source file, from its extension. Never force\n * `tsx` onto plain `.ts`: JSX-ambiguous TypeScript such as\n * `const identity = <T>(value: T) => value` is valid TS but rejected\n * by a TSX parse, and the resulting parse failure would silently drop\n * the file's directive and poison signals. Plain JS parses as `jsx`\n * (JSX-in-.js is common in Vite projects and JS has no generic-arrow\n * ambiguity for the x-syntax to break).\n */\nexport function langForFile(file: string): 'ts' | 'tsx' | 'jsx' {\n if (/\\.tsx$/.test(file)) return 'tsx';\n if (/\\.[mc]?ts$/.test(file)) return 'ts';\n return 'jsx';\n}\n\n/**\n * Detect the first directive of a module's original source.\n *\n * Never string-match or regex directives (design/43): `hasDirective`\n * from @vitejs/plugin-rsc checks for the `directive` property on\n * ExpressionStatements — the same check the RSC plugin runs to decide\n * boundaries, so classification agrees with what the framework actually\n * does with the file. The directive must be in prologue position (before\n * any import or statement) per the ECMAScript spec. Non-JS sources\n * (CSS, MDX, assets) and sources that fail to parse return null — no\n * directive is a safe default; classification then falls back to graph\n * membership.\n */\nexport function detectDirective(code: string, file: string): Directive {\n if (!PARSEABLE_EXTENSIONS.test(file)) return null;\n let body: DirectiveBody;\n try {\n // Vite's parseAst returns oxc-typed nodes; hasDirective expects the\n // estree shapes. They are structurally identical for the top-level\n // statement scan — the same bridge the rest of the codebase uses\n // (plugin-context.ts parse memo, routing/export-detect.ts).\n body = parseAst(code, { lang: langForFile(file) }).body as unknown as DirectiveBody;\n } catch {\n return null;\n }\n if (hasDirective(body, 'use client')) return 'use client';\n if (hasDirective(body, 'use server')) return 'use server';\n return null;\n}\n\n/** Optional context for classification. */\nexport interface ClassifyContext {\n /**\n * Absolute path of the app directory. When provided, convention-based\n * provisional classification only applies to files inside it — a\n * `lib/page.tsx` is not a route file, and the scanner would never\n * recognize it there.\n */\n appDir?: string;\n /**\n * Configured pageExtensions, when the caller has resolved the app\n * config. Omitted → scanner defaults.\n */\n pageExtensions?: readonly string[];\n}\n\n/**\n * Whether a file is a route-convention source the framework itself\n * imports (page/layout/route/… plus metadata routes). Conventions are\n * derived from the routing scanner and the metadata route table — never\n * a local duplicate list (drift lesson). Each family carries the\n * scanner's own extension rules.\n *\n * Shared by the provisional classifier below and by chains.ts, whose\n * RSC-side walks terminate at the first route-convention file\n * (design/47 §7) — the two must never disagree about what counts.\n *\n * `pageExtensions` mirrors the scanner's parameter: callers that have\n * resolved the app config pass the configured set (a `page.mdx` app);\n * callers without config access fall back to the defaults and tolerate\n * the mismatch.\n */\nexport function isConventionSourceFile(file: string, pageExtensions?: readonly string[]): boolean {\n const ext = extname(file).slice(1);\n const base = basename(file, `.${ext}`);\n return (\n isRouteConventionFile(base, ext, pageExtensions) ||\n isStaticMetadataExtension(base, ext) ||\n isDynamicMetadataExtension(base, ext)\n );\n}\n\n/**\n * Provisional kind for a file no graph has confirmed (design/47 §2):\n * directive first, then filename convention, else `unreferenced`.\n */\nfunction provisionalKind(file: string, directive: Directive, ctx?: ClassifyContext): ModuleKind {\n if (directive === 'use client') return 'client-boundary';\n if (directive === 'use server') return 'server-action';\n // Normalize both sides before the containment check: on Windows,\n // `file` may arrive slash-normalized (Vite module-node paths are)\n // while appDir comes from node:path with backslashes — comparing raw\n // would misreport every provisional route file as unreferenced.\n if (ctx?.appDir && !normalizePath(file).startsWith(normalizePath(ctx.appDir) + '/')) {\n return 'unreferenced';\n }\n if (isConventionSourceFile(file, ctx?.pageExtensions)) return 'server';\n return 'unreferenced';\n}\n\n/**\n * Classify one module from its observation. Pure — all frontends call\n * this with membership data from their own graph source.\n */\nexport function classifyModule(\n obs: ModuleObservation,\n ctx?: ClassifyContext\n): ModuleClassification {\n const referenced = obs.inRsc || obs.inSsr || obs.inClient === true;\n const subtreeUnknown = obs.inSsr && obs.ssrTransformed === false;\n const envs = { rsc: obs.inRsc, ssr: obs.inSsr, client: obs.inClient === true };\n\n if (!referenced) {\n return {\n file: obs.file,\n kind: provisionalKind(obs.file, obs.directive, ctx),\n envs,\n pending: true,\n subtreeUnknown: false,\n };\n }\n\n // Directive wins over membership: a 'use client' file also has an RSC\n // graph node (the client-reference proxy), and a 'use server' module\n // appears in the SSR graph when referenced from client forms — the\n // directive is what those memberships mean.\n let kind: ModuleKind;\n if (obs.directive === 'use client') kind = 'client-boundary';\n else if (obs.directive === 'use server') kind = 'server-action';\n else if (obs.inRsc && obs.inSsr) kind = 'shared';\n else if (obs.inRsc) kind = 'server';\n else kind = 'client-internal';\n\n return { file: obs.file, kind, envs, pending: false, subtreeUnknown };\n}\n\n/** Classify a batch of observations, sorted by file for stable output. */\nexport function classifyModules(\n observations: Iterable<ModuleObservation>,\n ctx?: ClassifyContext\n): ModuleClassification[] {\n return [...observations]\n .map((obs) => classifyModule(obs, ctx))\n .sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));\n}\n","/**\n * chains — boundary-aware import-chain stitching (the \"why\" answer).\n *\n * The human-meaningful import chain for a client-reachable module is a\n * JOIN of two graphs: RSC-graph importers ABOVE the `'use client'`\n * boundary and SSR-graph importers BELOW it. Walking `importers` naively\n * in one graph produces virtual-entry noise (`virtual:vite-rsc/css`)\n * instead of semantic importers — the design/47 §7 findings this module\n * bakes in:\n *\n * - RSC-side walks terminate at `virtual:timber-route-manifest` or the\n * first route-convention file (page/layout/…), never the literal RSC\n * entry — route files' importer is the manifest, whose importers are\n * framework internals; the manifest is the semantic root.\n * - `\\0virtual:vite-rsc/*` importers are filtered so they can never\n * surface in a chain.\n * - When multiple boundaries import the target, one chain is reported\n * PER BOUNDARY — a single BFS shortest path would pick arbitrarily.\n *\n * The chain/hop shapes and text formatting are shared with doc 43's\n * `timber why` so both commands emit the same contract: hops carry\n * `{ id, directive, boundary }`, and one shortest chain is emitted per\n * (boundary, reaching route entry) pair — doc 43's \"one shortest path\n * per reaching entry\", crossed with §7's per-boundary rule.\n *\n * Design docs: 47-module-environment-tooling.md §7, 43-analyze-cli.md\n * §Output Formats.\n */\n\nimport { relative, isAbsolute } from 'node:path';\nimport { normalizePath } from 'vite';\nimport { isConventionSourceFile, type Directive } from './classify.ts';\n\n/**\n * Duck-typed view of a Vite environment module node — a supertype of\n * `EnvironmentModuleNode` for the members the walk reads, so live\n * dev-server graphs and crawl graphs both satisfy it structurally.\n */\nexport interface ChainModuleNode {\n id: string | null;\n url: string;\n file: string | null;\n importers: Set<ChainModuleNode>;\n}\n\n/** Duck-typed view of a Vite environment module graph. */\nexport interface ChainGraph {\n getModulesByFile(file: string): Set<ChainModuleNode> | undefined;\n}\n\n/** Per-hop directive in doc 43's JSON vocabulary. */\nexport type HopDirective = 'client' | 'server' | null;\n\nexport interface ChainHop {\n /** Absolute file path, or a virtual module id printed verbatim. */\n id: string;\n directive: HopDirective;\n /** True only on the hop that crosses the server→client boundary. */\n boundary: boolean;\n}\n\nexport interface ImportChain {\n /**\n * Semantic root: a route-convention file, or the route-manifest\n * virtual id when the walk reached it without passing one. Null when\n * the RSC-side walk dead-ended (no root found) — the chain then\n * starts at its deepest known importer and consumers must present it\n * as partial.\n */\n entry: string | null;\n /**\n * The `'use client'` file this chain crosses; null for a pure\n * server-side chain.\n */\n boundary: string | null;\n /** Hops in forward import order, entry → … → target. */\n hops: ChainHop[];\n}\n\nexport interface ChainContext {\n rsc: ChainGraph;\n ssr: ChainGraph;\n /** Absolute app directory — scopes route-convention root detection. */\n appDir: string;\n /**\n * Configured pageExtensions, when the caller has resolved the app\n * config — a `page.mdx` app's route files are only roots with it.\n * Omitted → scanner defaults.\n */\n pageExtensions?: readonly string[];\n /**\n * First directive of a file's ORIGINAL source (design/43: never the\n * transformed output, where directives are stripped/proxied).\n */\n getDirective(file: string): Directive;\n}\n\n/**\n * The route manifest appears under several ids depending on environment\n * and build phase: `virtual:timber-route-manifest`,\n * `\\0virtual:timber-route-manifest`, and a root-prefixed form\n * (plugins/routing.ts) — match by substring.\n */\nfunction isRouteManifest(key: string): boolean {\n return key.includes('virtual:timber-route-manifest');\n}\n\n/** plugin-rsc internal virtuals (observed: virtual:vite-rsc/css). */\nfunction isViteRscInternal(key: string): boolean {\n return key.includes('virtual:vite-rsc/');\n}\n\n/**\n * File-level vertex key for a node: the file path when Vite resolved\n * one, else the virtual id. Multiple nodes can exist per file (query\n * variants); collapsing to the file keeps chains at source granularity.\n */\nfunction vertexKey(node: ChainModuleNode): string | null {\n return node.file ?? node.id ?? node.url ?? null;\n}\n\n/**\n * Separator for path signatures. Sorts below every printable character\n * so comparing joined signatures equals comparing key sequences.\n */\nconst SIG_SEP = '\\u0000';\n\n/**\n * Backwards level-order BFS over `importers` from `start`, collapsing\n * nodes to file-level vertices and filtering plugin-rsc internals.\n *\n * Shortest-path ties are broken GLOBALLY: each vertex's predecessor is\n * the candidate whose full path signature (the id sequence from the\n * target outward) is lexicographically smallest — per-frontier\n * candidate selection, not merely per-parent importer sorting, so\n * reconverging equal-length branches still yield the advertised\n * representative path (doc 43 §Tie-Breaking).\n *\n * `stopAt` marks terminal vertices. By default their importers are not\n * expanded (RSC walks must not proceed past a route root into\n * framework internals); with `continuePastTerminals` the traversal\n * records the terminal and keeps walking — the SSR walk needs this so\n * an outer boundary that imports an inner boundary is still\n * discovered (one chain per boundary).\n */\nfunction walkImporters(\n graph: ChainGraph,\n start: string,\n stopAt: (key: string) => boolean,\n continuePastTerminals = false\n): { predecessors: Map<string, string | null>; terminals: string[] } {\n const startNodes = graph.getModulesByFile(start);\n const predecessors = new Map<string, string | null>();\n const terminals: string[] = [];\n if (!startNodes || startNodes.size === 0) return { predecessors, terminals };\n\n predecessors.set(start, null);\n const signatures = new Map<string, string>([[start, start]]);\n const nodesOf = new Map<string, ChainModuleNode[]>([[start, [...startNodes]]]);\n let frontier = [start];\n\n while (frontier.length > 0) {\n interface Candidate {\n sig: string;\n parent: string;\n observed: ChainModuleNode[];\n }\n const candidates = new Map<string, Candidate>();\n for (const key of frontier) {\n if (stopAt(key)) {\n terminals.push(key);\n if (!continuePastTerminals) continue;\n }\n for (const node of nodesOf.get(key) ?? []) {\n for (const importer of node.importers) {\n const importerKey = vertexKey(importer);\n if (!importerKey || isViteRscInternal(importerKey)) continue;\n if (predecessors.has(importerKey)) continue;\n // Every frontier vertex has a signature (set when its level\n // was fixed); the fallback only satisfies the type.\n const sig = (signatures.get(key) ?? key) + SIG_SEP + importerKey;\n const existing = candidates.get(importerKey);\n if (!existing) {\n candidates.set(importerKey, { sig, parent: key, observed: [importer] });\n } else {\n // Keep every observed node object (virtual ids have no file\n // index to re-fetch from), but the smallest signature wins\n // the predecessor slot.\n existing.observed.push(importer);\n if (sig < existing.sig) {\n existing.sig = sig;\n existing.parent = key;\n }\n }\n }\n }\n }\n // Fix the next level, ordered by signature so terminal discovery\n // order — and therefore chain order — is deterministic.\n const next = [...candidates.entries()].sort(([, a], [, b]) =>\n a.sig < b.sig ? -1 : a.sig > b.sig ? 1 : 0\n );\n frontier = [];\n for (const [importerKey, candidate] of next) {\n predecessors.set(importerKey, candidate.parent);\n signatures.set(importerKey, candidate.sig);\n // Re-fetch the file's FULL node set from the graph: the importer\n // edges seen so far may cover only one of several nodes per file\n // (query variants — ?direct, ?v=…), and expanding a partial set\n // would silently drop the sibling nodes' importers and dead-end\n // the chain. Virtual ids have no file index; use observed nodes.\n const fullNodes = graph.getModulesByFile(importerKey);\n nodesOf.set(\n importerKey,\n fullNodes && fullNodes.size > 0 ? [...fullNodes] : candidate.observed\n );\n frontier.push(importerKey);\n }\n }\n return { predecessors, terminals };\n}\n\n/** Follow predecessors from `from` back to the walk's start vertex. */\nfunction pathToStart(predecessors: Map<string, string | null>, from: string): string[] {\n const path: string[] = [];\n for (let key: string | null = from; key !== null; key = predecessors.get(key) ?? null) {\n path.push(key);\n }\n return path;\n}\n\nfunction isRouteRoot(key: string, ctx: ChainContext): boolean {\n // Manifest matching applies to virtual ids only — a real file whose\n // path happens to contain the substring must not become a root.\n if (!isAbsolute(key) && isRouteManifest(key)) return true;\n if (!isAbsolute(key)) return false;\n if (key !== ctx.appDir && !key.startsWith(ctx.appDir + '/')) return false;\n return isConventionSourceFile(key, ctx.pageExtensions);\n}\n\n/**\n * RSC-side walk: importers of `file` in the RSC graph up to the\n * semantic roots. Returns one chain PER reaching route entry (doc 43:\n * \"one shortest path per reaching entry\"), each with hops in forward\n * order (root → … → file), in discovery order (shallowest first,\n * signature tie-break). When no root was reached, a single partial\n * result (entry: null) carrying the deepest known path.\n */\nfunction rscSideChains(\n file: string,\n ctx: ChainContext\n): Array<{ entry: string | null; files: string[] }> {\n const { predecessors, terminals } = walkImporters(ctx.rsc, file, (key) => isRouteRoot(key, ctx));\n if (terminals.length > 0) {\n return terminals.map((root) => ({ entry: root, files: pathToStart(predecessors, root) }));\n }\n if (predecessors.size <= 1) {\n // Nothing above the file at all (or file absent from the graph).\n return [{ entry: null, files: [file] }];\n }\n // Dead end — no root found. Report from the deepest vertex on the\n // last-discovered shortest path so the partial chain is still useful.\n const keys = [...predecessors.keys()];\n const deepest = keys[keys.length - 1];\n return [{ entry: null, files: pathToStart(predecessors, deepest) }];\n}\n\n/** Map a source directive onto the doc-43 hop vocabulary. */\nfunction hopDirective(key: string, ctx: ChainContext): HopDirective {\n if (!isAbsolute(key)) return null;\n const directive = ctx.getDirective(key);\n if (directive === 'use client') return 'client';\n if (directive === 'use server') return 'server';\n return null;\n}\n\nfunction toChain(\n entry: string | null,\n boundary: string | null,\n files: string[],\n ctx: ChainContext\n): ImportChain {\n return {\n entry,\n boundary,\n hops: files.map((key) => ({\n id: key,\n directive: hopDirective(key, ctx),\n boundary: boundary !== null && key === boundary,\n })),\n };\n}\n\n/**\n * Stitch the import chain(s) for a target file.\n *\n * - Target in the SSR graph (client-side): walk SSR importers up to\n * each `'use client'` boundary that the RSC graph knows (nested\n * boundaries only reachable from other client components are walked\n * THROUGH — they exist in no RSC graph, so the chain must continue\n * to a boundary a server component actually references), then walk\n * RSC importers from that boundary to the semantic root. One chain\n * per boundary.\n * - Target in the RSC graph without `'use client'`: a pure server\n * chain (boundary: null). Shared modules get both kinds.\n * - Target IS an RSC-known boundary: an RSC-side chain from the target\n * itself, PLUS any outer-boundary chains the SSR walk finds — a\n * boundary imported by another boundary is pulled in by both. A\n * nested `'use client'` target the RSC graph does NOT know is\n * handled like any client-internal file — the SSR walk continues to\n * the boundary a server component references.\n */\nexport function stitchImportChains(rawTargetFile: string, rawCtx: ChainContext): ImportChain[] {\n // Vite module-node `file` values are slash-normalized even on\n // Windows, while node:path targets/appDir arrive backslashed there —\n // normalize both once so graph lookups and containment checks agree\n // (normalizePath only rewrites separators on Windows, so posix paths\n // pass through untouched).\n const targetFile = normalizePath(rawTargetFile);\n const ctx: ChainContext = { ...rawCtx, appDir: normalizePath(rawCtx.appDir) };\n\n const chains: ImportChain[] = [];\n const targetDirective = ctx.getDirective(targetFile);\n const targetInRsc = (ctx.rsc.getModulesByFile(targetFile)?.size ?? 0) > 0;\n\n if (targetDirective === 'use client' && targetInRsc) {\n for (const above of rscSideChains(targetFile, ctx)) {\n chains.push(toChain(above.entry, targetFile, above.files, ctx));\n }\n }\n\n // Client-side chains: one per RSC-known boundary above the target.\n const isRscKnownBoundary = (key: string): boolean =>\n isAbsolute(key) &&\n ctx.getDirective(key) === 'use client' &&\n (ctx.rsc.getModulesByFile(key)?.size ?? 0) > 0;\n\n // continuePastTerminals: an outer boundary that imports an inner\n // boundary must still be discovered — one chain per boundary.\n const ssrWalk = walkImporters(ctx.ssr, targetFile, isRscKnownBoundary, true);\n for (const boundary of [...ssrWalk.terminals].sort()) {\n // A boundary target records itself as a terminal; its own chain is\n // the RSC-side one already pushed above.\n if (boundary === targetFile) continue;\n const below = pathToStart(ssrWalk.predecessors, boundary);\n for (const above of rscSideChains(boundary, ctx)) {\n // Stitch, dropping the duplicated boundary vertex from the RSC side.\n chains.push(toChain(above.entry, boundary, [...above.files.slice(0, -1), ...below], ctx));\n }\n }\n\n // Pure server chains when the RSC graph reaches the target directly.\n if (targetInRsc && targetDirective !== 'use client') {\n for (const above of rscSideChains(targetFile, ctx)) {\n chains.push(toChain(above.entry, null, above.files, ctx));\n }\n }\n\n // SSR-side dead end with nothing else to show: mirror the RSC side's\n // honesty and emit a partial chain from the deepest importer instead\n // of returning [] as if the file were in no graph. A singleton walk\n // (target in the SSR graph with no importers at all) still emits its\n // one-hop partial — graph membership is information.\n if (chains.length === 0 && ssrWalk.predecessors.size > 0) {\n const keys = [...ssrWalk.predecessors.keys()];\n const deepest = keys[keys.length - 1];\n chains.push(toChain(null, null, pathToStart(ssrWalk.predecessors, deepest), ctx));\n }\n\n return chains;\n}\n\n/**\n * Render chains in doc 43's text shape — shared by `timber graph` and\n * `timber why` so both commands print identically:\n *\n * ```\n * app/page.tsx\n * → app/nav.tsx ← 'use client' [boundary]\n * → src/lib/icons/chevron.ts\n * ```\n *\n * Paths under `projectRoot` print relative; virtual ids print verbatim\n * (doc 43 §Relative-vs-Absolute Paths). A partial chain (entry: null)\n * is prefixed with an honest marker instead of silently posing as\n * complete.\n */\nexport function formatChains(chains: ImportChain[], projectRoot: string): string {\n const root = normalizePath(projectRoot);\n const lines: string[] = [];\n for (const chain of chains) {\n if (lines.length > 0) lines.push('');\n if (chain.entry === null) {\n lines.push(' (partial chain — no route root found)');\n }\n chain.hops.forEach((hop, index) => {\n const display =\n isAbsolute(hop.id) && hop.id.startsWith(root + '/') ? relative(root, hop.id) : hop.id;\n const indent = ' '.repeat(index + 1);\n const arrow = index === 0 ? '' : '→ ';\n let annotation = '';\n if (hop.directive === 'client') {\n annotation = ` ← 'use client'${hop.boundary ? ' [boundary]' : ''}`;\n } else if (hop.directive === 'server') {\n annotation = ` ← 'use server'`;\n }\n lines.push(`${indent}${arrow}${display}${annotation}`);\n });\n }\n return lines.join('\\n');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chains-CpFg56UB.js","names":[],"sources":["../../src/analyze/classify.ts","../../src/analyze/chains.ts"],"sourcesContent":["/**\n * classify — environment-graph classification for project modules.\n *\n * The one classification module every frontend shares (design/47): the\n * `timber graph` CLI, the dev-server endpoint, the VS Code extension,\n * and the cold crawl must never disagree about a file's environment.\n * Classification intersects RSC/SSR module-graph membership with\n * first-directive detection to produce the decided taxonomy:\n *\n * - `server` — RSC graph only\n * - `client-boundary` — has `'use client'`\n * - `client-internal` — reachable only behind a boundary, no directive\n * - `shared` — instantiated in both environments; INFO, never a\n * warning — shared-by-design is a legitimate pattern\n * - `server-action` — has `'use server'`\n * - `unreferenced` — in no graph and no provisional signal; files with\n * a directive or filename convention get that\n * provisional kind instead, marked `pending`\n *\n * The browser (`client`) Vite graph stays empty until a real browser\n * requests modules, so membership is RSC × SSR — the SSR graph is a\n * faithful proxy for the client tree (design/47 §8, with the frontier\n * caveat below).\n *\n * Design docs: 47-module-environment-tooling.md (taxonomy, §8),\n * 43-analyze-cli.md (directive detection rules).\n */\n\nimport { basename, extname } from 'node:path';\nimport { parseAst, normalizePath } from 'vite';\nimport { hasDirective } from '../rsc-runtime/transforms.ts';\nimport { isRouteConventionFile } from '../routing/scanner.ts';\nimport {\n isDynamicMetadataExtension,\n isStaticMetadataExtension,\n} from '../server/metadata-routes.ts';\n\n/** The statement-list shape hasDirective consumes (estree Program body). */\ntype DirectiveBody = Parameters<typeof hasDirective>[0];\n\nexport type ModuleKind =\n | 'server'\n | 'client-boundary'\n | 'client-internal'\n | 'shared'\n | 'server-action'\n | 'unreferenced';\n\nexport type Directive = 'use client' | 'use server' | null;\n\n/**\n * What a frontend observed about one project module. Graph membership\n * comes from the live dev-server graphs or the cold crawl; the directive\n * comes from the module's original source (never the transformed output,\n * where the RSC plugin has already stripped or proxied it).\n */\nexport interface ModuleObservation {\n /** Absolute path of the source file. */\n file: string;\n /** Present in the RSC environment's module graph. */\n inRsc: boolean;\n /** Present in the SSR environment's module graph. */\n inSsr: boolean;\n /**\n * Present in the browser (`client`) environment's module graph. Only a\n * live dev server with a real connected browser can confirm this — the\n * client graph stays empty otherwise (design/47 §8) — so the cold\n * crawl always observes false. Defaults to false.\n */\n inClient?: boolean;\n /**\n * False when the SSR graph has a node for this file but Vite never\n * transformed it — the §8 signature of a module behind an unexecuted\n * dynamic import (`transformResult == null`). Its own membership is\n * trustworthy; its subtree is not. Defaults to true.\n */\n ssrTransformed?: boolean;\n /** First directive of the original source, or null. */\n directive: Directive;\n}\n\nexport interface ModuleClassification {\n file: string;\n kind: ModuleKind;\n /**\n * Raw per-environment graph membership, in doc 43's classification\n * vocabulary (`{ client, ssr, rsc }`). `kind` is the interpreted\n * answer; this is the evidence. `client` is confirmable only by a\n * live browser graph — static scans always report false.\n */\n envs: { rsc: boolean; ssr: boolean; client: boolean };\n /**\n * True when no graph has confirmed this module — the kind is\n * provisional, derived from directive + filename convention, and\n * frontends must visibly mark it \"pending\" (design/47 §2).\n */\n pending: boolean;\n /**\n * §8 frontier: the module is reachable (it ships to the browser) but\n * was never transformed, so modules behind it may be missing from the\n * graphs entirely. Consumers should transform-on-demand or surface\n * \"reachable, subtree unknown\".\n */\n subtreeUnknown: boolean;\n}\n\n/** File extensions eligible for directive detection. */\nconst PARSEABLE_EXTENSIONS = /\\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;\n\n/**\n * Parse language for a source file, from its extension. Never force\n * `tsx` onto plain `.ts`: JSX-ambiguous TypeScript such as\n * `const identity = <T>(value: T) => value` is valid TS but rejected\n * by a TSX parse, and the resulting parse failure would silently drop\n * the file's directive and poison signals. Plain JS parses as `jsx`\n * (JSX-in-.js is common in Vite projects and JS has no generic-arrow\n * ambiguity for the x-syntax to break).\n */\nexport function langForFile(file: string): 'ts' | 'tsx' | 'jsx' {\n if (/\\.tsx$/.test(file)) return 'tsx';\n if (/\\.[mc]?ts$/.test(file)) return 'ts';\n return 'jsx';\n}\n\n/**\n * Detect the first directive of a module's original source.\n *\n * Never string-match or regex directives (design/43): `hasDirective`\n * from @vitejs/plugin-rsc checks for the `directive` property on\n * ExpressionStatements — the same check the RSC plugin runs to decide\n * boundaries, so classification agrees with what the framework actually\n * does with the file. The directive must be in prologue position (before\n * any import or statement) per the ECMAScript spec. Non-JS sources\n * (CSS, MDX, assets) and sources that fail to parse return null — no\n * directive is a safe default; classification then falls back to graph\n * membership.\n */\nexport function detectDirective(code: string, file: string): Directive {\n if (!PARSEABLE_EXTENSIONS.test(file)) return null;\n let body: DirectiveBody;\n try {\n // Vite's parseAst returns oxc-typed nodes; hasDirective expects the\n // estree shapes. They are structurally identical for the top-level\n // statement scan — the same bridge the rest of the codebase uses\n // (plugin-context.ts parse memo, routing/export-detect.ts).\n body = parseAst(code, { lang: langForFile(file) }).body as unknown as DirectiveBody;\n } catch {\n return null;\n }\n if (hasDirective(body, 'use client')) return 'use client';\n if (hasDirective(body, 'use server')) return 'use server';\n return null;\n}\n\n/** Optional context for classification. */\nexport interface ClassifyContext {\n /**\n * Absolute path of the app directory. When provided, convention-based\n * provisional classification only applies to files inside it — a\n * `lib/page.tsx` is not a route file, and the scanner would never\n * recognize it there.\n */\n appDir?: string;\n /**\n * Configured pageExtensions, when the caller has resolved the app\n * config. Omitted → scanner defaults.\n */\n pageExtensions?: readonly string[];\n}\n\n/**\n * Whether a file is a route-convention source the framework itself\n * imports (page/layout/route/… plus metadata routes). Conventions are\n * derived from the routing scanner and the metadata route table — never\n * a local duplicate list (drift lesson). Each family carries the\n * scanner's own extension rules.\n *\n * Shared by the provisional classifier below and by chains.ts, whose\n * RSC-side walks terminate at the first route-convention file\n * (design/47 §7) — the two must never disagree about what counts.\n *\n * `pageExtensions` mirrors the scanner's parameter: callers that have\n * resolved the app config pass the configured set (a `page.mdx` app);\n * callers without config access fall back to the defaults and tolerate\n * the mismatch.\n */\nexport function isConventionSourceFile(file: string, pageExtensions?: readonly string[]): boolean {\n const ext = extname(file).slice(1);\n const base = basename(file, `.${ext}`);\n return (\n isRouteConventionFile(base, ext, pageExtensions) ||\n isStaticMetadataExtension(base, ext) ||\n isDynamicMetadataExtension(base, ext)\n );\n}\n\n/**\n * Provisional kind for a file no graph has confirmed (design/47 §2):\n * directive first, then filename convention, else `unreferenced`.\n */\nfunction provisionalKind(file: string, directive: Directive, ctx?: ClassifyContext): ModuleKind {\n if (directive === 'use client') return 'client-boundary';\n if (directive === 'use server') return 'server-action';\n // Normalize both sides before the containment check: on Windows,\n // `file` may arrive slash-normalized (Vite module-node paths are)\n // while appDir comes from node:path with backslashes — comparing raw\n // would misreport every provisional route file as unreferenced.\n if (ctx?.appDir && !normalizePath(file).startsWith(normalizePath(ctx.appDir) + '/')) {\n return 'unreferenced';\n }\n if (isConventionSourceFile(file, ctx?.pageExtensions)) return 'server';\n return 'unreferenced';\n}\n\n/**\n * Classify one module from its observation. Pure — all frontends call\n * this with membership data from their own graph source.\n */\nexport function classifyModule(\n obs: ModuleObservation,\n ctx?: ClassifyContext\n): ModuleClassification {\n const referenced = obs.inRsc || obs.inSsr || obs.inClient === true;\n const subtreeUnknown = obs.inSsr && obs.ssrTransformed === false;\n const envs = { rsc: obs.inRsc, ssr: obs.inSsr, client: obs.inClient === true };\n\n if (!referenced) {\n return {\n file: obs.file,\n kind: provisionalKind(obs.file, obs.directive, ctx),\n envs,\n pending: true,\n subtreeUnknown: false,\n };\n }\n\n // Directive wins over membership: a 'use client' file also has an RSC\n // graph node (the client-reference proxy), and a 'use server' module\n // appears in the SSR graph when referenced from client forms — the\n // directive is what those memberships mean.\n let kind: ModuleKind;\n if (obs.directive === 'use client') kind = 'client-boundary';\n else if (obs.directive === 'use server') kind = 'server-action';\n else if (obs.inRsc && obs.inSsr) kind = 'shared';\n else if (obs.inRsc) kind = 'server';\n else kind = 'client-internal';\n\n return { file: obs.file, kind, envs, pending: false, subtreeUnknown };\n}\n\n/** Classify a batch of observations, sorted by file for stable output. */\nexport function classifyModules(\n observations: Iterable<ModuleObservation>,\n ctx?: ClassifyContext\n): ModuleClassification[] {\n return [...observations]\n .map((obs) => classifyModule(obs, ctx))\n .sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));\n}\n","/**\n * chains — boundary-aware import-chain stitching (the \"why\" answer).\n *\n * The human-meaningful import chain for a client-reachable module is a\n * JOIN of two graphs: RSC-graph importers ABOVE the `'use client'`\n * boundary and SSR-graph importers BELOW it. Walking `importers` naively\n * in one graph produces virtual-entry noise (`virtual:vite-rsc/css`)\n * instead of semantic importers — the design/47 §7 findings this module\n * bakes in:\n *\n * - RSC-side walks terminate at `virtual:timber-route-manifest` or the\n * first route-convention file (page/layout/…), never the literal RSC\n * entry — route files' importer is the manifest, whose importers are\n * framework internals; the manifest is the semantic root.\n * - `\\0virtual:vite-rsc/*` importers are filtered so they can never\n * surface in a chain.\n * - When multiple boundaries import the target, one chain is reported\n * PER BOUNDARY — a single BFS shortest path would pick arbitrarily.\n *\n * The chain/hop shapes and text formatting are shared with doc 43's\n * `timber why` so both commands emit the same contract: hops carry\n * `{ id, directive, boundary }`, and one shortest chain is emitted per\n * (boundary, reaching route entry) pair — doc 43's \"one shortest path\n * per reaching entry\", crossed with §7's per-boundary rule.\n *\n * Design docs: 47-module-environment-tooling.md §7, 43-analyze-cli.md\n * §Output Formats.\n */\n\nimport { relative, isAbsolute } from 'node:path';\nimport { normalizePath } from 'vite';\nimport { isConventionSourceFile, type Directive } from './classify.ts';\n\n/**\n * Duck-typed view of a Vite environment module node — a supertype of\n * `EnvironmentModuleNode` for the members the walk reads, so live\n * dev-server graphs and crawl graphs both satisfy it structurally.\n */\nexport interface ChainModuleNode {\n id: string | null;\n url: string;\n file: string | null;\n importers: Set<ChainModuleNode>;\n}\n\n/** Duck-typed view of a Vite environment module graph. */\nexport interface ChainGraph {\n getModulesByFile(file: string): Set<ChainModuleNode> | undefined;\n}\n\n/** Per-hop directive in doc 43's JSON vocabulary. */\nexport type HopDirective = 'client' | 'server' | null;\n\nexport interface ChainHop {\n /** Absolute file path, or a virtual module id printed verbatim. */\n id: string;\n directive: HopDirective;\n /** True only on the hop that crosses the server→client boundary. */\n boundary: boolean;\n}\n\nexport interface ImportChain {\n /**\n * Semantic root: a route-convention file, or the route-manifest\n * virtual id when the walk reached it without passing one. Null when\n * the RSC-side walk dead-ended (no root found) — the chain then\n * starts at its deepest known importer and consumers must present it\n * as partial.\n */\n entry: string | null;\n /**\n * The `'use client'` file this chain crosses; null for a pure\n * server-side chain.\n */\n boundary: string | null;\n /** Hops in forward import order, entry → … → target. */\n hops: ChainHop[];\n}\n\nexport interface ChainContext {\n rsc: ChainGraph;\n ssr: ChainGraph;\n /** Absolute app directory — scopes route-convention root detection. */\n appDir: string;\n /**\n * Configured pageExtensions, when the caller has resolved the app\n * config — a `page.mdx` app's route files are only roots with it.\n * Omitted → scanner defaults.\n */\n pageExtensions?: readonly string[];\n /**\n * First directive of a file's ORIGINAL source (design/43: never the\n * transformed output, where directives are stripped/proxied).\n */\n getDirective(file: string): Directive;\n}\n\n/**\n * The route manifest appears under several ids depending on environment\n * and build phase: `virtual:timber-route-manifest`,\n * `\\0virtual:timber-route-manifest`, and a root-prefixed form\n * (plugins/routing.ts) — match by substring.\n */\nfunction isRouteManifest(key: string): boolean {\n return key.includes('virtual:timber-route-manifest');\n}\n\n/** plugin-rsc internal virtuals (observed: virtual:vite-rsc/css). */\nfunction isViteRscInternal(key: string): boolean {\n return key.includes('virtual:vite-rsc/');\n}\n\n/**\n * File-level vertex key for a node: the file path when Vite resolved\n * one, else the virtual id. Multiple nodes can exist per file (query\n * variants); collapsing to the file keeps chains at source granularity.\n */\nfunction vertexKey(node: ChainModuleNode): string | null {\n return node.file ?? node.id ?? node.url ?? null;\n}\n\n/**\n * Separator for path signatures. Sorts below every printable character\n * so comparing joined signatures equals comparing key sequences.\n */\nconst SIG_SEP = '\\u0000';\n\n/**\n * Backwards level-order BFS over `importers` from `start`, collapsing\n * nodes to file-level vertices and filtering plugin-rsc internals.\n *\n * Shortest-path ties are broken GLOBALLY: each vertex's predecessor is\n * the candidate whose full path signature (the id sequence from the\n * target outward) is lexicographically smallest — per-frontier\n * candidate selection, not merely per-parent importer sorting, so\n * reconverging equal-length branches still yield the advertised\n * representative path (doc 43 §Tie-Breaking).\n *\n * `stopAt` marks terminal vertices. By default their importers are not\n * expanded (RSC walks must not proceed past a route root into\n * framework internals); with `continuePastTerminals` the traversal\n * records the terminal and keeps walking — the SSR walk needs this so\n * an outer boundary that imports an inner boundary is still\n * discovered (one chain per boundary).\n */\nfunction walkImporters(\n graph: ChainGraph,\n start: string,\n stopAt: (key: string) => boolean,\n continuePastTerminals = false\n): { predecessors: Map<string, string | null>; terminals: string[] } {\n const startNodes = graph.getModulesByFile(start);\n const predecessors = new Map<string, string | null>();\n const terminals: string[] = [];\n if (!startNodes || startNodes.size === 0) return { predecessors, terminals };\n\n predecessors.set(start, null);\n const signatures = new Map<string, string>([[start, start]]);\n const nodesOf = new Map<string, ChainModuleNode[]>([[start, [...startNodes]]]);\n let frontier = [start];\n\n while (frontier.length > 0) {\n interface Candidate {\n sig: string;\n parent: string;\n observed: ChainModuleNode[];\n }\n const candidates = new Map<string, Candidate>();\n for (const key of frontier) {\n if (stopAt(key)) {\n terminals.push(key);\n if (!continuePastTerminals) continue;\n }\n for (const node of nodesOf.get(key) ?? []) {\n for (const importer of node.importers) {\n const importerKey = vertexKey(importer);\n if (!importerKey || isViteRscInternal(importerKey)) continue;\n if (predecessors.has(importerKey)) continue;\n // Every frontier vertex has a signature (set when its level\n // was fixed); the fallback only satisfies the type.\n const sig = (signatures.get(key) ?? key) + SIG_SEP + importerKey;\n const existing = candidates.get(importerKey);\n if (!existing) {\n candidates.set(importerKey, { sig, parent: key, observed: [importer] });\n } else {\n // Keep every observed node object (virtual ids have no file\n // index to re-fetch from), but the smallest signature wins\n // the predecessor slot.\n existing.observed.push(importer);\n if (sig < existing.sig) {\n existing.sig = sig;\n existing.parent = key;\n }\n }\n }\n }\n }\n // Fix the next level, ordered by signature so terminal discovery\n // order — and therefore chain order — is deterministic.\n const next = [...candidates.entries()].sort(([, a], [, b]) =>\n a.sig < b.sig ? -1 : a.sig > b.sig ? 1 : 0\n );\n frontier = [];\n for (const [importerKey, candidate] of next) {\n predecessors.set(importerKey, candidate.parent);\n signatures.set(importerKey, candidate.sig);\n // Re-fetch the file's FULL node set from the graph: the importer\n // edges seen so far may cover only one of several nodes per file\n // (query variants — ?direct, ?v=…), and expanding a partial set\n // would silently drop the sibling nodes' importers and dead-end\n // the chain. Virtual ids have no file index; use observed nodes.\n const fullNodes = graph.getModulesByFile(importerKey);\n nodesOf.set(\n importerKey,\n fullNodes && fullNodes.size > 0 ? [...fullNodes] : candidate.observed\n );\n frontier.push(importerKey);\n }\n }\n return { predecessors, terminals };\n}\n\n/** Follow predecessors from `from` back to the walk's start vertex. */\nfunction pathToStart(predecessors: Map<string, string | null>, from: string): string[] {\n const path: string[] = [];\n for (let key: string | null = from; key !== null; key = predecessors.get(key) ?? null) {\n path.push(key);\n }\n return path;\n}\n\nfunction isRouteRoot(key: string, ctx: ChainContext): boolean {\n // Manifest matching applies to virtual ids only — a real file whose\n // path happens to contain the substring must not become a root.\n if (!isAbsolute(key) && isRouteManifest(key)) return true;\n if (!isAbsolute(key)) return false;\n if (key !== ctx.appDir && !key.startsWith(ctx.appDir + '/')) return false;\n return isConventionSourceFile(key, ctx.pageExtensions);\n}\n\n/**\n * RSC-side walk: importers of `file` in the RSC graph up to the\n * semantic roots. Returns one chain PER reaching route entry (doc 43:\n * \"one shortest path per reaching entry\"), each with hops in forward\n * order (root → … → file), in discovery order (shallowest first,\n * signature tie-break). When no root was reached, a single partial\n * result (entry: null) carrying the deepest known path.\n */\nfunction rscSideChains(\n file: string,\n ctx: ChainContext\n): Array<{ entry: string | null; files: string[] }> {\n const { predecessors, terminals } = walkImporters(ctx.rsc, file, (key) => isRouteRoot(key, ctx));\n if (terminals.length > 0) {\n return terminals.map((root) => ({ entry: root, files: pathToStart(predecessors, root) }));\n }\n if (predecessors.size <= 1) {\n // Nothing above the file at all (or file absent from the graph).\n return [{ entry: null, files: [file] }];\n }\n // Dead end — no root found. Report from the deepest vertex on the\n // last-discovered shortest path so the partial chain is still useful.\n const keys = [...predecessors.keys()];\n const deepest = keys[keys.length - 1];\n return [{ entry: null, files: pathToStart(predecessors, deepest) }];\n}\n\n/** Map a source directive onto the doc-43 hop vocabulary. */\nfunction hopDirective(key: string, ctx: ChainContext): HopDirective {\n if (!isAbsolute(key)) return null;\n const directive = ctx.getDirective(key);\n if (directive === 'use client') return 'client';\n if (directive === 'use server') return 'server';\n return null;\n}\n\nfunction toChain(\n entry: string | null,\n boundary: string | null,\n files: string[],\n ctx: ChainContext\n): ImportChain {\n return {\n entry,\n boundary,\n hops: files.map((key) => ({\n id: key,\n directive: hopDirective(key, ctx),\n boundary: boundary !== null && key === boundary,\n })),\n };\n}\n\n/**\n * Stitch the import chain(s) for a target file.\n *\n * - Target in the SSR graph (client-side): walk SSR importers up to\n * each `'use client'` boundary that the RSC graph knows (nested\n * boundaries only reachable from other client components are walked\n * THROUGH — they exist in no RSC graph, so the chain must continue\n * to a boundary a server component actually references), then walk\n * RSC importers from that boundary to the semantic root. One chain\n * per boundary.\n * - Target in the RSC graph without `'use client'`: a pure server\n * chain (boundary: null). Shared modules get both kinds.\n * - Target IS an RSC-known boundary: an RSC-side chain from the target\n * itself, PLUS any outer-boundary chains the SSR walk finds — a\n * boundary imported by another boundary is pulled in by both. A\n * nested `'use client'` target the RSC graph does NOT know is\n * handled like any client-internal file — the SSR walk continues to\n * the boundary a server component references.\n */\nexport function stitchImportChains(rawTargetFile: string, rawCtx: ChainContext): ImportChain[] {\n // Vite module-node `file` values are slash-normalized even on\n // Windows, while node:path targets/appDir arrive backslashed there —\n // normalize both once so graph lookups and containment checks agree\n // (normalizePath only rewrites separators on Windows, so posix paths\n // pass through untouched).\n const targetFile = normalizePath(rawTargetFile);\n const ctx: ChainContext = { ...rawCtx, appDir: normalizePath(rawCtx.appDir) };\n\n const chains: ImportChain[] = [];\n const targetDirective = ctx.getDirective(targetFile);\n const targetInRsc = (ctx.rsc.getModulesByFile(targetFile)?.size ?? 0) > 0;\n\n if (targetDirective === 'use client' && targetInRsc) {\n for (const above of rscSideChains(targetFile, ctx)) {\n chains.push(toChain(above.entry, targetFile, above.files, ctx));\n }\n }\n\n // Client-side chains: one per RSC-known boundary above the target.\n const isRscKnownBoundary = (key: string): boolean =>\n isAbsolute(key) &&\n ctx.getDirective(key) === 'use client' &&\n (ctx.rsc.getModulesByFile(key)?.size ?? 0) > 0;\n\n // continuePastTerminals: an outer boundary that imports an inner\n // boundary must still be discovered — one chain per boundary.\n const ssrWalk = walkImporters(ctx.ssr, targetFile, isRscKnownBoundary, true);\n for (const boundary of [...ssrWalk.terminals].sort()) {\n // A boundary target records itself as a terminal; its own chain is\n // the RSC-side one already pushed above.\n if (boundary === targetFile) continue;\n const below = pathToStart(ssrWalk.predecessors, boundary);\n for (const above of rscSideChains(boundary, ctx)) {\n // Stitch, dropping the duplicated boundary vertex from the RSC side.\n chains.push(toChain(above.entry, boundary, [...above.files.slice(0, -1), ...below], ctx));\n }\n }\n\n // Pure server chains when the RSC graph reaches the target directly.\n if (targetInRsc && targetDirective !== 'use client') {\n for (const above of rscSideChains(targetFile, ctx)) {\n chains.push(toChain(above.entry, null, above.files, ctx));\n }\n }\n\n // SSR-side dead end with nothing else to show: mirror the RSC side's\n // honesty and emit a partial chain from the deepest importer instead\n // of returning [] as if the file were in no graph. A singleton walk\n // (target in the SSR graph with no importers at all) still emits its\n // one-hop partial — graph membership is information.\n if (chains.length === 0 && ssrWalk.predecessors.size > 0) {\n const keys = [...ssrWalk.predecessors.keys()];\n const deepest = keys[keys.length - 1];\n chains.push(toChain(null, null, pathToStart(ssrWalk.predecessors, deepest), ctx));\n }\n\n return chains;\n}\n\n/**\n * Render chains in doc 43's text shape — shared by `timber graph` and\n * `timber why` so both commands print identically:\n *\n * ```\n * app/page.tsx\n * → app/nav.tsx ← 'use client' [boundary]\n * → src/lib/icons/chevron.ts\n * ```\n *\n * Paths under `projectRoot` print relative; virtual ids print verbatim\n * (doc 43 §Relative-vs-Absolute Paths). A partial chain (entry: null)\n * is prefixed with an honest marker instead of silently posing as\n * complete.\n */\nexport function formatChains(chains: ImportChain[], projectRoot: string): string {\n const root = normalizePath(projectRoot);\n const lines: string[] = [];\n for (const chain of chains) {\n if (lines.length > 0) lines.push('');\n if (chain.entry === null) {\n lines.push(' (partial chain — no route root found)');\n }\n chain.hops.forEach((hop, index) => {\n const display =\n isAbsolute(hop.id) && hop.id.startsWith(root + '/') ? relative(root, hop.id) : hop.id;\n const indent = ' '.repeat(index + 1);\n const arrow = index === 0 ? '' : '→ ';\n let annotation = '';\n if (hop.directive === 'client') {\n annotation = ` ← 'use client'${hop.boundary ? ' [boundary]' : ''}`;\n } else if (hop.directive === 'server') {\n annotation = ` ← 'use server'`;\n }\n lines.push(`${indent}${arrow}${display}${annotation}`);\n });\n }\n return lines.join('\\n');\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as scanRoutes } from "./scanner-
|
|
2
|
-
import { a as CODEGEN_OUTPUT, c as writeRouteCodegen, n as formatConventionWarnings, o as ENV_DTS_FILENAME, r as lintConventions } from "./convention-lint-
|
|
3
|
-
import {
|
|
1
|
+
import { n as scanRoutes } from "./scanner-Bw0oq1HB.js";
|
|
2
|
+
import { a as CODEGEN_OUTPUT, c as writeRouteCodegen, n as formatConventionWarnings, o as ENV_DTS_FILENAME, r as lintConventions } from "./convention-lint-jKTwKwPe.js";
|
|
3
|
+
import { resolveTimberContext } from "./cli-context-By7k2xVo.js";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -36,8 +36,6 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
36
36
|
*
|
|
37
37
|
* Design docs: 11-platform.md §CLI, 09-typescript.md, 28-npm-packaging.md
|
|
38
38
|
*/
|
|
39
|
-
/** The plugin that publishes timber's resolved context on its Vite `api`. */
|
|
40
|
-
var TIMBER_ROOT_SYNC_PLUGIN = "timber-root-sync";
|
|
41
39
|
/** The npm package that provides tsgo. Named in the missing-tool message. */
|
|
42
40
|
var TSGO_PACKAGE = "@typescript/native-preview";
|
|
43
41
|
/**
|
|
@@ -67,54 +65,15 @@ function resolveTsgoEntry(root, resolveFrom = (from, specifier) => createRequire
|
|
|
67
65
|
throw new Error(`[timber] Cannot find ${TSGO_PACKAGE}, which \`timber check\` uses to type-check.\n\n Install it as a dev dependency:\n\n pnpm add -D ${TSGO_PACKAGE}\n\n (resolution failed: ${lastError instanceof Error ? lastError.message : String(lastError)})`);
|
|
68
66
|
}
|
|
69
67
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* `
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*/
|
|
77
|
-
var defaultResolveViteConfig = async (inlineConfig, command, defaultMode, defaultNodeEnv) => {
|
|
78
|
-
const { resolveConfig } = await import("vite");
|
|
79
|
-
return resolveConfig(inlineConfig, command, defaultMode, defaultNodeEnv);
|
|
80
|
-
};
|
|
81
|
-
/**
|
|
82
|
-
* The mode defaults `timber build` resolves under.
|
|
83
|
-
*
|
|
84
|
-
* `resolveConfig`'s own defaults are `'development'` for both, but nothing
|
|
85
|
-
* reaches it with those on a build: `createBuilder` goes through Vite's
|
|
86
|
-
* internal `resolveConfigToBuild`, which calls
|
|
87
|
-
* `resolveConfig(inlineConfig, 'build', 'production', 'production')`. A config
|
|
88
|
-
* written as `defineConfig(({ mode }) => …)`, or one that loads `.env.production`,
|
|
89
|
-
* therefore resolves to a *different* app under the two. Passing the defaults
|
|
90
|
-
* explicitly is what makes "check validates what build builds" true rather than
|
|
91
|
-
* true-by-coincidence for configs that ignore mode (TIM-1332, round 6).
|
|
92
|
-
*/
|
|
93
|
-
var BUILD_MODE_DEFAULTS = ["production", "production"];
|
|
94
|
-
/**
|
|
95
|
-
* Resolve root, appDir and pageExtensions the way `timber build` resolves them.
|
|
96
|
-
*
|
|
97
|
-
* Reading `timber.config.ts` directly is **not** equivalent, which is the whole
|
|
98
|
-
* reason this goes through Vite (TIM-1332, review round 3):
|
|
99
|
-
*
|
|
100
|
-
* - Config passed inline — `timber({ pageExtensions: [...] })` in
|
|
101
|
-
* `vite.config.ts` — lives only in the plugin's closure. Nothing that
|
|
102
|
-
* re-reads the config file can see it, so a scan built that way silently uses
|
|
103
|
-
* default page extensions and writes a route map missing the app's pages.
|
|
104
|
-
* `examples/next-playground-migration` is exactly this shape.
|
|
105
|
-
* - `root` is Vite's resolved root, not the caller's cwd, and the two differ in
|
|
106
|
-
* the monorepo layouts TIM-498 describes. `appDir` resolves against it.
|
|
107
|
-
*
|
|
108
|
-
* There is no fallback to a second, hand-rolled resolution. A project whose
|
|
109
|
-
* Vite config does not produce the timber plugin cannot be built either, so a
|
|
110
|
-
* check that quietly proceeded on a guessed tree would report a verdict about
|
|
111
|
-
* an app that does not exist.
|
|
68
|
+
* Resolve root, appDir and pageExtensions the way `timber build` resolves them
|
|
69
|
+
* — through Vite, never by re-reading `timber.config.ts` (see `cli-context.ts`).
|
|
70
|
+
* Inline `timber({ pageExtensions: [...] })` is the case that bites: a scan
|
|
71
|
+
* that re-read the file would silently use default page extensions and write a
|
|
72
|
+
* route map missing the app's pages. `examples/next-playground-migration` is
|
|
73
|
+
* exactly this shape.
|
|
112
74
|
*/
|
|
113
75
|
async function resolveRouteInputs(options = {}, deps = {}) {
|
|
114
|
-
const
|
|
115
|
-
const rootSync = plugins.find((plugin) => plugin.name === TIMBER_ROOT_SYNC_PLUGIN);
|
|
116
|
-
if (!rootSync || !isTimberPluginApi(rootSync.api)) throw new Error("[timber] Could not read the resolved timber configuration from your Vite config.\n\n `timber check` resolves your project through Vite so it validates the same\n routes `timber build` builds — including config passed inline to timber().\n\n Check that vite.config.ts exists and lists the timber plugin:\n\n import { timber } from '@timber-js/app';\n export default { plugins: [timber()] };\n");
|
|
117
|
-
const ctx = rootSync.api.getContext();
|
|
76
|
+
const ctx = await resolveTimberContext("check", options.viteConfigFile, deps.resolveViteConfig);
|
|
118
77
|
return {
|
|
119
78
|
root: ctx.root,
|
|
120
79
|
appDir: ctx.appDir,
|
|
@@ -290,4 +249,4 @@ async function runTypeCheck(options = {}, deps = {}) {
|
|
|
290
249
|
//#endregion
|
|
291
250
|
export { checkRoutes, runTypeCheck };
|
|
292
251
|
|
|
293
|
-
//# sourceMappingURL=cli-check-
|
|
252
|
+
//# sourceMappingURL=cli-check-C6Ev6wBO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-check-C6Ev6wBO.js","names":[],"sources":["../../src/cli-check.ts"],"sourcesContent":["/**\n * `timber check` — the implementation behind the CLI command.\n *\n * Two passes, in order:\n *\n * 1. **Routes.** Resolve the project through Vite — so root, `appDir`, and\n * `pageExtensions` are the ones `timber build` uses, including config\n * passed inline to `timber()` — then scan, run the convention linter, and\n * write the route codegen (`.timber/timber-routes.d.ts`,\n * `.timber/segment-types.d.ts`, `timber-env.d.ts`).\n *\n * Codegen runs *before* the type pass on purpose: those\n * files are what give `<Link href>`, `useSegmentParams()` and\n * `MiddlewareContext` their per-route types (design/09-typescript.md\n * §\"Typed Routes\"), and they are build artifacts — a fresh clone that has\n * never run `timber dev` or `timber build` does not have them, so a type\n * pass on its own would report errors about correct code. A clone whose\n * routes changed since the last build has the same problem with stale ones.\n * 2. **Types.** `tsgo --noEmit` over the project.\n *\n * tsgo is spawned as `node <@typescript/native-preview>/lib/tsgo.js`, never as\n * a bare `tsgo` off PATH — the package is an *optional peer dependency*, so\n * there is no reason for its shim to be on a user's PATH, and the shim is a\n * `.cmd` on Windows which cannot be spawned without a shell. `shell: true`\n * would then concatenate the argv into an unescaped command string, so a\n * project path containing a space would break on exactly the platform the\n * shell was there to support. This mirrors `scripts/build-types.js`, whose\n * header comment is the specification for the pattern.\n *\n * Design docs: 11-platform.md §CLI, 09-typescript.md, 28-npm-packaging.md\n */\n\nimport { createRequire } from 'node:module';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { scanRoutes } from './routing/scanner.ts';\nimport { lintConventions, formatConventionWarnings } from './routing/convention-lint.ts';\nimport { writeRouteCodegen, CODEGEN_OUTPUT, ENV_DTS_FILENAME } from './routing/codegen-write.ts';\nimport { resolveTimberContext, type ResolveViteConfig } from './cli-context.ts';\n\n/** The npm package that provides tsgo. Named in the missing-tool message. */\nconst TSGO_PACKAGE = '@typescript/native-preview';\n\n/**\n * The two generated declaration paths, relative to the project root, as they\n * appear in a resolved tsconfig file list. Derived from the writer's own\n * constants — a rename there cannot leave this check hunting for a file nothing\n * writes.\n */\nconst GENERATED_TYPE_PATHS = [ENV_DTS_FILENAME, CODEGEN_OUTPUT];\n\n/**\n * Run the compiler to completion.\n *\n * Resolves with its exit status (`null` when a signal killed it) and rejects\n * only when the process could not be started — the two outcomes `runTypeCheck`\n * has to keep apart. The seam is this narrow on purpose: a test double is a\n * plain async function, with no `ChildProcess` shape to imitate.\n */\nexport type RunTsgo = (command: string, args: string[], cwd: string) => Promise<number | null>;\n\n/**\n * Run the compiler and capture its stdout — used for `--showConfig`, whose\n * output is data we read rather than diagnostics the user reads.\n */\nexport type ReadTsgoConfig = (command: string, args: string[], cwd: string) => Promise<string>;\n\n/** @internal Dependency injection for testing. */\nexport interface CheckDeps {\n /**\n * Resolve tsgo's own JS entry point. Throws when the package is not\n * installed — the caller turns that into an install instruction.\n */\n resolveTsgoEntry?: () => string;\n runTsgo?: RunTsgo;\n readTsgoConfig?: ReadTsgoConfig;\n resolveViteConfig?: ResolveViteConfig;\n}\n\nexport interface CheckOptions {\n /** Project root, for resolving tsgo. Defaults to `process.cwd()`. */\n root?: string;\n /**\n * Path to a **tsconfig**, forwarded to tsgo as `--project` and sourced from\n * the CLI's own `--project`. The route pass takes no part in it: it resolves\n * the app's configuration through Vite, as `timber build` does.\n */\n config?: string;\n}\n\n/**\n * Locate `@typescript/native-preview`'s JS entry.\n *\n * Resolution is attempted from this package first (it is declared an optional\n * peer dependency, so a package manager links it here when the user installs\n * it) and then from the project root (covering a hoisted install, or a user who\n * added it without the peer link). Throws if neither finds it.\n */\nexport function resolveTsgoEntry(\n root: string,\n resolveFrom: (from: string, specifier: string) => string = (from, specifier) =>\n createRequire(from).resolve(specifier)\n): string {\n // `createRequire` takes a filename or a file URL; a plain absolute path is\n // the portable spelling for the project root (a `file://` prefix on a Windows\n // path is not a valid URL). Same form as `checkPeerDependencies`.\n const candidates = [import.meta.url, join(root, 'package.json')];\n let lastError: unknown;\n\n for (const from of candidates) {\n try {\n // The package exports no subpath for its entry, so resolve the manifest\n // and walk to the module its `bin/tsgo` shim imports.\n const manifest = resolveFrom(from, `${TSGO_PACKAGE}/package.json`);\n return join(dirname(manifest), 'lib', 'tsgo.js');\n } catch (err) {\n lastError = err;\n }\n }\n\n throw new Error(\n `[timber] Cannot find ${TSGO_PACKAGE}, which \\`timber check\\` uses to type-check.\\n\\n` +\n ` Install it as a dev dependency:\\n\\n` +\n ` pnpm add -D ${TSGO_PACKAGE}\\n\\n` +\n ` (resolution failed: ${lastError instanceof Error ? lastError.message : String(lastError)})`\n );\n}\n\n/** What the route pass needs from the command line. */\nexport interface RouteOptions {\n /**\n * Path to a **Vite** config file, from `--config`. Undefined means Vite's own\n * discovery, the same as `timber dev`/`build` without the flag. A project\n * that builds with a custom Vite config has to be checkable with it too, or\n * `check` resolves a different app than `build` does (TIM-1332, round 4).\n */\n viteConfigFile?: string;\n}\n\n/** Everything the route scan needs, as the build itself resolves it. */\nexport interface RouteInputs {\n root: string;\n appDir: string;\n pageExtensions: string[] | undefined;\n}\n\n/**\n * Resolve root, appDir and pageExtensions the way `timber build` resolves them\n * — through Vite, never by re-reading `timber.config.ts` (see `cli-context.ts`).\n * Inline `timber({ pageExtensions: [...] })` is the case that bites: a scan\n * that re-read the file would silently use default page extensions and write a\n * route map missing the app's pages. `examples/next-playground-migration` is\n * exactly this shape.\n */\nexport async function resolveRouteInputs(\n options: RouteOptions = {},\n deps: CheckDeps = {}\n): Promise<RouteInputs> {\n const ctx = await resolveTimberContext('check', options.viteConfigFile, deps.resolveViteConfig);\n return { root: ctx.root, appDir: ctx.appDir, pageExtensions: ctx.config.pageExtensions };\n}\n\n/**\n * Resolve the app directory, scan it, fail on proven-fatal route\n * misconfigurations, print warnings, and write the route codegen the type pass\n * depends on.\n *\n * Error-level lint findings throw rather than print, matching what\n * `plugins/routing.ts` does at build time: the tree is not fit to publish, so\n * it must not reach codegen either. Warnings are printed and do not fail the\n * command — same severity contract as dev and build, so `timber check` cannot\n * disagree with them about what a valid app is.\n */\nexport async function checkRoutes(\n options: RouteOptions = {},\n deps: CheckDeps = {}\n): Promise<RouteInputs> {\n const inputs = await resolveRouteInputs(options, deps);\n const { root, appDir, pageExtensions } = inputs;\n\n const tree = scanRoutes(appDir, { pageExtensions });\n\n const warnings = lintConventions(tree, appDir);\n const fatal = warnings.filter((w) => w.level === 'error');\n if (fatal.length > 0) {\n throw new Error(formatConventionWarnings(fatal, { color: false }));\n }\n if (warnings.length > 0) {\n process.stderr.write(`${formatConventionWarnings(warnings)}\\n`);\n }\n\n // Awaited, unlike the dev/build path: the type pass reads these files, so a\n // failed write would be reported as type errors in the user's own code.\n // Reported as a codegen failure rather than surfaced as a bare `EROFS`/\n // `EACCES` from somewhere unnamed — the raw error says which path could not\n // be written but not which pass wanted it.\n try {\n await writeRouteCodegen(tree, appDir, root);\n } catch (err) {\n throw new Error(\n `[timber] Failed to write the generated route types, which the type check reads: ` +\n `${err instanceof Error ? err.message : String(err)}`\n );\n }\n\n verifyCodegenImports(root);\n\n // Returned so the type pass runs against the same root these files landed in.\n return inputs;\n}\n\n/**\n * Verify that the import specifiers in the generated route types resolve on the\n * filesystem. A `.d.ts` whose import fails does NOT produce a TypeScript error —\n * every type that depends on it silently becomes `any`. The generated file\n * contains type-level `_AssertSchemaNotAny` / `_AssertCodecTypeNotAny` guards\n * that catch this at typecheck time, but a diagnostic here fires earlier and\n * names the exact import that is broken rather than leaving the user to decode\n * a constraint error.\n */\nexport function verifyCodegenImports(root: string): void {\n const dtsPath = join(root, CODEGEN_OUTPUT);\n let content: string;\n try {\n content = readFileSync(dtsPath, 'utf-8');\n } catch {\n return;\n }\n\n const dtsDir = dirname(dtsPath);\n\n // Check relative imports (default imports only — the generated codegen uses\n // `import type schema from '...'` for relative paths). Named imports from\n // relative paths are not emitted today; if codegen adds them, extend this.\n const RELATIVE_IMPORT_RE = /^import\\s+type\\s+\\w+\\s+from\\s+'(\\.\\.?\\/[^']+)'/gm;\n const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx'];\n let match;\n while ((match = RELATIVE_IMPORT_RE.exec(content)) !== null) {\n const specifier = match[1];\n const base = join(dtsDir, specifier);\n const resolves = existsSync(base) || EXTENSIONS.some((ext) => existsSync(base + ext));\n if (!resolves) {\n process.stderr.write(\n `[timber] WARNING: the generated ${CODEGEN_OUTPUT} imports '${specifier}',\\n` +\n ` but no file resolves at ${base}{${EXTENSIONS.join(',')}}.\\n` +\n ` Every type that depends on this import will silently become \\`any\\`.\\n\\n` +\n ` Check that your app/ directory is where timber expects it.\\n\\n`\n );\n }\n }\n\n // Bare-specifier imports (`@timber-js/app/params`) are NOT checked here.\n // They are all dependencies of @timber-js/app itself — if timber is installed,\n // they resolve. And createRequire uses CJS resolution which cannot follow\n // ESM-only subpath exports, producing false warnings (TIM-1348 review).\n}\n\n/**\n * The production `RunTsgo`: spawn the compiler with its stdio inherited, so its\n * diagnostics stream to the terminal as they are produced (and are not capped\n * by a buffer, as `execFile` output would be).\n */\nconst spawnTsgo: RunTsgo = async (command, args, cwd) => {\n const { spawn } = await import('node:child_process');\n return new Promise<number | null>((resolve, reject) => {\n // No `shell: true`: Node would concatenate the argv into an unescaped\n // command string, so a project path containing a space would break.\n const child = spawn(command, args, { stdio: 'inherit', cwd });\n child.on('error', reject);\n child.on('close', (code) => resolve(code));\n });\n};\n\n/**\n * The production `ReadTsgoConfig`: `--showConfig`, with stdout captured.\n *\n * `maxBuffer` is raised well past `execFile`'s 1 MB default because the output\n * is one line per file in the program — a large app can exceed that, and\n * overflowing it would make this advisory go quiet on exactly the projects\n * where a misconfigured `include` costs the most. A size-dependent silence is\n * indistinguishable from \"everything is fine\".\n */\nconst execTsgoConfig: ReadTsgoConfig = async (command, args, cwd) => {\n const { execFile } = await import('node:child_process');\n return new Promise<string>((resolve, reject) => {\n execFile(command, args, { cwd, maxBuffer: 64 * 1024 * 1024 }, (err, stdout) =>\n err ? reject(err) : resolve(stdout)\n );\n });\n};\n\n/**\n * Warn when the generated route declarations will not reach the type checker.\n *\n * Writing `.timber/timber-routes.d.ts` and `timber-env.d.ts` is only half the\n * job: TypeScript loads them only if the project's `include`/`files` actually\n * pull `timber-env.d.ts` in (it is what `/// <reference>`s the route map). A\n * tsconfig with an explicit `include` that lists only source directories —\n * `[\"app\", \"*.config.ts\"]`, which is what `create-timber-app` shipped before\n * TIM-1332 — silently omits it, and every `<Link href>` then type-checks\n * against the empty placeholder instead of the generated route union. The\n * check would pass while answering the wrong question.\n *\n * The resolved file list comes from `tsgo --showConfig` rather than from our\n * own reading of the tsconfig: `include`, `exclude`, `files`, and `extends`\n * compose in ways that are the compiler's to decide, and a second\n * implementation of them would be a duplicate that drifts. `--showConfig` only\n * resolves globs, so it costs no type checking.\n *\n * This warns rather than fails. An app that never uses typed routes is\n * correct without those declarations, and a hard error would refuse it — the\n * one outcome a check must not produce for a working project. When the answer\n * cannot be determined (tsgo errored, output was not the expected JSON), the\n * warning is skipped: an advisory that fires on an unknown is noise.\n */\nasync function warnIfRouteTypesUnreachable(\n tsgoEntry: string,\n configArgs: string[],\n cwd: string,\n readConfig: ReadTsgoConfig\n): Promise<void> {\n let files: unknown;\n try {\n const raw = await readConfig(process.execPath, [tsgoEntry, '--showConfig', ...configArgs], cwd);\n files = (JSON.parse(raw) as { files?: unknown }).files;\n } catch {\n return;\n }\n if (!Array.isArray(files)) return;\n\n // Suffix match, not path resolution: `--showConfig` prints paths relative to\n // whichever tsconfig the compiler *found*, which for a bare `timber check`\n // is a directory we never learn. Over-matching only suppresses an advisory.\n //\n // The bare form is matched as well as the `/`-prefixed one. tsgo emits\n // `./timber-env.d.ts` today, but a leading `./` is cosmetic and an emitter\n // that dropped it would turn this into a warning printed at every correctly\n // configured project — the failure direction that trains people to ignore it.\n const reaches = files.some((file) => {\n if (typeof file !== 'string') return false;\n const normalized = file.replace(/\\\\/g, '/');\n return GENERATED_TYPE_PATHS.some(\n (path) => normalized === path || normalized.endsWith(`/${path}`)\n );\n });\n if (reaches) return;\n\n process.stderr.write(\n `[timber] The generated route types are not part of this TypeScript project, so\\n` +\n ` \\`<Link href>\\` and \\`useSegmentParams()\\` are being checked against empty\\n` +\n ` placeholders. Your tsconfig's \"include\" does not pull in timber-env.d.ts.\\n\\n` +\n ` To fix: add it to \"include\":\\n\\n` +\n ` \"include\": [\"app\", \"*.config.ts\", \"timber-env.d.ts\"]\\n\\n`\n );\n}\n\n/**\n * Run `tsgo --noEmit`, streaming its output.\n *\n * Rejects with the real numeric exit status on type errors. A failure to *start*\n * the compiler is reported separately — the historical bug this replaces\n * surfaced ENOENT as \"Type check failed with exit code undefined\", which read\n * as a type error and sent users looking at their own code.\n */\nexport async function runTypeCheck(\n options: CheckOptions = {},\n deps: CheckDeps = {}\n): Promise<void> {\n // The *resolved* root, handed over by the route pass — not `process.cwd()`.\n // The two differ whenever `--config` names a Vite config elsewhere, and the\n // route pass wrote `.timber/*` and `timber-env.d.ts` under the resolved one.\n // Running tsgo from cwd would then discover a different tsconfig entirely and\n // type-check a different project than the one just validated (TIM-1332, round\n // 5) — routes checked for the app, types checked for the monorepo around it.\n const root = options.root ?? process.cwd();\n const tsgoEntry = (deps.resolveTsgoEntry ?? (() => resolveTsgoEntry(root)))();\n const runTsgo = deps.runTsgo ?? spawnTsgo;\n\n // An explicit `--project` is a path the user typed at their own prompt, so it\n // is resolved against *their* cwd before the compiler is moved to the project\n // root. Without `--project`, tsgo discovers `tsconfig.json` from `root`,\n // which is where the declarations it needs were just written.\n const configArgs = options.config ? ['--project', resolve(process.cwd(), options.config)] : [];\n\n // Before checking: say so if the declarations the route pass just wrote will\n // not be part of the program. Otherwise the check passes while `<Link href>`\n // is measured against a placeholder. Same root and same argv as the check\n // itself, or it would inspect a different program than the one tsgo reads.\n await warnIfRouteTypesUnreachable(\n tsgoEntry,\n configArgs,\n root,\n deps.readTsgoConfig ?? execTsgoConfig\n );\n\n let code: number | null;\n try {\n code = await runTsgo(process.execPath, [tsgoEntry, '--noEmit', ...configArgs], root);\n } catch (err) {\n // Could not start the compiler at all. Kept distinct from a nonzero exit:\n // the version this replaced reported ENOENT as \"Type check failed with\n // exit code undefined\", which reads as a verdict on the user's types.\n throw new Error(\n `[timber] Failed to start the type checker: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n\n if (code === 0) return;\n\n // A null code means a signal killed the process rather than it exiting.\n // Say so instead of printing \"exit code null\".\n throw new Error(\n code === null\n ? 'Type check terminated before it finished'\n : `Type check failed with exit code ${code}`\n );\n}\n"],"mappings":"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"}
|