@timber-js/app 0.2.0-alpha.8 → 0.2.0-alpha.81
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/LICENSE +8 -0
- package/dist/_chunks/actions-Dg-ANYHb.js +421 -0
- package/dist/_chunks/actions-Dg-ANYHb.js.map +1 -0
- package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-HS0LGUl2.js} +1 -1
- package/dist/_chunks/als-registry-HS0LGUl2.js.map +1 -0
- package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
- package/dist/_chunks/{debug-gwlJkDuf.js → debug-ECi_61pb.js} +2 -2
- package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
- package/dist/_chunks/define-C77ScO0m.js +106 -0
- package/dist/_chunks/define-C77ScO0m.js.map +1 -0
- package/dist/_chunks/define-CZqDwhSu.js +199 -0
- package/dist/_chunks/define-CZqDwhSu.js.map +1 -0
- package/dist/_chunks/define-cookie-C2IkoFGN.js +94 -0
- package/dist/_chunks/define-cookie-C2IkoFGN.js.map +1 -0
- package/dist/_chunks/{format-DviM89f0.js → dev-warnings-DpGRGoDi.js} +5 -44
- package/dist/_chunks/dev-warnings-DpGRGoDi.js.map +1 -0
- package/dist/_chunks/format-CYBGxKtc.js +14 -0
- package/dist/_chunks/format-CYBGxKtc.js.map +1 -0
- package/dist/_chunks/{interception-BOoWmLUA.js → interception-Dpn_UfAD.js} +171 -97
- package/dist/_chunks/interception-Dpn_UfAD.js.map +1 -0
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js +41 -0
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js.map +1 -0
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-DS3eKNmf.js} +1 -1
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-DS3eKNmf.js.map} +1 -1
- package/dist/_chunks/request-context-qMsWgy9C.js +478 -0
- package/dist/_chunks/request-context-qMsWgy9C.js.map +1 -0
- package/dist/_chunks/schema-bridge-C3xl_vfb.js +86 -0
- package/dist/_chunks/schema-bridge-C3xl_vfb.js.map +1 -0
- package/dist/_chunks/segment-classify-BDNn6EzD.js +65 -0
- package/dist/_chunks/segment-classify-BDNn6EzD.js.map +1 -0
- package/dist/_chunks/segment-context-fHFLF1PE.js +34 -0
- package/dist/_chunks/segment-context-fHFLF1PE.js.map +1 -0
- package/dist/_chunks/{ssr-data-MjmprTmO.js → ssr-data-DzuI0bIV.js} +1 -1
- package/dist/_chunks/{ssr-data-MjmprTmO.js.map → ssr-data-DzuI0bIV.js.map} +1 -1
- package/dist/_chunks/stale-reload-BX5gL1r-.js +64 -0
- package/dist/_chunks/stale-reload-BX5gL1r-.js.map +1 -0
- package/dist/_chunks/{tracing-CemImE6h.js → tracing-CCYbKn5n.js} +60 -9
- package/dist/_chunks/tracing-CCYbKn5n.js.map +1 -0
- package/dist/_chunks/use-params-Br9YSUFV.js +295 -0
- package/dist/_chunks/use-params-Br9YSUFV.js.map +1 -0
- package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-Lo_s_pw2.js} +4 -4
- package/dist/_chunks/use-query-states-Lo_s_pw2.js.map +1 -0
- package/dist/adapters/cloudflare-dev.d.ts +109 -0
- package/dist/adapters/cloudflare-dev.d.ts.map +1 -0
- package/dist/adapters/cloudflare-dev.js +73 -0
- package/dist/adapters/cloudflare-dev.js.map +1 -0
- package/dist/adapters/cloudflare-kv-cache.d.ts +64 -0
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -0
- package/dist/adapters/cloudflare-kv-cache.js +95 -0
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +148 -12
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +135 -11
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +17 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +56 -13
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/cache-api.d.ts +24 -0
- package/dist/cache/cache-api.d.ts.map +1 -0
- package/dist/cache/handler-store.d.ts +31 -0
- package/dist/cache/handler-store.d.ts.map +1 -0
- package/dist/cache/index.d.ts +23 -7
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +142 -80
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/singleflight.d.ts +18 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/sizeof.d.ts +22 -0
- package/dist/cache/sizeof.d.ts.map +1 -0
- package/dist/cache/timber-cache.d.ts +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cli.d.ts +6 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -3
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-dev.d.ts +27 -1
- package/dist/client/browser-dev.d.ts.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts +17 -0
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -0
- package/dist/client/browser-entry/hmr.d.ts +21 -0
- package/dist/client/browser-entry/hmr.d.ts.map +1 -0
- package/dist/client/browser-entry/hydrate.d.ts +46 -0
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -0
- package/dist/client/browser-entry/index.d.ts +30 -0
- package/dist/client/browser-entry/index.d.ts.map +1 -0
- package/dist/client/browser-entry/post-hydration.d.ts +26 -0
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -0
- package/dist/client/browser-entry/router-init.d.ts +23 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -0
- package/dist/client/browser-entry/rsc-stream.d.ts +24 -0
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -0
- package/dist/client/browser-entry/scroll.d.ts +19 -0
- package/dist/client/browser-entry/scroll.d.ts.map +1 -0
- package/dist/client/error-boundary.d.ts +12 -5
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +10 -4
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/error-reconstituter.d.ts +54 -0
- package/dist/client/error-reconstituter.d.ts.map +1 -0
- package/dist/client/form.d.ts +3 -3
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/history.d.ts +19 -4
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.d.ts +7 -21
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +210 -1017
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +18 -0
- package/dist/client/internal.d.ts.map +1 -0
- package/dist/client/internal.js +890 -0
- package/dist/client/internal.js.map +1 -0
- package/dist/client/link-pending-store.d.ts +63 -0
- package/dist/client/link-pending-store.d.ts.map +1 -0
- package/dist/client/link.d.ts +90 -32
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/nav-link-store.d.ts +36 -0
- package/dist/client/nav-link-store.d.ts.map +1 -0
- package/dist/client/navigation-api-types.d.ts +90 -0
- package/dist/client/navigation-api-types.d.ts.map +1 -0
- package/dist/client/navigation-api.d.ts +115 -0
- package/dist/client/navigation-api.d.ts.map +1 -0
- package/dist/client/navigation-context.d.ts +13 -2
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/{transition-root.d.ts → navigation-root.d.ts} +42 -8
- package/dist/client/navigation-root.d.ts.map +1 -0
- package/dist/client/nuqs-adapter.d.ts.map +1 -1
- package/dist/client/router-ref.d.ts +1 -1
- package/dist/client/router.d.ts +70 -4
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +38 -3
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +1 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-outlet.d.ts +63 -0
- package/dist/client/segment-outlet.d.ts.map +1 -0
- package/dist/client/ssr-data.d.ts +13 -4
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/stale-reload.d.ts +15 -0
- package/dist/client/stale-reload.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +5 -5
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/use-link-status.d.ts +5 -5
- package/dist/client/use-link-status.d.ts.map +1 -1
- package/dist/client/use-params.d.ts +6 -4
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/{use-navigation-pending.d.ts → use-pending-navigation.d.ts} +4 -4
- package/dist/client/use-pending-navigation.d.ts.map +1 -0
- package/dist/client/use-query-states.d.ts +1 -1
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/client/use-router.d.ts +1 -1
- package/dist/codec.d.ts +33 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +2 -0
- package/dist/config-types.d.ts +227 -0
- package/dist/config-types.d.ts.map +1 -0
- package/dist/content/index.d.ts +1 -10
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +0 -2
- package/dist/cookies/define-cookie.d.ts +35 -14
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -83
- package/dist/fonts/css.d.ts +1 -0
- package/dist/fonts/css.d.ts.map +1 -1
- package/dist/index.d.ts +45 -192
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12357 -11925
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +107 -0
- package/dist/plugin-context.d.ts.map +1 -0
- package/dist/plugins/adapter-build.d.ts +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts +2 -2
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +3 -3
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/client-chunks.d.ts +32 -0
- package/dist/plugins/client-chunks.d.ts.map +1 -0
- package/dist/plugins/content.d.ts +1 -1
- package/dist/plugins/content.d.ts.map +1 -1
- package/dist/plugins/dev-browser-logs.d.ts +84 -0
- package/dist/plugins/dev-browser-logs.d.ts.map +1 -0
- package/dist/plugins/dev-error-overlay.d.ts +26 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
- package/dist/plugins/dev-logs.d.ts +1 -1
- package/dist/plugins/dev-logs.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +19 -5
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/mdx.d.ts +1 -1
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts +6 -5
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +4 -4
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts +2 -2
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/index.d.ts +2 -0
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/index.js +3 -2
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +46 -0
- package/dist/routing/segment-classify.d.ts.map +1 -0
- package/dist/routing/status-file-lint.d.ts +2 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/routing/types.d.ts +16 -4
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/rsc-runtime/rsc.d.ts +1 -1
- package/dist/rsc-runtime/rsc.d.ts.map +1 -1
- package/dist/rsc-runtime/ssr.d.ts +12 -0
- package/dist/rsc-runtime/ssr.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +76 -0
- package/dist/schema-bridge.d.ts.map +1 -0
- package/dist/search-params/define.d.ts +139 -0
- package/dist/search-params/define.d.ts.map +1 -0
- package/dist/search-params/index.d.ts +4 -7
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +32 -441
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/registry.d.ts +2 -2
- package/dist/search-params/registry.d.ts.map +1 -1
- package/dist/search-params/wrappers.d.ts +53 -0
- package/dist/search-params/wrappers.d.ts.map +1 -0
- package/dist/segment-params/define.d.ts +78 -0
- package/dist/segment-params/define.d.ts.map +1 -0
- package/dist/segment-params/index.d.ts +3 -0
- package/dist/segment-params/index.d.ts.map +1 -0
- package/dist/segment-params/index.js +2 -0
- package/dist/server/access-gate.d.ts +4 -0
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-client.d.ts +25 -6
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/action-encryption.d.ts +76 -0
- package/dist/server/action-encryption.d.ts.map +1 -0
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +3 -6
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +32 -4
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +2 -2
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/debug.d.ts +1 -1
- package/dist/server/default-logger.d.ts +22 -0
- package/dist/server/default-logger.d.ts.map +1 -0
- package/dist/server/deny-page-resolver.d.ts +52 -0
- package/dist/server/deny-page-resolver.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/dev-holding-server.d.ts +52 -0
- package/dist/server/dev-holding-server.d.ts.map +1 -0
- package/dist/server/dev-warnings.d.ts +1 -21
- package/dist/server/dev-warnings.d.ts.map +1 -1
- package/dist/server/early-hints.d.ts +13 -5
- package/dist/server/early-hints.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +7 -1
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/fallback-error.d.ts +4 -3
- package/dist/server/fallback-error.d.ts.map +1 -1
- package/dist/server/flight-injection-state.d.ts +66 -0
- package/dist/server/flight-injection-state.d.ts.map +1 -0
- package/dist/server/flight-scripts.d.ts +42 -0
- package/dist/server/flight-scripts.d.ts.map +1 -0
- package/dist/server/flush.d.ts.map +1 -1
- package/dist/server/form-data.d.ts +29 -0
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +51 -11
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +5 -43
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +37 -2798
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +46 -0
- package/dist/server/internal.d.ts.map +1 -0
- package/dist/server/internal.js +2883 -0
- package/dist/server/internal.js.map +1 -0
- package/dist/server/logger.d.ts +25 -7
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/middleware-runner.d.ts +19 -4
- package/dist/server/middleware-runner.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +113 -0
- package/dist/server/node-stream-transforms.d.ts.map +1 -0
- package/dist/server/page-deny-boundary.d.ts +31 -0
- package/dist/server/page-deny-boundary.d.ts.map +1 -0
- package/dist/server/pipeline-interception.d.ts +1 -1
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline-metadata.d.ts +6 -0
- package/dist/server/pipeline-metadata.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +42 -10
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +69 -18
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-timeout.d.ts +51 -0
- package/dist/server/render-timeout.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +112 -43
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +27 -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 +9 -2
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/api-handler.d.ts +2 -2
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +26 -13
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +48 -5
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +8 -3
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +3 -3
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +4 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-bridge.d.ts +1 -1
- package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +19 -4
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/safe-load.d.ts +46 -0
- package/dist/server/safe-load.d.ts.map +1 -0
- package/dist/server/sitemap-generator.d.ts +129 -0
- package/dist/server/sitemap-generator.d.ts.map +1 -0
- package/dist/server/sitemap-handler.d.ts +22 -0
- package/dist/server/sitemap-handler.d.ts.map +1 -0
- package/dist/server/slot-resolver.d.ts +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +22 -0
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +39 -21
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +50 -0
- package/dist/server/ssr-wrappers.d.ts.map +1 -0
- package/dist/server/status-code-resolver.d.ts +1 -1
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/stream-utils.d.ts +36 -0
- package/dist/server/stream-utils.d.ts.map +1 -0
- package/dist/server/tracing.d.ts +4 -4
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +22 -19
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -4
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/version-skew.d.ts +61 -0
- package/dist/server/version-skew.d.ts.map +1 -0
- package/dist/shared/merge-search-params.d.ts +22 -0
- package/dist/shared/merge-search-params.d.ts.map +1 -0
- package/dist/shims/font-google.d.ts +1 -1
- package/dist/shims/font-google.d.ts.map +1 -1
- package/dist/shims/font-google.js +42 -0
- package/dist/shims/font-google.js.map +1 -0
- package/dist/shims/font-local.d.ts +26 -0
- package/dist/shims/font-local.d.ts.map +1 -0
- package/dist/shims/font-local.js +20 -0
- package/dist/shims/font-local.js.map +1 -0
- package/dist/shims/headers.d.ts +2 -1
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/navigation-client.d.ts +1 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +3 -2
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/utils/directive-parser.d.ts +5 -2
- package/dist/utils/directive-parser.d.ts.map +1 -1
- package/dist/utils/state-machine.d.ts +80 -0
- package/dist/utils/state-machine.d.ts.map +1 -0
- package/package.json +56 -22
- package/src/adapters/cloudflare-dev.ts +177 -0
- package/src/adapters/cloudflare-kv-cache.ts +142 -0
- package/src/adapters/cloudflare.ts +342 -28
- package/src/adapters/compress-module.ts +24 -4
- package/src/adapters/nitro.ts +52 -8
- package/src/adapters/wrangler.d.ts +7 -0
- package/src/cache/cache-api.ts +38 -0
- package/src/cache/handler-store.ts +68 -0
- package/src/cache/index.ts +81 -18
- package/src/cache/singleflight.ts +62 -4
- package/src/cache/sizeof.ts +31 -0
- package/src/cache/timber-cache.ts +24 -20
- package/src/cli.ts +16 -6
- package/src/client/browser-dev.ts +128 -1
- package/src/client/browser-entry/action-dispatch.ts +116 -0
- package/src/client/browser-entry/hmr.ts +81 -0
- package/src/client/browser-entry/hydrate.ts +145 -0
- package/src/client/browser-entry/index.ts +138 -0
- package/src/client/browser-entry/post-hydration.ts +119 -0
- package/src/client/browser-entry/router-init.ts +193 -0
- package/src/client/browser-entry/rsc-stream.ts +157 -0
- package/src/client/browser-entry/scroll.ts +27 -0
- package/src/client/error-boundary.tsx +48 -16
- package/src/client/error-reconstituter.tsx +65 -0
- package/src/client/form.tsx +9 -7
- package/src/client/history.ts +26 -4
- package/src/client/index.ts +19 -38
- package/src/client/internal.ts +57 -0
- package/src/client/link-pending-store.ts +111 -0
- package/src/client/link.tsx +329 -97
- package/src/client/nav-link-store.ts +47 -0
- package/src/client/navigation-api-types.ts +112 -0
- package/src/client/navigation-api.ts +332 -0
- package/src/client/navigation-context.ts +31 -6
- package/src/client/navigation-root.tsx +342 -0
- package/src/client/nuqs-adapter.tsx +16 -3
- package/src/client/router-ref.ts +1 -1
- package/src/client/router.ts +299 -72
- package/src/client/rsc-fetch.ts +97 -8
- package/src/client/segment-cache.ts +1 -1
- package/src/client/segment-outlet.tsx +86 -0
- package/src/client/ssr-data.ts +13 -5
- package/src/client/stale-reload.ts +72 -3
- package/src/client/top-loader.tsx +16 -8
- package/src/client/use-link-status.ts +7 -7
- package/src/client/use-params.ts +7 -5
- package/src/client/{use-navigation-pending.ts → use-pending-navigation.ts} +6 -6
- package/src/client/use-query-states.ts +3 -3
- package/src/client/use-router.ts +1 -1
- package/src/codec.ts +49 -0
- package/src/config-types.ts +225 -0
- package/src/content/index.ts +5 -13
- package/src/cookies/define-cookie.ts +78 -25
- package/src/cookies/index.ts +8 -0
- package/src/fonts/css.ts +2 -1
- package/src/index.ts +295 -354
- package/src/plugin-context.ts +240 -0
- package/src/plugins/adapter-build.ts +9 -3
- package/src/plugins/build-manifest.ts +13 -2
- package/src/plugins/build-report.ts +3 -3
- package/src/plugins/client-chunks.ts +65 -0
- package/src/plugins/content.ts +1 -1
- package/src/plugins/dev-browser-logs.ts +288 -0
- package/src/plugins/dev-error-overlay.ts +70 -1
- package/src/plugins/dev-logs.ts +1 -1
- package/src/plugins/dev-server.ts +70 -9
- package/src/plugins/entries.ts +71 -10
- package/src/plugins/fonts.ts +168 -61
- package/src/plugins/mdx.ts +1 -1
- package/src/plugins/routing.ts +57 -17
- package/src/plugins/server-action-exports.ts +1 -1
- package/src/plugins/server-bundle.ts +32 -1
- package/src/plugins/shims.ts +135 -35
- package/src/plugins/static-build.ts +17 -11
- package/src/routing/codegen.ts +165 -105
- package/src/routing/index.ts +2 -0
- package/src/routing/scanner.ts +93 -23
- package/src/routing/segment-classify.ts +89 -0
- package/src/routing/status-file-lint.ts +3 -2
- package/src/routing/types.ts +17 -4
- package/src/rsc-runtime/rsc.ts +2 -0
- package/src/rsc-runtime/ssr.ts +50 -0
- package/src/rsc-runtime/vendor-types.d.ts +7 -0
- package/src/{search-params/codecs.ts → schema-bridge.ts} +57 -20
- package/src/search-params/define.ts +482 -0
- package/src/search-params/index.ts +14 -20
- package/src/search-params/registry.ts +2 -2
- package/src/search-params/wrappers.ts +85 -0
- package/src/segment-params/define.ts +279 -0
- package/src/segment-params/index.ts +9 -0
- package/src/server/access-gate.tsx +70 -29
- package/src/server/action-client.ts +46 -11
- package/src/server/action-encryption.ts +144 -0
- package/src/server/action-handler.ts +21 -4
- package/src/server/actions.ts +10 -9
- package/src/server/als-registry.ts +34 -6
- package/src/server/build-manifest.ts +10 -4
- package/src/server/compress.ts +25 -7
- package/src/server/debug.ts +1 -1
- package/src/server/default-logger.ts +99 -0
- package/src/server/deny-page-resolver.ts +154 -0
- package/src/server/deny-renderer.ts +24 -38
- package/src/server/dev-holding-server.ts +185 -0
- package/src/server/dev-warnings.ts +4 -49
- package/src/server/early-hints.ts +36 -15
- package/src/server/error-boundary-wrapper.ts +74 -22
- package/src/server/fallback-error.ts +31 -15
- package/src/server/flight-injection-state.ts +113 -0
- package/src/server/flight-scripts.ts +62 -0
- package/src/server/flush.ts +2 -1
- package/src/server/form-data.ts +76 -0
- package/src/server/html-injectors.ts +280 -120
- package/src/server/index.ts +25 -177
- package/src/server/internal.ts +169 -0
- package/src/server/logger.ts +44 -36
- package/src/server/middleware-runner.ts +31 -4
- package/src/server/node-stream-transforms.ts +509 -0
- package/src/server/page-deny-boundary.tsx +56 -0
- package/src/server/pipeline-interception.ts +17 -16
- package/src/server/pipeline-metadata.ts +13 -0
- package/src/server/pipeline.ts +227 -62
- package/src/server/primitives.ts +111 -28
- package/src/server/render-timeout.ts +108 -0
- package/src/server/request-context.ts +293 -132
- package/src/server/route-element-builder.ts +283 -191
- package/src/server/route-handler.ts +24 -4
- package/src/server/route-matcher.ts +24 -20
- package/src/server/rsc-entry/api-handler.ts +15 -16
- package/src/server/rsc-entry/error-renderer.ts +300 -89
- package/src/server/rsc-entry/helpers.ts +134 -5
- package/src/server/rsc-entry/index.ts +200 -112
- package/src/server/rsc-entry/rsc-payload.ts +65 -18
- package/src/server/rsc-entry/rsc-stream.ts +65 -13
- package/src/server/rsc-entry/ssr-bridge.ts +14 -5
- package/src/server/rsc-entry/ssr-renderer.ts +168 -38
- package/src/server/safe-load.ts +60 -0
- package/src/server/sitemap-generator.ts +338 -0
- package/src/server/sitemap-handler.ts +126 -0
- package/src/server/slot-resolver.ts +244 -229
- package/src/server/ssr-entry.ts +211 -32
- package/src/server/ssr-render.ts +289 -67
- package/src/server/ssr-wrappers.tsx +139 -0
- package/src/server/status-code-resolver.ts +1 -1
- package/src/server/stream-utils.ts +213 -0
- package/src/server/tracing.ts +20 -9
- package/src/server/tree-builder.ts +92 -58
- package/src/server/types.ts +3 -6
- package/src/server/version-skew.ts +104 -0
- package/src/shared/merge-search-params.ts +55 -0
- package/src/shims/font-google.ts +1 -1
- package/src/shims/font-local.ts +34 -0
- package/src/shims/headers.ts +5 -1
- package/src/shims/navigation-client.ts +1 -1
- package/src/shims/navigation.ts +7 -2
- package/src/utils/directive-parser.ts +5 -2
- package/src/utils/state-machine.ts +111 -0
- package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
- package/dist/_chunks/debug-gwlJkDuf.js.map +0 -1
- package/dist/_chunks/format-DviM89f0.js.map +0 -1
- package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
- package/dist/_chunks/request-context-DIkVh_jG.js +0 -330
- package/dist/_chunks/request-context-DIkVh_jG.js.map +0 -1
- package/dist/_chunks/tracing-CemImE6h.js.map +0 -1
- package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
- package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
- package/dist/_chunks/use-query-states-D5KaffOK.js.map +0 -1
- package/dist/cache/register-cached-function.d.ts +0 -17
- package/dist/cache/register-cached-function.d.ts.map +0 -1
- package/dist/client/browser-entry.d.ts +0 -21
- package/dist/client/browser-entry.d.ts.map +0 -1
- package/dist/client/link-status-provider.d.ts +0 -11
- package/dist/client/link-status-provider.d.ts.map +0 -1
- package/dist/client/transition-root.d.ts.map +0 -1
- package/dist/client/use-navigation-pending.d.ts.map +0 -1
- package/dist/cookies/index.js.map +0 -1
- package/dist/plugins/cache-transform.d.ts +0 -36
- package/dist/plugins/cache-transform.d.ts.map +0 -1
- package/dist/plugins/dynamic-transform.d.ts +0 -72
- package/dist/plugins/dynamic-transform.d.ts.map +0 -1
- package/dist/search-params/analyze.d.ts +0 -54
- package/dist/search-params/analyze.d.ts.map +0 -1
- package/dist/search-params/builtin-codecs.d.ts +0 -105
- package/dist/search-params/builtin-codecs.d.ts.map +0 -1
- package/dist/search-params/codecs.d.ts +0 -53
- package/dist/search-params/codecs.d.ts.map +0 -1
- package/dist/search-params/create.d.ts +0 -106
- package/dist/search-params/create.d.ts.map +0 -1
- package/dist/server/prerender.d.ts +0 -77
- package/dist/server/prerender.d.ts.map +0 -1
- package/dist/server/response-cache.d.ts +0 -54
- package/dist/server/response-cache.d.ts.map +0 -1
- package/src/cache/register-cached-function.ts +0 -103
- package/src/client/browser-entry.ts +0 -678
- package/src/client/link-status-provider.tsx +0 -30
- package/src/client/transition-root.tsx +0 -166
- package/src/plugins/cache-transform.ts +0 -199
- package/src/plugins/dynamic-transform.ts +0 -161
- package/src/search-params/analyze.ts +0 -192
- package/src/search-params/builtin-codecs.ts +0 -228
- package/src/search-params/create.ts +0 -321
- package/src/server/prerender.ts +0 -139
- package/src/server/response-cache.ts +0 -410
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudflare.js","names":[],"sources":["../../src/adapters/cloudflare.ts"],"sourcesContent":["// Cloudflare Workers adapter\n//\n// Primary deployment target. Generates a Workers-compatible entry point\n// and wrangler.jsonc configuration. See design/11-platform.md §\"Cloudflare Workers\".\n\nimport { writeFile, mkdir, cp } from 'node:fs/promises';\nimport { execFile } from 'node:child_process';\nimport { join, relative } from 'node:path';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { TimberPlatformAdapter, TimberConfig } from './types';\n// Inlined from server/asset-headers.ts — adapters are loaded by Node at\n// Vite startup time, before Vite's module resolver is available, so cross-\n// directory .ts imports don't resolve.\nconst IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';\nconst STATIC_CACHE = 'public, max-age=3600, must-revalidate';\n\nfunction generateHeadersFile(): string {\n return `# Auto-generated by @timber-js/app — static asset cache headers.\n# See design/25-production-deployments.md §\"CDN / Edge Cache\"\n\n/assets/*\n Cache-Control: ${IMMUTABLE_CACHE}\n\n/*\n Cache-Control: ${STATIC_CACHE}\n`;\n}\n\n// ─── Bindings passthrough ─────────────────────────────────────────────────\n// ALS stores the env object per-request so server components and middleware\n// can access KV, D1, DO, R2, Queues, etc. via getCloudflareBindings().\n// No global fallback — if called outside a request, it throws.\n// See design/11-platform.md §\"Platform Target\" and design/25-production-deployments.md.\n\nconst bindingsAls = new AsyncLocalStorage<Record<string, unknown>>();\n\n/**\n * Get Cloudflare Worker bindings for the current request.\n *\n * Returns the `env` object passed to the Worker's `fetch` handler,\n * giving direct access to KV, D1, Durable Objects, R2, Queues, and\n * any other bindings configured in `wrangler.jsonc`.\n *\n * Must be called within a request context (server component, middleware,\n * server action). Throws outside a request.\n *\n * @example\n * ```ts\n * import { getCloudflareBindings } from '@timber-js/app/adapters/cloudflare'\n *\n * export default async function Page() {\n * const { MY_KV, MY_DB } = getCloudflareBindings()\n * const data = await MY_KV.get('key')\n * return <div>{data}</div>\n * }\n * ```\n */\nexport function getCloudflareBindings<\n T extends Record<string, unknown> = Record<string, unknown>,\n>(): T {\n const env = bindingsAls.getStore();\n if (!env) {\n throw new Error(\n 'getCloudflareBindings() called outside a Cloudflare Workers request context. ' +\n 'It can only be called from server components, middleware, or server actions ' +\n 'when running on the Cloudflare adapter.'\n );\n }\n return env as T;\n}\n\n/**\n * Run a function with Cloudflare bindings available via getCloudflareBindings().\n * @internal Used by wrapWithExecutionContext.\n */\nexport function runWithBindings<T>(env: Record<string, unknown>, fn: () => T): T {\n return bindingsAls.run(env, fn);\n}\n\n/** Options for the Cloudflare Workers adapter. */\nexport interface CloudflareAdapterOptions {\n /**\n * Cloudflare compatibility date.\n * @default Current date in YYYY-MM-DD format at build time.\n */\n compatibilityDate?: string;\n\n /**\n * Additional compatibility flags.\n * @default ['nodejs_compat']\n */\n compatibilityFlags?: string[];\n\n /**\n * Custom wrangler.jsonc fields to merge.\n * Overrides generated values.\n */\n wrangler?: Record<string, unknown>;\n}\n\n/**\n * Create a Cloudflare Workers adapter.\n *\n * @example\n * ```ts\n * import { cloudflare } from '@timber-js/app/adapters/cloudflare'\n *\n * export default {\n * output: 'server',\n * adapter: cloudflare(),\n * }\n * ```\n */\nexport function cloudflare(options: CloudflareAdapterOptions = {}): TimberPlatformAdapter {\n return {\n name: 'cloudflare',\n\n async buildOutput(config: TimberConfig, buildDir: string) {\n const outDir = join(buildDir, 'cloudflare');\n await mkdir(outDir, { recursive: true });\n\n // Copy client assets to static output.\n // When client JavaScript is disabled, skip .js files — only CSS,\n // fonts, images, and other static assets are needed.\n const clientDir = join(buildDir, 'client');\n const staticDir = join(outDir, 'static');\n await mkdir(staticDir, { recursive: true });\n await cp(clientDir, staticDir, {\n recursive: true,\n filter: config.clientJavascriptDisabled ? (src: string) => !src.endsWith('.js') : undefined,\n }).catch(() => {\n // Client dir may not exist when client JavaScript is disabled\n });\n\n // Write _headers file for static asset cache control.\n // Cloudflare Workers Static Assets reads this to set Cache-Control\n // headers on responses. Hashed assets get immutable; others get 1h.\n await writeFile(join(staticDir, '_headers'), generateHeadersFile());\n\n // Copy server bundles (rsc + ssr) into the output directory.\n // These are already fully bundled by Vite with resolve.noExternal: true.\n const rscDir = join(buildDir, 'rsc');\n const ssrDir = join(buildDir, 'ssr');\n await cp(rscDir, join(outDir, 'rsc'), { recursive: true });\n await cp(ssrDir, join(outDir, 'ssr'), { recursive: true });\n\n // Write the build manifest init module (if manifest data was produced).\n // This must be imported before the RSC handler so the global is set\n // when virtual:timber-build-manifest evaluates.\n if (config.manifestInit) {\n await writeFile(join(outDir, '_timber-manifest-init.js'), config.manifestInit);\n }\n\n // Generate the Workers entry point\n const hasManifestInit = !!config.manifestInit;\n const workerEntry = generateWorkerEntry(outDir, outDir, hasManifestInit);\n await writeFile(join(outDir, '_worker.js'), workerEntry);\n\n // Generate wrangler.jsonc\n const wranglerConfig = generateWranglerConfig(config, options);\n await writeFile(join(outDir, 'wrangler.jsonc'), JSON.stringify(wranglerConfig, null, 2));\n },\n\n async preview(_config: TimberConfig, buildDir: string) {\n const cmd = generatePreviewCommand(buildDir);\n await spawnPreviewProcess(cmd.command, cmd.args, cmd.cwd);\n },\n\n // Default no-op. wrapWithExecutionContext() replaces this per-request\n // with a function that routes to ctx.waitUntil().\n waitUntil(_promise: Promise<unknown>) {},\n };\n}\n\n/**\n * Wrap a timber request handler to bind the Cloudflare execution context\n * for `waitUntil()` support and env bindings passthrough.\n * Called from the generated worker entry.\n *\n * This function:\n * 1. Binds `adapter.waitUntil()` to `ctx.waitUntil()` per-request\n * 2. Makes `env` accessible via `getCloudflareBindings()` per-request via ALS\n */\nexport function wrapWithExecutionContext(\n adapter: TimberPlatformAdapter,\n handler: (req: Request) => Promise<Response>\n): ExportedHandler<Record<string, unknown>> {\n return {\n async fetch(\n request: Request,\n env: Record<string, unknown>,\n ctx: ExecutionContext\n ): Promise<Response> {\n // Bind the adapter's waitUntil to the Workers execution context\n const originalWaitUntil = adapter.waitUntil;\n adapter.waitUntil = (promise: Promise<unknown>) => {\n ctx.waitUntil(promise);\n };\n\n try {\n // Run the handler within ALS so getCloudflareBindings() works\n return await runWithBindings(env, () => handler(request));\n } finally {\n // Restore (in case adapter is reused across isolate resets)\n adapter.waitUntil = originalWaitUntil;\n }\n },\n };\n}\n\n// ─── Exported helpers (used by tests and build) ─────────────────────────────\n\n/** @internal Exported for testing. */\nexport function generateWorkerEntry(\n buildDir: string,\n outDir: string,\n hasManifestInit = false\n): string {\n // The RSC entry is the main request handler — it exports the fetch handler as default.\n // The Vite RSC plugin outputs it to rsc/index.js.\n let rscEntryRelative = relative(outDir, join(buildDir, 'rsc', 'index.js'));\n // Ensure the import path starts with ./ for ESM compatibility\n if (!rscEntryRelative.startsWith('.')) {\n rscEntryRelative = './' + rscEntryRelative;\n }\n\n // Build manifest init must be imported before the RSC handler so that\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set when the virtual module evaluates.\n // ESM guarantees imports are evaluated in order.\n const manifestImport = hasManifestInit ? \"import './_timber-manifest-init.js'\\n\" : '';\n\n return `// Generated by @timber-js/app/adapters/cloudflare\n// Do not edit — this file is regenerated on each build.\n\n${manifestImport}import handler from '${rscEntryRelative}'\n\n// Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.\n// See design/25-production-deployments.md §\"TIMBER_RUNTIME\".\nglobalThis.process ??= { env: {} }\nprocess.env.TIMBER_RUNTIME = 'cloudflare'\n\nexport default { fetch: handler }\n`;\n}\n\n/** @internal Exported for testing. */\nexport function generateWranglerConfig(\n config: TimberConfig,\n options: CloudflareAdapterOptions\n): Record<string, unknown> {\n const compatDate = options.compatibilityDate ?? new Date().toISOString().slice(0, 10);\n\n const flags = options.compatibilityFlags ?? ['nodejs_compat'];\n\n const base: Record<string, unknown> = {\n name: 'timber-app',\n main: '_worker.js',\n compatibility_date: compatDate,\n compatibility_flags: flags,\n // The build output is already fully bundled by Vite — skip wrangler's\n // esbuild pass to avoid issues with top-level await and module format.\n no_bundle: true,\n find_additional_modules: true,\n rules: [{ type: 'ESModule', globs: ['**/*.js'] }],\n assets: {\n directory: './static',\n },\n };\n\n // Merge user overrides\n if (options.wrangler) {\n return { ...base, ...options.wrangler };\n }\n\n return base;\n}\n\n// ─── Preview ─────────────────────────────────────────────────────────────────\n\n/** Command descriptor for preview — testable without spawning a process. */\nexport interface PreviewCommand {\n command: string;\n args: string[];\n cwd: string;\n}\n\n/** @internal Exported for testing. */\nexport function generatePreviewCommand(buildDir: string): PreviewCommand {\n const cfDir = join(buildDir, 'cloudflare');\n return {\n command: 'wrangler',\n args: ['dev', '--local', '--config', join(cfDir, 'wrangler.jsonc')],\n cwd: cfDir,\n };\n}\n\n/**\n * Spawn a long-running preview process and pipe stdio to the parent.\n * Resolves when the process exits.\n */\nfunction spawnPreviewProcess(command: string, args: string[], cwd: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const child = execFile(command, args, { cwd }, (err) => {\n if (err) reject(err);\n else resolve();\n });\n child.stdout?.pipe(process.stdout);\n child.stderr?.pipe(process.stderr);\n });\n}\n\n// ─── Cloudflare Workers type stubs ───────────────────────────────────────────\n// Minimal type declarations so this file compiles without @cloudflare/workers-types.\n// In production builds, users install @cloudflare/workers-types themselves.\n\ndeclare global {\n interface ExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n passThroughOnException(): void;\n }\n\n interface ExportedHandler<Env = Record<string, unknown>> {\n fetch?(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> | Response;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cloudflare.js","names":[],"sources":["../../src/adapters/cloudflare.ts"],"sourcesContent":["// Cloudflare Workers adapter\n//\n// Primary deployment target. Generates a Workers-compatible entry point\n// and wrangler.jsonc configuration. See design/11-platform.md §\"Cloudflare Workers\".\n\nimport { writeFile, mkdir, cp } from 'node:fs/promises';\nimport { execFile } from 'node:child_process';\nimport { join, relative } from 'node:path';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { TimberPlatformAdapter, TimberConfig } from './types';\n// Inlined from server/asset-headers.ts — adapters are loaded by Node at\n// Vite startup time, before Vite's module resolver is available, so cross-\n// directory .ts imports don't resolve.\nconst IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';\nconst STATIC_CACHE = 'public, max-age=3600, must-revalidate';\n\nfunction generateHeadersFile(): string {\n return `# Auto-generated by @timber-js/app — static asset cache headers.\n# See design/25-production-deployments.md §\"CDN / Edge Cache\"\n\n/assets/*\n Cache-Control: ${IMMUTABLE_CACHE}\n\n/*\n Cache-Control: ${STATIC_CACHE}\n`;\n}\n\n// ─── Bindings passthrough ─────────────────────────────────────────────────\n// ALS stores the env object per-request so server components and middleware\n// can access KV, D1, DO, R2, Queues, etc. via getCloudflareBindings().\n// No global fallback — if called outside a request, it throws.\n// See design/11-platform.md §\"Platform Target\" and design/25-production-deployments.md.\n//\n// The ALS is stored on globalThis via Symbol.for so it survives Vite's\n// module instance split in dev mode. The Vite host process (where\n// cloudflareDevBindings → runWithBindings runs) and the RSC module runner\n// (where getCloudflareBindings runs) load separate instances of this file,\n// each with their own module-level variables. globalThis + Symbol.for\n// ensures both share the same ALS — same pattern React uses for\n// Symbol.for('react.element').\n\nconst BINDINGS_ALS_KEY = Symbol.for('timber:cf-bindings-als');\n\nfunction getBindingsAls(): AsyncLocalStorage<Record<string, unknown>> {\n let als = (globalThis as any)[BINDINGS_ALS_KEY] as\n | AsyncLocalStorage<Record<string, unknown>>\n | undefined;\n if (!als) {\n als = new AsyncLocalStorage<Record<string, unknown>>();\n (globalThis as any)[BINDINGS_ALS_KEY] = als;\n }\n return als;\n}\n\n/**\n * Get Cloudflare Worker bindings for the current request.\n *\n * Returns the `env` object passed to the Worker's `fetch` handler,\n * giving direct access to KV, D1, Durable Objects, R2, Queues, and\n * any other bindings configured in `wrangler.jsonc`.\n *\n * Must be called within a request context (server component, middleware,\n * server action). Throws outside a request.\n *\n * @example\n * ```ts\n * import { getCloudflareBindings } from '@timber-js/app/adapters/cloudflare'\n *\n * export default async function Page() {\n * const { MY_KV, MY_DB } = getCloudflareBindings()\n * const data = await MY_KV.get('key')\n * return <div>{data}</div>\n * }\n * ```\n */\nexport function getCloudflareBindings<T = Record<string, unknown>>(): T {\n const env = getBindingsAls().getStore();\n if (!env) {\n throw new Error(\n 'getCloudflareBindings() called outside a Cloudflare Workers request context. ' +\n 'It can only be called from server components, middleware, or server actions ' +\n 'when running on the Cloudflare adapter.'\n );\n }\n return env as T;\n}\n\n/**\n * Run a function with Cloudflare bindings available via getCloudflareBindings().\n * @internal Used by wrapWithExecutionContext.\n */\nexport function runWithBindings<T>(env: Record<string, unknown>, fn: () => T): T {\n return getBindingsAls().run(env, fn);\n}\n\n// ─── Binding configuration types ──────────────────────────────────────────────\n// Declarative binding config that maps to wrangler.jsonc binding sections.\n// See design/35-cloudflare-primitives.md §\"Binding Declarations in Adapter Config\".\n\n/** KV namespace binding. `id` defaults to `''` (filled in by wrangler or dashboard). */\nexport interface CloudflareKVBinding {\n name: string;\n id?: string;\n}\n\n/** D1 database binding. `database_id` is required (from Cloudflare dashboard). */\nexport interface CloudflareD1Binding {\n name: string;\n database_id: string;\n}\n\n/** R2 bucket binding. `bucket_name` must match the bucket created in Cloudflare. */\nexport interface CloudflareR2Binding {\n name: string;\n bucket_name: string;\n}\n\n/** Queue producer binding. `queue` is the queue name in Cloudflare. */\nexport interface CloudflareQueueProducer {\n name: string;\n queue: string;\n}\n\n/** Queue consumer declaration. Attached to the worker, not a binding name. */\nexport interface CloudflareQueueConsumer {\n queue: string;\n max_batch_size?: number;\n max_retries?: number;\n dead_letter_queue?: string;\n}\n\n/** Durable Object binding. `script_name` is for external DO references. */\nexport interface CloudflareDurableObjectBinding {\n name: string;\n class_name: string;\n script_name?: string;\n}\n\n/**\n * Declarative Cloudflare bindings configuration.\n *\n * These are converted to the appropriate wrangler.jsonc sections\n * (`kv_namespaces`, `d1_databases`, `r2_buckets`, `queues`, `durable_objects`).\n * The `wrangler` escape hatch overrides any generated binding sections\n * if there's a conflict.\n *\n * @example\n * ```ts\n * cloudflare({\n * bindings: {\n * kv: [{ name: 'TIMBER_CACHE' }],\n * d1: [{ name: 'MY_DB', database_id: 'xxxx' }],\n * r2: [{ name: 'MY_BUCKET', bucket_name: 'my-bucket' }],\n * queues: {\n * producers: [{ name: 'EMAIL_QUEUE', queue: 'email-queue' }],\n * consumers: [{ queue: 'email-queue', max_batch_size: 10 }],\n * },\n * durableObjects: [{ name: 'MY_DO', class_name: 'MyDurableObject' }],\n * },\n * })\n * ```\n */\nexport interface CloudflareBindings {\n kv?: CloudflareKVBinding[];\n d1?: CloudflareD1Binding[];\n r2?: CloudflareR2Binding[];\n queues?: {\n producers?: CloudflareQueueProducer[];\n consumers?: CloudflareQueueConsumer[];\n };\n durableObjects?: CloudflareDurableObjectBinding[];\n}\n\n/** Options for the Cloudflare Workers adapter. */\nexport interface CloudflareAdapterOptions {\n /**\n * Cloudflare compatibility date.\n * @default Current date in YYYY-MM-DD format at build time.\n */\n compatibilityDate?: string;\n\n /**\n * Additional compatibility flags.\n * @default ['nodejs_compat']\n */\n compatibilityFlags?: string[];\n\n /**\n * Declarative Cloudflare bindings. Generates the appropriate\n * `kv_namespaces`, `d1_databases`, `r2_buckets`, `queues`, and\n * `durable_objects` sections in wrangler.jsonc.\n *\n * If both `bindings` and `wrangler` specify the same section,\n * `wrangler` wins (it's the escape hatch).\n */\n bindings?: CloudflareBindings;\n\n /**\n * Custom wrangler.jsonc fields to merge.\n * Overrides generated values (including bindings-generated sections).\n */\n wrangler?: Record<string, unknown>;\n\n /**\n * Path to a module that exports additional Worker handlers (queue, scheduled, email, etc.).\n * The module is imported in the generated `_worker.js` and its named exports are\n * spread into the default export alongside `fetch`.\n *\n * The module receives `(batch/event, env, ctx)` — standard Cloudflare handler signatures.\n * It does NOT have access to timber's request pipeline (no ALS, no getCloudflareBindings).\n * Use `env` directly for bindings.\n *\n * @example\n * ```ts\n * // timber.config.ts\n * adapter: cloudflare({ workerHandlers: './src/worker-handlers.ts' })\n *\n * // src/worker-handlers.ts\n * export async function queue(batch, env) { ... }\n * export async function scheduled(controller, env, ctx) { ... }\n * ```\n */\n workerHandlers?: string;\n}\n\n/**\n * Create a Cloudflare Workers adapter.\n *\n * @example\n * ```ts\n * import { cloudflare } from '@timber-js/app/adapters/cloudflare'\n *\n * export default {\n * output: 'server',\n * adapter: cloudflare(),\n * }\n * ```\n */\nexport function cloudflare(options: CloudflareAdapterOptions = {}): TimberPlatformAdapter {\n return {\n name: 'cloudflare',\n\n async buildOutput(config: TimberConfig, buildDir: string) {\n const outDir = join(buildDir, 'cloudflare');\n await mkdir(outDir, { recursive: true });\n\n // Copy client assets to static output.\n // When client JavaScript is disabled, skip .js files — only CSS,\n // fonts, images, and other static assets are needed.\n const clientDir = join(buildDir, 'client');\n const staticDir = join(outDir, 'static');\n await mkdir(staticDir, { recursive: true });\n await cp(clientDir, staticDir, {\n recursive: true,\n filter: config.clientJavascriptDisabled ? (src: string) => !src.endsWith('.js') : undefined,\n }).catch(() => {\n // Client dir may not exist when client JavaScript is disabled\n });\n\n // Write _headers file for static asset cache control.\n // Cloudflare Workers Static Assets reads this to set Cache-Control\n // headers on responses. Hashed assets get immutable; others get 1h.\n await writeFile(join(staticDir, '_headers'), generateHeadersFile());\n\n // Copy server bundles (rsc + ssr) into the output directory.\n // These are already fully bundled by Vite with resolve.noExternal: true.\n const rscDir = join(buildDir, 'rsc');\n const ssrDir = join(buildDir, 'ssr');\n await cp(rscDir, join(outDir, 'rsc'), { recursive: true });\n await cp(ssrDir, join(outDir, 'ssr'), { recursive: true });\n\n // Write the build manifest init module (if manifest data was produced).\n // This must be imported before the RSC handler so the global is set\n // when virtual:timber-build-manifest evaluates.\n if (config.manifestInit) {\n await writeFile(join(outDir, '_timber-manifest-init.js'), config.manifestInit);\n }\n\n // Compile optional worker handlers (queue, scheduled, etc.)\n // Uses Vite's build API to bundle the TypeScript source into ESM.\n let hasWorkerHandlers = false;\n if (options.workerHandlers) {\n const handlersEntry = join(process.cwd(), options.workerHandlers);\n const { build: viteBuild } = await import('vite');\n await viteBuild({\n configFile: false,\n logLevel: 'info',\n build: {\n lib: {\n entry: handlersEntry,\n formats: ['es'],\n fileName: () => '_worker-handlers.js',\n },\n outDir,\n emptyOutDir: false,\n minify: false,\n rollupOptions: {\n external: [/^node:/],\n },\n },\n });\n hasWorkerHandlers = true;\n }\n\n // Generate the Workers entry point\n const hasManifestInit = !!config.manifestInit;\n const workerEntry = generateWorkerEntry(outDir, outDir, hasManifestInit, hasWorkerHandlers);\n await writeFile(join(outDir, '_worker.js'), workerEntry);\n\n // Generate wrangler.jsonc\n const wranglerConfig = generateWranglerConfig(config, options);\n await writeFile(join(outDir, 'wrangler.jsonc'), JSON.stringify(wranglerConfig, null, 2));\n },\n\n async preview(_config: TimberConfig, buildDir: string) {\n const cmd = generatePreviewCommand(buildDir);\n await spawnPreviewProcess(cmd.command, cmd.args, cmd.cwd);\n },\n\n // Default no-op. wrapWithExecutionContext() replaces this per-request\n // with a function that routes to ctx.waitUntil().\n waitUntil(_promise: Promise<unknown>) {},\n };\n}\n\n/**\n * Wrap a timber request handler to bind the Cloudflare execution context\n * for `waitUntil()` support and env bindings passthrough.\n * Called from the generated worker entry.\n *\n * This function:\n * 1. Binds `adapter.waitUntil()` to `ctx.waitUntil()` per-request\n * 2. Makes `env` accessible via `getCloudflareBindings()` per-request via ALS\n */\nexport function wrapWithExecutionContext(\n adapter: TimberPlatformAdapter,\n handler: (req: Request) => Promise<Response>\n): CfExportedHandler<Record<string, unknown>> {\n return {\n async fetch(\n request: Request,\n env: Record<string, unknown>,\n ctx: CfExecutionContext\n ): Promise<Response> {\n // Bind the adapter's waitUntil to the Workers execution context\n const originalWaitUntil = adapter.waitUntil;\n adapter.waitUntil = (promise: Promise<unknown>) => {\n ctx.waitUntil(promise);\n };\n\n try {\n // Run the handler within ALS so getCloudflareBindings() works\n return await runWithBindings(env, () => handler(request));\n } finally {\n // Restore (in case adapter is reused across isolate resets)\n adapter.waitUntil = originalWaitUntil;\n }\n },\n };\n}\n\n// ─── Exported helpers (used by tests and build) ─────────────────────────────\n\n/** @internal Exported for testing. */\nexport function generateWorkerEntry(\n buildDir: string,\n outDir: string,\n hasManifestInit = false,\n hasWorkerHandlers = false\n): string {\n // The RSC entry is the main request handler — it exports the fetch handler as default.\n // The Vite RSC plugin outputs it to rsc/index.js.\n let rscEntryRelative = relative(outDir, join(buildDir, 'rsc', 'index.js'));\n // Ensure the import path starts with ./ for ESM compatibility\n if (!rscEntryRelative.startsWith('.')) {\n rscEntryRelative = './' + rscEntryRelative;\n }\n\n // Build manifest init must be imported before the RSC handler so that\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set when the virtual module evaluates.\n // ESM guarantees imports are evaluated in order.\n const manifestImport = hasManifestInit ? \"import './_timber-manifest-init.js'\\n\" : '';\n\n // Optional additional Worker handlers (queue, scheduled, email, etc.)\n // Compiled by buildOutput via Vite's build API into _worker-handlers.js.\n // The module's named exports are spread into the default export alongside fetch.\n const handlersImport = hasWorkerHandlers\n ? \"import * as workerHandlers from './_worker-handlers.js'\\n\"\n : '';\n const handlersSpread = hasWorkerHandlers ? ' ...workerHandlers,\\n' : '';\n\n return `// Generated by @timber-js/app/adapters/cloudflare\n// Do not edit — this file is regenerated on each build.\n\nimport { AsyncLocalStorage } from 'node:async_hooks'\n${manifestImport}import handler from '${rscEntryRelative}'\n${handlersImport}\n// Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.\n// See design/25-production-deployments.md §\"TIMBER_RUNTIME\".\nglobalThis.process ??= { env: {} }\nprocess.env.TIMBER_RUNTIME = 'cloudflare'\n\n// Bind Cloudflare env to ALS so getCloudflareBindings() works at runtime.\n// Uses the same Symbol.for key as getCloudflareBindings() reads from.\nconst ALS_KEY = Symbol.for('timber:cf-bindings-als')\nlet bindingsAls = globalThis[ALS_KEY]\nif (!bindingsAls) {\n bindingsAls = new AsyncLocalStorage()\n globalThis[ALS_KEY] = bindingsAls\n}\n\nexport default {\n${handlersSpread} async fetch(request, env, ctx) {\n return bindingsAls.run(env, () => handler(request))\n }\n}\n`;\n}\n\n/** Wrangler binding sections generated from `CloudflareBindings`. */\nexport interface WranglerBindingsConfig {\n kv_namespaces?: Array<{ binding: string; id: string }>;\n d1_databases?: Array<{ binding: string; database_id: string }>;\n r2_buckets?: Array<{ binding: string; bucket_name: string }>;\n queues?: {\n producers?: Array<{ binding: string; queue: string }>;\n consumers?: Array<Record<string, unknown>>;\n };\n durable_objects?: {\n bindings: Array<Record<string, string>>;\n };\n}\n\n/**\n * Convert declarative `CloudflareBindings` into wrangler.jsonc binding sections.\n *\n * Maps:\n * - `kv` → `kv_namespaces`\n * - `d1` → `d1_databases`\n * - `r2` → `r2_buckets`\n * - `queues` → `queues` (producers + consumers)\n * - `durableObjects` → `durable_objects`\n *\n * Empty arrays are omitted from the output.\n *\n * @internal Exported for testing.\n */\nexport function generateBindingsConfig(\n bindings: CloudflareBindings | undefined\n): WranglerBindingsConfig {\n if (!bindings) return {};\n\n const result: WranglerBindingsConfig = {};\n\n // KV namespaces\n if (bindings.kv && bindings.kv.length > 0) {\n result.kv_namespaces = bindings.kv.map((kv) => ({\n binding: kv.name,\n id: kv.id ?? '',\n }));\n }\n\n // D1 databases\n if (bindings.d1 && bindings.d1.length > 0) {\n result.d1_databases = bindings.d1.map((d1) => ({\n binding: d1.name,\n database_id: d1.database_id,\n }));\n }\n\n // R2 buckets\n if (bindings.r2 && bindings.r2.length > 0) {\n result.r2_buckets = bindings.r2.map((r2) => ({\n binding: r2.name,\n bucket_name: r2.bucket_name,\n }));\n }\n\n // Queues (producers and/or consumers)\n if (bindings.queues) {\n const queues: Record<string, unknown> = {};\n if (bindings.queues.producers && bindings.queues.producers.length > 0) {\n queues.producers = bindings.queues.producers.map((p) => ({\n binding: p.name,\n queue: p.queue,\n }));\n }\n if (bindings.queues.consumers && bindings.queues.consumers.length > 0) {\n queues.consumers = bindings.queues.consumers.map((c) => {\n const consumer: Record<string, unknown> = { queue: c.queue };\n if (c.max_batch_size !== undefined) consumer.max_batch_size = c.max_batch_size;\n if (c.max_retries !== undefined) consumer.max_retries = c.max_retries;\n if (c.dead_letter_queue !== undefined) consumer.dead_letter_queue = c.dead_letter_queue;\n return consumer;\n });\n }\n if (Object.keys(queues).length > 0) {\n result.queues = queues;\n }\n }\n\n // Durable Objects\n if (bindings.durableObjects && bindings.durableObjects.length > 0) {\n result.durable_objects = {\n bindings: bindings.durableObjects.map((dobj) => {\n const entry: Record<string, string> = {\n name: dobj.name,\n class_name: dobj.class_name,\n };\n if (dobj.script_name) entry.script_name = dobj.script_name;\n return entry;\n }),\n };\n }\n\n return result;\n}\n\n/**\n * One-level deep merge: for each key in `override`, if both the base and\n * override values are plain objects (not arrays, not null), merge their\n * keys with override winning on conflicts. Everything else (primitives,\n * arrays, null) is replaced outright — matching the intuitive behavior\n * where `kv_namespaces: [...]` replaces fully but\n * `durable_objects: { migrations }` merges with generated `bindings`.\n */\nfunction shallowDeepMerge(\n base: Record<string, unknown>,\n override: Record<string, unknown>\n): Record<string, unknown> {\n const result = { ...base };\n for (const key of Object.keys(override)) {\n const baseVal = result[key];\n const overVal = override[key];\n if (isPlainObject(baseVal) && isPlainObject(overVal)) {\n result[key] = { ...baseVal, ...overVal };\n } else {\n result[key] = overVal;\n }\n }\n return result;\n}\n\n/** True for `{}` literals — false for arrays, null, Date, etc. */\nfunction isPlainObject(val: unknown): val is Record<string, unknown> {\n return val !== null && typeof val === 'object' && !Array.isArray(val);\n}\n\n/** @internal Exported for testing. */\nexport function generateWranglerConfig(\n config: TimberConfig,\n options: CloudflareAdapterOptions\n): Record<string, unknown> {\n const compatDate = options.compatibilityDate ?? new Date().toISOString().slice(0, 10);\n\n const flags = options.compatibilityFlags ?? ['nodejs_compat'];\n\n const base: Record<string, unknown> = {\n name: 'timber-app',\n main: '_worker.js',\n compatibility_date: compatDate,\n compatibility_flags: flags,\n // The build output is already fully bundled by Vite — skip wrangler's\n // esbuild pass to avoid issues with top-level await and module format.\n no_bundle: true,\n find_additional_modules: true,\n rules: [{ type: 'ESModule', globs: ['**/*.js'] }],\n assets: {\n directory: './static',\n },\n };\n\n // Layer 1: merge bindings-generated sections into base\n const bindingsConfig = generateBindingsConfig(options.bindings);\n const merged = { ...base, ...bindingsConfig };\n\n // Layer 2: wrangler escape hatch with deep merge for nested plain objects.\n // A shallow spread would replace entire sections like durable_objects and\n // queues — e.g. adding migrations via wrangler would drop the generated\n // durable_objects.bindings. Deep merge preserves generated keys while\n // letting wrangler override on actual key-level conflicts.\n if (options.wrangler) {\n return shallowDeepMerge(merged, options.wrangler);\n }\n\n return merged;\n}\n\n// ─── Preview ─────────────────────────────────────────────────────────────────\n\n/** Command descriptor for preview — testable without spawning a process. */\nexport interface PreviewCommand {\n command: string;\n args: string[];\n cwd: string;\n}\n\n/** @internal Exported for testing. */\nexport function generatePreviewCommand(buildDir: string): PreviewCommand {\n const cfDir = join(buildDir, 'cloudflare');\n return {\n command: 'wrangler',\n args: ['dev', '--local', '--config', join(cfDir, 'wrangler.jsonc')],\n cwd: cfDir,\n };\n}\n\n/**\n * Spawn a long-running preview process and pipe stdio to the parent.\n * Resolves when the process exits.\n */\nfunction spawnPreviewProcess(command: string, args: string[], cwd: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const child = execFile(command, args, { cwd }, (err) => {\n if (err) reject(err);\n else resolve();\n });\n child.stdout?.pipe(process.stdout);\n child.stderr?.pipe(process.stderr);\n });\n}\n\n// ─── Cloudflare Workers type stubs ───────────────────────────────────────────\n// Local type declarations so this file compiles without @cloudflare/workers-types.\n// These are NOT declared globally — that would clash with @cloudflare/workers-types\n// when users install it. Instead, they're module-scoped and used only within\n// this file's function signatures.\n\n/** @internal Minimal stub — use @cloudflare/workers-types for full types. */\nexport interface CfExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n passThroughOnException(): void;\n}\n\n/** @internal Minimal stub — use @cloudflare/workers-types for full types. */\nexport interface CfExportedHandler<Env = Record<string, unknown>> {\n fetch?(request: Request, env: Env, ctx: CfExecutionContext): Promise<Response> | Response;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compress-module.d.ts","sourceRoot":"","sources":["../../src/adapters/compress-module.ts"],"names":[],"mappings":"AAaA;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"compress-module.d.ts","sourceRoot":"","sources":["../../src/adapters/compress-module.ts"],"names":[],"mappings":"AAaA;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAmG/C"}
|
package/dist/adapters/nitro.d.ts
CHANGED
|
@@ -28,6 +28,22 @@ export interface NitroAdapterOptions {
|
|
|
28
28
|
* @default 'node-server'
|
|
29
29
|
*/
|
|
30
30
|
preset?: NitroPreset;
|
|
31
|
+
/**
|
|
32
|
+
* Enable application-level gzip compression for HTML and RSC responses.
|
|
33
|
+
*
|
|
34
|
+
* When `true` (default), the origin compresses responses using the Web
|
|
35
|
+
* `CompressionStream` API. This is useful for self-hosted deployments
|
|
36
|
+
* where no reverse proxy or CDN handles compression.
|
|
37
|
+
*
|
|
38
|
+
* Set to `false` when deploying behind a reverse proxy (nginx, caddy)
|
|
39
|
+
* or CDN (Cloudflare, Fastly, Vercel) that compresses at the edge.
|
|
40
|
+
* Disabling origin compression saves CPU on the Node.js event loop —
|
|
41
|
+
* compressing 1MB+ streaming HTML responses takes 10-15ms of main
|
|
42
|
+
* thread time per request, directly reducing throughput under load.
|
|
43
|
+
*
|
|
44
|
+
* @default true
|
|
45
|
+
*/
|
|
46
|
+
compress?: boolean;
|
|
31
47
|
/**
|
|
32
48
|
* Additional Nitro configuration to merge into the generated config.
|
|
33
49
|
* Overrides default values for the selected preset.
|
|
@@ -52,7 +68,7 @@ export interface NitroAdapterOptions {
|
|
|
52
68
|
*/
|
|
53
69
|
export declare function nitro(options?: NitroAdapterOptions): TimberPlatformAdapter;
|
|
54
70
|
/** @internal Exported for testing. */
|
|
55
|
-
export declare function generateNitroEntry(buildDir: string, outDir: string, preset: NitroPreset, hasManifestInit?: boolean): string;
|
|
71
|
+
export declare function generateNitroEntry(buildDir: string, outDir: string, preset: NitroPreset, compress?: boolean, hasManifestInit?: boolean): string;
|
|
56
72
|
/** @internal Exported for testing. */
|
|
57
73
|
export declare function generateNitroConfig(preset: NitroPreset, userConfig?: Record<string, unknown>): string;
|
|
58
74
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nitro.d.ts","sourceRoot":"","sources":["../../src/adapters/nitro.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AAsBnE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,aAAa,GACb,SAAS,GACT,cAAc,GACd,YAAY,GACZ,aAAa,GACb,iBAAiB,GACjB,aAAa,GACb,KAAK,CAAC;AAEV,2CAA2C;AAC3C,UAAU,YAAY;IACpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAgFD,qCAAqC;AACrC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,mBAAwB,GAAG,qBAAqB,
|
|
1
|
+
{"version":3,"file":"nitro.d.ts","sourceRoot":"","sources":["../../src/adapters/nitro.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AAsBnE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,aAAa,GACb,SAAS,GACT,cAAc,GACd,YAAY,GACZ,aAAa,GACb,iBAAiB,GACjB,aAAa,GACb,KAAK,CAAC;AAEV,2CAA2C;AAC3C,UAAU,YAAY;IACpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAgFD,qCAAqC;AACrC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,mBAAwB,GAAG,qBAAqB,CA2F9E;AAID,sCAAsC;AACtC,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,EACnB,QAAQ,UAAO,EACf,eAAe,UAAQ,GACtB,MAAM,CAwFR;AAED,sCAAsC;AACtC,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,WAAW,EACnB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAyBR;AAOD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CA6LnF;AAED,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,sCAAsC;AACtC,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAClB,mBAAmB,GAAG,IAAI,CAY5B;AA8DD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAEjE"}
|
package/dist/adapters/nitro.js
CHANGED
|
@@ -13,9 +13,11 @@ import { execFile } from "node:child_process";
|
|
|
13
13
|
function generateCompressModule() {
|
|
14
14
|
return `// Generated by @timber-js/app — response compression for self-hosted deployments.
|
|
15
15
|
// Do not edit — this file is regenerated on each build.
|
|
16
|
-
// Uses
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
// Uses node:zlib createGzip() (C++ native) on Node.js, falls back to
|
|
17
|
+
// CompressionStream (Web API) on other runtimes. Brotli is left to CDNs/reverse
|
|
18
|
+
// proxies — at streaming quality levels its ratio advantage is marginal.
|
|
19
|
+
import { Readable } from 'node:stream';
|
|
20
|
+
import { createGzip, constants } from 'node:zlib';
|
|
19
21
|
|
|
20
22
|
const COMPRESSIBLE_TYPES = new Set([
|
|
21
23
|
'text/html', 'text/css', 'text/plain', 'text/xml', 'text/javascript',
|
|
@@ -62,7 +64,25 @@ function shouldCompress(response) {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
function compressWithGzip(body) {
|
|
65
|
-
|
|
67
|
+
// Use node:zlib (C++ native) for gzip compression. The Web Streams
|
|
68
|
+
// CompressionStream works but every chunk crosses the JS/Promise boundary.
|
|
69
|
+
// node:zlib.createGzip() compresses entirely in C++ with zero per-chunk
|
|
70
|
+
// Promise overhead.
|
|
71
|
+
//
|
|
72
|
+
// Convert: Web ReadableStream → Node Readable → pipe through gzip →
|
|
73
|
+
// Node Readable → Readable.toWeb() → Web ReadableStream
|
|
74
|
+
try {
|
|
75
|
+
const nodeReadable = Readable.fromWeb(body);
|
|
76
|
+
// Z_SYNC_FLUSH ensures each chunk is flushed immediately so the browser
|
|
77
|
+
// receives the HTML shell before Suspense boundaries resolve. Without it,
|
|
78
|
+
// gzip buffers internally and breaks streaming.
|
|
79
|
+
const gzip = createGzip({ flush: constants.Z_SYNC_FLUSH });
|
|
80
|
+
const compressed = nodeReadable.pipe(gzip);
|
|
81
|
+
return Readable.toWeb(compressed);
|
|
82
|
+
} catch {
|
|
83
|
+
// Fallback: CompressionStream (CF Workers, or if node:stream unavailable)
|
|
84
|
+
return body.pipeThrough(new CompressionStream('gzip'));
|
|
85
|
+
}
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
export function compressResponse(request, response) {
|
|
@@ -189,6 +209,7 @@ var PRESET_CONFIGS = {
|
|
|
189
209
|
*/
|
|
190
210
|
function nitro(options = {}) {
|
|
191
211
|
const preset = options.preset ?? "node-server";
|
|
212
|
+
const compress = options.compress ?? true;
|
|
192
213
|
const presetConfig = PRESET_CONFIGS[preset];
|
|
193
214
|
const pendingPromises = [];
|
|
194
215
|
return {
|
|
@@ -213,7 +234,7 @@ function nitro(options = {}) {
|
|
|
213
234
|
const rscContent = await readFile(rscEntry, "utf-8");
|
|
214
235
|
await writeFile(rscEntry, `${config.manifestInit}\n${rscContent}`);
|
|
215
236
|
}
|
|
216
|
-
const entry = generateNitroEntry(buildDir, outDir, preset);
|
|
237
|
+
const entry = generateNitroEntry(buildDir, outDir, preset, compress);
|
|
217
238
|
await writeFile(join(outDir, "entry.ts"), entry);
|
|
218
239
|
await runNitroBuild(outDir, preset, options.nitroConfig);
|
|
219
240
|
},
|
|
@@ -232,7 +253,7 @@ function nitro(options = {}) {
|
|
|
232
253
|
};
|
|
233
254
|
}
|
|
234
255
|
/** @internal Exported for testing. */
|
|
235
|
-
function generateNitroEntry(buildDir, outDir, preset, hasManifestInit = false) {
|
|
256
|
+
function generateNitroEntry(buildDir, outDir, preset, compress = true, hasManifestInit = false) {
|
|
236
257
|
const serverEntryRelative = "./rsc/index.js";
|
|
237
258
|
const runtimeName = PRESET_CONFIGS[preset].runtimeName;
|
|
238
259
|
const earlyHints = PRESET_CONFIGS[preset].supportsEarlyHints;
|
|
@@ -274,8 +295,7 @@ function generateNitroEntry(buildDir, outDir, preset, hasManifestInit = false) {
|
|
|
274
295
|
// Do not edit — this file is regenerated on each build.
|
|
275
296
|
|
|
276
297
|
${hasManifestInit ? "import './_timber-manifest-init.js'\n" : ""}import handler, { runWithEarlyHintsSender${supportsWaitUntil ? ", runWithWaitUntil" : ""} } from '${serverEntryRelative}'
|
|
277
|
-
import { compressResponse } from './_compress.mjs'
|
|
278
|
-
|
|
298
|
+
${compress ? "import { compressResponse } from './_compress.mjs'\n" : ""}
|
|
279
299
|
// Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.
|
|
280
300
|
// See design/25-production-deployments.md §"TIMBER_RUNTIME".
|
|
281
301
|
process.env.TIMBER_RUNTIME = '${runtimeName}'
|
|
@@ -289,7 +309,7 @@ export default async function timberHandler(event) {
|
|
|
289
309
|
// h3 v2: event.req is the Web Request
|
|
290
310
|
const webRequest = event.req
|
|
291
311
|
${handlerCall}
|
|
292
|
-
return compressResponse(webRequest, webResponse)
|
|
312
|
+
return ${compress ? "compressResponse(webRequest, webResponse)" : "webResponse"}
|
|
293
313
|
}
|
|
294
314
|
`;
|
|
295
315
|
}
|
|
@@ -458,14 +478,37 @@ const server = createServer(async (req, res) => {
|
|
|
458
478
|
|
|
459
479
|
if (webResponse.body) {
|
|
460
480
|
const reader = webResponse.body.getReader();
|
|
461
|
-
|
|
481
|
+
|
|
482
|
+
// Cancel the reader when the client disconnects. This causes any
|
|
483
|
+
// pending reader.read() to reject, breaking the pump loop. Critical
|
|
484
|
+
// for SSE and other infinite streams — without this, disconnected
|
|
485
|
+
// clients leak readers.
|
|
486
|
+
let clientDisconnected = false;
|
|
487
|
+
const onClose = () => {
|
|
488
|
+
clientDisconnected = true;
|
|
489
|
+
reader.cancel('Client disconnected').catch(() => {});
|
|
490
|
+
};
|
|
491
|
+
res.on('close', onClose);
|
|
492
|
+
|
|
493
|
+
try {
|
|
462
494
|
while (true) {
|
|
463
495
|
const { done, value } = await reader.read();
|
|
464
|
-
if (done)
|
|
496
|
+
if (done) break;
|
|
465
497
|
res.write(value);
|
|
466
498
|
}
|
|
467
|
-
}
|
|
468
|
-
|
|
499
|
+
} catch (err) {
|
|
500
|
+
// reader.cancel() from the close handler causes read() to reject.
|
|
501
|
+
// This is expected on client disconnect — not an error.
|
|
502
|
+
if (!clientDisconnected) {
|
|
503
|
+
throw err;
|
|
504
|
+
}
|
|
505
|
+
} finally {
|
|
506
|
+
res.off('close', onClose);
|
|
507
|
+
reader.releaseLock();
|
|
508
|
+
if (!res.writableEnded) {
|
|
509
|
+
res.end();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
469
512
|
} else {
|
|
470
513
|
res.end();
|
|
471
514
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/compress-module.ts","../../src/adapters/nitro.ts"],"sourcesContent":["// Generated compression module template for self-hosted deployments.\n//\n// This file generates a standalone ESM module (_compress.mjs) that is\n// written to the build output during adapter buildOutput(). It's imported\n// by the Nitro entry and preview server at runtime.\n//\n// Uses CompressionStream (Web API) for gzip. Brotli is left to CDNs/reverse\n// proxies — at streaming quality levels its ratio advantage is marginal and\n// node:zlib buffers output internally, breaking streaming.\n// Cloudflare Workers don't need this — the edge auto-compresses.\n//\n// See design/25-production-deployments.md.\n\n/**\n * Generate a standalone ESM module that exports compressResponse().\n *\n * Written to `_compress.mjs` during buildOutput. Imported by the Nitro entry\n * and preview server.\n *\n * @internal Exported for testing.\n */\nexport function generateCompressModule(): string {\n return `// Generated by @timber-js/app — response compression for self-hosted deployments.\n// Do not edit — this file is regenerated on each build.\n// Uses CompressionStream (Web API) for gzip. Brotli is left to CDNs/reverse\n// proxies — at streaming quality levels its ratio advantage is marginal and\n// node:zlib buffers output internally, breaking streaming.\n\nconst COMPRESSIBLE_TYPES = new Set([\n 'text/html', 'text/css', 'text/plain', 'text/xml', 'text/javascript',\n 'text/x-component', 'application/json', 'application/javascript',\n 'application/xml', 'application/xhtml+xml', 'application/rss+xml',\n 'application/atom+xml', 'image/svg+xml',\n]);\n\nconst NO_COMPRESS_STATUSES = new Set([204, 304]);\n\nfunction negotiateEncoding(acceptEncoding) {\n if (!acceptEncoding) return null;\n // Parse tokens with quality values. Per RFC 9110 §12.5.3, q=0 means\n // \"not acceptable\" — the client explicitly rejects that encoding.\n // Brotli (br) is intentionally not handled at the application level.\n const parts = acceptEncoding.split(',');\n for (const part of parts) {\n const [token, ...params] = part.split(';');\n const name = token.trim().toLowerCase();\n if (name !== 'gzip') continue;\n let qValue = 1;\n for (const param of params) {\n const trimmed = param.trim().toLowerCase();\n if (trimmed.startsWith('q=')) {\n qValue = parseFloat(trimmed.slice(2));\n if (Number.isNaN(qValue)) qValue = 1;\n break;\n }\n }\n if (qValue > 0) return 'gzip';\n }\n return null;\n}\n\nfunction shouldCompress(response) {\n if (!response.body) return false;\n if (NO_COMPRESS_STATUSES.has(response.status)) return false;\n if (response.headers.has('Content-Encoding')) return false;\n const contentType = response.headers.get('Content-Type');\n if (!contentType) return false;\n const mimeType = contentType.split(';')[0].trim().toLowerCase();\n if (mimeType === 'text/event-stream') return false;\n return COMPRESSIBLE_TYPES.has(mimeType);\n}\n\nfunction compressWithGzip(body) {\n return body.pipeThrough(new CompressionStream('gzip'));\n}\n\nexport function compressResponse(request, response) {\n if (!shouldCompress(response)) return response;\n const acceptEncoding = request.headers.get('Accept-Encoding') || '';\n const encoding = negotiateEncoding(acceptEncoding);\n if (!encoding) return response;\n const compressedBody = compressWithGzip(response.body);\n const headers = new Headers(response.headers);\n headers.set('Content-Encoding', encoding);\n headers.delete('Content-Length');\n const existingVary = headers.get('Vary');\n if (existingVary) {\n if (!existingVary.toLowerCase().includes('accept-encoding')) {\n headers.set('Vary', existingVary + ', Accept-Encoding');\n }\n } else {\n headers.set('Vary', 'Accept-Encoding');\n }\n return new Response(compressedBody, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n`;\n}\n","// Nitro adapter — multi-platform deployment\n//\n// Covers everything except Cloudflare Workers: Node.js, Bun, Vercel,\n// Netlify, AWS Lambda, Deno Deploy, Azure Functions. Nitro handles\n// compression, graceful shutdown, static file serving, and platform quirks.\n// See design/11-platform.md and design/25-production-deployments.md.\n\nimport { writeFile, readFile, mkdir, cp } from 'node:fs/promises';\nimport { execFile } from 'node:child_process';\nimport { join, relative } from 'node:path';\nimport type { TimberPlatformAdapter, TimberConfig } from './types';\nimport { generateCompressModule } from './compress-module.js';\n// Inlined from server/asset-headers.ts — adapters are loaded by Node at\n// Vite startup time, before Vite's module resolver is available, so cross-\n// directory .ts imports don't resolve.\nconst IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';\nconst STATIC_CACHE = 'public, max-age=3600, must-revalidate';\n\nfunction generateHeadersFile(): string {\n return `# Auto-generated by @timber-js/app — static asset cache headers.\n# See design/25-production-deployments.md §\"CDN / Edge Cache\"\n\n/assets/*\n Cache-Control: ${IMMUTABLE_CACHE}\n\n/*\n Cache-Control: ${STATIC_CACHE}\n`;\n}\n\n// ─── Presets ─────────────────────────────────────────────────────────────────\n\n/**\n * Supported Nitro deployment presets.\n *\n * Each preset maps to a Nitro deployment target. The adapter generates\n * the appropriate configuration and entry point for the selected platform.\n */\nexport type NitroPreset =\n | 'vercel'\n | 'vercel-edge'\n | 'netlify'\n | 'netlify-edge'\n | 'aws-lambda'\n | 'deno-deploy'\n | 'azure-functions'\n | 'node-server'\n | 'bun';\n\n/** Preset-specific Nitro configuration. */\ninterface PresetConfig {\n /** Nitro preset name passed to the Nitro build. */\n nitroPreset: string;\n /** Output directory name within the build dir. */\n outputDir: string;\n /** Whether the runtime supports waitUntil. */\n supportsWaitUntil: boolean;\n /** Whether the runtime supports application-level 103 Early Hints. */\n supportsEarlyHints: boolean;\n /** Value for TIMBER_RUNTIME env var. See design/25-production-deployments.md. */\n runtimeName: string;\n /** Additional nitro.config fields for this preset. */\n extraConfig?: Record<string, unknown>;\n}\n\nconst PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {\n 'vercel': {\n nitroPreset: 'vercel',\n outputDir: '.vercel/output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel',\n extraConfig: { vercel: { functions: { maxDuration: 30 } } },\n },\n 'vercel-edge': {\n nitroPreset: 'vercel-edge',\n outputDir: '.vercel/output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel-edge',\n },\n 'netlify': {\n nitroPreset: 'netlify',\n outputDir: '.netlify/functions-internal',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'netlify',\n },\n 'netlify-edge': {\n nitroPreset: 'netlify-edge',\n outputDir: '.netlify/edge-functions',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'netlify-edge',\n },\n 'aws-lambda': {\n nitroPreset: 'aws-lambda',\n outputDir: '.output',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'aws-lambda',\n },\n 'deno-deploy': {\n nitroPreset: 'deno-deploy',\n outputDir: '.output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'deno-deploy',\n },\n 'azure-functions': {\n nitroPreset: 'azure-functions',\n outputDir: '.output',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'azure-functions',\n },\n 'node-server': {\n nitroPreset: 'node-server',\n outputDir: '.output',\n supportsWaitUntil: true,\n // Disabled by default: most node-server deployments sit behind a\n // reverse proxy (nginx, caddy, traefik) that doesn't support 103\n // Early Hints over HTTP/1.1. Sending 103 causes nginx to intermittently\n // treat the response as an error and retry after proxy_connect_timeout\n // (~5s stalls on ~23% of requests). Link headers on the 200 response\n // are the safe fallback — Cloudflare and other CDNs convert them to\n // 103 automatically at the edge (over HTTP/2+ to the browser).\n supportsEarlyHints: false,\n runtimeName: 'node-server',\n },\n 'bun': {\n nitroPreset: 'bun',\n outputDir: '.output',\n supportsWaitUntil: true,\n // Disabled for same reason as node-server — reverse proxies choke on 103.\n // Link headers on the 200 response are converted to 103 by CDNs.\n supportsEarlyHints: false,\n runtimeName: 'bun',\n },\n};\n\n// ─── Options ─────────────────────────────────────────────────────────────────\n\n/** Options for the Nitro adapter. */\nexport interface NitroAdapterOptions {\n /**\n * Deployment preset. Determines the target platform.\n * @default 'node-server'\n */\n preset?: NitroPreset;\n\n /**\n * Additional Nitro configuration to merge into the generated config.\n * Overrides default values for the selected preset.\n */\n nitroConfig?: Record<string, unknown>;\n}\n\n// ─── Adapter ─────────────────────────────────────────────────────────────────\n\n/**\n * Create a Nitro-based adapter for multi-platform deployment.\n *\n * Nitro abstracts deployment targets — the same timber.js app can deploy\n * to Vercel, Netlify, AWS, Deno Deploy, or Azure by changing the preset.\n *\n * @example\n * ```ts\n * import { nitro } from '@timber-js/app/adapters/nitro'\n *\n * export default {\n * output: 'server',\n * adapter: nitro({ preset: 'vercel' }),\n * }\n * ```\n */\nexport function nitro(options: NitroAdapterOptions = {}): TimberPlatformAdapter {\n const preset = options.preset ?? 'node-server';\n const presetConfig = PRESET_CONFIGS[preset];\n const pendingPromises: Promise<unknown>[] = [];\n\n return {\n name: `nitro-${preset}`,\n\n async buildOutput(config: TimberConfig, buildDir: string) {\n const outDir = join(buildDir, 'nitro');\n await mkdir(outDir, { recursive: true });\n\n // Copy client assets to public directory.\n // When client JavaScript is disabled, skip .js files — only CSS,\n // fonts, images, and other static assets are needed.\n const clientDir = join(buildDir, 'client');\n const publicDir = join(outDir, 'public');\n await mkdir(publicDir, { recursive: true });\n await cp(clientDir, publicDir, {\n recursive: true,\n filter: config.clientJavascriptDisabled ? (src: string) => !src.endsWith('.js') : undefined,\n }).catch(() => {\n // Client dir may not exist when client JavaScript is disabled\n });\n\n // Write _headers file for platforms that support it (Netlify, etc.).\n // See design/25-production-deployments.md §\"CDN / Edge Cache\"\n await writeFile(join(publicDir, '_headers'), generateHeadersFile());\n\n // Write the build manifest init module (if manifest data was produced).\n if (config.manifestInit) {\n await writeFile(join(outDir, '_timber-manifest-init.js'), config.manifestInit);\n }\n\n // Write the compression helper module for runtime use.\n // See design/25-production-deployments.md — self-hosted deployments\n // need application-level compression (Cloudflare handles it at the edge).\n await writeFile(join(outDir, '_compress.mjs'), generateCompressModule());\n\n // Copy rsc/ssr build output into the nitro dir so imports stay local\n // during the Nitro bundling step (avoids broken relative paths in output).\n await cp(join(buildDir, 'rsc'), join(outDir, 'rsc'), { recursive: true });\n await cp(join(buildDir, 'ssr'), join(outDir, 'ssr'), { recursive: true }).catch(() => {});\n\n // Prepend the manifest assignment directly into the RSC entry so\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set before any module reads it.\n // This must be top-level code, not an import, because rollup tree-shakes\n // side-effect-only globalThis assignments from imported modules.\n if (config.manifestInit) {\n const rscEntry = join(outDir, 'rsc', 'index.js');\n const rscContent = await readFile(rscEntry, 'utf-8');\n await writeFile(rscEntry, `${config.manifestInit}\\n${rscContent}`);\n }\n\n // Generate the Nitro entry point (imports from ./rsc/ within nitro dir)\n const entry = generateNitroEntry(buildDir, outDir, preset);\n await writeFile(join(outDir, 'entry.ts'), entry);\n\n // Run the Nitro build to produce a production-ready server bundle.\n // The output goes to dist/nitro/.output/server/index.mjs (for node-server preset).\n // Config is passed programmatically — no nitro.config.ts file needed.\n await runNitroBuild(outDir, preset, options.nitroConfig);\n },\n\n // Only presets that produce a locally-runnable server get preview().\n // Serverless presets (vercel, netlify, aws-lambda, etc.) have no\n // local runtime — Vite's built-in preview is the fallback.\n preview: LOCALLY_PREVIEWABLE.has(preset)\n ? async (_config: TimberConfig, buildDir: string) => {\n // Generate a standalone preview server that uses Node's built-in\n // HTTP server. The Nitro entry.ts can't be run directly because\n // it imports h3 (a Nitro dependency not available at runtime).\n const previewScript = generatePreviewScript(buildDir, preset);\n const scriptPath = join(buildDir, 'nitro', '_preview-server.mjs');\n await writeFile(scriptPath, previewScript);\n\n const command = preset === 'bun' ? 'bun' : 'node';\n await spawnNitroPreview(command, [scriptPath], join(buildDir, 'nitro'));\n }\n : undefined,\n\n waitUntil: presetConfig.supportsWaitUntil\n ? (promise: Promise<unknown>) => {\n const tracked = promise.catch((err) => {\n console.error('[timber] waitUntil promise rejected:', err);\n });\n pendingPromises.push(tracked);\n }\n : undefined,\n };\n}\n\n// ─── Entry Generation ────────────────────────────────────────────────────────\n\n/** @internal Exported for testing. */\nexport function generateNitroEntry(\n buildDir: string,\n outDir: string,\n preset: NitroPreset,\n hasManifestInit = false\n): string {\n // The RSC entry is the main request handler — it exports the fetch handler as default.\n // rsc/ is copied into the nitro dir so the import is local.\n // The manifest init is prepended to rsc/index.js before the nitro build,\n // so globalThis.__TIMBER_BUILD_MANIFEST__ is set before any code reads it.\n const serverEntryRelative = './rsc/index.js';\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n const earlyHints = PRESET_CONFIGS[preset].supportsEarlyHints;\n const supportsWaitUntil = PRESET_CONFIGS[preset].supportsWaitUntil;\n\n // On node-server and bun, wrap the handler with ALS so the pipeline\n // can send 103 Early Hints via res.writeEarlyHints(). Other presets\n // either don't support 103 or handle it at the CDN level.\n //\n // For presets that support waitUntil, bridge h3's event.waitUntil()\n // to timber's waitUntil() primitive via the ALS bridge.\n // See design/11-platform.md §\"waitUntil()\".\n let handlerCall: string;\n if (earlyHints && supportsWaitUntil) {\n handlerCall = ` const nodeRes = event.node?.res\n const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')\n ? (links) => { try { nodeRes.writeEarlyHints({ link: links }) } catch {} }\n : undefined\n\n const waitUntilFn = (typeof event.waitUntil === 'function')\n ? (p) => event.waitUntil(p)\n : undefined\n\n const callHandler = () => handler(webRequest)\n let wrappedHandler = earlyHintsSender\n ? () => runWithEarlyHintsSender(earlyHintsSender, callHandler)\n : callHandler\n const webResponse = waitUntilFn\n ? await runWithWaitUntil(waitUntilFn, wrappedHandler)\n : await wrappedHandler()`;\n } else if (earlyHints) {\n handlerCall = ` const nodeRes = event.node?.res\n const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')\n ? (links) => { try { nodeRes.writeEarlyHints({ link: links }) } catch {} }\n : undefined\n\n const webResponse = earlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest)`;\n } else if (supportsWaitUntil) {\n handlerCall = ` const waitUntilFn = (typeof event.waitUntil === 'function')\n ? (p) => event.waitUntil(p)\n : undefined\n\n const webResponse = waitUntilFn\n ? await runWithWaitUntil(waitUntilFn, () => handler(webRequest))\n : await handler(webRequest)`;\n } else {\n handlerCall = ` const webResponse = await handler(webRequest)`;\n }\n\n // Build manifest init must be imported before the handler so that\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set when the virtual module evaluates.\n // ESM guarantees imports are evaluated in order.\n const manifestImport = hasManifestInit ? \"import './_timber-manifest-init.js'\\n\" : '';\n\n // Import runWithWaitUntil only when the preset supports it.\n const waitUntilImport = supportsWaitUntil ? ', runWithWaitUntil' : '';\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\n${manifestImport}import handler, { runWithEarlyHintsSender${waitUntilImport} } from '${serverEntryRelative}'\nimport { compressResponse } from './_compress.mjs'\n\n// Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.\n// See design/25-production-deployments.md §\"TIMBER_RUNTIME\".\nprocess.env.TIMBER_RUNTIME = '${runtimeName}'\n\n// Nitro's index.mjs wraps this import with defineLazyEventHandler, which\n// already handles the event handler protocol. We export a plain async\n// function — no defineEventHandler wrapper needed.\n// Nitro bundles h3 into its internal _libs/ directory but doesn't make\n// it available as a bare specifier in the output's node_modules.\nexport default async function timberHandler(event) {\n // h3 v2: event.req is the Web Request\n const webRequest = event.req\n${handlerCall}\n return compressResponse(webRequest, webResponse)\n}\n`;\n}\n\n/** @internal Exported for testing. */\nexport function generateNitroConfig(\n preset: NitroPreset,\n userConfig?: Record<string, unknown>\n): string {\n const presetConfig = PRESET_CONFIGS[preset];\n\n const config: Record<string, unknown> = {\n preset: presetConfig.nitroPreset,\n entry: './entry.ts',\n output: { dir: presetConfig.outputDir },\n // Static asset cache headers — hashed assets are immutable, others get 1h.\n // See design/25-production-deployments.md §\"CDN / Edge Cache\"\n routeRules: {\n '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n },\n ...presetConfig.extraConfig,\n ...userConfig,\n };\n\n const configJson = JSON.stringify(config, null, 2);\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\nimport { defineNitroConfig } from 'nitro/config'\n\nexport default defineNitroConfig(${configJson})\n`;\n}\n\n// ─── Preview ─────────────────────────────────────────────────────────────────\n\n/** Presets that produce a locally-runnable server entry. */\nconst LOCALLY_PREVIEWABLE = new Set<NitroPreset>(['node-server', 'bun']);\n\n/**\n * Generate a standalone preview server script that uses Node's built-in\n * HTTP server. This bypasses Nitro entirely — the Nitro entry.ts imports\n * h3 which isn't available outside a Nitro build. For local preview we\n * just need to serve static files and route requests to the RSC handler.\n *\n * @internal Exported for testing.\n */\nexport function generatePreviewScript(buildDir: string, preset: NitroPreset): string {\n const rscEntryRelative = relative(join(buildDir, 'nitro'), join(buildDir, 'rsc', 'index.js'));\n const rscEntry = rscEntryRelative.startsWith('.') ? rscEntryRelative : './' + rscEntryRelative;\n const publicDir = './public';\n const manifestInitPath = './_timber-manifest-init.js';\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n\n return `// Generated by @timber-js/app — standalone preview server.\n// Uses Node's built-in HTTP server to serve static assets and route\n// dynamic requests through the RSC handler. No Nitro/h3 dependency.\n\nimport { createServer } from 'node:http';\nimport { readFile, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { existsSync } from 'node:fs';\n\n// Set runtime before importing the handler.\nprocess.env.TIMBER_RUNTIME = '${runtimeName}';\n\n// Load the build manifest if it exists.\nconst __dirname = fileURLToPath(new URL('.', import.meta.url));\nconst manifestPath = join(__dirname, '${manifestInitPath}');\nif (existsSync(manifestPath)) {\n await import('${manifestInitPath}');\n}\n\n// Import the RSC handler (default export is the fetch-like handler).\nconst { default: handler, runWithEarlyHintsSender } = await import('${rscEntry}');\n\n// Import compression helper for self-hosted response compression.\nconst { compressResponse } = await import('./_compress.mjs');\n\nconst MIME_TYPES = {\n '.html': 'text/html',\n '.js': 'application/javascript',\n '.mjs': 'application/javascript',\n '.css': 'text/css',\n '.json': 'application/json',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.otf': 'font/otf',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.webm': 'video/webm',\n '.mp4': 'video/mp4',\n '.txt': 'text/plain',\n '.xml': 'application/xml',\n '.wasm': 'application/wasm',\n};\n\nconst publicDir = join(__dirname, '${publicDir}');\nconst port = parseInt(process.env.PORT || '3000', 10);\nconst host = process.env.HOST || process.env.HOSTNAME || 'localhost';\n\nconst server = createServer(async (req, res) => {\n const url = new URL(req.url || '/', \\`http://\\${host}:\\${port}\\`);\n\n // Try serving static files from the public directory first.\n const filePath = join(publicDir, url.pathname);\n // Prevent path traversal.\n if (filePath.startsWith(publicDir)) {\n try {\n const fileStat = await stat(filePath);\n if (fileStat.isFile()) {\n const ext = extname(filePath);\n const contentType = MIME_TYPES[ext] || 'application/octet-stream';\n const body = await readFile(filePath);\n // Hashed assets get immutable cache, others get short cache.\n const cacheControl = url.pathname.startsWith('/assets/')\n ? 'public, max-age=31536000, immutable'\n : 'public, max-age=3600, must-revalidate';\n res.writeHead(200, {\n 'Content-Type': contentType,\n 'Content-Length': body.length,\n 'Cache-Control': cacheControl,\n });\n res.end(body);\n return;\n }\n } catch {\n // File not found — fall through to the RSC handler.\n }\n }\n\n // Convert Node request to Web Request.\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value) {\n if (Array.isArray(value)) {\n for (const v of value) headers.append(key, v);\n } else {\n headers.set(key, value);\n }\n }\n }\n\n let body = undefined;\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n body = await new Promise((resolve) => {\n const chunks = [];\n req.on('data', (chunk) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks)));\n });\n }\n\n const webRequest = new Request(url.href, {\n method: req.method,\n headers,\n body,\n duplex: body ? 'half' : undefined,\n });\n\n try {\n // Support 103 Early Hints when available.\n const earlyHintsSender = (typeof res.writeEarlyHints === 'function')\n ? (links) => { try { res.writeEarlyHints({ link: links }); } catch {} }\n : undefined;\n\n const rawResponse = earlyHintsSender && runWithEarlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest);\n\n // Compress the response for self-hosted deployments.\n const webResponse = compressResponse(webRequest, rawResponse);\n\n // Write the response back to the Node response.\n res.writeHead(webResponse.status, Object.fromEntries(webResponse.headers.entries()));\n\n if (webResponse.body) {\n const reader = webResponse.body.getReader();\n const pump = async () => {\n while (true) {\n const { done, value } = await reader.read();\n if (done) { res.end(); return; }\n res.write(value);\n }\n };\n await pump();\n } else {\n res.end();\n }\n } catch (err) {\n console.error('[timber preview] Request error:', err);\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n }\n res.end('Internal Server Error');\n }\n});\n\nserver.listen(port, host, () => {\n console.log();\n console.log(' ⚡ timber preview server running at:');\n console.log();\n console.log(\\` ➜ http://\\${host}:\\${port}\\`);\n console.log();\n});\n`;\n}\n\n/** Command descriptor for Nitro preview — testable without spawning. */\nexport interface NitroPreviewCommand {\n command: string;\n args: string[];\n cwd: string;\n}\n\n/** @internal Exported for testing. */\nexport function generateNitroPreviewCommand(\n buildDir: string,\n preset: NitroPreset\n): NitroPreviewCommand | null {\n if (!LOCALLY_PREVIEWABLE.has(preset)) return null;\n\n const nitroDir = join(buildDir, 'nitro');\n const entryPath = join(nitroDir, 'entry.ts');\n\n const command = preset === 'bun' ? 'bun' : 'node';\n return {\n command,\n args: [entryPath],\n cwd: nitroDir,\n };\n}\n\n/**\n * Run the Nitro production build using the programmatic API.\n * Uses dynamic import so nitro is only loaded at build time.\n * Externalizes the timber RSC/SSR output — those files are pre-built\n * by timber and have internal references that nitro's bundler can't follow.\n */\nasync function runNitroBuild(\n nitroDir: string,\n preset: NitroPreset,\n userConfig?: Record<string, unknown>\n): Promise<void> {\n const presetConfig = PRESET_CONFIGS[preset];\n const {\n createNitro,\n build: nitroBuild,\n prepare,\n copyPublicAssets,\n } = await import('nitro/builder');\n\n const nitro = await createNitro({\n rootDir: nitroDir,\n preset: presetConfig.nitroPreset,\n // Use renderer.entry so Nitro wraps our handler with its server runtime\n // (HTTP server, static file serving, graceful shutdown, etc.).\n // Using `entry` directly would bypass the Nitro server runtime.\n renderer: { handler: join(nitroDir, 'entry.ts') },\n output: { dir: join(nitroDir, presetConfig.outputDir) },\n routeRules: {\n '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n },\n // Don't bundle the timber RSC/SSR build output — it has its own\n // internal file references that nitro's bundler can't follow.\n // Mark them as external so rollup leaves the imports as-is.\n rollupConfig: {\n external: [/\\.\\.\\/rsc\\//],\n },\n ...presetConfig.extraConfig,\n ...userConfig,\n });\n\n await prepare(nitro);\n await copyPublicAssets(nitro);\n await nitroBuild(nitro);\n await nitro.close();\n}\n\n/** Spawn a Nitro preview process and pipe stdio. */\nfunction spawnNitroPreview(command: string, args: string[], cwd: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const child = execFile(command, args, { cwd }, (err) => {\n if (err) reject(err);\n else resolve();\n });\n child.stdout?.pipe(process.stdout);\n child.stderr?.pipe(process.stderr);\n });\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Get the preset configuration for a given preset name.\n * @internal Exported for testing.\n */\nexport function getPresetConfig(preset: NitroPreset): PresetConfig {\n return PRESET_CONFIGS[preset];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/compress-module.ts","../../src/adapters/nitro.ts"],"sourcesContent":["// Generated compression module template for self-hosted deployments.\n//\n// This file generates a standalone ESM module (_compress.mjs) that is\n// written to the build output during adapter buildOutput(). It's imported\n// by the Nitro entry and preview server at runtime.\n//\n// Uses CompressionStream (Web API) for gzip. Brotli is left to CDNs/reverse\n// proxies — at streaming quality levels its ratio advantage is marginal and\n// node:zlib buffers output internally, breaking streaming.\n// Cloudflare Workers don't need this — the edge auto-compresses.\n//\n// See design/25-production-deployments.md.\n\n/**\n * Generate a standalone ESM module that exports compressResponse().\n *\n * Written to `_compress.mjs` during buildOutput. Imported by the Nitro entry\n * and preview server.\n *\n * @internal Exported for testing.\n */\nexport function generateCompressModule(): string {\n return `// Generated by @timber-js/app — response compression for self-hosted deployments.\n// Do not edit — this file is regenerated on each build.\n// Uses node:zlib createGzip() (C++ native) on Node.js, falls back to\n// CompressionStream (Web API) on other runtimes. Brotli is left to CDNs/reverse\n// proxies — at streaming quality levels its ratio advantage is marginal.\nimport { Readable } from 'node:stream';\nimport { createGzip, constants } from 'node:zlib';\n\nconst COMPRESSIBLE_TYPES = new Set([\n 'text/html', 'text/css', 'text/plain', 'text/xml', 'text/javascript',\n 'text/x-component', 'application/json', 'application/javascript',\n 'application/xml', 'application/xhtml+xml', 'application/rss+xml',\n 'application/atom+xml', 'image/svg+xml',\n]);\n\nconst NO_COMPRESS_STATUSES = new Set([204, 304]);\n\nfunction negotiateEncoding(acceptEncoding) {\n if (!acceptEncoding) return null;\n // Parse tokens with quality values. Per RFC 9110 §12.5.3, q=0 means\n // \"not acceptable\" — the client explicitly rejects that encoding.\n // Brotli (br) is intentionally not handled at the application level.\n const parts = acceptEncoding.split(',');\n for (const part of parts) {\n const [token, ...params] = part.split(';');\n const name = token.trim().toLowerCase();\n if (name !== 'gzip') continue;\n let qValue = 1;\n for (const param of params) {\n const trimmed = param.trim().toLowerCase();\n if (trimmed.startsWith('q=')) {\n qValue = parseFloat(trimmed.slice(2));\n if (Number.isNaN(qValue)) qValue = 1;\n break;\n }\n }\n if (qValue > 0) return 'gzip';\n }\n return null;\n}\n\nfunction shouldCompress(response) {\n if (!response.body) return false;\n if (NO_COMPRESS_STATUSES.has(response.status)) return false;\n if (response.headers.has('Content-Encoding')) return false;\n const contentType = response.headers.get('Content-Type');\n if (!contentType) return false;\n const mimeType = contentType.split(';')[0].trim().toLowerCase();\n if (mimeType === 'text/event-stream') return false;\n return COMPRESSIBLE_TYPES.has(mimeType);\n}\n\nfunction compressWithGzip(body) {\n // Use node:zlib (C++ native) for gzip compression. The Web Streams\n // CompressionStream works but every chunk crosses the JS/Promise boundary.\n // node:zlib.createGzip() compresses entirely in C++ with zero per-chunk\n // Promise overhead.\n //\n // Convert: Web ReadableStream → Node Readable → pipe through gzip →\n // Node Readable → Readable.toWeb() → Web ReadableStream\n try {\n const nodeReadable = Readable.fromWeb(body);\n // Z_SYNC_FLUSH ensures each chunk is flushed immediately so the browser\n // receives the HTML shell before Suspense boundaries resolve. Without it,\n // gzip buffers internally and breaks streaming.\n const gzip = createGzip({ flush: constants.Z_SYNC_FLUSH });\n const compressed = nodeReadable.pipe(gzip);\n return Readable.toWeb(compressed);\n } catch {\n // Fallback: CompressionStream (CF Workers, or if node:stream unavailable)\n return body.pipeThrough(new CompressionStream('gzip'));\n }\n}\n\nexport function compressResponse(request, response) {\n if (!shouldCompress(response)) return response;\n const acceptEncoding = request.headers.get('Accept-Encoding') || '';\n const encoding = negotiateEncoding(acceptEncoding);\n if (!encoding) return response;\n const compressedBody = compressWithGzip(response.body);\n const headers = new Headers(response.headers);\n headers.set('Content-Encoding', encoding);\n headers.delete('Content-Length');\n const existingVary = headers.get('Vary');\n if (existingVary) {\n if (!existingVary.toLowerCase().includes('accept-encoding')) {\n headers.set('Vary', existingVary + ', Accept-Encoding');\n }\n } else {\n headers.set('Vary', 'Accept-Encoding');\n }\n return new Response(compressedBody, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n`;\n}\n","// Nitro adapter — multi-platform deployment\n//\n// Covers everything except Cloudflare Workers: Node.js, Bun, Vercel,\n// Netlify, AWS Lambda, Deno Deploy, Azure Functions. Nitro handles\n// compression, graceful shutdown, static file serving, and platform quirks.\n// See design/11-platform.md and design/25-production-deployments.md.\n\nimport { writeFile, readFile, mkdir, cp } from 'node:fs/promises';\nimport { execFile } from 'node:child_process';\nimport { join, relative } from 'node:path';\nimport type { TimberPlatformAdapter, TimberConfig } from './types';\nimport { generateCompressModule } from './compress-module.js';\n// Inlined from server/asset-headers.ts — adapters are loaded by Node at\n// Vite startup time, before Vite's module resolver is available, so cross-\n// directory .ts imports don't resolve.\nconst IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';\nconst STATIC_CACHE = 'public, max-age=3600, must-revalidate';\n\nfunction generateHeadersFile(): string {\n return `# Auto-generated by @timber-js/app — static asset cache headers.\n# See design/25-production-deployments.md §\"CDN / Edge Cache\"\n\n/assets/*\n Cache-Control: ${IMMUTABLE_CACHE}\n\n/*\n Cache-Control: ${STATIC_CACHE}\n`;\n}\n\n// ─── Presets ─────────────────────────────────────────────────────────────────\n\n/**\n * Supported Nitro deployment presets.\n *\n * Each preset maps to a Nitro deployment target. The adapter generates\n * the appropriate configuration and entry point for the selected platform.\n */\nexport type NitroPreset =\n | 'vercel'\n | 'vercel-edge'\n | 'netlify'\n | 'netlify-edge'\n | 'aws-lambda'\n | 'deno-deploy'\n | 'azure-functions'\n | 'node-server'\n | 'bun';\n\n/** Preset-specific Nitro configuration. */\ninterface PresetConfig {\n /** Nitro preset name passed to the Nitro build. */\n nitroPreset: string;\n /** Output directory name within the build dir. */\n outputDir: string;\n /** Whether the runtime supports waitUntil. */\n supportsWaitUntil: boolean;\n /** Whether the runtime supports application-level 103 Early Hints. */\n supportsEarlyHints: boolean;\n /** Value for TIMBER_RUNTIME env var. See design/25-production-deployments.md. */\n runtimeName: string;\n /** Additional nitro.config fields for this preset. */\n extraConfig?: Record<string, unknown>;\n}\n\nconst PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {\n 'vercel': {\n nitroPreset: 'vercel',\n outputDir: '.vercel/output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel',\n extraConfig: { vercel: { functions: { maxDuration: 30 } } },\n },\n 'vercel-edge': {\n nitroPreset: 'vercel-edge',\n outputDir: '.vercel/output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel-edge',\n },\n 'netlify': {\n nitroPreset: 'netlify',\n outputDir: '.netlify/functions-internal',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'netlify',\n },\n 'netlify-edge': {\n nitroPreset: 'netlify-edge',\n outputDir: '.netlify/edge-functions',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'netlify-edge',\n },\n 'aws-lambda': {\n nitroPreset: 'aws-lambda',\n outputDir: '.output',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'aws-lambda',\n },\n 'deno-deploy': {\n nitroPreset: 'deno-deploy',\n outputDir: '.output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'deno-deploy',\n },\n 'azure-functions': {\n nitroPreset: 'azure-functions',\n outputDir: '.output',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'azure-functions',\n },\n 'node-server': {\n nitroPreset: 'node-server',\n outputDir: '.output',\n supportsWaitUntil: true,\n // Disabled by default: most node-server deployments sit behind a\n // reverse proxy (nginx, caddy, traefik) that doesn't support 103\n // Early Hints over HTTP/1.1. Sending 103 causes nginx to intermittently\n // treat the response as an error and retry after proxy_connect_timeout\n // (~5s stalls on ~23% of requests). Link headers on the 200 response\n // are the safe fallback — Cloudflare and other CDNs convert them to\n // 103 automatically at the edge (over HTTP/2+ to the browser).\n supportsEarlyHints: false,\n runtimeName: 'node-server',\n },\n 'bun': {\n nitroPreset: 'bun',\n outputDir: '.output',\n supportsWaitUntil: true,\n // Disabled for same reason as node-server — reverse proxies choke on 103.\n // Link headers on the 200 response are converted to 103 by CDNs.\n supportsEarlyHints: false,\n runtimeName: 'bun',\n },\n};\n\n// ─── Options ─────────────────────────────────────────────────────────────────\n\n/** Options for the Nitro adapter. */\nexport interface NitroAdapterOptions {\n /**\n * Deployment preset. Determines the target platform.\n * @default 'node-server'\n */\n preset?: NitroPreset;\n\n /**\n * Enable application-level gzip compression for HTML and RSC responses.\n *\n * When `true` (default), the origin compresses responses using the Web\n * `CompressionStream` API. This is useful for self-hosted deployments\n * where no reverse proxy or CDN handles compression.\n *\n * Set to `false` when deploying behind a reverse proxy (nginx, caddy)\n * or CDN (Cloudflare, Fastly, Vercel) that compresses at the edge.\n * Disabling origin compression saves CPU on the Node.js event loop —\n * compressing 1MB+ streaming HTML responses takes 10-15ms of main\n * thread time per request, directly reducing throughput under load.\n *\n * @default true\n */\n compress?: boolean;\n\n /**\n * Additional Nitro configuration to merge into the generated config.\n * Overrides default values for the selected preset.\n */\n nitroConfig?: Record<string, unknown>;\n}\n\n// ─── Adapter ─────────────────────────────────────────────────────────────────\n\n/**\n * Create a Nitro-based adapter for multi-platform deployment.\n *\n * Nitro abstracts deployment targets — the same timber.js app can deploy\n * to Vercel, Netlify, AWS, Deno Deploy, or Azure by changing the preset.\n *\n * @example\n * ```ts\n * import { nitro } from '@timber-js/app/adapters/nitro'\n *\n * export default {\n * output: 'server',\n * adapter: nitro({ preset: 'vercel' }),\n * }\n * ```\n */\nexport function nitro(options: NitroAdapterOptions = {}): TimberPlatformAdapter {\n const preset = options.preset ?? 'node-server';\n const compress = options.compress ?? true;\n const presetConfig = PRESET_CONFIGS[preset];\n const pendingPromises: Promise<unknown>[] = [];\n\n return {\n name: `nitro-${preset}`,\n\n async buildOutput(config: TimberConfig, buildDir: string) {\n const outDir = join(buildDir, 'nitro');\n await mkdir(outDir, { recursive: true });\n\n // Copy client assets to public directory.\n // When client JavaScript is disabled, skip .js files — only CSS,\n // fonts, images, and other static assets are needed.\n const clientDir = join(buildDir, 'client');\n const publicDir = join(outDir, 'public');\n await mkdir(publicDir, { recursive: true });\n await cp(clientDir, publicDir, {\n recursive: true,\n filter: config.clientJavascriptDisabled ? (src: string) => !src.endsWith('.js') : undefined,\n }).catch(() => {\n // Client dir may not exist when client JavaScript is disabled\n });\n\n // Write _headers file for platforms that support it (Netlify, etc.).\n // See design/25-production-deployments.md §\"CDN / Edge Cache\"\n await writeFile(join(publicDir, '_headers'), generateHeadersFile());\n\n // Write the build manifest init module (if manifest data was produced).\n if (config.manifestInit) {\n await writeFile(join(outDir, '_timber-manifest-init.js'), config.manifestInit);\n }\n\n // Write the compression helper module for runtime use.\n // See design/25-production-deployments.md — self-hosted deployments\n // need application-level compression (Cloudflare handles it at the edge).\n await writeFile(join(outDir, '_compress.mjs'), generateCompressModule());\n\n // Copy rsc/ssr build output into the nitro dir so imports stay local\n // during the Nitro bundling step (avoids broken relative paths in output).\n await cp(join(buildDir, 'rsc'), join(outDir, 'rsc'), { recursive: true });\n await cp(join(buildDir, 'ssr'), join(outDir, 'ssr'), { recursive: true }).catch(() => {});\n\n // Prepend the manifest assignment directly into the RSC entry so\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set before any module reads it.\n // This must be top-level code, not an import, because rollup tree-shakes\n // side-effect-only globalThis assignments from imported modules.\n if (config.manifestInit) {\n const rscEntry = join(outDir, 'rsc', 'index.js');\n const rscContent = await readFile(rscEntry, 'utf-8');\n await writeFile(rscEntry, `${config.manifestInit}\\n${rscContent}`);\n }\n\n // Generate the Nitro entry point (imports from ./rsc/ within nitro dir)\n const entry = generateNitroEntry(buildDir, outDir, preset, compress);\n await writeFile(join(outDir, 'entry.ts'), entry);\n\n // Run the Nitro build to produce a production-ready server bundle.\n // The output goes to dist/nitro/.output/server/index.mjs (for node-server preset).\n // Config is passed programmatically — no nitro.config.ts file needed.\n await runNitroBuild(outDir, preset, options.nitroConfig);\n },\n\n // Only presets that produce a locally-runnable server get preview().\n // Serverless presets (vercel, netlify, aws-lambda, etc.) have no\n // local runtime — Vite's built-in preview is the fallback.\n preview: LOCALLY_PREVIEWABLE.has(preset)\n ? async (_config: TimberConfig, buildDir: string) => {\n // Generate a standalone preview server that uses Node's built-in\n // HTTP server. The Nitro entry.ts can't be run directly because\n // it imports h3 (a Nitro dependency not available at runtime).\n const previewScript = generatePreviewScript(buildDir, preset);\n const scriptPath = join(buildDir, 'nitro', '_preview-server.mjs');\n await writeFile(scriptPath, previewScript);\n\n const command = preset === 'bun' ? 'bun' : 'node';\n await spawnNitroPreview(command, [scriptPath], join(buildDir, 'nitro'));\n }\n : undefined,\n\n waitUntil: presetConfig.supportsWaitUntil\n ? (promise: Promise<unknown>) => {\n const tracked = promise.catch((err) => {\n console.error('[timber] waitUntil promise rejected:', err);\n });\n pendingPromises.push(tracked);\n }\n : undefined,\n };\n}\n\n// ─── Entry Generation ────────────────────────────────────────────────────────\n\n/** @internal Exported for testing. */\nexport function generateNitroEntry(\n buildDir: string,\n outDir: string,\n preset: NitroPreset,\n compress = true,\n hasManifestInit = false\n): string {\n // The RSC entry is the main request handler — it exports the fetch handler as default.\n // rsc/ is copied into the nitro dir so the import is local.\n // The manifest init is prepended to rsc/index.js before the nitro build,\n // so globalThis.__TIMBER_BUILD_MANIFEST__ is set before any code reads it.\n const serverEntryRelative = './rsc/index.js';\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n const earlyHints = PRESET_CONFIGS[preset].supportsEarlyHints;\n const supportsWaitUntil = PRESET_CONFIGS[preset].supportsWaitUntil;\n\n // On node-server and bun, wrap the handler with ALS so the pipeline\n // can send 103 Early Hints via res.writeEarlyHints(). Other presets\n // either don't support 103 or handle it at the CDN level.\n //\n // For presets that support waitUntil, bridge h3's event.waitUntil()\n // to timber's waitUntil() primitive via the ALS bridge.\n // See design/11-platform.md §\"waitUntil()\".\n let handlerCall: string;\n if (earlyHints && supportsWaitUntil) {\n handlerCall = ` const nodeRes = event.node?.res\n const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')\n ? (links) => { try { nodeRes.writeEarlyHints({ link: links }) } catch {} }\n : undefined\n\n const waitUntilFn = (typeof event.waitUntil === 'function')\n ? (p) => event.waitUntil(p)\n : undefined\n\n const callHandler = () => handler(webRequest)\n let wrappedHandler = earlyHintsSender\n ? () => runWithEarlyHintsSender(earlyHintsSender, callHandler)\n : callHandler\n const webResponse = waitUntilFn\n ? await runWithWaitUntil(waitUntilFn, wrappedHandler)\n : await wrappedHandler()`;\n } else if (earlyHints) {\n handlerCall = ` const nodeRes = event.node?.res\n const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')\n ? (links) => { try { nodeRes.writeEarlyHints({ link: links }) } catch {} }\n : undefined\n\n const webResponse = earlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest)`;\n } else if (supportsWaitUntil) {\n handlerCall = ` const waitUntilFn = (typeof event.waitUntil === 'function')\n ? (p) => event.waitUntil(p)\n : undefined\n\n const webResponse = waitUntilFn\n ? await runWithWaitUntil(waitUntilFn, () => handler(webRequest))\n : await handler(webRequest)`;\n } else {\n handlerCall = ` const webResponse = await handler(webRequest)`;\n }\n\n // Build manifest init must be imported before the handler so that\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set when the virtual module evaluates.\n // ESM guarantees imports are evaluated in order.\n const manifestImport = hasManifestInit ? \"import './_timber-manifest-init.js'\\n\" : '';\n\n // Import runWithWaitUntil only when the preset supports it.\n const waitUntilImport = supportsWaitUntil ? ', runWithWaitUntil' : '';\n\n const compressImport = compress ? \"import { compressResponse } from './_compress.mjs'\\n\" : '';\n const compressCall = compress ? 'compressResponse(webRequest, webResponse)' : 'webResponse';\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\n${manifestImport}import handler, { runWithEarlyHintsSender${waitUntilImport} } from '${serverEntryRelative}'\n${compressImport}\n// Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.\n// See design/25-production-deployments.md §\"TIMBER_RUNTIME\".\nprocess.env.TIMBER_RUNTIME = '${runtimeName}'\n\n// Nitro's index.mjs wraps this import with defineLazyEventHandler, which\n// already handles the event handler protocol. We export a plain async\n// function — no defineEventHandler wrapper needed.\n// Nitro bundles h3 into its internal _libs/ directory but doesn't make\n// it available as a bare specifier in the output's node_modules.\nexport default async function timberHandler(event) {\n // h3 v2: event.req is the Web Request\n const webRequest = event.req\n${handlerCall}\n return ${compressCall}\n}\n`;\n}\n\n/** @internal Exported for testing. */\nexport function generateNitroConfig(\n preset: NitroPreset,\n userConfig?: Record<string, unknown>\n): string {\n const presetConfig = PRESET_CONFIGS[preset];\n\n const config: Record<string, unknown> = {\n preset: presetConfig.nitroPreset,\n entry: './entry.ts',\n output: { dir: presetConfig.outputDir },\n // Static asset cache headers — hashed assets are immutable, others get 1h.\n // See design/25-production-deployments.md §\"CDN / Edge Cache\"\n routeRules: {\n '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n },\n ...presetConfig.extraConfig,\n ...userConfig,\n };\n\n const configJson = JSON.stringify(config, null, 2);\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\nimport { defineNitroConfig } from 'nitro/config'\n\nexport default defineNitroConfig(${configJson})\n`;\n}\n\n// ─── Preview ─────────────────────────────────────────────────────────────────\n\n/** Presets that produce a locally-runnable server entry. */\nconst LOCALLY_PREVIEWABLE = new Set<NitroPreset>(['node-server', 'bun']);\n\n/**\n * Generate a standalone preview server script that uses Node's built-in\n * HTTP server. This bypasses Nitro entirely — the Nitro entry.ts imports\n * h3 which isn't available outside a Nitro build. For local preview we\n * just need to serve static files and route requests to the RSC handler.\n *\n * @internal Exported for testing.\n */\nexport function generatePreviewScript(buildDir: string, preset: NitroPreset): string {\n const rscEntryRelative = relative(join(buildDir, 'nitro'), join(buildDir, 'rsc', 'index.js'));\n const rscEntry = rscEntryRelative.startsWith('.') ? rscEntryRelative : './' + rscEntryRelative;\n const publicDir = './public';\n const manifestInitPath = './_timber-manifest-init.js';\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n\n return `// Generated by @timber-js/app — standalone preview server.\n// Uses Node's built-in HTTP server to serve static assets and route\n// dynamic requests through the RSC handler. No Nitro/h3 dependency.\n\nimport { createServer } from 'node:http';\nimport { readFile, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { existsSync } from 'node:fs';\n\n// Set runtime before importing the handler.\nprocess.env.TIMBER_RUNTIME = '${runtimeName}';\n\n// Load the build manifest if it exists.\nconst __dirname = fileURLToPath(new URL('.', import.meta.url));\nconst manifestPath = join(__dirname, '${manifestInitPath}');\nif (existsSync(manifestPath)) {\n await import('${manifestInitPath}');\n}\n\n// Import the RSC handler (default export is the fetch-like handler).\nconst { default: handler, runWithEarlyHintsSender } = await import('${rscEntry}');\n\n// Import compression helper for self-hosted response compression.\nconst { compressResponse } = await import('./_compress.mjs');\n\nconst MIME_TYPES = {\n '.html': 'text/html',\n '.js': 'application/javascript',\n '.mjs': 'application/javascript',\n '.css': 'text/css',\n '.json': 'application/json',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.otf': 'font/otf',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.webm': 'video/webm',\n '.mp4': 'video/mp4',\n '.txt': 'text/plain',\n '.xml': 'application/xml',\n '.wasm': 'application/wasm',\n};\n\nconst publicDir = join(__dirname, '${publicDir}');\nconst port = parseInt(process.env.PORT || '3000', 10);\nconst host = process.env.HOST || process.env.HOSTNAME || 'localhost';\n\nconst server = createServer(async (req, res) => {\n const url = new URL(req.url || '/', \\`http://\\${host}:\\${port}\\`);\n\n // Try serving static files from the public directory first.\n const filePath = join(publicDir, url.pathname);\n // Prevent path traversal.\n if (filePath.startsWith(publicDir)) {\n try {\n const fileStat = await stat(filePath);\n if (fileStat.isFile()) {\n const ext = extname(filePath);\n const contentType = MIME_TYPES[ext] || 'application/octet-stream';\n const body = await readFile(filePath);\n // Hashed assets get immutable cache, others get short cache.\n const cacheControl = url.pathname.startsWith('/assets/')\n ? 'public, max-age=31536000, immutable'\n : 'public, max-age=3600, must-revalidate';\n res.writeHead(200, {\n 'Content-Type': contentType,\n 'Content-Length': body.length,\n 'Cache-Control': cacheControl,\n });\n res.end(body);\n return;\n }\n } catch {\n // File not found — fall through to the RSC handler.\n }\n }\n\n // Convert Node request to Web Request.\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value) {\n if (Array.isArray(value)) {\n for (const v of value) headers.append(key, v);\n } else {\n headers.set(key, value);\n }\n }\n }\n\n let body = undefined;\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n body = await new Promise((resolve) => {\n const chunks = [];\n req.on('data', (chunk) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks)));\n });\n }\n\n const webRequest = new Request(url.href, {\n method: req.method,\n headers,\n body,\n duplex: body ? 'half' : undefined,\n });\n\n try {\n // Support 103 Early Hints when available.\n const earlyHintsSender = (typeof res.writeEarlyHints === 'function')\n ? (links) => { try { res.writeEarlyHints({ link: links }); } catch {} }\n : undefined;\n\n const rawResponse = earlyHintsSender && runWithEarlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest);\n\n // Compress the response for self-hosted deployments.\n const webResponse = compressResponse(webRequest, rawResponse);\n\n // Write the response back to the Node response.\n res.writeHead(webResponse.status, Object.fromEntries(webResponse.headers.entries()));\n\n if (webResponse.body) {\n const reader = webResponse.body.getReader();\n\n // Cancel the reader when the client disconnects. This causes any\n // pending reader.read() to reject, breaking the pump loop. Critical\n // for SSE and other infinite streams — without this, disconnected\n // clients leak readers.\n let clientDisconnected = false;\n const onClose = () => {\n clientDisconnected = true;\n reader.cancel('Client disconnected').catch(() => {});\n };\n res.on('close', onClose);\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n res.write(value);\n }\n } catch (err) {\n // reader.cancel() from the close handler causes read() to reject.\n // This is expected on client disconnect — not an error.\n if (!clientDisconnected) {\n throw err;\n }\n } finally {\n res.off('close', onClose);\n reader.releaseLock();\n if (!res.writableEnded) {\n res.end();\n }\n }\n } else {\n res.end();\n }\n } catch (err) {\n console.error('[timber preview] Request error:', err);\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n }\n res.end('Internal Server Error');\n }\n});\n\nserver.listen(port, host, () => {\n console.log();\n console.log(' ⚡ timber preview server running at:');\n console.log();\n console.log(\\` ➜ http://\\${host}:\\${port}\\`);\n console.log();\n});\n`;\n}\n\n/** Command descriptor for Nitro preview — testable without spawning. */\nexport interface NitroPreviewCommand {\n command: string;\n args: string[];\n cwd: string;\n}\n\n/** @internal Exported for testing. */\nexport function generateNitroPreviewCommand(\n buildDir: string,\n preset: NitroPreset\n): NitroPreviewCommand | null {\n if (!LOCALLY_PREVIEWABLE.has(preset)) return null;\n\n const nitroDir = join(buildDir, 'nitro');\n const entryPath = join(nitroDir, 'entry.ts');\n\n const command = preset === 'bun' ? 'bun' : 'node';\n return {\n command,\n args: [entryPath],\n cwd: nitroDir,\n };\n}\n\n/**\n * Run the Nitro production build using the programmatic API.\n * Uses dynamic import so nitro is only loaded at build time.\n * Externalizes the timber RSC/SSR output — those files are pre-built\n * by timber and have internal references that nitro's bundler can't follow.\n */\nasync function runNitroBuild(\n nitroDir: string,\n preset: NitroPreset,\n userConfig?: Record<string, unknown>\n): Promise<void> {\n const presetConfig = PRESET_CONFIGS[preset];\n const {\n createNitro,\n build: nitroBuild,\n prepare,\n copyPublicAssets,\n } = await import('nitro/builder');\n\n const nitro = await createNitro({\n rootDir: nitroDir,\n preset: presetConfig.nitroPreset,\n // Use renderer.entry so Nitro wraps our handler with its server runtime\n // (HTTP server, static file serving, graceful shutdown, etc.).\n // Using `entry` directly would bypass the Nitro server runtime.\n renderer: { handler: join(nitroDir, 'entry.ts') },\n output: { dir: join(nitroDir, presetConfig.outputDir) },\n routeRules: {\n '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n },\n // Don't bundle the timber RSC/SSR build output — it has its own\n // internal file references that nitro's bundler can't follow.\n // Mark them as external so rollup leaves the imports as-is.\n rollupConfig: {\n external: [/\\.\\.\\/rsc\\//],\n },\n ...presetConfig.extraConfig,\n ...userConfig,\n });\n\n await prepare(nitro);\n await copyPublicAssets(nitro);\n await nitroBuild(nitro);\n await nitro.close();\n}\n\n/** Spawn a Nitro preview process and pipe stdio. */\nfunction spawnNitroPreview(command: string, args: string[], cwd: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const child = execFile(command, args, { cwd }, (err) => {\n if (err) reject(err);\n else resolve();\n });\n child.stdout?.pipe(process.stdout);\n child.stderr?.pipe(process.stderr);\n });\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Get the preset configuration for a given preset name.\n * @internal Exported for testing.\n */\nexport function getPresetConfig(preset: NitroPreset): PresetConfig {\n return PRESET_CONFIGS[preset];\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CacheOptions } from './index';
|
|
2
|
+
/**
|
|
3
|
+
* Public caching API: `cache(fn, opts)`.
|
|
4
|
+
*
|
|
5
|
+
* Wraps an async function with cross-request caching. Uses the configured
|
|
6
|
+
* cache handler (defaults to MemoryCacheHandler, overridable via timber.config.ts).
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { cache } from '@timber-js/app/cache';
|
|
10
|
+
*
|
|
11
|
+
* const getUser = cache(
|
|
12
|
+
* async (id: string) => db.users.findUnique({ where: { id } }),
|
|
13
|
+
* { ttl: 60, tags: (id) => [`user:${id}`] }
|
|
14
|
+
* );
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function cache<Fn extends (...args: any[]) => Promise<any>>(fn: Fn, opts: CacheOptions<Fn>): Fn;
|
|
18
|
+
export declare namespace cache {
|
|
19
|
+
var invalidate: (opts: {
|
|
20
|
+
key?: string;
|
|
21
|
+
tag?: string;
|
|
22
|
+
}) => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=cache-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-api.d.ts","sourceRoot":"","sources":["../../src/cache/cache-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAI5C;;;;;;;;;;;;;;GAcG;AAEH,wBAAgB,KAAK,CAAC,EAAE,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,EAC/D,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,YAAY,CAAC,EAAE,CAAC,GACrB,EAAE,CAEJ;yBALe,KAAK;2BAe8B;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,KAAG,OAAO,CAAC,IAAI,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level cache handler singleton.
|
|
3
|
+
*
|
|
4
|
+
* Lazily initialized to MemoryCacheHandler on first access. The framework
|
|
5
|
+
* replaces this at boot from timber.config.ts via setCacheHandler().
|
|
6
|
+
*
|
|
7
|
+
* This module avoids importing from ./index to prevent circular dependencies.
|
|
8
|
+
*/
|
|
9
|
+
interface CacheHandlerLike {
|
|
10
|
+
get(key: string): Promise<{
|
|
11
|
+
value: unknown;
|
|
12
|
+
stale: boolean;
|
|
13
|
+
} | null>;
|
|
14
|
+
set(key: string, value: unknown, opts: {
|
|
15
|
+
ttl: number;
|
|
16
|
+
tags: string[];
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
invalidate(opts: {
|
|
19
|
+
key?: string;
|
|
20
|
+
tag?: string;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/** Replace the active cache handler. Called by the framework at boot. */
|
|
24
|
+
export declare function setCacheHandler(h: CacheHandlerLike): void;
|
|
25
|
+
/**
|
|
26
|
+
* Get the active cache handler. Creates a default MemoryCacheHandler on
|
|
27
|
+
* first access if none has been set via setCacheHandler().
|
|
28
|
+
*/
|
|
29
|
+
export declare function getCacheHandler(): CacheHandlerLike;
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=handler-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler-store.d.ts","sourceRoot":"","sources":["../../src/cache/handler-store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,UAAU,gBAAgB;IACxB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACrE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvF,UAAU,CAAC,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE;AAID,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAEzD;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,gBAAgB,CAOlD"}
|
package/dist/cache/index.d.ts
CHANGED
|
@@ -17,14 +17,29 @@ export interface CacheOptions<Fn extends (...args: any[]) => any> {
|
|
|
17
17
|
key?: (...args: Parameters<Fn>) => string;
|
|
18
18
|
staleWhileRevalidate?: boolean;
|
|
19
19
|
tags?: string[] | ((...args: Parameters<Fn>) => string[]);
|
|
20
|
+
/** Timeout (ms) for singleflight-coalesced calls. Prevents hung fn() from
|
|
21
|
+
* permanently blocking all future callers for the same cache key. See TIM-518. */
|
|
22
|
+
timeoutMs?: number;
|
|
20
23
|
}
|
|
21
24
|
export interface MemoryCacheHandlerOptions {
|
|
22
25
|
/** Maximum number of entries. Oldest accessed entries are evicted first. Default: 1000. */
|
|
26
|
+
maxEntries?: number;
|
|
27
|
+
/**
|
|
28
|
+
* @deprecated Use `maxEntries` instead. Will be removed in a future release.
|
|
29
|
+
* Alias for `maxEntries` — maximum number of entries (not bytes).
|
|
30
|
+
*/
|
|
23
31
|
maxSize?: number;
|
|
32
|
+
/** Maximum total byte budget for all cached values. Oldest entries are evicted when exceeded. Default: no limit. */
|
|
33
|
+
maxBytes?: number;
|
|
34
|
+
/** Maximum byte size for a single cache entry. Entries exceeding this are silently dropped. Default: no limit. */
|
|
35
|
+
maxEntryBytes?: number;
|
|
24
36
|
}
|
|
25
37
|
export declare class MemoryCacheHandler implements CacheHandler {
|
|
26
38
|
private store;
|
|
27
|
-
private
|
|
39
|
+
private maxEntries;
|
|
40
|
+
private maxBytes;
|
|
41
|
+
private maxEntryBytes;
|
|
42
|
+
private currentBytes;
|
|
28
43
|
constructor(opts?: MemoryCacheHandlerOptions);
|
|
29
44
|
get(key: string): Promise<{
|
|
30
45
|
value: unknown;
|
|
@@ -40,13 +55,14 @@ export declare class MemoryCacheHandler implements CacheHandler {
|
|
|
40
55
|
}): Promise<void>;
|
|
41
56
|
/** Number of entries currently in the cache. */
|
|
42
57
|
get size(): number;
|
|
58
|
+
/** Estimated total byte size of all cached values. */
|
|
59
|
+
get bytes(): number;
|
|
60
|
+
/** Evict the oldest entry (front of Map). */
|
|
61
|
+
private evictOldest;
|
|
43
62
|
}
|
|
44
63
|
export { RedisCacheHandler } from './redis-handler';
|
|
45
64
|
export type { RedisClient } from './redis-handler';
|
|
46
|
-
export {
|
|
47
|
-
export {
|
|
48
|
-
export
|
|
49
|
-
export { stableStringify } from './stable-stringify';
|
|
50
|
-
export { createSingleflight } from './singleflight';
|
|
51
|
-
export type { Singleflight } from './singleflight';
|
|
65
|
+
export { cache } from './cache-api';
|
|
66
|
+
export { setCacheHandler, getCacheHandler } from './handler-store';
|
|
67
|
+
export { estimateByteSize } from './sizeof';
|
|
52
68
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACrE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvF,UAAU,CAAC,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE;AAGD,MAAM,WAAW,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC,KAAK,MAAM,CAAC;IAC1C,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC;IAC1D;uFACmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,yBAAyB;IACxC,2FAA2F;IAC3F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oHAAoH;IACpH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kHAAkH;IAClH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,kBAAmB,YAAW,YAAY;IACrD,OAAO,CAAC,KAAK,CAGT;IACJ,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,YAAY,CAAK;gBAEb,IAAI,CAAC,EAAE,yBAAyB;IAOtC,GAAG,CAAC,GAAG,EAAE,MAAM;;;;IAYf,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE;IAwCtE,UAAU,CAAC,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;IAkBrD,gDAAgD;IAChD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,sDAAsD;IACtD,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,6CAA6C;IAC7C,OAAO,CAAC,WAAW;CAQpB;AAED,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC"}
|