@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
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"error-boundary-BrHmG-2X.js","names":[],"sources":["../../src/shared/payload-root.ts","../../src/client/rsc-fetch.ts","../../src/client/unload-guard.ts","../../src/client/stale-client.ts","../../src/client/router-skew.ts","../../src/client/router-effects.ts","../../src/client/error-boundary.tsx"],"sourcesContent":["/**\n * The shape of an RSC payload's root row.\n *\n * Every tree the server sends the browser is serialized as\n * `{ tree, params, slotParams }` — the route's element tree, plus the coerced\n * params it rendered with, as *siblings* rather than as a wrapper around the\n * tree. See design/41-global-params.md §\"Transport\".\n *\n * Two reasons it is a sibling field rather than a provider inside the payload\n * (TIM-1297, which is TIM-1294's mechanism corrected):\n *\n * 1. **A provider inside the payload cannot serve a partial navigation.** The\n * client keeps the previous tree and splices the new payload in at the\n * first updated `SegmentOutlet`, so the payload's own provider lands\n * *below* the retained region — and the retained region's root is the\n * *departing* route's provider, which therefore wins for every reader in a\n * skipped layout. Wrapping something above it does not help: context\n * resolves to the nearest provider, and the departing one is nearer.\n * Params belong to the *navigation*, so the provider has to be owned by the\n * client, above the merge point, and there must be exactly one.\n *\n * 2. **Element props are not readable at the instant the root resolves.**\n * Flight outlines a React element's props and fills the record in after the\n * root row resolves, so a reader that unblocks on the root sees `{}` — and\n * since the object is filled *in place*, its identity never changes and any\n * memo keyed on it caches the empty snapshot permanently. A plain model\n * field has no such window: the root chunk stays blocked until everything\n * it references has resolved.\n *\n * This module is the single declaration of that shape. The server builds roots\n * through `server/publish-params.ts` and the client reads them through the\n * helpers here — producer and consumer do not each describe the payload.\n */\n\nimport type { CoercedParams } from './param-value.ts';\nimport type { SlotParamsRecord } from './slot-params.ts';\n\n/** The params published beside a tree. */\nexport interface PublishedParams {\n /** The main route's coerced params. */\n params: CoercedParams;\n /** Per-slot params, or null when the route rendered no slots with params. */\n slotParams: SlotParamsRecord | null;\n}\n\n/**\n * Params ready to publish: settled, or a promise the root provider suspends on.\n *\n * Both forms exist on purpose. The hydration path cannot await — `hydrateRoot`\n * has to be called before the inline Flight stream has necessarily produced\n * the root row — so it hands over the promise and suspends exactly where React\n * was already going to suspend on the tree. Every other path settles it inside\n * the navigation transition first, which is what keeps a decode failure inside\n * the app's own error boundaries instead of above them (TIM-1297).\n */\nexport type ParamsSource = PublishedParams | Promise<PublishedParams>;\n\n/** The root row of an RSC payload. */\nexport interface RscPayloadRoot extends PublishedParams {\n /** The route's element tree. */\n tree: unknown;\n}\n\n/**\n * What a reader gets when the value it was handed is not a payload root.\n *\n * In the browser this is unreachable for a real response: all four producers\n * of a route payload go through `withPublishedParams`, and a client talking to\n * a different build is answered with 204 at Stage 1c\n * (`server/pipeline-phases.ts`) before any payload exists. It is reachable in\n * the router's test/fallback path, where `decodeRsc` is absent and the\n * \"payload\" is the raw response text.\n */\nexport const NO_PUBLISHED_PARAMS: PublishedParams = { params: {}, slotParams: null };\n\n/** True when `value` carries published params — a root, or a read of one. */\nfunction hasPublishedParams(value: unknown): value is PublishedParams {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'params' in value &&\n typeof (value as PublishedParams).params === 'object' &&\n (value as PublishedParams).params !== null\n );\n}\n\n/**\n * Read the params published beside a tree.\n *\n * Accepts a payload root *or* a `PublishedParams` already split off one, so\n * the client can pre-resolve on the navigation path and still hand the same\n * value to the same provider. Never throws.\n */\nexport function readPublishedParams(source: unknown): PublishedParams {\n if (!hasPublishedParams(source)) return NO_PUBLISHED_PARAMS;\n return { params: source.params, slotParams: source.slotParams ?? null };\n}\n\n/**\n * Read the renderable tree out of a payload root.\n *\n * Returns the value unchanged when it is not a root — the router's fallback\n * path stores raw response text under the same name, and a test asserting on\n * that text should see the text.\n */\nexport function readPayloadTree(root: unknown): unknown {\n if (typeof root === 'object' && root !== null && 'tree' in root && hasPublishedParams(root)) {\n return (root as RscPayloadRoot).tree;\n }\n return root;\n}\n\n/** A decoded payload, split into the two things the client does with it. */\nexport interface SplitPayload {\n /** The renderable tree — an element, or a thenable React suspends on. */\n tree: unknown;\n /**\n * The params published beside it. Settled when the root already is, so the\n * hydration path can render without an extra await.\n *\n * **Never rejects.** A decode failure has to surface where React renders\n * `tree`, so the error boundary *around the tree* catches it. Rejecting here\n * too would take down the provider above every boundary in the app, turning\n * a contained failure into a blank page (TIM-1297).\n */\n params: ParamsSource;\n}\n\n/**\n * Split a decoded payload root into its tree and its params.\n *\n * Accepts a settled root or a thenable of one, and preserves which it was: a\n * settled root splits synchronously, so no code path gains a suspend point it\n * did not already have.\n */\nexport function splitPayloadRoot(root: unknown): SplitPayload {\n if (\n typeof root !== 'object' ||\n root === null ||\n typeof (root as PromiseLike<unknown>).then !== 'function'\n ) {\n return { tree: readPayloadTree(root), params: readPublishedParams(root) };\n }\n const settled = root as PromiseLike<unknown>;\n const tree = Promise.resolve(settled).then(readPayloadTree);\n // The rejection still reaches React through its own read of `tree`; this\n // only marks it handled for the case where nothing renders it (a partial\n // navigation whose segment update finds no matching outlet).\n tree.catch(() => {});\n const params = Promise.resolve(settled).then(readPublishedParams, () => NO_PUBLISHED_PARAMS);\n return { tree, params };\n}\n","/**\n * RSC Fetch — handles fetching and parsing RSC Flight payloads.\n *\n * Extracted from router.ts to keep both files under the 500-line limit.\n * This module handles:\n * - Cache-busting URL generation for RSC requests\n * - Building RSC request headers (Accept, X-Timber-State-Tree)\n * - Extracting metadata from RSC response headers\n * - Fetching and decoding RSC payloads\n *\n * See design/19-client-navigation.md §\"RSC Payload Handling\"\n */\n\nimport {\n rscCacheKey,\n recordLookup,\n randomRscCacheKey,\n RSC_KEY_PARAM,\n} from '../shared/rsc-cache-key.ts';\nimport type { RscManifest, RscManifestEntry } from '../shared/rsc-manifest.ts';\nimport {\n splitPayloadRoot,\n readPublishedParams,\n type PublishedParams,\n} from '../shared/payload-root.ts';\nimport { normalizeBasePath, stripBasePath, withBasePath } from '../shared/base-path.ts';\nimport { rscPayloadPath } from '../shared/rsc-payload-path.ts';\nimport type { SegmentInfo, StateTree } from '../shared/segment-info.ts';\nimport { TIMBER_ERROR_HEADER } from '../shared/rsc-error-envelope.ts';\nimport { RSC_CONTENT_TYPE, isRscContentType } from '../shared/rsc-media-type.ts';\nimport type { RouterDeps } from './router-types.ts';\n\n// ─── Types ───────────────────────────────────────────────────────\n\n/** Result of fetching an RSC payload — includes segment metadata. */\nexport interface FetchResult {\n /** The renderable tree, split off the payload root. */\n payload: unknown;\n /**\n * The params the same payload published beside that tree.\n *\n * Not a separate channel: both fields come out of one `splitPayloadRoot`\n * call on one decoded root, so a response cannot deliver a tree and another\n * route's params. They used to arrive on their own header, which is how a\n * navigation could render one route and publish another's (TIM-1285,\n * TIM-1294).\n */\n params: PublishedParams | Promise<PublishedParams>;\n /**\n * Promise that settles when the RSC decode completes or fails.\n * The payload thenable is NOT awaited before returning (for streaming),\n * so callers must monitor this to catch async decode errors\n * (truncated streams, Flight parse failures) that would otherwise\n * become unhandled rejections.\n */\n decodePromise: Promise<void> | null;\n /** Segment metadata from X-Timber-Segments header for populating the segment cache. */\n segmentInfo: SegmentInfo[] | null;\n /** Segment paths that were skipped by the server (for client-side merging). */\n skippedSegments: string[] | null;\n}\n\n// ─── URL Helpers ─────────────────────────────────────────────────\n\n/**\n * Append a `_rsc=<key>` query parameter to the URL (TIM-1268).\n *\n * The key is a deterministic hash of the RSC request headers, so identical\n * requests share a URL and the payload becomes cacheable. It also keeps the\n * document and the payload on different URLs, so a shared cache cannot\n * return HTML for an RSC request (or vice versa) even where `Vary` is\n * opt-in configuration rather than default behaviour — Cloudflare and\n * CloudFront both. See GHSA-wfc6-r584-vfw7, design/13-security.md.\n *\n * The origin recomputes this key from the received headers and refuses to\n * let a mismatched response be shared-cached, so a caller cannot claim\n * another client's key while sending its own headers.\n *\n * Falls back to a random value when no key can be derived — `crypto.subtle`\n * is absent in non-secure browsing contexts (plain `http://` on a LAN\n * address). Dropping the parameter instead would put the payload back on\n * the document URL, where a query-keyed cache that ignores `Vary` could\n * store a Flight response under `/about` and serve it to an HTML\n * navigation. The random parameter this feature replaced did separate those\n * representations, and the fallback must not be worse than what it\n * replaced. The origin cannot verify a random key, so it refuses to\n * share-cache the response — which is exactly the old behaviour, where a\n * unique URL was never a cache hit anyway.\n *\n * Strips any #fragment before appending — fragments are client-only and\n * fetch() discards them, so _rsc would land inside the hash and be lost.\n */\nasync function appendRscParam(url: string, headers: Record<string, string>): Promise<string> {\n const hashIndex = url.indexOf('#');\n const urlWithoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);\n const key = (await rscCacheKey(recordLookup(headers))) ?? randomRscCacheKey();\n const separator = urlWithoutHash.includes('?') ? '&' : '?';\n return `${urlWithoutHash}${separator}${RSC_KEY_PARAM}=${key}`;\n}\n\n// ─── Deployment ID ───────────────────────────────────────────────\n\n/**\n * The client's deployment ID, set at bootstrap from the runtime config.\n * Sent with every RSC/action request for version skew detection.\n * Null in dev mode. See TIM-446.\n */\nlet clientDeploymentId: string | null = null;\n\n/** Set the client deployment ID. Called once at bootstrap. */\nexport function setClientDeploymentId(id: string | null): void {\n clientDeploymentId = id;\n}\n\n/** Get the client deployment ID. */\nexport function getClientDeploymentId(): string | null {\n return clientDeploymentId;\n}\n\n// ─── Base Path ───────────────────────────────────────────────────\n\n/**\n * The deployment base path (Vite's resolved `base`), normalized.\n * Set at bootstrap from `virtual:timber-config`; `'/'` unless the app is\n * deployed under a sub-path. See design/11-platform.md, TIM-1261.\n *\n * **The base is applied in exactly one place: at fetch time**\n * (`toStaticRscUrl`). The static build writes the RSC manifest with\n * root-relative keys *and* root-relative URLs, so the generated site stays\n * relocatable and there is no second copy of the base to drift from this one.\n */\nlet basePath = '/';\n\n/** Set the deployment base path. Called once at bootstrap. */\nexport function setBasePath(base: string | undefined): void {\n basePath = normalizeBasePath(base);\n}\n\n/** Get the normalized deployment base path. @internal Exported for testing. */\nexport function getBasePath(): string {\n return basePath;\n}\n\n// ─── Static Mode ────────────────────────────────────────────────\n\n/**\n * When true, RSC fetches use _rsc/*.rsc file URLs instead of\n * the route URL with Accept headers. Static hosts ignore Accept\n * headers, so the client must fetch the pre-generated .rsc files\n * directly. Set at bootstrap from virtual:timber-config output mode.\n */\nlet staticMode = false;\n\nexport function setStaticMode(enabled: boolean): void {\n staticMode = enabled;\n}\n\nexport function isStaticMode(): boolean {\n return staticMode;\n}\n\n/**\n * RSC manifest mapping unhashed → content-hashed `.rsc` URLs. Populated from\n * `window.__TIMBER_RSC_MANIFEST__` (injected into HTML during static\n * generation). Shape shared with the static build that writes it — see\n * `shared/rsc-manifest.ts`. See TIM-1254.\n */\nlet rscManifest: RscManifest | null = null;\n\nfunction getRscManifest(): RscManifest | null {\n if (rscManifest) return rscManifest;\n if (typeof window !== 'undefined' && (window as any).__TIMBER_RSC_MANIFEST__) {\n rscManifest = (window as any).__TIMBER_RSC_MANIFEST__;\n }\n return rscManifest;\n}\n\n/** @internal Exported for testing. */\nexport function setRscManifest(manifest: RscManifest | null): void {\n rscManifest = manifest;\n}\n\n/** A resolved static payload target: the URL to fetch, and how it was resolved. */\ninterface StaticRscTarget {\n /** Fetchable URL — base-prefixed. */\n url: string;\n /**\n * True when the RSC manifest supplied a content-hashed filename. The hash\n * guarantees freshness, so no `_rsc` cache key is appended (TIM-1254).\n */\n hashed: boolean;\n}\n\n/**\n * Resolve a route URL to the `_rsc/*.rsc` file to fetch. The naming rule is\n * `shared/rsc-payload-path.ts`, shared with the build that writes the files.\n *\n * When an RSC manifest is available (hashed filenames from TIM-1254),\n * the manifest is consulted to resolve to the hashed path.\n *\n * / → /_rsc/index.rsc (or /_rsc/index-B7YxEKdN.rsc with manifest)\n * /about → /_rsc/about/index.rsc (or /_rsc/about/index-C8ZzFLfO.rsc)\n * /blog/hello → /_rsc/blog/hello/index.rsc\n *\n * The manifest is written and read in route space (no base prefix), so the\n * base is applied here — once, on the way out. See client/base-path.ts.\n */\nfunction toStaticRscUrl(url: string): StaticRscTarget {\n const unhashed = toUnhashedRscUrl(url);\n const hashedUrl = manifestLookup(unhashed);\n return { url: withBasePath(basePath, hashedUrl ?? unhashed), hashed: hashedUrl !== null };\n}\n\n/**\n * Compute the unhashed `_rsc/*.rsc` path for a route URL.\n *\n * Returns a **route-space** path: the deployment base is stripped from the\n * incoming URL and is NOT re-applied, because this value doubles as the RSC\n * manifest key, and the manifest is base-less. `toStaticRscUrl` applies the\n * base to whatever is actually fetched.\n *\n * @internal Exported for testing.\n */\nexport function toUnhashedRscUrl(url: string): string {\n const hashIndex = url.indexOf('#');\n const queryIndex = url.indexOf('?');\n const hashEnd = hashIndex === -1 ? url.length : hashIndex;\n const queryEnd = queryIndex === -1 ? url.length : queryIndex;\n const end = Math.min(hashEnd, queryEnd);\n let pathname = stripBasePath(basePath, url.slice(0, end));\n // Strip trailing slash (unless root) to match static build output naming\n if (pathname.length > 1 && pathname.endsWith('/')) {\n pathname = pathname.slice(0, -1);\n }\n return rscPayloadPath(pathname);\n}\n\n/**\n * Look up a key in the RSC manifest, falling back to percent-decoded\n * lookup for encoded browser URLs. Returns the entry or null.\n */\nfunction manifestEntry(key: string): RscManifestEntry | null {\n const manifest = getRscManifest();\n if (!manifest) return null;\n if (manifest[key]) return manifest[key];\n try {\n const decoded = decodeURIComponent(key);\n if (decoded !== key && manifest[decoded]) return manifest[decoded];\n } catch {\n // Malformed percent sequence (e.g. /100%.rsc) — skip decoded lookup\n }\n return null;\n}\n\n/**\n * Look up the hashed URL for an unhashed RSC path.\n */\nfunction manifestLookup(key: string): string | null {\n return manifestEntry(key)?.url ?? null;\n}\n\n// The manifest used to inline each route's params, because a generated\n// `.rsc` file carries no response headers and the header channel therefore\n// did not exist on the static path (TIM-1255, TIM-1285). The `.rsc` file now\n// carries them itself — they are props inside the payload — so the static\n// path needs no special case at all.\n\n// ─── Reload Signal ───────────────────────────────────────────────\n\n/** Header name used by the server to signal a version skew reload. */\nexport const RELOAD_HEADER = 'X-Timber-Reload';\n\n/** Header name for the client's deployment ID. */\nexport const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';\n\n/**\n * Check if a response signals a version skew reload.\n * Triggers a full page reload if the server indicates the client is stale.\n */\nexport function checkReloadSignal(response: Response): boolean {\n return response.headers.get(RELOAD_HEADER) === '1';\n}\n\n// ─── Header Builder ──────────────────────────────────────────────\n\n/**\n * Build the headers for an RSC payload request.\n *\n * **Every header added here is hashed into the payload URL** as the `_rsc`\n * cache key (see `rscCacheKey`), and URLs are logged by proxies, CDNs, and\n * origin access logs. So: no credentials, no session identifiers, no\n * user-supplied content. `fnv1aHash` is a cache-key hash, not a KDF — a\n * low-entropy secret would be recoverable from the URL by brute force.\n * Authentication already travels on cookies, which are not part of this\n * object and never enter the key.\n */\nexport function buildRscHeaders(\n stateTree: StateTree | undefined,\n currentUrl?: string\n): Record<string, string> {\n const headers: Record<string, string> = {\n Accept: RSC_CONTENT_TYPE,\n };\n if (stateTree) {\n headers['X-Timber-State-Tree'] = JSON.stringify(stateTree);\n }\n // Send current URL for intercepting route resolution.\n // The server uses this to determine if an intercepting route should\n // render instead of the actual target route (modal pattern).\n // See design/07-routing.md §\"Intercepting Routes\"\n if (currentUrl) {\n headers['X-Timber-URL'] = currentUrl;\n }\n // Send deployment ID for version skew detection (TIM-446).\n // The server compares this against the current build's ID.\n // On mismatch, the server signals a reload instead of returning\n // an RSC payload with mismatched module references.\n if (clientDeploymentId) {\n headers[DEPLOYMENT_ID_HEADER] = clientDeploymentId;\n }\n return headers;\n}\n\n// ─── Response Header Extraction ──────────────────────────────────\n\n/** Dev-only warning for malformed framework headers. Tree-shaken in production. */\nfunction warnMalformedHeader(headerName: string, raw: string): void {\n if (process.env.NODE_ENV !== 'production') {\n const preview = raw.length > 200 ? raw.slice(0, 200) + '…' : raw;\n console.warn(\n `[timber] Malformed ${headerName} header \\u2014 JSON.parse failed. ` +\n `This indicates a framework bug or header corruption. Raw (first 200 chars): ${preview}`\n );\n }\n}\n\n/**\n * Extract segment metadata from the X-Timber-Segments response header.\n * Returns null if the header is missing or malformed.\n *\n * Format: JSON array of {path, isRequestDependent} objects describing the rendered\n * segment chain from root to leaf. Used to populate the client-side\n * segment cache for state tree diffing on subsequent navigations.\n */\nexport function extractSegmentInfo(response: Response): SegmentInfo[] | null {\n const header = response.headers.get('X-Timber-Segments');\n if (!header) return null;\n try {\n return JSON.parse(header);\n } catch {\n warnMalformedHeader('X-Timber-Segments', header);\n return null;\n }\n}\n\n/**\n * Extract skipped segment paths from the X-Timber-Skipped-Segments header.\n * Returns null if the header is missing or malformed.\n *\n * When the server skips sync layouts the client already has cached,\n * it sends this header listing the skipped segment paths (outermost first).\n * The client uses this to merge the partial payload with cached segments.\n */\nexport function extractSkippedSegments(response: Response): string[] | null {\n const header = response.headers.get('X-Timber-Skipped-Segments');\n if (!header) return null;\n try {\n const parsed = JSON.parse(header);\n return Array.isArray(parsed) ? parsed : null;\n } catch {\n warnMalformedHeader('X-Timber-Skipped-Segments', header);\n return null;\n }\n}\n\n// ─── Redirect Error ──────────────────────────────────────────────\n\n/**\n * Thrown when an RSC payload response contains X-Timber-Redirect header.\n * Caught in navigate() to trigger a soft router navigation to the redirect target.\n */\nexport class RedirectError extends Error {\n readonly redirectUrl: string;\n constructor(url: string) {\n super(`Server redirect to ${url}`);\n this.redirectUrl = url;\n }\n}\n\n/**\n * Thrown when the server signals a version skew (X-Timber-Reload header).\n * Caught in navigate() to trigger a full page reload.\n * See TIM-446.\n */\nexport class VersionSkewError extends Error {\n constructor() {\n super('Version skew detected — server has been redeployed');\n }\n}\n\n/**\n * Thrown when the server returns an error for an RSC payload request.\n * The server sends X-Timber-Error header and a JSON body instead of a\n * broken RSC stream for any RenderError (4xx or 5xx). Caught in\n * navigate() to trigger a hard navigation so the server can render\n * the error page as HTML.\n *\n * See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n */\nexport class ServerErrorResponse extends Error {\n readonly status: number;\n readonly url: string;\n constructor(status: number, url: string) {\n super(`Server error ${status} during navigation to ${url}`);\n this.status = status;\n this.url = url;\n }\n}\n\n/**\n * Thrown when the RSC fetch response has a Content-Type that is not\n * text/x-component — e.g., a static asset (image, CSS, JS) served\n * for a same-origin URL that isn't a route. The response body is\n * cancelled immediately (headers-only cost). Caught in navigate()\n * to trigger a hard navigation; caught in prefetch() to store a\n * negative cache entry so click hard-navigates without a second fetch.\n *\n * See TIM-1231.\n */\nexport class NonRscResponse extends Error {\n readonly url: string;\n constructor(url: string) {\n super(`Non-RSC response for ${url}`);\n this.url = url;\n }\n}\n\n// ─── Stream Completion Tracking ───────────────────────────────────\n\n/**\n * Wrap a response body stream to track when it's fully consumed.\n * Returns a new body that passes all chunks through unchanged, plus\n * a `done` promise that resolves when the last chunk is read (or\n * rejects if the stream errors).\n *\n * createFromFetch's thenable resolves on shell arrival, but callers need to\n * know when the stream is fully decoded: that is what `navigateTransition`'s\n * returned promise means, and what the router's pending store, the Navigation\n * API deferred and `<Link>`'s `isPending` are all timed against.\n */\nfunction trackStreamCompletion(body: ReadableStream<Uint8Array>): {\n body: ReadableStream<Uint8Array>;\n done: Promise<void>;\n} {\n let resolveDone!: () => void;\n let rejectDone!: (e: unknown) => void;\n const done = new Promise<void>((res, rej) => {\n resolveDone = res;\n rejectDone = rej;\n });\n\n const reader = body.getReader();\n const tracked = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const result = await reader.read();\n if (result.done) {\n controller.close();\n resolveDone();\n } else {\n controller.enqueue(result.value);\n }\n } catch (error) {\n controller.error(error);\n rejectDone(error);\n }\n },\n cancel(reason) {\n reader.cancel(reason);\n resolveDone();\n },\n });\n\n return { body: tracked, done };\n}\n\n// ─── Fetch ───────────────────────────────────────────────────────\n\n/**\n * Fetch an RSC payload from the server. If a decodeRsc function is provided,\n * the response is decoded into a React element tree via createFromFetch.\n * Otherwise, the raw response text is returned (test mode).\n */\nexport async function fetchRscPayload(\n url: string,\n deps: RouterDeps,\n stateTree?: { segments: string[] },\n currentUrl?: string,\n signal?: AbortSignal\n): Promise<FetchResult> {\n // In static mode, fetch the pre-generated _rsc/*.rsc file directly\n // instead of the route URL with Accept headers. Static hosts ignore\n // Accept headers, so the route URL would return HTML.\n const staticTarget = staticMode ? toStaticRscUrl(url) : null;\n const fetchTarget = staticTarget ? staticTarget.url : url;\n // Static hosts resolve neither partial payloads nor intercepting routes,\n // so neither the state tree nor the source URL is sent — and neither may\n // fragment the cache key of a file that does not vary on them.\n const headers = staticMode\n ? buildRscHeaders(undefined, undefined)\n : buildRscHeaders(stateTree, currentUrl);\n // Skip the cache key when the manifest supplies a content-hashed\n // URL — the hash itself guarantees freshness, and the param would defeat\n // the immutable cache headers on /_rsc/* (TIM-1254).\n const rscUrl = staticTarget?.hashed ? fetchTarget : await appendRscParam(fetchTarget, headers);\n // Deriving the cache key is async (SubtleCrypto), so the fetch is no\n // longer issued in the same tick as the call. A navigation superseded\n // during that window would otherwise still hit the network with an\n // already-aborted signal.\n signal?.throwIfAborted();\n if (deps.decodeRsc) {\n // Production path: use createFromFetch for streaming RSC decoding.\n // createFromFetch takes a Promise<Response> and progressively parses\n // the RSC Flight stream as chunks arrive.\n //\n // Intercept the response to read segment metadata before createFromFetch\n // consumes the body. Reading headers does NOT consume the body stream.\n const fetchPromise = deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n let segmentInfo: SegmentInfo[] | null = null;\n let skippedSegments: string[] | null = null;\n // Track when the full RSC body stream is consumed (not just shell).\n // Initialized to resolved for bodyless responses; overwritten when\n // the response has a body.\n let streamDone: Promise<void> = Promise.resolve();\n\n const wrappedPromise = fetchPromise.then((response) => {\n // Version skew detection (TIM-446): if the server signals a reload,\n // throw VersionSkewError so the caller (router navigate) can trigger\n // a full page reload.\n if (checkReloadSignal(response)) {\n throw new VersionSkewError();\n }\n // Detect server-side redirects. The server returns 204 + X-Timber-Redirect\n // for RSC payload requests instead of a raw 302, because fetch with\n // redirect: \"manual\" turns 302s into opaque redirects (status 0, null body)\n // which crashes createFromFetch when it tries to read the body stream.\n const redirectLocation =\n response.headers.get('X-Timber-Redirect') ||\n (response.status >= 300 && response.status < 400 ? response.headers.get('Location') : null);\n if (redirectLocation) {\n throw new RedirectError(redirectLocation);\n }\n // Detect server error responses. The server returns X-Timber-Error header\n // with a JSON body instead of a broken RSC stream for any RenderError\n // (4xx or 5xx). Hard-navigate so the server renders the error page as HTML.\n // See design/10-error-handling.md §\"Error Page Rendering for Client Navigation\"\n if (response.headers.get(TIMBER_ERROR_HEADER) === '1') {\n throw new ServerErrorResponse(response.status, url);\n }\n // Content-Type guard: reject non-RSC responses before createFromFetch\n // tries to parse the body as Flight data.\n // In static mode, accept octet-stream/text/plain/absent content-type\n // (static hosts serve .rsc files with these), but still reject 404s\n // and text/html (missing .rsc file → host returns 404 page or SPA\n // HTML fallback). See TIM-1231, TIM-1243, TIM-1247.\n if (staticMode) {\n const contentType = response.headers.get('content-type');\n if (\n !response.ok ||\n (contentType && contentType.split(';')[0].trim().toLowerCase() === 'text/html')\n ) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n } else {\n if (!isRscContentType(response.headers.get('content-type'))) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n }\n // Metadata (<title>/<meta>/<link>) now rides the RSC Flight payload\n // as React elements — React 19 Float handles them. See TIM-1151.\n segmentInfo = extractSegmentInfo(response);\n skippedSegments = extractSkippedSegments(response);\n\n // Wrap the body to track full stream consumption. createFromFetch's\n // thenable resolves when the root model (shell) arrives, but we need\n // to know when ALL chunks are read — that is what the navigation\n // promise resolving means, and what keeps the pending store set.\n if (response.body) {\n const tracked = trackStreamCompletion(response.body);\n streamDone = tracked.done;\n streamDone.catch(() => {}); // prevent unhandled rejection\n return new Response(tracked.body, {\n headers: response.headers,\n status: response.status,\n });\n }\n return response;\n });\n // Await headers so segmentInfo is populated.\n await wrappedPromise;\n // Start decoding but do NOT await — return the in-progress thenable.\n // React can render a Flight thenable directly: it suspends on unresolved\n // parts and progressively renders as chunks arrive, spreading work across\n // frames instead of blocking the main thread in one burst.\n const root = deps.decodeRsc(wrappedPromise);\n // One split, one root: the tree and the params it was rendered with come\n // apart here and nowhere else, so nothing downstream has to keep them\n // paired by hand.\n const { tree: payload, params } = splitPayloadRoot(root);\n // Combine stream completion with payload error propagation.\n // streamDone marks the end of the RSC stream, which is what the\n // navigation promise resolves on. payloadError propagates\n // decode failures (stale client references, Flight parse errors)\n // so the router's catch block can trigger recovery (stale reload).\n const payloadError = new Promise<void>((_, reject) => {\n Promise.resolve(root).then(() => {}, reject);\n });\n payloadError.catch(() => {});\n const decodePromise = Promise.race([streamDone, payloadError]);\n // Mark handled, like payloadError above. The same rejection reaches the\n // router through `payload`, and a navigation that recovers by hard\n // navigating (version skew, non-RSC response) hangs deliberately and never\n // awaits this one — leaving it to surface as an unhandled rejection.\n decodePromise.catch(() => {});\n return {\n payload,\n params,\n decodePromise,\n segmentInfo,\n skippedSegments,\n };\n }\n // Test/fallback path: return raw text\n const response = await deps.fetch(rscUrl, { headers, redirect: 'manual', signal });\n // Check for redirect in test path too\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('Location');\n if (location) {\n throw new RedirectError(location);\n }\n }\n // Server error guard (same as production path above).\n if (response.headers.get(TIMBER_ERROR_HEADER) === '1') {\n throw new ServerErrorResponse(response.status, url);\n }\n // Content-Type guard (same as production path above). See TIM-1231, TIM-1243, TIM-1247.\n if (staticMode) {\n const fallbackContentType = response.headers.get('content-type');\n if (\n !response.ok ||\n (fallbackContentType &&\n fallbackContentType.split(';')[0].trim().toLowerCase() === 'text/html')\n ) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n } else {\n if (!isRscContentType(response.headers.get('content-type'))) {\n response.body?.cancel();\n throw new NonRscResponse(url);\n }\n }\n // Test/fallback path: raw text carries no payload root, so no params.\n return {\n payload: await response.text(),\n params: readPublishedParams(undefined),\n decodePromise: null,\n segmentInfo: extractSegmentInfo(response),\n skippedSegments: extractSkippedSegments(response),\n };\n}\n","/**\n * Page unload detection — suppresses spurious errors during page refresh/navigation.\n *\n * When the user refreshes the page or navigates away while React is still\n * streaming Suspense content, the aborted connection causes streaming errors.\n * These are not application errors — they're a side effect of the browser\n * tearing down the connection. This module tracks whether the page is being\n * unloaded so error boundaries and error handlers can suppress abort-related\n * errors during the unload window.\n *\n * This is the single owner of \"is this document going away\". Anything that\n * needs the answer imports `isPageUnloading()` — an unload flag maintained a\n * second time somewhere else is a flag that will drift out of agreement with\n * this one (TIM-1260).\n *\n * Mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * See design/10-error-handling.md §\"Known limitation: deny() inside Suspense and hydration\"\n */\n\nimport { unloading, _setUnloading } from './state.ts';\n\n/**\n * Departure is *committed*: `pagehide` has fired, or this document has called\n * reload() on itself. `beforeunload` alone doesn't qualify — it is\n * speculative, and the user can still cancel out of it.\n */\nlet committed = false;\n\nif (typeof window !== 'undefined') {\n window.addEventListener('beforeunload', () => {\n _setUnloading(true);\n\n // An app with an unsaved-changes prompt can have this cancelled. Nothing\n // fires to say so, so the only signal is that the page is still running\n // afterwards — and timers are blocked while the modal is up, so this task\n // cannot run until the user has chosen. If they left, `pagehide` has\n // already committed and this is a no-op.\n setTimeout(() => {\n if (!committed) _setUnloading(false);\n }, 0);\n });\n\n // pagehide is the committed-departure event, and fires for both navigations\n // and bfcache entry. Safari in particular may skip beforeunload entirely.\n window.addEventListener('pagehide', () => {\n committed = true;\n _setUnloading(true);\n });\n\n window.addEventListener('pageshow', (event) => {\n // A bfcache restore un-does the teardown `pagehide` announced: the same\n // document is alive again, with this module's state intact. Without the\n // reset, every consumer would go on suppressing errors on a live page.\n if (event.persisted) {\n committed = false;\n _setUnloading(false);\n }\n });\n}\n\n/**\n * Returns true if the page is currently being unloaded (user refreshed\n * or navigated away). Error boundaries should suppress errors in this state.\n */\nexport function isPageUnloading(): boolean {\n return unloading;\n}\n","/**\n * Stale Client — knowing when this bundle has been superseded.\n *\n * Layer 1 of version skew handling (see design/33-version-skew.md) is\n * proactive: the server compares `X-Timber-Deployment-Id` against the current\n * build and answers a stale client with `X-Timber-Reload: 1`. That covers\n * every RSC navigation and every server action — as long as there is a server\n * in the request path.\n *\n * This module is the reactive half, plus the state both halves feed. It\n * recognizes the two error shapes a stale client produces on its own:\n *\n * 1. **Stale client references** — React's Flight client throws\n * \"Could not find the module …\" when an RSC payload names a client module\n * ID that isn't in this bundle's manifest.\n * 2. **Chunk load failures** — a content-hashed chunk that no longer exists on\n * the CDN fails to `import()`. Every engine words this differently.\n *\n * It matters most for `output: 'static'`, where there is no server to send the\n * reload header and this is the *only* skew detection that can run.\n *\n * **Nothing here reloads the page.** A stale client is recorded, not acted on;\n * the router turns the next user-initiated navigation into a full document\n * load. See design/33-version-skew.md §\"Recovery on the next navigation\" for\n * why — a self-issued `location.reload()` cannot tell a recovery from a bounce\n * between two live deployments, and mid-rollout it produces exactly that loop.\n *\n * See TIM-1260, TIM-1275.\n */\n\n// The only import: the shared unload state. Kept minimal because this module\n// is reached from the error boundary, a `'use client'` component evaluated in\n// the SSR graph too — a dependency on the client fetch layer would drag that\n// graph along with it.\nimport { isPageUnloading } from './unload-guard.ts';\n\n// ─── Error Shapes ────────────────────────────────────────────────\n\n/**\n * Substrings that identify a failed dynamic import, lowercased for\n * case-insensitive matching. Engines and bundlers each word this\n * differently and none of them expose a stable error code.\n */\nconst CHUNK_LOAD_PATTERNS = [\n // Chromium\n 'failed to fetch dynamically imported module',\n // Firefox\n 'error loading dynamically imported module',\n // Safari\n 'importing a module script failed',\n // Vite's preload helper, for the stylesheet of a lazily-imported chunk\n 'unable to preload css for',\n];\n\n/** Substring React's Flight client uses when a client module ID is unknown. */\nconst STALE_REFERENCE_PATTERN = 'could not find the module';\n\n/**\n * Pull a comparable message out of an unknown thrown value. Rejections cross\n * enough boundaries (event handlers, `unhandledrejection`, Vite's preload\n * event) that the value is not reliably an `Error`.\n */\nfunction messageOf(error: unknown): string {\n if (typeof error === 'string') return error.toLowerCase();\n if (error instanceof Error) return error.message.toLowerCase();\n if (error && typeof error === 'object' && 'message' in error) {\n const message = (error as { message: unknown }).message;\n if (typeof message === 'string') return message.toLowerCase();\n }\n return '';\n}\n\n/**\n * True when the error is React's \"client reference not in this manifest\"\n * failure — the payload was produced by a build this bundle doesn't match.\n */\nexport function isStaleClientReference(error: unknown): boolean {\n return messageOf(error).includes(STALE_REFERENCE_PATTERN);\n}\n\n/**\n * True when the error is a failed dynamic import or CSS preload — the\n * content-hashed chunk this bundle asks for is gone from the CDN.\n */\nexport function isChunkLoadError(error: unknown): boolean {\n if (error instanceof Error && error.name === 'ChunkLoadError') return true;\n const message = messageOf(error);\n return CHUNK_LOAD_PATTERNS.some((pattern) => message.includes(pattern));\n}\n\n// ─── Staleness ───────────────────────────────────────────────────\n\n/**\n * True once this client is known to be running against a superseded\n * deployment. One-way: a document that has gone stale cannot come back, since\n * the only way to get fresh code is to load a new document.\n *\n * In-memory on purpose. Persisting it would be wrong — the next document gets\n * whatever build the server hands it, and if that one is stale too it will\n * find out the same way.\n */\nlet stale = false;\n\n/**\n * True once {@link initStaleClient} has seen a deployment ID — i.e. this is a\n * production build. False in dev, where nothing is ever stale: HMR owns code\n * updates, and a failed dynamic import is a real error the developer needs to\n * see rather than something a page load fixes.\n */\nlet deployed = false;\n\n/** Whether this client has been superseded by a newer deployment. */\nexport function isClientStale(): boolean {\n return stale;\n}\n\n/**\n * Record that the origin has told us this client is stale — the `X-Timber-Reload`\n * header on an RSC or action response. Authoritative, so it is not gated on\n * anything: a dev server never sends the header in the first place.\n */\nexport function markClientStale(): void {\n stale = true;\n}\n\n/**\n * Record staleness if `error` is one of the shapes a superseded bundle\n * produces. Returns whether it did, so a caller mid-navigation can decide to\n * hard-navigate instead of surfacing the error.\n *\n * Inference, unlike {@link markClientStale}, so it is gated on being a\n * production build. This is the single place that gate lives — call sites\n * cannot see each other, and \"remember to gate the next one too\" is not a\n * guarantee.\n */\nexport function markStaleFromError(error: unknown): boolean {\n if (!deployed) return false;\n\n // A document being torn down aborts its in-flight dynamic imports, and an\n // aborted import reads exactly like a chunk the CDN has dropped. That is not\n // evidence of staleness, and the bit outlives the teardown that produced it:\n // a document entering the back-forward cache is frozen, not destroyed, so a\n // false positive recorded on the way out would cost the *restored* page its\n // client-side navigation for good. `unload-guard.ts` clears its own flag on\n // a persisted `pageshow`; this one is one-way by design, so it has to not be\n // set in the first place.\n if (isPageUnloading()) return false;\n\n if (!isStaleClientReference(error) && !isChunkLoadError(error)) return false;\n stale = true;\n return true;\n}\n\n// ─── Initialization ──────────────────────────────────────────────\n\n/**\n * The underlying error Vite attaches to a `vite:preloadError` event.\n *\n * Narrowed at runtime rather than cast: the property is Vite's, not ours, and\n * a shape change should fall back to matching the event itself rather than\n * silently reading `undefined` forever.\n */\nfunction preloadErrorPayload(event: Event): unknown {\n return 'payload' in event ? (event as Event & { payload: unknown }).payload : event;\n}\n\n/**\n * Arm the module, and register the listeners that catch stale-client errors\n * never passing through the router: a lazily imported client component, a\n * preloaded stylesheet, anything that rejects outside a navigation.\n *\n * No-op when `deploymentId` is null — that is dev. Called from the browser\n * entry before hydration, so it precedes any render or navigation that could\n * throw.\n */\nexport function initStaleClient(deploymentId: string | null): void {\n if (!deploymentId) return;\n deployed = true;\n\n // Vite dispatches this for failed chunk/CSS preloads. Note what is *not*\n // here: `preventDefault()`. Recording staleness is not recovering from this\n // error — the page is still broken until the user navigates — so Vite must\n // go on rethrowing it rather than have it silently swallowed.\n window.addEventListener('vite:preloadError', (event) => {\n markStaleFromError(preloadErrorPayload(event));\n });\n\n window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {\n markStaleFromError(event.reason);\n });\n}\n","/**\n * Router Skew Recording — turning a rejection into \"this bundle is superseded\".\n *\n * Detection lives in `stale-client.ts`; this is the one place the router (and\n * anything else holding an RSC rejection) turns an error into that verdict.\n * Recovery is not here: every path that gives up on client-side navigation —\n * skew or not — leaves through `createSpaExits()` in `router-effects.ts`.\n *\n * See design/33-version-skew.md §\"Recovery on the next navigation\"\n */\n\nimport { VersionSkewError } from './rsc-fetch.ts';\nimport { markClientStale, markStaleFromError } from './stale-client.ts';\n\n/**\n * Record a version skew if `error` is one, and report whether it was.\n *\n * `VersionSkewError` is the origin saying so outright; the reactive shapes are\n * inferred from the error text and are a no-op in dev.\n *\n * Closes over nothing — anywhere an RSC payload can fail needs it, including\n * places that have no router instance to hand.\n */\nexport function recordSkew(error: unknown): boolean {\n if (error instanceof VersionSkewError) {\n markClientStale();\n return true;\n }\n return markStaleFromError(error);\n}\n","/**\n * Router Effects — full-page navigation and post-paint scroll.\n *\n * The two things the router does *to the document* rather than to its own\n * state: leaving the SPA entirely, and moving the scroll position once React\n * has committed. Extracted from `router.ts` to keep that file focused on\n * navigation state (see design/18-build-system.md §\"No file >500 lines\").\n *\n * See design/19-client-navigation.md §\"Scroll Restoration\"\n */\n\nimport { setHardNavigating } from './navigation-root.tsx';\nimport type { RenderOwner } from './navigation-transition.ts';\nimport { RedirectError, ServerErrorResponse, NonRscResponse } from './rsc-fetch.ts';\nimport { recordSkew } from './router-skew.ts';\n\n/** The subset of `RouterDeps` the post-paint scroll effects need. */\nexport interface ScrollEffectDeps {\n scrollTo: (x: number, y: number) => void;\n afterPaint?: (callback: () => void) => void;\n scrollToHash?: (hash: string) => boolean;\n}\n\nexport interface ScrollEffects {\n /**\n * Schedule scroll restoration after the next paint and fire the\n * scroll-restored event. Used by navigate, popstate, and refresh.\n */\n restoreScrollAfterPaint: (scrollY: number) => void;\n /**\n * Scroll to the element matching the URL #fragment after paint, falling\n * back to scroll-to-top when no element matches (matching full-page-load\n * behavior for an unknown fragment). Used by forward navigation to a\n * hash-bearing URL (TIM-1035).\n */\n scrollToHashAfterPaint: (hash: string) => void;\n}\n\nexport function createScrollEffects(deps: ScrollEffectDeps): ScrollEffects {\n /** Run a callback after the next paint (after React commit). */\n function afterPaint(callback: () => void): void {\n if (deps.afterPaint) {\n deps.afterPaint(callback);\n } else {\n callback();\n }\n }\n\n function restoreScrollAfterPaint(scrollY: number): void {\n afterPaint(() => {\n deps.scrollTo(0, scrollY);\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n }\n\n function scrollToHashAfterPaint(hash: string): void {\n afterPaint(() => {\n if (deps.scrollToHash?.(hash) !== true) {\n deps.scrollTo(0, 0);\n }\n window.dispatchEvent(new Event('timber:scroll-restored'));\n });\n }\n\n return { restoreScrollAfterPaint, scrollToHashAfterPaint };\n}\n\n/**\n * Leave the SPA and never come back.\n *\n * Every router path that gives up on client-side navigation does the same\n * three things: flag the hard navigation so the Navigation API and React stop\n * acting on a dying document, perform the document load, then block forever so\n * the caller cannot carry on rendering into it. The returned promise is\n * deliberately unresolvable — `location` assignment does not stop this turn of\n * the event loop.\n *\n * Callers: server error, non-RSC response, and every version skew path\n * ([33-version-skew.md](../../../../design/33-version-skew.md)).\n */\nexport function leaveSpa(url: string, fromUrl: string): Promise<never> {\n setHardNavigating(true);\n hardNavigate(url, fromUrl);\n return new Promise<never>(() => {});\n}\n\nexport interface SpaExitDeps {\n /** The owner that holds the router right now, or null when idle. */\n currentOwner: () => RenderOwner | null;\n /** Supersede whatever is in flight and take ownership. */\n supersede: () => void;\n}\n\nexport interface SpaExits {\n /**\n * Leave the SPA — but only if `owner` still owns the router. Every\n * failure a navigation can end on (a server error, a non-RSC response, a\n * version skew) can surface long after the user has clicked something else:\n * a fetch that was never cancellable, a Flight thenable that rejects late.\n * Hard navigating to the abandoned URL would undo that click, so a\n * navigation that no longer owns the router does nothing and lets the one\n * that does drive. Never resolves when it does navigate.\n *\n * Must be called from inside the `runNavigation()` callback. That function's\n * `finally` clears the current controller, so an ownership check downstream\n * of it compares against `null` and never leaves at all.\n */\n leaveSpaIfOwned: (owner: RenderOwner, url: string, fromUrl: string) => Promise<void>;\n\n /**\n * Leave the SPA from a path that never entered `runNavigation()` — the\n * stale-client short circuits, which decide before any fetch is attempted.\n *\n * Supersedes first. Nothing else on those paths takes ownership, so without\n * it an in-flight navigation keeps the router: when its fetch or decode\n * fails a moment later it still passes its own ownership check and hard\n * navigates to *its* URL, overwriting the document load started here.\n */\n leaveSpaSuperseding: (url: string, fromUrl: string) => Promise<never>;\n}\n\n/**\n * The two ownership-aware ways out of the SPA.\n *\n * Both wrap {@link leaveSpa} with the question \"is this navigation still the\n * one the user is waiting for?\" — asked before leaving on a failure, answered\n * by force on a path that decided before any navigation began. They live\n * together because every caller of `leaveSpa()` needs one or the other, and\n * an unguarded call is the bug (TIM-1275, TIM-1276).\n */\nexport function createSpaExits({ currentOwner, supersede }: SpaExitDeps): SpaExits {\n async function leaveSpaIfOwned(owner: RenderOwner, url: string, fromUrl: string): Promise<void> {\n if (currentOwner() !== owner) return;\n await leaveSpa(url, fromUrl);\n }\n\n function leaveSpaSuperseding(url: string, fromUrl: string): Promise<never> {\n supersede();\n return leaveSpa(url, fromUrl);\n }\n\n return { leaveSpaIfOwned, leaveSpaSuperseding };\n}\n\nexport interface NavigationRecoveryDeps {\n /** The owner that holds the router right now, or null when idle. */\n currentOwner: () => RenderOwner | null;\n leaveSpaIfOwned: SpaExits['leaveSpaIfOwned'];\n /** Replace the current entry with `url` — the router's own `navigate()`. */\n navigate: (url: string) => Promise<void>;\n}\n\n/**\n * Answer the failures an RSC fetch can end on, or report that this one is not\n * ours to answer.\n *\n * Returns `true` when the error was handled; the caller rethrows on `false`.\n * Handling a failure by leaving the SPA never resolves at all — the document\n * is going away and the caller must not carry on rendering into it.\n *\n * There is exactly one family of these and one response to it, so it lives in\n * one function rather than once per fetch path. `navigate()`, `refresh()` and\n * an uncached traversal all reach it: the latter two used to rethrow instead,\n * and since nobody awaits a traversal that meant pressing Back on a 500 left\n * the user on the old document with an unhandled rejection (TIM-1277).\n *\n * Order matters in exactly one way: `recordSkew()` ends in error-*text*\n * matching, so it goes last and can never pre-empt a framework control-flow\n * signal whose message happens to read like a stale bundle. The class checks\n * above it are mutually exclusive, so their order is free.\n *\n * Must be called from inside the `runNavigation()` callback — see\n * {@link SpaExits.leaveSpaIfOwned}.\n */\nexport function createNavigationRecovery({\n currentOwner,\n leaveSpaIfOwned,\n navigate,\n}: NavigationRecoveryDeps) {\n return async function recoverFromNavigationError(\n error: unknown,\n owner: RenderOwner,\n url: string,\n fromUrl: string\n ): Promise<boolean> {\n if (error instanceof RedirectError) {\n // Same ownership rule as leaving the SPA: a superseded navigation must\n // not steer the document to the destination it was abandoned for.\n if (currentOwner() !== owner) return true;\n await navigate(error.redirectUrl);\n return true;\n }\n // A server error, a non-RSC response and a version skew all end the same\n // way: hand `url` back to the server as a document load, so it can render\n // the outcome as HTML (design/10-error-handling.md §\"Error Page Rendering\n // for Client Navigation\").\n if (\n error instanceof ServerErrorResponse ||\n error instanceof NonRscResponse ||\n recordSkew(error)\n ) {\n await leaveSpaIfOwned(owner, url, fromUrl);\n return true;\n }\n return false;\n };\n}\n\n/**\n * Perform the full document load.\n *\n * `fromUrl` is the URL the navigation departed from, passed explicitly\n * because the address bar may have already been updated.\n *\n * When the target differs from the departure point only by #fragment,\n * assigning `href` is a hash change, not a load — so assign first (to update\n * the address bar) and then reload (TIM-1235).\n */\nexport function hardNavigate(url: string, fromUrl: string): void {\n const current = new URL(fromUrl, window.location.origin);\n const target = new URL(url, window.location.origin);\n if (target.pathname === current.pathname && target.search === current.search) {\n window.location.href = url;\n window.location.reload();\n } else {\n window.location.href = url;\n }\n}\n","'use client';\n\n/**\n * Framework-injected React error boundary.\n *\n * Catches errors thrown by children and renders a fallback component\n * with the appropriate props based on error type:\n * - DenySignal (4xx) → { status, data }\n * - RenderError (5xx) → { error, digest, reset }\n * - Unhandled error → { error, digest: null, reset }\n *\n * The `status` prop controls which errors this boundary catches:\n * - Specific code (e.g. 403) → only that status\n * - Category (400) → any 4xx\n * - Category (500) → any 5xx\n * - Omitted → catches everything (error.tsx behavior)\n *\n * See design/10-error-handling.md §\"Status-Code Files\"\n */\n\nimport { Component, createContext, createElement, useContext, type ReactNode } from 'react';\nimport { getSsrData } from './ssr-data.ts';\nimport { getRouterOrNull } from './router-ref.ts';\nimport { markStaleFromError } from './stale-client.ts';\nimport { StatusPageMarker } from './status-page-marker.tsx';\nimport { leaveSpa } from './router-effects.ts';\n// Page unload detection: when the page is being torn down, error boundaries\n// suppress activation — the error is from the aborted connection, not the\n// application. unload-guard.ts is the single owner of that state; this module\n// used to keep its own copy, which drifted (TIM-1260).\nimport { isPageUnloading } from './unload-guard.ts';\n\n// ─── Ancestry Context ────────────────────────────────────────────────────────\n\n/**\n * Tracks the chain of segment keys from root to the current boundary.\n * Each keyed `TimberErrorBoundary` appends its `segmentKey`; unkeyed\n * slot boundaries pass the parent value through. The claim rule uses\n * this to find the first *reachable* owner from the `owners` list,\n * eliminating the owner-below-throw-site escape. See TIM-1450.\n */\nconst DenyAncestryContext = createContext<readonly string[]>([]);\n\nexport { DenyAncestryContext };\n\n// ─── Digest Types ────────────────────────────────────────────────────────────\n\n/** Structured digest returned by RSC onError for DenySignal. */\ninterface DenyDigest {\n type: 'deny';\n status: number;\n data: unknown;\n /**\n * Ordered candidate owners — segment keys of segments with matching deny\n * pages, best match first. Present = addressed (a boundary should render\n * it). In-tree hoists produce a single-element list; late addressing\n * (TIM-1450) produces the full candidate list so the boundary can find the\n * first *reachable* owner via its ancestry context.\n */\n owners?: string[];\n}\n\n/** Structured digest returned by RSC onError for RenderError. */\ninterface RenderErrorDigest {\n type: 'render-error';\n code: string;\n data: unknown;\n status: number;\n}\n\n/** Structured digest returned by RSC onError for RedirectSignal. */\ninterface RedirectDigest {\n type: 'redirect';\n location: string;\n status: number;\n}\n\ntype ParsedDigest = DenyDigest | RenderErrorDigest | RedirectDigest;\n\n// ─── Props & State ───────────────────────────────────────────────────────────\n\nexport interface TimberErrorBoundaryProps {\n /** The component to render when an error is caught. */\n fallbackComponent?: (...args: unknown[]) => ReactNode;\n /**\n * Status code filter. If set, only catches errors matching this status.\n * 400 = any 4xx, 500 = any 5xx, specific number = exact match.\n */\n status?: number;\n /**\n * When true, this boundary wraps a parallel slot. Slot denials are\n * graceful degradation — they must NOT change the HTTP status code.\n */\n isSlotBoundary?: boolean;\n /**\n * Segment key of the segment that declared this boundary. Only meaningful\n * against a deny digest carrying `owners`. See the decline rule in\n * `render()` and TIM-1356.\n */\n segmentKey?: string;\n /**\n * Catch denies only; re-throw everything else. Set on the copy of a\n * segment's boundaries placed outside its layout, which gives a deny thrown\n * from that layout's own chrome a catcher keyed to the segment — without\n * changing where a 5xx from the same chrome goes. See TIM-1450.\n */\n denyOnly?: boolean;\n /**\n * The deny chain's terminal: the outermost framework boundary, which claims\n * any deny that streamed past every segment boundary and renders the\n * framework default page. Implies `denyOnly`. Only a deny thrown from a\n * layout's chrome with no status file at or above that layout gets here —\n * the segment copies (`denyOnly`) claim every other shape. See TIM-1450.\n */\n lastResort?: boolean;\n children: ReactNode;\n}\n\ninterface TimberErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n// Module-level guard: the first boundary to catch a given redirect digest\n// schedules the navigation and marks it handled here. Subsequent renders\n// (React retries the error-boundary render path multiple times in dev, and\n// outer boundaries that also see the error) become no-ops, preventing\n// duplicate router.navigate() calls.\nconst _handledRedirects = new WeakSet<object>();\n\n// The same guard for the terminal's exit: React may render the fallback\n// path more than once for one error, and the document load must start once.\nconst _handledExits = new WeakSet<object>();\n\n// Companion guard keyed by location: client-render redirect() (the\n// next/navigation shim) throws a FRESH error instance on every render\n// attempt, and React replays the errored render — so the WeakSet alone\n// would schedule the navigation twice. While a navigation to a location\n// is pending, further redirect errors for the same location are no-ops.\nlet _pendingRedirectLocation: string | null = null;\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport class TimberErrorBoundary extends Component<\n TimberErrorBoundaryProps,\n TimberErrorBoundaryState\n> {\n constructor(props: TimberErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): TimberErrorBoundaryState {\n // Suppress error boundaries during page unload (refresh/navigate away).\n // The aborted connection causes React's streaming hydration to error,\n // but the page is about to be replaced — showing an error boundary\n // would be a jarring flash for the user.\n if (isPageUnloading()) {\n return { hasError: false, error: null };\n }\n\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error): void {\n // A stale bundle most often surfaces here rather than as a rejected\n // navigation: a client reference missing from this build's manifest, or\n // a lazily-imported chunk the CDN no longer has, thrown during render.\n //\n // Recording it is all that happens — this boundary still renders its\n // fallback, and the recovery is the user's next navigation, which the\n // router turns into a full document load. Reloading from here would be a\n // page load the user did not ask for, on a page that may be mid-rollout\n // and would come back just as stale (TIM-1275).\n markStaleFromError(error);\n }\n\n componentDidUpdate(prevProps: TimberErrorBoundaryProps): void {\n // Reset error state when children change (e.g. client-side navigation).\n // Without this, navigating from one error page to another keeps the\n // stale error — getDerivedStateFromError doesn't re-fire for new children.\n if (this.state.hasError && prevProps.children !== this.props.children) {\n this.setState({ hasError: false, error: null });\n }\n }\n\n /** Reset the error state so children re-render. */\n private reset = () => {\n this.setState({ hasError: false, error: null });\n };\n\n static override contextType = DenyAncestryContext;\n declare context: readonly string[];\n\n private get childAncestry(): readonly string[] {\n const parent = this.context ?? [];\n const key = this.props.segmentKey;\n // A segment has two keyed boundary sets (outside and inside its layout);\n // the inner one sees its own key already present and adds nothing.\n return key && !parent.includes(key) ? [...parent, key] : parent;\n }\n\n render(): ReactNode {\n if (!this.state.hasError || !this.state.error) {\n return createElement(\n DenyAncestryContext.Provider,\n { value: this.childAncestry },\n this.props.children\n );\n }\n\n const error = this.state.error;\n const parsed = parseDigest(error);\n\n // RedirectSignal handling splits by environment:\n //\n // - SSR (no window): re-throw so the Fizz shell fails and the server\n // pipeline catch block can produce a proper HTTP 3xx response. See\n // design/04-authorization.md.\n //\n // - Client (window exists): a RedirectSignal that leaked into the RSC\n // Flight payload during client navigation would otherwise bubble past\n // every error boundary to `window.onerror`, which in dev triggers the\n // Vite error overlay (false positive — navigation itself works).\n // Instead, ask the router to perform an SPA navigation to the target\n // and render nothing while it takes over. The router's own catch path\n // handles cross-origin / hard navigations. See TIM-838.\n if (parsed?.type === 'redirect') {\n if (typeof window === 'undefined') {\n throw error;\n }\n // Dedupe: React may render the error-boundary fallback path more\n // than once for a single caught error (dev-mode double render, and\n // additional boundaries above us in the tree). Only schedule the\n // navigation on the first render that sees this error instance.\n const alreadyHandled =\n _handledRedirects.has(error) || _pendingRedirectLocation === parsed.location;\n if (!alreadyHandled) {\n _handledRedirects.add(error);\n const router = getRouterOrNull();\n if (router) {\n _pendingRedirectLocation = parsed.location;\n // Schedule the navigation outside the render phase to avoid\n // setState-during-render warnings from router state updates.\n queueMicrotask(() => {\n router\n .navigate(parsed.location, { replace: true })\n .catch(() => {\n // Fall back to a hard navigation if the soft navigation fails.\n window.location.href = parsed.location;\n })\n .finally(() => {\n _pendingRedirectLocation = null;\n });\n });\n } else {\n // No router available (shouldn't happen post-bootstrap) — fall\n // back to a hard navigation so the user still ends up at the\n // target.\n window.location.href = parsed.location;\n }\n }\n // Nothing to show, but still a subtree replaced: a layout that\n // redirected after I/O never mounted its children, and the commit that\n // renders this empty spot must not teach the cache otherwise — or the\n // redirect's own navigation would advertise those layouts as mounted\n // and the server would skip into an outlet the client does not have.\n return this.markStatusPage(null);\n }\n\n // Owner-addressed denies: the digest carries an ordered `owners` list\n // of segment keys with matching deny pages. This boundary claims the\n // deny only when it is the first *reachable* owner — i.e. its\n // segmentKey appears in `owners` AND in its ancestry (the chain from\n // root to here). Unreachable owners (below the throw site) are skipped,\n // and the first ancestor that IS reachable claims. This eliminates the\n // owner-below-throw-site escape (TIM-1450).\n //\n // Unkeyed boundaries (slots, or boundaries without deny chains) never\n // claim an addressed deny. See TIM-1356.\n if (parsed?.type === 'deny' && Array.isArray(parsed.owners) && parsed.owners.length > 0) {\n // Slot boundaries always claim addressed denies — graceful degradation\n // within the slot is correct regardless of which segment the owner\n // chain resolved to. Without this, the deny escapes the slot and\n // becomes a page-level 4xx. See TIM-1450.\n if (!this.props.lastResort && !this.props.isSlotBoundary) {\n const reachable = parsed.owners.find((k: string) => this.childAncestry.includes(k));\n if (!this.props.segmentKey || reachable !== this.props.segmentKey) {\n throw error;\n }\n }\n }\n // Deny-only boundaries (the outer segment copies) re-throw everything\n // else so a crash in a layout's chrome still reaches the documented 5xx\n // flow instead of a status page for an unrelated error.\n if (this.props.denyOnly && !this.props.lastResort && parsed?.type !== 'deny') {\n throw error;\n }\n // The terminal is where that flow ends on the client. A non-deny row\n // nothing claimed — a RenderError or a plain crash on a route with no\n // matching `error.tsx` — has no page to render in-SPA; React would\n // unmount the root. So the terminal does what the router does for an\n // error envelope (design/10 §\"Error Page Rendering for Client\n // Navigation\"): leave the SPA, and let the document request render the\n // real error page with the real status. On the server it re-throws so\n // the Fizz shell fails and the pipeline serves that page directly. The\n // envelope still covers errors that land inside Flight's opening window;\n // this covers the ones that land after it, which used to be held for\n // (TIM-1495, TIM-1497).\n if (this.props.lastResort && parsed?.type !== 'deny') {\n if (typeof window === 'undefined') {\n throw error;\n }\n if (!_handledExits.has(error)) {\n _handledExits.add(error);\n const router = getRouterOrNull();\n const current = window.location.pathname + window.location.search;\n // The row arrived for the navigation in flight, whose URL is not on\n // the address bar yet; fall back to the current document.\n const target = router?.getPendingUrl() ?? current;\n queueMicrotask(() => {\n void leaveSpa(target, current);\n });\n }\n return null;\n }\n\n // If this boundary has a status filter, check whether the error matches.\n // Non-matching errors re-throw so an outer boundary can catch them.\n if (this.props.status != null) {\n const errorStatus = getErrorStatus(parsed, error);\n if (errorStatus == null || !statusMatches(this.props.status, errorStatus)) {\n // Re-throw: this boundary doesn't handle this error.\n throw error;\n }\n }\n\n // Report DenySignal handling so the pipeline skips the redundant\n // renderDenyPage() re-render.\n //\n // For segment boundaries: also set the HTTP status code from the\n // DenySignal so the Response has the correct 4xx status. This runs\n // synchronously during Fizz rendering, BEFORE onShellReady.\n //\n // For slot boundaries: set _denyHandledByBoundary but do NOT change\n // statusCode — slot denials are graceful degradation.\n //\n // See TIM-664, LOCAL-298.\n if (parsed?.type === 'deny') {\n const ssrData = getSsrData();\n if (ssrData?._navContext) {\n ssrData._navContext._denyHandledByBoundary = true;\n if (!this.props.isSlotBoundary) {\n ssrData._navContext.statusCode = parsed.status;\n }\n }\n }\n\n return this.markStatusPage(this.renderFallback(parsed, error));\n }\n\n /**\n * The status page this boundary shows for a row it claimed. Its shape is\n * the file's documented contract: TSX status files get `{ status,\n * data }`, `error.tsx` gets `{ error, digest, reset }` (plus\n * the deny extras for dual-shape implementations — without the error prop,\n * an error.tsx reading error.message crashes on deny fall-through, TIM-1081).\n */\n private renderFallback(parsed: ParsedDigest | null, error: Error): ReactNode {\n if (parsed?.type === 'deny') {\n if (this.props.status != null) {\n return createElement(this.props.fallbackComponent as never, {\n status: parsed.status,\n data: parsed.data,\n });\n }\n return createElement(this.props.fallbackComponent as never, {\n error: denyError(parsed.status, error),\n digest: null,\n reset: this.reset,\n status: parsed.status,\n data: parsed.data,\n });\n }\n\n // 5xx / RenderError / unhandled error\n const digest =\n parsed?.type === 'render-error' ? { code: parsed.code, data: parsed.data } : null;\n\n return createElement(this.props.fallbackComponent as never, {\n error,\n digest,\n reset: this.reset,\n });\n }\n\n /**\n * A status page (or, for a redirect row, nothing at all) replaced part of\n * the main route, and the segment cache must not learn a chain that never\n * mounted; the marker tells the navigation that commits this tree. A boundary that declined in `render()` re-threw\n * before getting here, so only the boundary that put a page on screen\n * speaks. A slot degrading leaves the page and its chain intact, so slot\n * boundaries stay silent. See `status-page-marker.tsx` (TIM-1495).\n */\n private markStatusPage(page: ReactNode): ReactNode {\n if (this.props.isSlotBoundary) return page;\n return createElement(StatusPageMarker, null, page);\n }\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Parse the structured digest from the error.\n * React sets `error.digest` from the string returned by RSC's onError.\n */\nfunction parseDigest(error: Error): ParsedDigest | null {\n const raw = (error as { digest?: string }).digest;\n if (typeof raw !== 'string') return null;\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && typeof parsed.type === 'string') {\n return parsed as ParsedDigest;\n }\n } catch (e) {\n console.debug('[timber] error digest is not valid JSON:', e instanceof Error ? e.message : e);\n }\n return null;\n}\n\n/**\n * The Error an `error.tsx` serving a deny should receive.\n *\n * The error that actually arrives here is React's, not the framework's: a\n * server-thrown error crossing Flight has its message and name replaced with\n * a generic redacted string in production, by design. An `error.tsx` written\n * per the docs branches on `error.message` — so handing it the redacted error\n * gives it nothing to branch on, while the in-tree path hands the same file a\n * `DenySignal` named error reading `Access denied with status <N>` (see\n * `renderDenyEntry`). This reconstructs that shape from the digest, which is\n * where the status already is.\n *\n * Derived from the status alone — no stack, no server data — so it is safe on\n * the client for the same reason the digest is. See design/13-security.md\n * §\"Errors don't leak\", TIM-1081.\n */\nfunction denyError(status: number, original: Error): Error {\n // A dev build may have preserved something more useful than the redaction.\n if (original.name === 'DenySignal') return original;\n const error = new Error(`Access denied with status ${status}`);\n error.name = 'DenySignal';\n return error;\n}\n\n/**\n * Extract the HTTP status code from a parsed digest or error message.\n * Falls back to message pattern matching for errors without a digest.\n */\nfunction getErrorStatus(parsed: ParsedDigest | null, error: Error): number | null {\n if (parsed?.type === 'deny') return parsed.status;\n if (parsed?.type === 'render-error') return parsed.status;\n if (parsed?.type === 'redirect') return parsed.status;\n\n // Fallback: parse DenySignal message pattern for errors that lost their digest\n const match = error.message.match(/^Access denied with status (\\d+)$/);\n if (match) return parseInt(match[1], 10);\n\n // Unhandled errors are implicitly 500\n return 500;\n}\n\n/**\n * Check whether an error's status matches the boundary's status filter.\n * Category markers (400, 500) match any status in that range.\n */\nfunction statusMatches(boundaryStatus: number, errorStatus: number): boolean {\n // Category catch-all: 400 matches any 4xx, 500 matches any 5xx\n if (boundaryStatus === 400) return errorStatus >= 400 && errorStatus <= 499;\n if (boundaryStatus === 500) return errorStatus >= 500 && errorStatus <= 599;\n // Exact match\n return boundaryStatus === errorStatus;\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fs-identity-D7vb6GdI.js","names":[],"sources":["../../src/shared/static-platform-files.ts","../../src/adapters/fs-identity.ts"],"sourcesContent":["/**\n * Platform files the static build is allowed to overwrite in the output.\n *\n * The build merges two trees into the deployed directory — the client bundle\n * (which carries everything the user put in `public/`) and the pre-rendered\n * site. That merge otherwise refuses overlaps, because a silent overwrite\n * leaves one of two artifacts deployed with nothing recording which\n * (TIM-1266).\n *\n * These names are the exception, and only because the pre-rendered copy is\n * *derived from* the one it replaces rather than competing with it:\n * `plugins/static-build.ts` reads `public/_redirects` and writes the\n * generated rules plus the user's, so the winner is a superset of the loser.\n *\n * That holds only for a file the current pass produced, which is why\n * `cleanConditionalArtifacts` deletes a previous build's `_redirects` before\n * generation: written only when a pass finds redirects, it would otherwise\n * survive into a pass that produced nothing and claim an exemption it no\n * longer earns.\n *\n * `_headers` is NOT here. It is not written into the pre-rendered tree at\n * all — adapters generate it directly into the output, folding in the\n * user's copy as they go (TIM-1326).\n *\n * Declared once so the code that performs the merge and the code that\n * produces the merged file cannot disagree about which names are exempt.\n *\n * Isomorphic: no server, client, or build-time imports.\n */\n\n/** Platform redirect rules (Cloudflare Pages, Netlify). See TIM-1245/TIM-1249. */\nexport const REDIRECTS_FILE = '_redirects';\n\n/** Platform header rules (Cloudflare Pages, Netlify). See TIM-1241/TIM-1326. */\nexport const HEADERS_FILE = '_headers';\n\n/**\n * Where a user authors platform files. Stated once because two modules read\n * from here — `plugins/static-build.ts` for `_redirects`, adapters for\n * `_headers` — and they must look in the same place.\n *\n * Both read the **project source**, never the build output. The generated\n * file lands under the same name in the output directory, so a reader aimed\n * there would fold the previous build's generated rules back in and compound\n * them on every rebuild.\n */\nexport const USER_PLATFORM_DIR = 'public';\n\n/** Output-relative paths whose pre-rendered copy subsumes the `public/` one. */\nexport const USER_MERGED_PLATFORM_FILES: readonly string[] = [REDIRECTS_FILE];\n\n/**\n * Output paths the framework writes itself, which no route may claim.\n *\n * These are written outside the per-entry writer — `_redirects` after\n * generation, `_headers` by the adapter after the copy — so the collision\n * tracker never sees them and a route rendering to one of these paths is\n * overwritten with nothing recording it.\n *\n * Reserved rather than tracked, because there is no version of \"serve a route\n * at `/_headers`\" that works: Cloudflare Pages and Netlify read these two\n * files themselves and never serve them, so the route is unservable however\n * the conflict is resolved.\n *\n * That justification is per-file, not per-mechanism. `.nojekyll` is written\n * the same way and is deliberately NOT here: only the `github-pages` target\n * writes it, and everywhere else `/.nojekyll` is an ordinary servable path,\n * so reserving it globally would refuse a valid Cloudflare site. Its write\n * site skips the write when a file is already there instead — GitHub Pages\n * only cares that the file exists.\n *\n * Same reasoning as the synthetic 404 entry using an internal urlPath so a\n * real `/404` route cannot collide with `404.html`.\n */\nexport const RESERVED_OUTPUT_PATHS: readonly string[] = [REDIRECTS_FILE, HEADERS_FILE];\n","/**\n * What the output filesystem considers \"the same file\".\n *\n * Two build steps compare paths to decide whether two artifacts would land on\n * one file — the render pass (`plugins/static-build.ts`) and the adapter's\n * tree merge (`build-output-helper.ts`). Both are predicting a filesystem\n * decision, so both ask the filesystem rather than comparing raw strings\n * (TIM-1266).\n *\n * Two properties matter, and they are independent:\n *\n * - **Case folding.** macOS APFS and Windows NTFS fold by default, ext4 does\n * not. Dynamic params `Foo` and `foo` are one file on a Mac and two on\n * Linux CI.\n * - **Unicode normalization.** APFS compares normalization-insensitively, so\n * `café` spelled NFC (`é`) and NFD (`e` + combining acute) address one\n * file. This holds even on a *case-sensitive* APFS volume, which is why it\n * is probed separately rather than inferred from case folding.\n *\n * Probed rather than read off `process.platform`: a case-sensitive volume on\n * macOS and a case-insensitive mount on Linux are both ordinary, and the\n * answer that matters is the one for the directory being written.\n *\n * Leaf module — Node built-ins only. Deliberately not in `adapters/shared.ts`,\n * which `server/asset-headers.ts` re-exports from and which therefore must\n * stay free of `node:fs` for the Workers bundle.\n */\n\nimport { writeFile, stat, rm } from 'node:fs/promises';\nimport { randomUUID } from 'node:crypto';\nimport { join } from 'node:path';\n\n/** How the output filesystem decides that two paths name one file. */\nexport interface FsIdentity {\n /** `Foo` and `foo` are the same file. */\n caseInsensitive: boolean;\n /** Canonically equivalent Unicode spellings are the same file. */\n normalizationInsensitive: boolean;\n /**\n * Trailing dots and spaces are stripped from each segment, so `foo.` and\n * `foo` are the same file. Win32 does this in path resolution rather than\n * in the filesystem, which is why it is probed like the others.\n */\n stripsTrailingDotsAndSpaces: boolean;\n}\n\n/** Every path comparison is exact — the conservative answer when unprobed. */\nexport const EXACT_FS_IDENTITY: FsIdentity = {\n caseInsensitive: false,\n normalizationInsensitive: false,\n stripsTrailingDotsAndSpaces: false,\n};\n\n/**\n * Probe `dir` by writing one file and asking for it under other spellings.\n *\n * Returns {@link EXACT_FS_IDENTITY} when the probe cannot run. That\n * under-detects, but a directory this cannot write to is one the caller is\n * about to fail on anyway, with a clearer error than a missed collision.\n */\nexport async function detectFsIdentity(dir: string): Promise<FsIdentity> {\n // `café` — NFC, with a precomposed é. The UUID contributes the\n // hex letters the case probe needs.\n const name = `.timber-probe-${randomUUID()}-café`;\n const path = join(dir, name);\n\n try {\n await writeFile(path, '');\n const [caseInsensitive, normalizationInsensitive, stripsTrailingDotsAndSpaces] =\n await Promise.all([\n exists(join(dir, name.toUpperCase())),\n // NFD spells the same string as `e` + U+0301; a filesystem that\n // resolves it to the NFC file compares normalization-insensitively.\n exists(join(dir, name.normalize('NFD'))),\n // Win32 strips trailing dots and spaces during path resolution, so\n // asking for `name.` reaches `name`.\n exists(join(dir, `${name}.`)),\n ]);\n return { caseInsensitive, normalizationInsensitive, stripsTrailingDotsAndSpaces };\n } catch {\n return EXACT_FS_IDENTITY;\n } finally {\n await rm(path, { force: true }).catch(() => {});\n }\n}\n\n/**\n * Fold a path to the identity the filesystem compares it under.\n *\n * Normalization first, then case: `toLowerCase()` operates per code point, so\n * folding a decomposed string first would leave the combining marks in a\n * different order than folding a precomposed one.\n *\n * An approximation of the filesystem's own algorithm — APFS and NTFS fold per\n * their own Unicode tables, which `toLowerCase()` matches for everything short\n * of the exotic. It errs toward *finding* a collision, and the cost of a false\n * positive is a build error naming two routes rather than a silent overwrite.\n */\nexport function fsKey(path: string, identity: FsIdentity): string {\n let key = identity.caseInsensitive ? caseFold(path) : path;\n if (identity.normalizationInsensitive) key = key.normalize('NFC');\n if (identity.stripsTrailingDotsAndSpaces) key = key.split('/').map(stripTrailing).join('/');\n return key;\n}\n\n/**\n * Approximate Unicode case folding, which is what case-insensitive\n * filesystems actually compare under.\n *\n * `toLowerCase()` is not it. Greek `σ` and final `ς` are both already\n * lowercase and stay distinct under it, yet NTFS upcases both to `Σ` and APFS\n * folds `ς` to `σ` — so two routes differing only in that letter are one file\n * on both, and a lowercase key admitted them as two claims (TIM-1266).\n * Uppercasing first collapses them.\n *\n * The residual is over-folding, in these cases and a few like them:\n *\n * | pair | folded together here | NTFS / APFS |\n * | --------- | -------------------- | ----------------- |\n * | `σ` / `ς` | yes | yes — the fix |\n * | `ß` / `ss`| yes | no |\n * | `ı` / `i` | yes | no |\n * | `fi` / `fi`| yes | no |\n *\n * That direction is deliberate. Over-folding costs a build error naming two\n * routes, which a developer fixes by renaming one; under-folding costs a\n * payload silently overwritten by another route's, which nothing downstream\n * can detect. An exact fold would need `Intl.Collator`, which compares but\n * emits no sort key, so using it would turn a hash lookup into a pairwise\n * scan over every claimed path.\n */\nfunction caseFold(value: string): string {\n return value.toUpperCase().toLowerCase();\n}\n\n/**\n * Drop trailing dots and spaces from one path segment, as Win32 does.\n *\n * A segment that is nothing but dots and spaces is left alone: `.` and `..`\n * are traversal, which the caller rejects before reaching here, and emptying\n * a segment would merge two unrelated paths rather than the aliases this\n * models.\n */\nfunction stripTrailing(segment: string): string {\n const stripped = segment.replace(/[. ]+$/, '');\n return stripped === '' ? segment : stripped;\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,IAAa,iBAAiB;;AAG9B,IAAa,eAAe;;;;;;;;;;;AAY5B,IAAa,oBAAoB;;AAGjC,IAAa,6BAAgD,CAAC,cAAc;;;;;;;;;;;;;;;;;;;;;;;;AAyB5E,IAAa,wBAA2C,CAAC,gBAAgB,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC3BrF,IAAa,oBAAgC;CAC3C,iBAAiB;CACjB,0BAA0B;CAC1B,6BAA6B;AAC/B;;;;;;;;AASA,eAAsB,iBAAiB,KAAkC;CAGvE,MAAM,OAAO,iBAAiB,WAAW,EAAE;CAC3C,MAAM,OAAO,KAAK,KAAK,IAAI;CAE3B,IAAI;EACF,MAAM,UAAU,MAAM,EAAE;EACxB,MAAM,CAAC,iBAAiB,0BAA0B,+BAChD,MAAM,QAAQ,IAAI;GAChB,OAAO,KAAK,KAAK,KAAK,YAAY,CAAC,CAAC;GAGpC,OAAO,KAAK,KAAK,KAAK,UAAU,KAAK,CAAC,CAAC;GAGvC,OAAO,KAAK,KAAK,GAAG,KAAK,EAAE,CAAC;EAC9B,CAAC;EACH,OAAO;GAAE;GAAiB;GAA0B;EAA4B;CAClF,QAAQ;EACN,OAAO;CACT,UAAU;EACR,MAAM,GAAG,MAAM,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;CAChD;AACF;;;;;;;;;;;;;AAcA,SAAgB,MAAM,MAAc,UAA8B;CAChE,IAAI,MAAM,SAAS,kBAAkB,SAAS,IAAI,IAAI;CACtD,IAAI,SAAS,0BAA0B,MAAM,IAAI,UAAU,KAAK;CAChE,IAAI,SAAS,6BAA6B,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,KAAK,GAAG;CAC1F,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAS,SAAS,OAAuB;CACvC,OAAO,MAAM,YAAY,CAAC,CAAC,YAAY;AACzC;;;;;;;;;AAUA,SAAS,cAAc,SAAyB;CAC9C,MAAM,WAAW,QAAQ,QAAQ,UAAU,EAAE;CAC7C,OAAO,aAAa,KAAK,UAAU;AACrC;AAEA,eAAe,OAAO,MAAgC;CACpD,IAAI;EACF,MAAM,KAAK,IAAI;EACf,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graph-cache-CP4GEmf9.js","names":[],"sources":["../../src/plugins/dev-discovery.ts","../../src/analyze/graph-cache.ts"],"sourcesContent":["/**\n * Dev-server discovery records — `.timber/dev/<pid>-<port>-<token>.json`.\n *\n * On startup each dev server writes `{ port, pid, host, protocol }` to its own record\n * file under the app's `.timber/dev/` so the `timber graph` CLI and the\n * VS Code extension can discover a running dev server for the\n * live-graph fast path.\n *\n * One record file per server, one writer per file. This is what makes\n * multi-server roots (Vite auto-increments the port across processes;\n * programmatic embeddings run several servers in one process) safe\n * without any coordination: there is no shared file to displace, no\n * ownership to arbitrate, and no shutdown-order dependency — each\n * server removes exactly its own record.\n *\n * Consumers must list the directory and trust a record only if its pid\n * is alive AND a probe request to the endpoint succeeds; anything else\n * is treated as absent (silent fallback), never an error. Stale records\n * (crashed servers) are therefore the normal case — startup sweeps\n * dead-pid records best-effort, and consumers validate the rest away.\n *\n * The directory layout and file format are implementation details, not\n * a public contract.\n *\n * Design doc: 47-module-environment-tooling.md §5, 21-dev-server.md.\n */\n\nimport type { Server } from 'node:net';\nimport { Server as TlsServer } from 'node:tls';\nimport { randomUUID } from 'node:crypto';\nimport { mkdirSync, writeFileSync, rmSync, readFileSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** Relative path of the discovery directory within the project root. */\nexport const DEV_DISCOVERY_DIR = join('.timber', 'dev');\n\nexport interface DevRecord {\n port: number;\n pid: number;\n /**\n * The bound address, as reported by the socket. Consumers probing the\n * endpoint need it for servers bound to non-default interfaces;\n * wildcard addresses (`0.0.0.0`, `::`) should be probed via loopback.\n */\n host: string;\n /**\n * Probe scheme. `server.https` dev servers (including HTTP/2) answer\n * only TLS — an http:// probe of one would fail and wrongly trigger\n * the cold-crawl fallback.\n */\n protocol: 'http' | 'https';\n /**\n * Vite `base` path. When non-root (e.g. `/my-app/`), the WebSocket\n * upgrade path is `base + hmrPath`.\n */\n base?: string;\n /**\n * HMR WebSocket path (from `server.ws.path` or deprecated\n * `server.hmr.path`). Absent when no path is configured (default `/`).\n */\n hmrPath?: string;\n /**\n * Custom WebSocket host override (from `server.ws.host` or deprecated\n * `server.hmr.host`). When set, the WS connection targets this host\n * instead of the HTTP server's bound address.\n */\n hmrHost?: string;\n /**\n * Custom WebSocket port override (from `server.ws.clientPort` ??\n * `server.ws.port`, or deprecated `server.hmr.clientPort` ??\n * `server.hmr.port`). When set, the WS connection targets this port\n * instead of the HTTP server's bound port.\n */\n hmrPort?: number;\n /**\n * Custom WebSocket protocol override (from `server.ws.protocol` or\n * deprecated `server.hmr.protocol`). When set (e.g. `'wss'`), the\n * upgrade request uses the corresponding HTTP scheme.\n */\n hmrProtocol?: string;\n /**\n * Vite 8 WebSocket token (from `config.webSocketToken`). Required for\n * browser WS upgrades; the extension passes it as `?token=` on the\n * WebSocket URL.\n */\n webSocketToken?: string;\n /**\n * Absolute path to the Vite config file this server was started with.\n * When multiple dev servers share a project root but use different\n * configs, the fast path must only accept the record matching the\n * config the CLI resolved (TIM-1393).\n */\n configFile?: string;\n}\n\n/** Best-effort \"is this pid alive\" — signal 0 probes without killing. */\nexport function isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // EPERM means the process exists but is owned by someone else.\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Remove sibling records whose pid is no longer alive. Purely a\n * tidiness sweep — consumers already ignore records that fail the\n * pid + probe validation, so missing a stale record here is harmless.\n */\nfunction sweepDeadRecords(dir: string): void {\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return;\n }\n for (const entry of entries) {\n if (!entry.endsWith('.json')) continue;\n const filePath = join(dir, entry);\n try {\n const record = JSON.parse(readFileSync(filePath, 'utf8')) as Partial<DevRecord>;\n // Remove only records that parse AND name a dead pid. An\n // unreadable file may be a sibling's record mid-write — leave it;\n // consumers ignore records that fail validation anyway.\n if (typeof record.pid === 'number' && !isPidAlive(record.pid)) {\n rmSync(filePath, { force: true });\n }\n } catch {\n // Unreadable — skip, never delete another writer's in-flight file.\n }\n }\n}\n\n/**\n * Register discovery-record lifecycle handlers on the dev server's HTTP\n * server. Writes `.timber/dev/<pid>-<port>-<token>.json` once the server is\n * listening (the bound port is only known then) and removes exactly\n * that record on clean shutdown.\n *\n * All filesystem work is best-effort: a failure to write or remove a\n * record must never break the dev server. `httpServer` is null in\n * middleware mode — no port exists, so there is nothing to advertise.\n * Typed as `net.Server` — the common base of Vite's `http.Server` and\n * `Http2SecureServer` variants, which carries the events and `address()`.\n */\nexport function registerDevDiscovery(\n httpServer: Server | null,\n root: string,\n config?: {\n configFile?: string | false;\n base?: string;\n server?: {\n hmr?:\n | { path?: string; host?: string; port?: number; clientPort?: number; protocol?: string }\n | boolean;\n ws?:\n | { path?: string; host?: string; port?: number; clientPort?: number; protocol?: string }\n | false;\n };\n webSocketToken?: string;\n }\n): void {\n if (!httpServer) return;\n\n const dir = join(root, DEV_DISCOVERY_DIR);\n let recordPath: string | null = null;\n\n httpServer.on('listening', () => {\n const address = httpServer.address();\n // Unix-socket listeners have a string address — no port to advertise.\n if (address === null || typeof address === 'string') return;\n const hmrOpts = typeof config?.server?.hmr === 'object' ? config.server.hmr : undefined;\n const wsOpts = typeof config?.server?.ws === 'object' ? config.server.ws : undefined;\n const record: DevRecord = {\n port: address.port,\n pid: process.pid,\n host: address.address,\n // Vite's https/h2 servers are tls.Server subclasses; plain HTTP\n // and h2c are not.\n protocol: httpServer instanceof TlsServer ? 'https' : 'http',\n base: config?.base && config.base !== '/' ? config.base : undefined,\n hmrPath: wsOpts?.path ?? hmrOpts?.path,\n hmrHost: wsOpts?.host ?? hmrOpts?.host,\n hmrPort:\n wsOpts?.clientPort || wsOpts?.port || hmrOpts?.clientPort || hmrOpts?.port || undefined,\n hmrProtocol: wsOpts?.protocol ?? hmrOpts?.protocol,\n webSocketToken: config?.webSocketToken,\n configFile: typeof config?.configFile === 'string' ? config.configFile : undefined,\n };\n // pid+port alone is not a unique server identity (same port on two\n // interfaces; worker threads and duplicate module copies share a\n // pid but not module state, so a counter cannot disambiguate\n // either). A random token makes the filename unique regardless of\n // process/isolate topology; pid and port stay in the name only for\n // human debuggability.\n recordPath = join(dir, `${record.pid}-${record.port}-${randomUUID()}.json`);\n try {\n mkdirSync(dir, { recursive: true });\n writeFileSync(recordPath, JSON.stringify(record));\n sweepDeadRecords(dir);\n } catch {\n // Best-effort — consumers fall back to the cold crawl.\n }\n });\n\n httpServer.on('close', () => {\n if (!recordPath) return;\n try {\n // Only ever this server's own record — sibling servers' records\n // are separate files and survive any shutdown order untouched.\n rmSync(recordPath, { force: true });\n } catch {\n // Best-effort — a stale record is validated away by consumers.\n }\n });\n}\n","/**\n * graph-cache — the mtime-keyed output cache for `timber graph`.\n *\n * The cold crawl costs seconds (design/47 §6) and its cost is setup +\n * dependency compilation, not project-file count — so the cacheable\n * unit is the classification OUTPUT, never Vite's transforms. One\n * crawl per project state: the first question after a clone pays for\n * the crawl, every subsequent question is a cache hit until the\n * project changes.\n *\n * Validity is a fingerprint over (path, mtime, size) of every project\n * source file — the same disk walk the crawl itself uses (scan.ts), so\n * cache coverage and crawl coverage cannot drift — plus the config\n * files and the nearest lockfile, since dependency changes change\n * resolution (export conditions can flip a file's environment without\n * touching project code).\n *\n * Deliberately vite-free: a cache hit answers without importing Vite.\n *\n * Design docs: 47-module-environment-tooling.md §\"Cold-path policy\".\n */\n\nimport { createHash } from 'node:crypto';\nimport { createRequire } from 'node:module';\nimport { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join } from 'node:path';\nimport { buildSourceExtensions, scanProjectSourceFiles } from './scan.ts';\nimport type { CrawlReport } from './crawl.ts';\n\n/**\n * JSON contract version (design/47 §9) — one constant for every\n * frontend (CLI and dev-server endpoint), so the two cannot drift.\n * Lives here rather than graph-command.ts because this module is\n * vite-free and both frontends already depend on the analyze core.\n */\nexport const GRAPH_JSON_VERSION = 1;\n\n/** Bump when the report shape or fingerprint inputs change. */\nexport const GRAPH_CACHE_VERSION = 2;\n\n/** Cache location under the app root. */\nconst CACHE_RELATIVE_PATH = join('.timber', 'graph-cache.json');\n\n/**\n * Config/manifest files at the root that participate in the\n * fingerprint. The `.env` family is included because a Vite config\n * using `loadEnv(mode, …)` can select aliases, plugins, the app root,\n * or pageExtensions from it — the disk scan skips dot-prefixed files,\n * so without these entries an `.env.production` edit would never miss\n * the cache (codex review, PR #1056). The build resolves under mode\n * `production`; the development variants are tracked anyway — they are\n * loadable through `loadEnv` with any mode argument, and an absent\n * file contributes nothing.\n */\nconst ROOT_MANIFEST_FILES = [\n 'package.json',\n 'tsconfig.json',\n 'timber.config.ts',\n 'timber.config.js',\n 'timber.config.mjs',\n '.env',\n '.env.local',\n '.env.production',\n '.env.production.local',\n '.env.development',\n '.env.development.local',\n];\n\n/** Lockfile names, searched upward from the root (monorepo workspaces). */\nconst LOCKFILE_NAMES = [\n 'pnpm-lock.yaml',\n 'package-lock.json',\n 'yarn.lock',\n 'bun.lock',\n 'bun.lockb',\n];\n\nexport interface GraphCacheFile {\n version: number;\n fingerprint: string;\n /** The Vite config file the cached crawl booted with. */\n configFile: string;\n /**\n * The resolved pageExtensions from the timber config at crawl time.\n * Stored so the vite-free hit path can reproduce the same scan\n * extension set (custom extensions like 'foo' must be admitted on\n * both the write and the hit path, or the fingerprint disagrees\n * with what was cached).\n */\n pageExtensions?: string[];\n /**\n * Files the fingerprint tracks BEYOND the disk scan, discovered at\n * crawl time: Vite's `configFileDependencies` (a config helper\n * imported from outside the app root can reshape the whole app) and\n * every file the report itself classifies (the graph can reach files\n * the scan skips — `app/.well-known/page.tsx`, a route under a\n * directory named `dist`). The hit path re-stats exactly this list,\n * so an edit to any of them misses the cache.\n */\n trackedExtras: string[];\n report: CrawlReport;\n}\n\n/**\n * Follow a tsconfig/jsconfig `extends` chain, returning every file on\n * it (the start file included). A base config outside the app —\n * `\"extends\": \"../tsconfig.base.json\"` with a `paths` mapping — can\n * reshape Vite's resolution without touching anything else the\n * fingerprint tracks. Parsing is a targeted regex, not JSONC: tsconfig\n * allows comments, and all we need is the `extends` value. Package\n * specifiers resolve through Node resolution (an approximation of\n * TypeScript's — the `.json` append covers the common shapes): a\n * WORKSPACE package like `@repo/tsconfig/base.json` is a link, so its\n * edits change neither the lockfile nor any scanned file and must be\n * fingerprinted directly; registry packages resolve too, harmlessly.\n * Cycles and unresolvable targets just end the walk (bounded at 16).\n */\n/**\n * Strip JSONC comments so a commented-out `\"extends\"` line cannot\n * shadow the real one. String-aware: comment markers inside string\n * literals (a path containing `//`) survive.\n */\nexport function stripJsonComments(raw: string): string {\n let out = '';\n let inString = false;\n for (let i = 0; i < raw.length; i++) {\n const ch = raw[i];\n if (inString) {\n out += ch;\n if (ch === '\\\\') {\n out += raw[i + 1] ?? '';\n i++;\n } else if (ch === '\"') {\n inString = false;\n }\n } else if (ch === '\"') {\n inString = true;\n out += ch;\n } else if (ch === '/' && raw[i + 1] === '/') {\n while (i < raw.length && raw[i] !== '\\n') i++;\n out += '\\n';\n } else if (ch === '/' && raw[i + 1] === '*') {\n i += 2;\n while (i < raw.length && !(raw[i] === '*' && raw[i + 1] === '/')) i++;\n i++;\n } else {\n out += ch;\n }\n }\n return out;\n}\n\nasync function collectTsconfigChain(startFile: string): Promise<string[]> {\n const chain: string[] = [];\n const queue = [startFile];\n const seen = new Set(queue);\n while (queue.length > 0 && chain.length < 16) {\n const current = queue.shift()!;\n let raw: string;\n try {\n raw = stripJsonComments(await readFile(current, 'utf8'));\n } catch {\n continue;\n }\n chain.push(current);\n const match = raw.match(/\"extends\"\\s*:\\s*(\"([^\"]+)\"|\\[([^\\]]*)\\])/);\n if (!match) continue;\n const targets = match[2]\n ? [match[2]]\n : (match[3]?.match(/\"([^\"]+)\"/g) ?? []).map((t) => t.slice(1, -1));\n for (const target of targets) {\n let next: string | null = null;\n if (isAbsolute(target)) {\n // An absolute extends target names the base directly — joining\n // it under the current config's directory would fabricate a\n // path that does not exist and drop the real base.\n next = target;\n if (!/\\.json$/.test(next)) next += '.json';\n } else if (target.startsWith('.')) {\n next = join(dirname(current), target);\n if (!/\\.json$/.test(next)) next += '.json';\n } else {\n const req = createRequire(current);\n for (const candidate of [target, `${target}.json`, `${target}/tsconfig.json`]) {\n try {\n next = req.resolve(candidate);\n break;\n } catch {\n // try the next shape\n }\n }\n }\n if (next && !seen.has(next)) {\n seen.add(next);\n queue.push(next);\n }\n }\n }\n return chain;\n}\n\n/** Nearest lockfile at or above `root`, or null. */\nasync function findLockfile(root: string): Promise<string | null> {\n let dir = root;\n for (;;) {\n for (const name of LOCKFILE_NAMES) {\n const candidate = join(dir, name);\n try {\n await stat(candidate);\n return candidate;\n } catch {\n // keep looking\n }\n }\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/** `path\\0mtime\\0size` entries for a file list; absent files contribute nothing. */\nexport async function fingerprintEntriesForFiles(files: Iterable<string>): Promise<string[]> {\n const entries: string[] = [];\n for (const file of new Set(files)) {\n try {\n const stats = await stat(file);\n entries.push(`${file}\\0${stats.mtimeMs}\\0${stats.size}`);\n } catch {\n // Absent files simply contribute nothing; when one appears\n // later, its entry appears and the fingerprint changes.\n }\n }\n return entries;\n}\n\n/**\n * Entries for the scan-derived base set: project source files, the\n * config, root manifests, and the nearest lockfile. Scan failures are\n * folded into the entry list rather than thrown: an unreadable\n * directory changes the answer's completeness, so it must change the\n * key too.\n */\nexport async function collectBaseFingerprintEntries(\n root: string,\n configFile: string,\n pageExtensions?: readonly string[]\n): Promise<string[]> {\n const extensions = buildSourceExtensions(pageExtensions);\n const sourceFiles: string[] = [];\n const failures: string[] = [];\n await scanProjectSourceFiles(\n root,\n sourceFiles,\n (failure) => failures.push(`${failure.dir}:${failure.message}`),\n 0,\n extensions\n );\n const tracked = [...sourceFiles, configFile, ...ROOT_MANIFEST_FILES.map((f) => join(root, f))];\n // Manifest/.env files beside the CONFIG too, when it lives outside\n // the app root — `loadEnv(mode, process.cwd())` in a monorepo config\n // reads the .env next to the config, not the one under root.\n const configDir = dirname(configFile);\n if (configDir !== root) {\n tracked.push(...ROOT_MANIFEST_FILES.map((f) => join(configDir, f)));\n }\n // ts/jsconfig extends chains: a base config outside the app can carry\n // the `paths` mappings resolution depends on.\n for (const name of ['tsconfig.json', 'jsconfig.json']) {\n tracked.push(...(await collectTsconfigChain(join(root, name))));\n if (configDir !== root) tracked.push(...(await collectTsconfigChain(join(configDir, name))));\n }\n const lockfile = await findLockfile(root);\n if (lockfile) tracked.push(lockfile);\n return [...failures, ...(await fingerprintEntriesForFiles(tracked))];\n}\n\n/** Hash an entry list order-independently. */\nexport function hashFingerprintEntries(entries: string[]): string {\n const unique = [...new Set(entries)].sort();\n return createHash('sha256').update(unique.join('\\n')).digest('hex');\n}\n\n/**\n * Fingerprint the project state `timber graph`'s answers depend on:\n * the scan-derived base set plus `extraFiles` (the cache's\n * trackedExtras on the hit path; config dependencies + report files on\n * the write path).\n */\nexport async function computeProjectFingerprint(\n root: string,\n configFile: string,\n extraFiles: readonly string[] = [],\n pageExtensions?: readonly string[]\n): Promise<string> {\n const entries = await collectBaseFingerprintEntries(root, configFile, pageExtensions);\n entries.push(...(await fingerprintEntriesForFiles(extraFiles)));\n return hashFingerprintEntries(entries);\n}\n\n/** Read the cache file; anything unreadable or version-mismatched is a miss. */\nexport async function readGraphCache(root: string): Promise<GraphCacheFile | null> {\n try {\n const raw = await readFile(join(root, CACHE_RELATIVE_PATH), 'utf8');\n const parsed = JSON.parse(raw) as Partial<GraphCacheFile>;\n if (\n parsed.version !== GRAPH_CACHE_VERSION ||\n typeof parsed.fingerprint !== 'string' ||\n typeof parsed.configFile !== 'string' ||\n !Array.isArray(parsed.trackedExtras) ||\n typeof parsed.report !== 'object' ||\n parsed.report === null\n ) {\n return null;\n }\n return parsed as GraphCacheFile;\n } catch {\n return null;\n }\n}\n\n/**\n * Write the cache atomically (temp file + rename) so a concurrent\n * reader — parallel agent invocations are the expected workload —\n * never sees a torn JSON file.\n */\nexport async function writeGraphCache(root: string, cache: GraphCacheFile): Promise<void> {\n const target = join(root, CACHE_RELATIVE_PATH);\n await mkdir(dirname(target), { recursive: true });\n const temp = `${target}.${process.pid}.tmp`;\n await writeFile(temp, JSON.stringify(cache));\n await rename(temp, target);\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"json-lossy-check-ClNvBM_3.js","names":[],"sources":["../../src/cache/json-lossy-check.ts"],"sourcesContent":["const LOSSY_TYPES = ['Date', 'Map', 'Set', 'BigInt'] as const;\n\nlet hasWarned = false;\n\n/**\n * Dev-mode warning when a cache value contains types that lose fidelity\n * through JSON round-trip. Only runs in development — no-op in production.\n *\n * Detects: Date (→ ISO string), Map/Set (→ {}), BigInt (→ throws).\n * Emits once per process to avoid log spam.\n */\nexport function warnLossyJsonValue(value: unknown, cacheKey: string): void {\n if (hasWarned) return;\n if (process.env.NODE_ENV === 'production') return;\n\n const lossy = findLossyTypes(value, new Set(), 0);\n if (lossy.size === 0) return;\n\n hasWarned = true;\n const types = [...lossy].join(', ');\n console.warn(\n `[timber] cache value for key \"${cacheKey}\" contains ${types} — ` +\n 'these types lose fidelity through JSON serialization in Redis/KV cache handlers. ' +\n 'Date becomes an ISO string, Map/Set become {}, BigInt throws. ' +\n 'The MemoryCacheHandler preserves them. Consider transforming values ' +\n 'to JSON-safe types before caching, or use MemoryCacheHandler for ' +\n 'values that require type preservation.'\n );\n}\n\nfunction findLossyTypes(value: unknown, seen: Set<unknown>, depth: number): Set<string> {\n const lossy = new Set<string>();\n if (depth > 8 || value === null || value === undefined) return lossy;\n if (typeof value === 'bigint') {\n lossy.add('BigInt');\n return lossy;\n }\n if (typeof value !== 'object' && typeof value !== 'function') return lossy;\n if (seen.has(value)) return lossy;\n seen.add(value);\n\n if (value instanceof Date) {\n lossy.add('Date');\n return lossy;\n }\n if (value instanceof Map) {\n lossy.add('Map');\n return lossy;\n }\n if (value instanceof Set) {\n lossy.add('Set');\n return lossy;\n }\n\n if (Array.isArray(value)) {\n for (const item of value) {\n for (const t of findLossyTypes(item, seen, depth + 1)) lossy.add(t);\n if (lossy.size === LOSSY_TYPES.length) return lossy;\n }\n return lossy;\n }\n\n for (const k of Object.keys(value as Record<string, unknown>)) {\n for (const t of findLossyTypes((value as Record<string, unknown>)[k], seen, depth + 1)) {\n lossy.add(t);\n }\n if (lossy.size === LOSSY_TYPES.length) return lossy;\n }\n return lossy;\n}\n\n/** Reset warning state — for tests only. */\nexport function _resetLossyWarning(): void {\n hasWarned = false;\n}\n"],"mappings":";AAAA,IAAM,cAAc;CAAC;CAAQ;CAAO;CAAO;AAAQ;AAEnD,IAAI,YAAY;;;;;;;;AAShB,SAAgB,mBAAmB,OAAgB,UAAwB;CACzE,IAAI,WAAW;CACf,IAAA,QAAA,IAAA,aAA6B,cAAc;CAE3C,MAAM,QAAQ,eAAe,uBAAO,IAAI,IAAI,GAAG,CAAC;CAChD,IAAI,MAAM,SAAS,GAAG;CAEtB,YAAY;CACZ,MAAM,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,IAAI;CAClC,QAAQ,KACN,iCAAiC,SAAS,aAAa,MAAM,8TAM/D;AACF;AAEA,SAAS,eAAe,OAAgB,MAAoB,OAA4B;CACtF,MAAM,wBAAQ,IAAI,IAAY;CAC9B,IAAI,QAAQ,KAAK,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO;CAC/D,IAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,IAAI,QAAQ;EAClB,OAAO;CACT;CACA,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO;CACrE,IAAI,KAAK,IAAI,KAAK,GAAG,OAAO;CAC5B,KAAK,IAAI,KAAK;CAEd,IAAI,iBAAiB,MAAM;EACzB,MAAM,IAAI,MAAM;EAChB,OAAO;CACT;CACA,IAAI,iBAAiB,KAAK;EACxB,MAAM,IAAI,KAAK;EACf,OAAO;CACT;CACA,IAAI,iBAAiB,KAAK;EACxB,MAAM,IAAI,KAAK;EACf,OAAO;CACT;CAEA,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxB,KAAK,MAAM,QAAQ,OAAO;GACxB,KAAK,MAAM,KAAK,eAAe,MAAM,MAAM,QAAQ,CAAC,GAAG,MAAM,IAAI,CAAC;GAClE,IAAI,MAAM,SAAS,YAAY,QAAQ,OAAO;EAChD;EACA,OAAO;CACT;CAEA,KAAK,MAAM,KAAK,OAAO,KAAK,KAAgC,GAAG;EAC7D,KAAK,MAAM,KAAK,eAAgB,MAAkC,IAAI,MAAM,QAAQ,CAAC,GACnF,MAAM,IAAI,CAAC;EAEb,IAAI,MAAM,SAAS,YAAY,QAAQ,OAAO;CAChD;CACA,OAAO;AACT"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"logger-k2DQ4EUf.js","names":[],"sources":["../../src/server/tracing.ts","../../src/server/error-formatter.ts","../../src/server/default-logger.ts","../../src/server/logger.ts"],"sourcesContent":["/**\n * Tracing — per-request trace ID via AsyncLocalStorage, OTEL span helpers.\n *\n * getTraceId() is always available in server code (middleware, access, components, actions).\n * Returns a 32-char lowercase hex string — the OTEL trace ID when an SDK is active,\n * or a crypto.randomUUID()-derived fallback otherwise.\n *\n * See design/17-logging.md §\"trace_id is Always Set\"\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { traceAls, type TraceStore } from './als-registry.ts';\n\n// Re-export the TraceStore type for public API consumers.\nexport type { TraceStore } from './als-registry.ts';\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/**\n * Returns the current request's trace ID — always a 32-char lowercase hex string.\n *\n * With OTEL: the real OTEL trace ID (matches Jaeger/Honeycomb/Datadog).\n * Without OTEL: crypto.randomUUID() with hyphens stripped.\n *\n * Throws if called outside a request context (no ALS store).\n */\nexport function getTraceId(): string {\n const store = traceAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getTraceId() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n return store.traceId;\n}\n\n/**\n * Returns the current OTEL span ID if available, undefined otherwise.\n */\nexport function getSpanId(): string | undefined {\n return traceAls.getStore()?.spanId;\n}\n\n// ─── Framework-Internal Helpers ───────────────────────────────────────────\n\n/**\n * Generate a 32-char lowercase hex ID from crypto.randomUUID().\n * Same format as OTEL trace IDs — zero-friction upgrade path.\n */\nexport function generateTraceId(): string {\n return randomUUID().replace(/-/g, '');\n}\n\n/**\n * Run a callback within a trace context. Used by the pipeline to establish\n * per-request ALS scope.\n */\nexport function runWithTraceId<T>(id: string, fn: () => T): T {\n return traceAls.run({ traceId: id }, fn);\n}\n\n/**\n * Replace the trace ID in the current ALS store. Used when OTEL creates\n * a root span and we want to switch from the UUID fallback to the real\n * OTEL trace ID.\n */\nexport function replaceTraceId(newTraceId: string, newSpanId?: string): void {\n const store = traceAls.getStore();\n if (store) {\n store.traceId = newTraceId;\n store.spanId = newSpanId;\n }\n}\n\n/**\n * Update the span ID in the current ALS store. Used when entering a new\n * OTEL span to keep log–trace correlation accurate.\n */\nexport function updateSpanId(newSpanId: string | undefined): void {\n const store = traceAls.getStore();\n if (store) {\n store.spanId = newSpanId;\n }\n}\n\n/**\n * Get the current trace store, or undefined if outside a request context.\n * Framework-internal — use getTraceId()/getSpanId() in user code.\n */\nexport function getTraceStore(): TraceStore | undefined {\n return traceAls.getStore();\n}\n\n// ─── Dev-Mode OTEL Auto-Init ─────────────────────────────────────────────\n\n/**\n * Well-known key marking dev tracing as initialized.\n *\n * The RSC entry module re-evaluates on every HMR invalidation and calls\n * initDevTracing() again — without this guard, each call would register a\n * fresh provider/processor and duplicate every span's output. Symbol.for()\n * survives module re-evaluation. See TIM-1067, B49.\n */\nconst DEV_TRACING_INIT_KEY = Symbol.for('timber.dev.tracing-initialized');\n\n/**\n * Initialize a minimal OTEL SDK in dev mode so spans are recorded and\n * fed to the DevSpanProcessor for dev log output.\n *\n * If the user already configured an OTEL SDK in register(), we add\n * our DevSpanProcessor alongside theirs. If no SDK is configured,\n * we create a BasicTracerProvider with our processor.\n *\n * Idempotent across module re-evaluations (HMR) — only the first call\n * registers. Only called in dev mode — zero overhead in production.\n */\nexport async function initDevTracing(\n config: import('../dev-tools/logger.ts').DevLoggerConfig\n): Promise<void> {\n const globals = globalThis as Record<symbol, unknown>;\n if (globals[DEV_TRACING_INIT_KEY]) return;\n\n const api = await getOtelApi();\n if (!api) return;\n\n let DevSpanProcessor: typeof import('../dev-tools/instrumentation.ts').DevSpanProcessor;\n let BasicTracerProvider: typeof import('@opentelemetry/sdk-trace-base').BasicTracerProvider;\n let AsyncLocalStorageContextManager: typeof import('@opentelemetry/context-async-hooks').AsyncLocalStorageContextManager;\n\n try {\n ({ DevSpanProcessor } = await import('../dev-tools/instrumentation.ts'));\n ({ BasicTracerProvider } = await import('@opentelemetry/sdk-trace-base'));\n ({ AsyncLocalStorageContextManager } = await import('@opentelemetry/context-async-hooks'));\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[timber] Dev tracing disabled — failed to load OTEL packages:\\n ${msg}`);\n return;\n }\n\n const processor = new DevSpanProcessor(config);\n\n // Register a context manager so OTEL can propagate the active span\n // across async boundaries. Without this, startActiveSpan can't make\n // spans \"active\" — child spans get random trace IDs and getActiveSpan()\n // returns undefined.\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n api.context.setGlobalContextManager(contextManager);\n\n // Create a minimal TracerProvider with our DevSpanProcessor.\n // If the user also configures an SDK in register(), their provider\n // will coexist — the global provider set last wins for new tracers,\n // but our processor captures all spans from the timber.js tracer.\n const provider = new BasicTracerProvider({\n spanProcessors: [processor],\n });\n api.trace.setGlobalTracerProvider(provider);\n\n // Reset cached tracer so next getTracer() picks up the new provider\n _tracer = undefined;\n\n globals[DEV_TRACING_INIT_KEY] = true;\n}\n\n// ─── Platform Tracer ─────────────────────────────────────────────────────\n\n/**\n * A native platform span. Mirrors the subset of the Cloudflare Workers\n * `Span` API that timber uses. See design/17-logging.md §\"Cloudflare Native\n * Spans\" and TIM-1135.\n */\nexport interface PlatformSpan {\n setAttribute(key: string, value: string | number | boolean): void;\n}\n\n/**\n * Adapter-provided native tracer. Callback-scoped like Cloudflare's\n * `tracing.enterSpan()` — the span starts when the callback is invoked and\n * ends when it returns or its promise settles. Nesting follows the\n * platform's async context.\n *\n * When registered, withSpan() wraps every framework span in a native span\n * *in addition to* the OTEL emission — dual emission, so external OTEL\n * collectors keep working alongside the platform's native trace view.\n *\n * Register via setPlatformTracer(), or by writing the\n * Symbol.for('timber:platform-tracer') key on globalThis from\n * adapter-generated entry code (what the Cloudflare _worker.js does).\n */\nexport interface PlatformTracer {\n enterSpan<T>(name: string, fn: (span: PlatformSpan) => T): T;\n}\n\n// globalThis + Symbol.for so a tracer registered by adapter-generated entry\n// code (a separate module instance) is visible in both the RSC and SSR\n// environments — same pattern as the cf-bindings ALS.\nconst PLATFORM_TRACER_KEY = Symbol.for('timber:platform-tracer');\n\n/** Register (or clear) the native platform tracer for this runtime. */\nexport function setPlatformTracer(tracer: PlatformTracer | undefined): void {\n (globalThis as Record<symbol, unknown>)[PLATFORM_TRACER_KEY] = tracer;\n}\n\n/** The registered native platform tracer, if any. */\nexport function getPlatformTracer(): PlatformTracer | undefined {\n return (globalThis as Record<symbol, unknown>)[PLATFORM_TRACER_KEY] as PlatformTracer | undefined;\n}\n\n// ─── OTEL Span Helpers ───────────────────────────────────────────────────\n\n/**\n * Attempt to get the @opentelemetry/api tracer. Returns undefined if the\n * package is not installed or no SDK is registered.\n *\n * timber.js depends on @opentelemetry/api as the vendor-neutral interface.\n * The API is a no-op by default — spans are only emitted when the developer\n * initializes an SDK in register().\n */\nlet _otelApi: typeof import('@opentelemetry/api') | null | undefined;\n\nasync function getOtelApi(): Promise<typeof import('@opentelemetry/api') | null> {\n if (_otelApi === undefined) {\n try {\n _otelApi = await import('@opentelemetry/api');\n } catch {\n _otelApi = null;\n }\n }\n return _otelApi;\n}\n\n/** OTEL tracer instance, lazily created. */\nlet _tracer: import('@opentelemetry/api').Tracer | null | undefined;\n\n/**\n * Get the timber.js OTEL tracer. Returns null if @opentelemetry/api is not available.\n */\nexport async function getTracer(): Promise<import('@opentelemetry/api').Tracer | null> {\n if (_tracer === undefined) {\n const api = await getOtelApi();\n if (api) {\n _tracer = api.trace.getTracer('timber.js');\n } else {\n _tracer = null;\n }\n }\n return _tracer;\n}\n\n/**\n * Run a function within a framework span. Composes two emission channels:\n *\n * - **Native platform span** — when an adapter registered a PlatformTracer\n * (Cloudflare Workers), the fn is wrapped in platformTracer.enterSpan()\n * so it appears in the platform's native trace view.\n * - **OTEL span** — when an OTEL SDK is active, the fn also runs inside an\n * OTEL span (dual emission). No SDK and no platform tracer = zero overhead.\n *\n * Automatically:\n * - Creates the span as a child of the current context\n * - Updates the ALS span ID for log–trace correlation\n * - Ends the span when the function completes\n * - Records exceptions on error (OTEL channel)\n */\nexport async function withSpan<T>(\n name: string,\n attributes: Record<string, string | number | boolean>,\n fn: () => T | Promise<T>\n): Promise<T> {\n const platformTracer = getPlatformTracer();\n if (!platformTracer) {\n return runOtelSpan(name, attributes, fn);\n }\n\n // Native platform span wraps the OTEL emission (dual emission). The\n // innermost platform span is tracked on the trace store so\n // setSpanAttribute() can reach it after creation — the platform API has\n // no getActiveSpan() equivalent.\n return platformTracer.enterSpan(name, async (span) => {\n for (const key of Object.keys(attributes)) {\n span.setAttribute(key, attributes[key]);\n }\n const store = traceAls.getStore();\n const prevPlatformSpan = store?.platformSpan;\n if (store) store.platformSpan = span;\n try {\n return await runOtelSpan(name, attributes, fn);\n } finally {\n if (store) store.platformSpan = prevPlatformSpan;\n }\n });\n}\n\n/** The OTEL half of withSpan() — no-op passthrough when no SDK is active. */\nasync function runOtelSpan<T>(\n name: string,\n attributes: Record<string, string | number | boolean>,\n fn: () => T | Promise<T>\n): Promise<T> {\n const tracer = await getTracer();\n if (!tracer) {\n return fn();\n }\n\n const api = (await getOtelApi())!;\n return tracer.startActiveSpan(name, { attributes }, async (span) => {\n const prevSpanId = getSpanId();\n updateSpanId(span.spanContext().spanId);\n try {\n const result = await fn();\n span.setStatus({ code: api.SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.setStatus({ code: api.SpanStatusCode.ERROR });\n if (error instanceof Error) {\n span.recordException(error);\n }\n throw error;\n } finally {\n span.end();\n updateSpanId(prevSpanId);\n }\n });\n}\n\n/**\n * Set an attribute on the current active span (if any).\n * Used for setting span attributes after span creation (e.g. timber.result on access spans).\n */\nexport async function setSpanAttribute(\n key: string,\n value: string | number | boolean\n): Promise<void> {\n // Forward to the innermost active native platform span, if any.\n const platformSpan = traceAls.getStore()?.platformSpan;\n if (platformSpan) {\n platformSpan.setAttribute(key, value);\n }\n\n const api = await getOtelApi();\n if (!api) return;\n\n const activeSpan = api.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.setAttribute(key, value);\n }\n}\n\n/**\n * Add a span event to the current active span (if any).\n * Used for timber.cache HIT/MISS events — recorded as span events, not child spans.\n */\nexport async function addSpanEvent(\n name: string,\n attributes?: Record<string, string | number | boolean>\n): Promise<void> {\n const api = await getOtelApi();\n if (!api) return;\n\n const activeSpan = api.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.addEvent(name, attributes);\n }\n}\n\n/**\n * Fire-and-forget span event — no await, no microtask overhead.\n *\n * Used on the cache hot path where awaiting addSpanEvent creates an\n * unnecessary microtask per cache operation. If OTEL is not loaded yet,\n * the event is silently dropped (acceptable for diagnostics).\n *\n * See TIM-370 for perf motivation.\n */\nexport function addSpanEventSync(\n name: string,\n attributes?: Record<string, string | number | boolean>\n): void {\n // Fast path: if OTEL API hasn't been loaded yet, skip entirely.\n // _otelApi is undefined (not yet loaded), null (failed to load), or the module.\n if (!_otelApi) return;\n\n const activeSpan = _otelApi.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.addEvent(name, attributes);\n }\n}\n\n/**\n * Try to extract the OTEL trace ID from the current active span context.\n * Returns undefined if OTEL is not active or no span exists.\n */\nexport async function getOtelTraceId(): Promise<{ traceId: string; spanId: string } | undefined> {\n const api = await getOtelApi();\n if (!api) return undefined;\n\n const activeSpan = api.trace.getActiveSpan();\n if (!activeSpan) return undefined;\n\n const ctx = activeSpan.spanContext();\n // OTEL uses \"0000000000000000\" as invalid trace IDs\n if (!ctx.traceId || ctx.traceId === '00000000000000000000000000000000') {\n return undefined;\n }\n\n return { traceId: ctx.traceId, spanId: ctx.spanId };\n}\n","/**\n * Error Formatter — rewrites SSR/RSC error messages to surface user code.\n *\n * When React or Vite throw errors during SSR, stack traces reference\n * vendored dependency paths (e.g. `.vite/deps_ssr/@vitejs_plugin-rsc_vendor_...`)\n * and mangled export names (`__vite_ssr_export_default__`). This module\n * rewrites error messages and stack traces to point at user code instead.\n *\n * Dev-only — in production, errors go through the structured logger\n * without formatting.\n */\n\n// ─── Stack Trace Rewriting ──────────────────────────────────────────────────\n\n/**\n * Patterns that identify internal Vite/RSC vendor paths in stack traces.\n * These are replaced with human-readable labels.\n */\nconst VENDOR_PATH_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [\n {\n pattern: /node_modules\\/\\.vite\\/deps_ssr\\/@vitejs_plugin-rsc_vendor_react-server-dom[^\\s)]+/g,\n replacement: '<react-server-dom>',\n },\n {\n pattern: /node_modules\\/\\.vite\\/deps_ssr\\/@vitejs_plugin-rsc_vendor[^\\s)]+/g,\n replacement: '<rsc-vendor>',\n },\n {\n pattern: /node_modules\\/\\.vite\\/deps_ssr\\/[^\\s)]+/g,\n replacement: '<vite-dep>',\n },\n {\n pattern: /node_modules\\/\\.vite\\/deps\\/[^\\s)]+/g,\n replacement: '<vite-dep>',\n },\n];\n\n/**\n * Patterns that identify Vite-mangled export names in error messages.\n */\nconst MANGLED_NAME_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [\n {\n pattern: /__vite_ssr_export_default__/g,\n replacement: '<default export>',\n },\n {\n pattern: /__vite_ssr_export_(\\w+)__/g,\n replacement: '<export $1>',\n },\n];\n\n/**\n * Rewrite an error's message and stack to replace internal Vite paths\n * and mangled names with human-readable labels.\n */\nexport function formatSsrError(error: unknown): string {\n if (!(error instanceof Error)) {\n return String(error);\n }\n\n let message = error.message;\n let stack = error.stack ?? '';\n\n // Rewrite mangled names in the message\n for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) {\n message = message.replace(pattern, replacement);\n }\n\n // Rewrite vendor paths in the stack\n for (const { pattern, replacement } of VENDOR_PATH_PATTERNS) {\n stack = stack.replace(pattern, replacement);\n }\n\n // Rewrite mangled names in the stack too\n for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) {\n stack = stack.replace(pattern, replacement);\n }\n\n // Extract hints from React-specific error patterns\n const hint = extractErrorHint(error.message);\n\n // Build formatted output: cleaned message, hint (if any), then cleaned stack\n const parts: string[] = [];\n parts.push(message);\n if (hint) {\n parts.push(` → ${hint}`);\n }\n\n // Include only the user-code frames from the stack (skip the first line\n // which is the message itself, and filter out vendor-only frames)\n const userFrames = extractUserFrames(stack);\n if (userFrames.length > 0) {\n parts.push('');\n parts.push(' User code in stack:');\n for (const frame of userFrames) {\n parts.push(` ${frame}`);\n }\n }\n\n return parts.join('\\n');\n}\n\n// ─── Error Hint Extraction ──────────────────────────────────────────────────\n\n/**\n * Extract a human-readable hint from common React/RSC error messages.\n *\n * React error messages contain useful information but the surrounding\n * context (vendor paths, mangled names) obscures it. This extracts the\n * actionable part as a one-line hint.\n */\nfunction extractErrorHint(message: string): string | null {\n // \"Functions cannot be passed directly to Client Components\"\n // Extract the component and prop name from the JSX-like syntax in the message\n const fnPassedMatch = message.match(/Functions cannot be passed directly to Client Components/);\n if (fnPassedMatch) {\n // Try to extract the prop name from the message\n // React formats: <... propName={function ...} ...>\n const propMatch = message.match(/<[^>]*?\\s(\\w+)=\\{function/);\n if (propMatch) {\n return `Prop \"${propMatch[1]}\" is a function — mark it \"use server\" or call it before passing`;\n }\n return 'A function prop was passed to a Client Component — mark it \"use server\" or call it before passing';\n }\n\n // \"Objects are not valid as a React child\"\n if (message.includes('Objects are not valid as a React child')) {\n return 'An object was rendered as JSX children — convert to string or extract the value';\n }\n\n // \"Cannot read properties of undefined/null\"\n const nullRefMatch = message.match(\n /Cannot read propert(?:y|ies) of (undefined|null) \\(reading '(\\w+)'\\)/\n );\n if (nullRefMatch) {\n return `Accessed .${nullRefMatch[2]} on ${nullRefMatch[1]} — check that the value exists`;\n }\n\n // \"X is not a function\"\n const notFnMatch = message.match(/(\\w+) is not a function/);\n if (notFnMatch) {\n return `\"${notFnMatch[1]}\" is not a function — check imports and exports`;\n }\n\n // \"Element type is invalid\"\n if (message.includes('Element type is invalid')) {\n return 'A component resolved to undefined/null — check default exports and import paths';\n }\n\n // \"Invalid hook call\" — hooks called outside React's render context.\n // In RSC, this typically means a 'use client' component was executed as a\n // server component instead of being serialized as a client reference.\n if (message.includes('Invalid hook call')) {\n return (\n 'A hook was called outside of a React component render. ' +\n \"If this is a 'use client' component, ensure the directive is at the very top of the file \" +\n '(before any imports) and that @vitejs/plugin-rsc is loaded correctly. ' +\n \"Barrel re-exports from non-'use client' files do not propagate the directive.\"\n );\n }\n\n return null;\n}\n\n// ─── Stack Frame Filtering ──────────────────────────────────────────────────\n\n/**\n * Extract stack frames that reference user code (not node_modules,\n * not framework internals).\n *\n * Returns at most 5 frames to keep output concise.\n */\nfunction extractUserFrames(stack: string): string[] {\n const lines = stack.split('\\n');\n const userFrames: string[] = [];\n\n for (const line of lines) {\n const trimmed = line.trim();\n // Skip non-frame lines\n if (!trimmed.startsWith('at ')) continue;\n // Skip node_modules, vendor, and internal frames\n if (\n trimmed.includes('node_modules') ||\n trimmed.includes('<react-server-dom>') ||\n trimmed.includes('<rsc-vendor>') ||\n trimmed.includes('<vite-dep>') ||\n trimmed.includes('node:internal')\n ) {\n continue;\n }\n userFrames.push(trimmed);\n if (userFrames.length >= 5) break;\n }\n\n return userFrames;\n}\n","/**\n * DefaultLogger — human-readable stderr logging when no custom logger is configured.\n *\n * Ships as the fallback so production deployments always have error visibility,\n * even without an `instrumentation.ts` logger export. Output is one line per\n * event, designed for `fly logs`, `kubectl logs`, Cloudflare dashboard tails, etc.\n *\n * Format:\n * [timber] ERROR message key=value key=value trace_id=4bf92f35\n * [timber] WARN message key=value key=value trace_id=4bf92f35\n * [timber] INFO message method=GET path=/dashboard status=200 durationMs=43 trace_id=4bf92f35\n *\n * Behavior:\n * - Suppressed entirely in dev mode (dev logging handles all output)\n * - `debug` suppressed unless TIMBER_DEBUG is set\n * - Replaced entirely when a custom logger is set via `setLogger()`\n *\n * See design/17-logging.md §\"DefaultLogger\"\n */\n\nimport { isDevMode, isDebug } from './debug.ts';\nimport { formatSsrError } from './error-formatter.ts';\nimport type { TimberLogger } from './logger.ts';\n\n/**\n * Format data fields as `key=value` pairs for human-readable output.\n * - `error` key is serialized via formatSsrError for stack trace cleanup\n * - `trace_id` is truncated to 8 chars for readability (full ID in OTEL)\n * - Other values are stringified inline\n */\nfunction formatDataFields(data?: Record<string, unknown>): string {\n if (!data) return '';\n\n const parts: string[] = [];\n let traceId: string | undefined;\n\n for (const [key, value] of Object.entries(data)) {\n if (key === 'trace_id') {\n // Defer trace_id to the end\n traceId = typeof value === 'string' ? value : String(value);\n continue;\n }\n if (key === 'error') {\n // Serialize errors with formatSsrError for clean output\n parts.push(`error=${formatSsrError(value)}`);\n continue;\n }\n if (value === undefined || value === null) continue;\n parts.push(`${key}=${value}`);\n }\n\n // trace_id always last, truncated to 8 chars for readability\n if (traceId) {\n parts.push(`trace_id=${traceId.slice(0, 8)}`);\n }\n\n return parts.length > 0 ? ' ' + parts.join(' ') : '';\n}\n\n/** Pad level string to fixed width for alignment. */\nfunction padLevel(level: string): string {\n return level.padEnd(5);\n}\n\nexport function createDefaultLogger(): TimberLogger {\n return {\n error(msg: string, data?: Record<string, unknown>): void {\n // Errors are ALWAYS logged, including dev mode. Suppressing errors\n // in dev causes silent 500s with no stack trace, making route.ts\n // and render errors impossible to debug. See TIM-555.\n const fields = formatDataFields(data);\n process.stderr.write(`[timber] ${padLevel('ERROR')} ${msg}${fields}\\n`);\n },\n\n warn(msg: string, data?: Record<string, unknown>): void {\n // Warnings are always logged — same rationale as errors.\n const fields = formatDataFields(data);\n process.stderr.write(`[timber] ${padLevel('WARN')} ${msg}${fields}\\n`);\n },\n\n info(msg: string, data?: Record<string, unknown>): void {\n // info is suppressed by default — per-request lines are too noisy\n // without a custom logger. Enable with TIMBER_DEBUG.\n if (isDevMode()) return;\n if (!isDebug()) return;\n const fields = formatDataFields(data);\n process.stderr.write(`[timber] ${padLevel('INFO')} ${msg}${fields}\\n`);\n },\n\n debug(msg: string, data?: Record<string, unknown>): void {\n // debug is suppressed in dev (dev logger handles it) and in\n // production unless TIMBER_DEBUG is explicitly set.\n if (isDevMode()) return;\n if (!isDebug()) return;\n const fields = formatDataFields(data);\n process.stderr.write(`[timber] ${padLevel('DEBUG')} ${msg}${fields}\\n`);\n },\n };\n}\n","/**\n * Logger — structured logging with environment-aware formatting.\n *\n * timber.js ships a DefaultLogger that writes human-readable lines to stderr\n * in production. Users can export a custom logger from instrumentation.ts to\n * replace it with pino, winston, or any TimberLogger-compatible object.\n *\n * See design/17-logging.md §\"Production Logging\"\n */\n\nimport { getTraceStore } from './tracing.ts';\nimport { createDefaultLogger } from './default-logger.ts';\nimport { isDevMode } from './debug.ts';\nimport { isControlFlowSignal } from './primitives.ts';\n\n// ─── Logger Interface ─────────────────────────────────────────────────────\n\n/** Any object with standard log methods satisfies this — pino, winston, consola, console. */\nexport interface TimberLogger {\n info(msg: string, data?: Record<string, unknown>): void;\n warn(msg: string, data?: Record<string, unknown>): void;\n error(msg: string, data?: Record<string, unknown>): void;\n debug(msg: string, data?: Record<string, unknown>): void;\n}\n\n// ─── Logger Registry ──────────────────────────────────────────────────────\n\n// Initialize with DefaultLogger so production errors are never silent.\n// Replaced when setLogger() is called from instrumentation.ts.\nlet _logger: TimberLogger = createDefaultLogger();\n\n/**\n * Set the user-provided logger. Called by the instrumentation loader\n * when it finds a `logger` export in instrumentation.ts. Replaces\n * the DefaultLogger entirely.\n */\nexport function setLogger(logger: TimberLogger): void {\n _logger = logger;\n}\n\n/**\n * Get the current logger. Always non-null — returns DefaultLogger when\n * no custom logger is configured.\n */\nexport function getLogger(): TimberLogger {\n return _logger;\n}\n\n// ─── Framework Log Helpers ────────────────────────────────────────────────\n\n/**\n * Inject trace_id and span_id into log data for log–trace correlation.\n * Always injects trace_id (never undefined). Injects span_id only when OTEL is active.\n */\nfunction withTraceContext(data?: Record<string, unknown>): Record<string, unknown> {\n const store = getTraceStore();\n const enriched: Record<string, unknown> = { ...data };\n if (store) {\n enriched.trace_id = store.traceId;\n if (store.spanId) {\n enriched.span_id = store.spanId;\n }\n }\n return enriched;\n}\n\n// ─── Framework Event Emitters ─────────────────────────────────────────────\n\n/** Log a completed request. Level: info. */\nexport function logRequestCompleted(data: {\n method: string;\n path: string;\n status: number;\n durationMs: number;\n /** Number of concurrent in-flight requests (including this one) at completion time. */\n concurrency?: number;\n}): void {\n _logger.info('request completed', withTraceContext(data));\n}\n\n/** Log request received. Level: debug. */\nexport function logRequestReceived(data: { method: string; path: string }): void {\n _logger.debug('request received', withTraceContext(data));\n}\n\n/** Log a slow request warning. Level: warn. */\nexport function logSlowRequest(data: {\n method: string;\n path: string;\n durationMs: number;\n threshold: number;\n /** Number of concurrent in-flight requests at the time the slow request completed. */\n concurrency?: number;\n}): void {\n _logger.warn('slow request exceeded threshold', withTraceContext(data));\n}\n\n/** Log middleware short-circuit. Level: debug. */\nexport function logMiddlewareShortCircuit(data: {\n method: string;\n path: string;\n status: number;\n}): void {\n _logger.debug('middleware short-circuited', withTraceContext(data));\n}\n\n/** Log unhandled error in middleware phase. Level: error. */\nexport function logMiddlewareError(data: { method: string; path: string; error: unknown }): void {\n if (isControlFlowSignal(data.error)) return;\n _logger.error('unhandled error in middleware phase', withTraceContext(data));\n}\n\n/** Log unhandled render-phase error. Level: error. */\nexport function logRenderError(data: {\n method: string;\n path: string;\n error: unknown;\n errorId?: string;\n}): void {\n if (isControlFlowSignal(data.error)) return;\n _logger.error('unhandled render-phase error', withTraceContext(data));\n}\n\n/** Log proxy.ts uncaught error. Level: error. */\nexport function logProxyError(data: { error: unknown }): void {\n _logger.error('proxy.ts threw uncaught error', withTraceContext(data));\n}\n\n/** Log unhandled error in server action. Level: error. */\nexport function logActionError(data: { method: string; path: string; error: unknown }): void {\n if (isControlFlowSignal(data.error)) return;\n _logger.error('unhandled server action error', withTraceContext(data));\n}\n\n/** Log unhandled error in route handler. Level: error. */\nexport function logRouteError(data: { method: string; path: string; error: unknown }): void {\n if (isControlFlowSignal(data.error)) return;\n _logger.error('unhandled route handler error', withTraceContext(data));\n}\n\n/** Log SSR streaming error (post-shell). Level: error. */\nexport function logStreamingError(data: { error: unknown }): void {\n _logger.error('SSR streaming error (post-shell)', withTraceContext(data));\n}\n\n/** Log waitUntil() adapter missing (once at startup). Level: warn. */\nexport function logWaitUntilUnsupported(): void {\n _logger.warn('adapter does not support waitUntil()');\n}\n\n/** Log waitUntil() promise rejection. Level: warn. */\nexport function logWaitUntilRejected(data: { error: unknown }): void {\n _logger.warn('waitUntil() promise rejected', withTraceContext(data));\n}\n\n/** Log staleWhileRevalidate refetch failure. Level: warn. */\nexport function logSwrRefetchFailed(data: { cacheKey: string; error: unknown }): void {\n _logger.warn('staleWhileRevalidate refetch failed', withTraceContext(data));\n}\n\n/** Log cache miss. Level: debug. */\nexport function logCacheMiss(data: { cacheKey: string }): void {\n _logger.debug('timber.cache MISS', withTraceContext(data));\n}\n\n// ─── Swallow Helper ───────────────────────────────────────────────────────\n\n/**\n * Log an intentionally swallowed error. Provides observability into catch\n * blocks that are deliberately empty — the error is consumed, never rethrown.\n *\n * Default level: `warn` in dev (so the overlay surfaces patterns), `debug`\n * in production (low noise unless TIMBER_DEBUG is set). Pass `opts.level`\n * to override.\n *\n * **Infallible** — swallow() itself never throws, even if the logger is\n * broken. A thrown swallow would turn a benign catch into a crash.\n */\nexport function swallow(err: unknown, reason: string, opts?: { level?: 'debug' | 'warn' }): void {\n try {\n const level = opts?.level ?? (isDevMode() ? 'warn' : 'debug');\n _logger[level](`swallowed: ${reason}`, withTraceContext({ error: err }));\n } catch {\n // swallow() must never throw.\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"navigation-context-D0TU0Jog.js","names":[],"sources":["../../src/client/navigation-context.ts"],"sourcesContent":["'use client';\n\n/**\n * NavigationContext — React context for navigation state.\n *\n * Holds the current pathname and search, updated atomically with the RSC\n * tree on each navigation. This replaces the previous useSyncExternalStore\n * approach for usePathname() and useSearchParams(), which suffered from a\n * timing gap: the new tree could commit before the external store\n * re-renders fired, causing a frame where both old and new active states\n * were visible simultaneously.\n *\n * Segment params are NOT here — see the note on NavigationState below.\n *\n * By wrapping the RSC payload element in NavigationProvider inside\n * renderRoot(), the context value and the element tree are passed to\n * reactRoot.render() in the same call — atomic by construction.\n * All consumers (usePathname, useSearchParams) see the new values in the\n * same render pass as the new tree.\n *\n * During SSR, the wrapper chain mounts this same provider with the\n * request's pathname and raw search string (TIM-1424), so the hooks\n * resolve through the identical code path on both sides. Per-request\n * isolation is by construction — the value is part of the request's\n * element tree.\n *\n * This module is `'use client'` and is never evaluated in the RSC\n * environment. The shims plugin resolves next/navigation to\n * navigation-rsc.ts in RSC, which has throwing stubs (TIM-1420).\n *\n * SINGLETON GUARANTEE: All shared mutable state uses globalThis via\n * Symbol.for keys. The RSC client bundler can duplicate this module\n * across chunks (browser-entry graph + client-reference graph). With\n * ESM output, each chunk gets its own module scope — module-level\n * variables would create separate singleton instances per chunk.\n * globalThis guarantees a single instance regardless of duplication.\n *\n * This workaround will be removed when Rolldown ships `format: 'app'`\n * (module registry format that deduplicates like webpack/Turbopack).\n * See design/27-chunking-strategy.md.\n *\n * See design/19-client-navigation.md §\"NavigationContext\"\n */\n\nimport { createContext, useContext, createElement, type ReactNode } from 'react';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\n\n// ---------------------------------------------------------------------------\n// Context type\n// ---------------------------------------------------------------------------\n\n/**\n * What the browser knows about the current location.\n *\n * Params are deliberately absent: they are a property of the tree the server\n * rendered, not of the address bar, and they now travel inside the payload as\n * the payload root. Keeping a copy here meant the router had to thread\n * the record from a response header into context on every path — navigation,\n * traversal, prefetch, revalidation — and each of those was a place to drop it\n * (TIM-1294).\n */\nexport interface NavigationState {\n pathname: string;\n search: string;\n}\n\n// ---------------------------------------------------------------------------\n// Lazy context initialization\n// ---------------------------------------------------------------------------\n\n/**\n * Context instances are stored on globalThis (NOT in module-level\n * variables) because the ESM bundler can duplicate this module across\n * chunks. Module-level variables would create separate instances per\n * chunk — the provider in NavigationRoot (index chunk) would use\n * context A while the consumer in usePendingNavigation (shared chunk)\n * reads from context B. globalThis guarantees a single instance.\n *\n * This module is `'use client'` so it is never evaluated in the RSC\n * environment — `createContext` and `useContext` are always available.\n * The shims plugin resolves next/navigation to navigation-rsc.ts in RSC,\n * which has throwing stubs for the hooks (TIM-1420).\n *\n * See design/27-chunking-strategy.md §\"Singleton Safety\"\n */\n\nconst NAV_CTX_KEY = Symbol.for('__timber_nav_ctx');\n\nfunction getOrCreateContext(): import('react').Context<NavigationState | null> {\n const existing = (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] as\n | import('react').Context<NavigationState | null>\n | undefined;\n if (existing !== undefined) return existing;\n const ctx = createContext<NavigationState | null>(null);\n (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] = ctx;\n return ctx;\n}\n\n/**\n * Read the navigation context. Returns null only when no provider is above\n * the caller — a component rendered outside the timber app.\n *\n * Internal — used by usePathname() and useSearchParams().\n */\nexport function useNavigationContext(): NavigationState | null {\n return useContext(getOrCreateContext());\n}\n\n// ---------------------------------------------------------------------------\n// Provider component\n// ---------------------------------------------------------------------------\n\nexport interface NavigationProviderProps {\n value: NavigationState;\n children?: ReactNode;\n}\n\n/**\n * Wraps children with NavigationContext.Provider.\n *\n * Used in browser-entry.ts renderRoot to wrap the RSC payload element\n * so that navigation state updates atomically with the tree render.\n */\nexport function NavigationProvider({\n value,\n children,\n}: NavigationProviderProps): import('react').ReactElement {\n return createElement(getOrCreateContext().Provider, { value }, children);\n}\n\n// ---------------------------------------------------------------------------\n// Module-level state for renderRoot to read\n// ---------------------------------------------------------------------------\n\n/**\n * Navigation state communicated between the router and renderRoot.\n *\n * The router calls setNavigationState() before renderRoot(). The\n * renderRoot callback reads via getNavigationState() to create the\n * NavigationProvider with the correct params/pathname.\n *\n * This is NOT used by hooks directly — hooks read from React context.\n *\n * Stored on globalThis (like the context instances above) because the\n * router lives in one chunk while renderRoot lives in another. Module-\n * level variables would be separate per chunk.\n */\nconst NAV_STATE_KEY = Symbol.for('__timber_nav_state');\n\nfunction _getNavStateStore(): { current: NavigationState } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[NAV_STATE_KEY]) {\n g[NAV_STATE_KEY] = { current: { pathname: '/', search: '' } };\n }\n return g[NAV_STATE_KEY] as { current: NavigationState };\n}\n\nexport function setNavigationState(state: NavigationState): void {\n _getNavStateStore().current = state;\n}\n\nexport function getNavigationState(): NavigationState {\n return _getNavStateStore().current;\n}\n\n// ---------------------------------------------------------------------------\n// Pending navigation state lives in the router, not here\n// ---------------------------------------------------------------------------\n\n/**\n * There was a second React context here — `PendingNavigationContext`, holding\n * the in-flight navigation URL, provided by `NavigationRoot` out of a\n * `useState` — plus a `usePendingNavigationUrl()` reader for it. Both are gone\n * (TIM-1307).\n *\n * Nothing read them. `usePendingNavigation()` (use-pending-navigation.ts) and\n * `TopLoader` both subscribe to the router's external pending store via\n * `useSyncExternalStore`: sync priority, immune to transition entanglement,\n * and cleared by `runNavigation`'s supersession-guarded `finally` (TIM-1034).\n * The context was a parallel representation of the same fact that no consumer\n * ever migrated to, and one that could not even agree with the store — a\n * navigation superseded by a cached popstate replay left its URL set until the\n * next full navigation, because the staleness guard skipped the clear and no\n * other path touched it.\n *\n * One representation, and it is the router's. See\n * design/19-client-navigation.md §\"How Pending State Works\".\n */\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsFA,IAAM,cAAc,OAAO,IAAI,kBAAkB;AAEjD,SAAS,qBAAsE;CAC7E,MAAM,WAAY,WAAuC;CAGzD,IAAI,aAAa,KAAA,GAAW,OAAO;CACnC,MAAM,MAAM,cAAsC,IAAI;CACtD,WAAwC,eAAe;CACvD,OAAO;AACT;;;;;;;AAQA,SAAgB,uBAA+C;CAC7D,OAAO,WAAW,mBAAmB,CAAC;AACxC;;;;;;;AAiBA,SAAgB,mBAAmB,EACjC,OACA,YACwD;CACxD,OAAO,cAAc,mBAAmB,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ;AACzE;;;;;;;;;;;;;;AAmBA,IAAM,gBAAgB,OAAO,IAAI,oBAAoB;AAErD,SAAS,oBAAkD;CACzD,MAAM,IAAI;CACV,IAAI,CAAC,EAAE,gBACL,EAAE,iBAAiB,EAAE,SAAS;EAAE,UAAU;EAAK,QAAQ;CAAG,EAAE;CAE9D,OAAO,EAAE;AACX;AAEA,SAAgB,mBAAmB,OAA8B;CAC/D,kBAAkB,CAAC,CAAC,UAAU;AAChC;AAEA,SAAgB,qBAAsC;CACpD,OAAO,kBAAkB,CAAC,CAAC;AAC7B"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-context-8idfR3UZ.js","names":[],"sources":["../../src/utils/startup-timer.ts","../../src/plugin-context.ts"],"sourcesContent":["/**\n * Startup timer — records named phases with their durations.\n *\n * Used by the plugin system to instrument cold start and report a\n * timing breakdown in dev mode. Zero overhead in production (disabled).\n *\n * See design/18-build-system.md, TIM-155.\n */\n\nimport { performance } from 'node:perf_hooks';\n\nexport interface PhaseRecord {\n name: string;\n startMs: number;\n durationMs: number;\n}\n\nexport interface StartupTimer {\n /** Mark the beginning of a named phase. */\n start(phase: string): void;\n /** Mark the end of a named phase. Returns duration in ms. */\n end(phase: string): number;\n /** Get all completed phase records, ordered by start time. */\n getPhases(): PhaseRecord[];\n /** Total elapsed time from first start() to last end(). */\n totalMs(): number;\n /** Format a human-readable summary string. */\n formatSummary(): string;\n}\n\n/**\n * Create a startup timer that records phase durations.\n */\nexport function createStartupTimer(): StartupTimer {\n const pending = new Map<string, number>();\n const phases: PhaseRecord[] = [];\n\n return {\n start(phase: string): void {\n pending.set(phase, performance.now());\n },\n\n end(phase: string): number {\n const startMs = pending.get(phase);\n if (startMs === undefined) {\n return 0;\n }\n pending.delete(phase);\n const durationMs = performance.now() - startMs;\n phases.push({ name: phase, startMs, durationMs });\n return durationMs;\n },\n\n getPhases(): PhaseRecord[] {\n return [...phases].sort((a, b) => a.startMs - b.startMs);\n },\n\n totalMs(): number {\n if (phases.length === 0) return 0;\n const sorted = this.getPhases();\n const first = sorted[0];\n const last = sorted[sorted.length - 1];\n return last.startMs + last.durationMs - first.startMs;\n },\n\n formatSummary(): string {\n const sorted = this.getPhases();\n if (sorted.length === 0) return 'No phases recorded.';\n\n const lines = sorted.map((p) => {\n const ms = p.durationMs.toFixed(1);\n return ` ${p.name.padEnd(30)} ${ms.padStart(8)}ms`;\n });\n\n const total = this.totalMs().toFixed(1);\n lines.push(` ${'total'.padEnd(30)} ${total.padStart(8)}ms`);\n\n return ['[timber] startup timing:', ...lines].join('\\n');\n },\n };\n}\n\n/**\n * No-op timer for production builds — all methods are empty.\n */\nexport function createNoopTimer(): StartupTimer {\n return {\n start() {},\n end() {\n return 0;\n },\n getPhases() {\n return [];\n },\n totalMs() {\n return 0;\n },\n formatSummary() {\n return '';\n },\n };\n}\n","/**\n * Plugin context — internal types and helpers for timber sub-plugins.\n *\n * These are NOT part of the user-facing API. They are consumed by\n * sub-plugins (routing, entries, shims, etc.) via relative imports.\n * The root entry point (index.ts) does not re-export them.\n *\n * Design doc: 18-build-system.md §\"Shared Plugin Context\"\n */\n\nimport { existsSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { createRequire } from 'node:module';\nimport { parseAst } from 'vite';\nimport type { RouteTree } from './routing/types.ts';\nimport type { BuildManifest } from './server/build-manifest.ts';\nimport type { CaptureSummary } from './server/prebuilt-builder.ts';\nimport type { StartupTimer } from './utils/startup-timer.ts';\nimport { createStartupTimer } from './utils/startup-timer.ts';\nimport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.ts';\nimport type { RscManifest } from './plugins/static-build.ts';\nimport type { HoldingServer } from './dev-tools/holding-server.ts';\nimport type { ProgramNode } from './plugins/callsite-ast.ts';\n\n// Re-export for sub-plugin convenience — they import from plugin-context.ts\nexport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.ts';\n\n// ── Client JavaScript config ─────────────────────────────────────────────\n\n/** Fully resolved client JavaScript configuration (no optionals). */\nexport interface ResolvedClientJavascript {\n disabled: boolean;\n enableHMRInDev: boolean;\n}\n\n/**\n * Resolve `clientJavascript` into a fully resolved config.\n */\nexport function resolveClientJavascript(config: TimberUserConfig): ResolvedClientJavascript {\n if (config.clientJavascript !== undefined) {\n if (typeof config.clientJavascript === 'boolean') {\n return {\n disabled: !config.clientJavascript,\n enableHMRInDev: !config.clientJavascript,\n };\n }\n return {\n disabled: config.clientJavascript.disabled,\n enableHMRInDev: config.clientJavascript.enableHMRInDev ?? config.clientJavascript.disabled,\n };\n }\n return { disabled: false, enableHMRInDev: false };\n}\n\n// ── Plugin Context ────────────────────────────────────────────────────────\n\n/**\n * Shared context object passed to all sub-plugins via closure.\n *\n * Sub-plugins communicate through this context — not through Vite's\n * plugin API or global state.\n * See design/18-build-system.md §\"Shared Plugin Context\".\n */\nexport interface PluginContext {\n config: TimberUserConfig;\n /** Resolved client JavaScript configuration */\n clientJavascript: ResolvedClientJavascript;\n /** The scanned route tree (populated by timber-routing, consumed by timber-entries) */\n routeTree: RouteTree | null;\n /** Absolute path to the app/ directory */\n appDir: string;\n /** Absolute path to the project root */\n root: string;\n /** Whether the dev server is running (set by timber-root-sync in configResolved) */\n dev: boolean;\n /**\n * Vite's resolved `base` (set by timber-root-sync in configResolved).\n * Always has a trailing slash; `'/'` unless the app is deployed under a\n * sub-path. Every URL the framework emits — assets, fonts, RSC payloads —\n * carries this prefix. This is the single copy: sub-plugins read it here\n * rather than capturing `config.base` in their own configResolved.\n *\n * **Availability:** written by timber-root-sync, which has no `enforce`, so\n * it is populated from `buildStart`/`load`/`generateBundle`/`configureServer`\n * onward — but NOT during the `configResolved` of an `enforce: 'pre'`\n * plugin, which Vite runs first. A pre plugin that needs the base inside\n * its own `configResolved` must read `config.base` there (timber-fonts\n * does). See TIM-1261.\n */\n base: string;\n /** CSS build manifest (populated by adapter after client build, null in dev) */\n buildManifest: BuildManifest | null;\n /** Per-build deployment ID for version skew detection (null in dev) */\n deploymentId: string | null;\n /** Startup timer for profiling cold start phases (active in dev, no-op in prod) */\n timer: StartupTimer;\n /** Holding server that binds the port during dev startup (closed in timber-dev-server) */\n holdingServer?: HoldingServer | null;\n /**\n * Resolved absolute path to the build output directory.\n *\n * Defaults to `<root>/.timber/dist`. Can be overridden via\n * `timber.config.ts` `buildDir` or Vite's `build.outDir`.\n * Timber config takes precedence when both are set.\n *\n * Used by adapter-build and cli preview.\n */\n buildDir: string;\n /**\n * Per-route client reference keys captured during the RSC build.\n * Maps absolute route file paths to the set of client reference\n * hash keys they transitively import. Used by adapter-build to\n * populate per-route modulepreload entries from clientReferenceDeps.\n */\n routeClientRefKeys?: Map<string, Set<string>>;\n /**\n * Static request-dependency verdict per segment entry file, keyed by the\n * absolute `filePath` the route scanner produced. Computed once by\n * `timber-request-dep`'s RSC `buildEnd` and serialized into\n * `buildManifest.requestDep` by `timber-build-manifest` (build only — dev\n * answers the same question through the `Symbol.for('timber:request-dep')`\n * bridge instead).\n *\n * `undefined` and `{}` are different states and the difference is the whole\n * of TIM-1300: an empty object means the analysis ran and found no segments,\n * `undefined` means it never ran, and the runtime cannot tell them apart —\n * every key is absent either way, so every segment falls to the conservative\n * `true` and nothing is ever skipped. The build fails loudly on `undefined`\n * rather than shipping that silently.\n */\n requestDepMap?: Record<string, boolean>;\n /**\n * Maps absolute module IDs of client references in non-facade RSC\n * chunks to the absolute paths of their consuming facade modules.\n * Built during the RSC `generateBundle` phase by timber-chunks,\n * consumed by `clientChunkGroup` to reclassify `shared:` modules\n * that are only used by a single route boundary.\n * See design/27-chunking-strategy.md, TIM-1107.\n */\n sharedModuleFacades?: Map<string, string[]>;\n /**\n * Module ID → prebuilt component IDs declared in it. Recorded by the\n * timber-prebuilt transform (RSC environment, production builds only),\n * consumed by the RSC `generateBundle` module-graph walk that computes\n * `routePrebuiltComponents`. See design/44-render-at-build-time.md.\n */\n prebuiltModuleComponents?: Map<string, string[]>;\n /**\n * Module ID → mapping of variable name to component ID for the prebuilt\n * components declared in that module. Used at `generateBundle` time to\n * trace imports from other modules back to specific component IDs.\n */\n prebuiltBindingNames?: Map<string, Map<string, string>>;\n /**\n * Component ID → distinct static call-site prop objects captured from\n * JSX usage sites during the transform and generateBundle phases.\n * Consumed by the post-build capture pass to render with actual props\n * instead of empty `{}`. See design/44 §Props Derivation.\n */\n prebuiltCallSiteProps?: Map<string, Record<string, unknown>[]>;\n /**\n * Route file path (page or layout, absolute) → prebuilt component IDs\n * transitively reachable from it. Computed during RSC `generateBundle`,\n * consumed by the post-build capture pass (TIM-1119) to decide which\n * components to render under which route's param combos.\n */\n routePrebuiltComponents?: Map<string, string[]>;\n /**\n * Page file paths that were desugared from `export const prerender` by\n * timber-prerender-sugar. The prebuilt transform uses this to verify\n * the generated `cache.component(...)` call was picked up.\n */\n prerenderSugarPages?: Set<string>;\n /**\n * Component ID → built chunk file name for prebuilt-declaring modules\n * that are only reachable via dynamic imports (import.meta.glob, lazy\n * routes). The capture pass imports these chunks to trigger\n * cache.component() registration before rendering. Populated during\n * RSC generateBundle alongside routePrebuiltComponents.\n */\n prebuiltLazyChunks?: Map<string, string>;\n /** Post-build prebuilt capture summary (populated by timber-prebuilt, consumed by build-report). */\n captureSummary?: CaptureSummary;\n /**\n * Content-keyed AST parse memo shared across cache/prebuilt/prerender-sugar\n * transforms. Collapses both cross-plugin and cross-environment duplicate\n * parses: identical code hits, rewritten code misses (correct because Vite\n * transforms chain — a rewrite produces different code). Bounded LRU (32\n * entries) so it doesn't hold programs for the whole build.\n *\n * Consumers MUST treat the returned ProgramNode as read-only — they collect\n * ranges and rewrite CODE via MagicString; the AST itself is never mutated.\n */\n parseCached?: ParseMemo;\n /**\n * Content-Type mappings for extensionless static API route outputs.\n * Populated during static generation, consumed by adapter-build to\n * write per-path Content-Type rules in _headers. See TIM-1241.\n */\n staticContentTypes?: Record<string, string>;\n /**\n * RSC flight payload manifest mapping unhashed → hashed URLs.\n * Populated during static generation, consumed by adapters to set\n * immutable cache headers for `_rsc/` files. See TIM-1254.\n */\n rscManifest?: RscManifest;\n}\n\n// ── AST parse memo ───────────────────────────────────────────────────────\n\nconst PARSE_MEMO_CAPACITY = 32;\n\n/**\n * Content-keyed LRU memo for `parseAst`. Transforms for the same module run\n * back-to-back across plugins and environments, so a tiny window captures the\n * wins. The returned ProgramNode is shared — callers MUST NOT mutate it.\n */\nexport class ParseMemo {\n private cache = new Map<string, ProgramNode>();\n\n /**\n * Parse code, returning a cached ProgramNode if the same code was parsed\n * before. The returned AST MUST NOT be mutated — it may be shared across\n * multiple consumers.\n */\n parse(code: string): ProgramNode {\n const existing = this.cache.get(code);\n if (existing) {\n // LRU refresh: move to end\n this.cache.delete(code);\n this.cache.set(code, existing);\n return existing;\n }\n const program = parseAst(code, { lang: 'tsx' }) as unknown as ProgramNode;\n if (this.cache.size >= PARSE_MEMO_CAPACITY) {\n // Evict oldest (first inserted)\n const firstKey = this.cache.keys().next().value!;\n this.cache.delete(firstKey);\n }\n this.cache.set(code, program);\n return program;\n }\n\n get size(): number {\n return this.cache.size;\n }\n}\n\n// ── App directory resolution ──────────────────────────────────────────────\n\n// ── Plugin API (Vite `plugin.api`) ────────────────────────────────────────\n\n/**\n * What `timber-root-sync` publishes on its Vite `plugin.api`.\n *\n * The single supported way to read timber's *resolved* configuration from\n * outside the plugin pipeline. Re-reading `timber.config.ts` is not equivalent:\n * config passed inline as `timber({ ... })` in `vite.config.ts` exists only in\n * the plugin's closure, and `root`/`appDir` are resolved against Vite's root\n * rather than the caller's cwd.\n */\nexport interface TimberPluginApi {\n /** The live plugin context. Fully populated from `configResolved` onward. */\n getContext(): PluginContext;\n}\n\n/** Narrow an unknown Vite `plugin.api` to timber's. */\nexport function isTimberPluginApi(api: unknown): api is TimberPluginApi {\n return (\n typeof api === 'object' &&\n api !== null &&\n 'getContext' in api &&\n typeof api.getContext === 'function'\n );\n}\n\n/**\n * Resolve the app directory. Checks (in order):\n * 1. Explicit `configAppDir` from timber.config.ts\n * 2. `<root>/app`\n * 3. `<root>/src/app`\n *\n * Throws if none exist.\n */\nexport function resolveAppDir(root: string, configAppDir?: string): string {\n if (configAppDir) {\n const explicit = join(root, configAppDir);\n if (!existsSync(explicit)) {\n throw new Error(`[timber] Configured appDir \"${configAppDir}\" does not exist at ${explicit}`);\n }\n return explicit;\n }\n\n const rootApp = join(root, 'app');\n if (existsSync(rootApp)) return rootApp;\n\n const srcApp = join(root, 'src', 'app');\n if (existsSync(srcApp)) return srcApp;\n\n throw new Error(\n `[timber] Could not find app directory. Expected \"app/\" or \"src/app/\" in ${root}. ` +\n `You can set appDir in timber.config.ts to specify a custom location.`\n );\n}\n\n// ── Plugin context factory ────────────────────────────────────────────────\n\nexport function createPluginContext(config?: TimberUserConfig, root?: string): PluginContext {\n const projectRoot = root ?? process.cwd();\n const resolvedConfig: TimberUserConfig = { ...config };\n return {\n config: resolvedConfig,\n clientJavascript: resolveClientJavascript(resolvedConfig),\n routeTree: null,\n appDir: join(projectRoot, 'app'),\n root: projectRoot,\n dev: false,\n base: '/',\n buildManifest: null,\n deploymentId: null,\n timer: createStartupTimer(),\n holdingServer: null,\n buildDir: resolveBuildDir(projectRoot, resolvedConfig.buildDir),\n parseCached: new ParseMemo(),\n };\n}\n\n// ── Build directory resolution ────────────────────────────────────────────\n\n/** Default build output directory (relative to root). */\nexport const DEFAULT_BUILD_DIR = join('.timber', 'dist');\n\n/**\n * Resolve the build output directory.\n *\n * Priority:\n * 1. Explicit `timberBuildDir` from timber.config.ts\n * 2. Explicit `viteBuildOutDir` from Vite's build.outDir (if not the default 'dist')\n * 3. Default: `.timber/dist`\n *\n * Returns an absolute path.\n */\nexport function resolveBuildDir(\n root: string,\n timberBuildDir?: string,\n viteBuildOutDir?: string\n): string {\n if (timberBuildDir) {\n return resolve(root, timberBuildDir);\n }\n if (viteBuildOutDir && viteBuildOutDir !== 'dist') {\n return resolve(root, viteBuildOutDir);\n }\n return join(root, DEFAULT_BUILD_DIR);\n}\n\n// ── Config file loading ───────────────────────────────────────────────────\n\n/**\n * Map known timber.config load failures to actionable hints (TIM-1040).\n * Returns null when the error has no known remedy beyond its own message.\n *\n * Known failure modes of loading timber.config.ts via native require():\n * - Node < 22.18 has no default type stripping → ERR_UNKNOWN_FILE_EXTENSION\n * - Non-erasable TS syntax (enums, namespaces, parameter properties)\n * → ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX / ERR_INVALID_TYPESCRIPT_SYNTAX\n * - ESM syntax in a `\"type\": \"commonjs\"` package → the stripped .ts is\n * parsed as CJS and throws SyntaxError: \"Unexpected token 'export'\"\n * (export statements) or \"Cannot use import statement outside a\n * module\" (import statements)\n */\nexport function configLoadHint(configName: string, err: unknown): string | null {\n const code = (err as NodeJS.ErrnoException | null)?.code;\n const isTs = configName.endsWith('.ts');\n\n if (isTs && code === 'ERR_UNKNOWN_FILE_EXTENSION') {\n return (\n 'Loading a TypeScript config requires Node.js 22.18+ (built-in type stripping). ' +\n 'Upgrade Node.js or rewrite the config as timber.config.js.'\n );\n }\n if (code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX' || code === 'ERR_INVALID_TYPESCRIPT_SYNTAX') {\n return (\n 'Node.js loads the config with type stripping, which only supports erasable TypeScript syntax — ' +\n 'replace enums, namespaces, and constructor parameter properties with plain objects and types.'\n );\n }\n if (\n isTs &&\n err instanceof SyntaxError &&\n /Unexpected token '(export|import)'|Cannot use import statement outside a module/.test(\n err.message\n )\n ) {\n return (\n 'The config uses ESM syntax but Node.js parsed it as CommonJS — ' +\n 'set \"type\": \"module\" in package.json, or use module.exports instead of export default.'\n );\n }\n return null;\n}\n\n/**\n * Load timber.config.ts (or .js, .mjs) from the project root.\n * Returns the config object or null if no config file is found.\n * Throws a descriptive error naming the file when a config file exists\n * but fails to load — a broken config must fail loudly, never silently\n * degrade (see TIM-1066). Known failure modes get an actionable hint\n * appended (see configLoadHint, TIM-1040).\n *\n * Uses require() — Node 22.12+ can require() ESM modules, and Node\n * 22.18+ strips types from .ts files by default (hence the engines\n * field requiring >=22.18.0). This keeps timber() synchronous — no\n * async config loading needed.\n * This is the single config loader — dev, build, and preview all use it.\n */\nexport function loadTimberConfigFile(root: string): TimberUserConfig | null {\n const configNames = ['timber.config.ts', 'timber.config.js', 'timber.config.mjs'];\n const req = createRequire(join(root, 'package.json'));\n\n for (const name of configNames) {\n const configPath = join(root, name);\n if (existsSync(configPath)) {\n try {\n const mod = req(configPath);\n return (mod.default ?? mod) as TimberUserConfig;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const hint = configLoadHint(name, err);\n throw new Error(\n `[timber] Failed to load ${configPath}: ${message}${hint ? `\\n${hint}` : ''}`,\n { cause: err }\n );\n }\n }\n }\n return null;\n}\n\n/**\n * Detect config keys set in both inline (vite.config.ts) and file (timber.config.ts)\n * and warn the user. The `output` key is excluded because it defaults to 'server'\n * in createPluginContext and would always appear as an inline key.\n *\n * Returns the list of conflicting key names (for testing).\n */\nexport function warnConfigConflicts(\n inline: TimberUserConfig,\n fileConfig: TimberUserConfig\n): string[] {\n const conflicts: string[] = [];\n for (const key of Object.keys(fileConfig) as (keyof TimberUserConfig)[]) {\n if (key in inline && inline[key] !== undefined) {\n conflicts.push(key);\n }\n }\n if (conflicts.length > 0) {\n console.warn(\n `[timber] Config conflict: ${conflicts.map((k) => `\"${k}\"`).join(', ')} set in both ` +\n `vite.config.ts (inline) and timber.config.ts. ` +\n `Move all config to timber.config.ts to avoid confusion. ` +\n `The inline value from vite.config.ts will be used.`\n );\n }\n return conflicts;\n}\n\n/**\n * Drop own keys whose value is `undefined` so they don't clobber file\n * config during spread merging. `timber({ buildDir: process.env.BUILD_DIR })`\n * with the var unset must not erase the timber.config.ts value.\n */\nfunction stripUndefined<T extends object>(obj: T): T {\n return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as T;\n}\n\n/**\n * Merge file-based config (timber.config.ts) with inline config (vite.config.ts).\n * Inline config takes precedence — file config only fills in missing fields.\n * Inline keys explicitly set to `undefined` are treated as absent, matching\n * the `!== undefined` check in warnConfigConflicts.\n */\nexport function mergeFileConfig(\n inline: TimberUserConfig,\n fileConfig: TimberUserConfig\n): TimberUserConfig {\n warnConfigConflicts(inline, fileConfig);\n const definedInline = stripUndefined(inline);\n\n return {\n ...fileConfig,\n ...definedInline,\n ...(fileConfig.limits && definedInline.limits\n ? { limits: { ...fileConfig.limits, ...stripUndefined(definedInline.limits) } }\n : {}),\n ...(fileConfig.dev && definedInline.dev\n ? { dev: { ...fileConfig.dev, ...stripUndefined(definedInline.dev) } }\n : {}),\n ...(fileConfig.mdx && definedInline.mdx\n ? { mdx: { ...fileConfig.mdx, ...stripUndefined(definedInline.mdx) } }\n : {}),\n };\n}\n"],"mappings":"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"}
|