@timber-js/app 0.2.0-alpha.6 → 0.2.0-alpha.61
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/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
- package/dist/_chunks/als-registry-Ba7URUIn.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-CT98cU9c.js +121 -0
- package/dist/_chunks/define-CT98cU9c.js.map +1 -0
- package/dist/_chunks/define-TK8C1M3x.js +279 -0
- package/dist/_chunks/define-TK8C1M3x.js.map +1 -0
- package/dist/_chunks/define-cookie-BWr_52kY.js +93 -0
- package/dist/_chunks/define-cookie-BWr_52kY.js.map +1 -0
- package/dist/_chunks/error-boundary-DpZJBCqh.js +211 -0
- package/dist/_chunks/error-boundary-DpZJBCqh.js.map +1 -0
- package/dist/_chunks/{format-DviM89f0.js → format-cX7wzEp2.js} +2 -2
- package/dist/_chunks/{format-DviM89f0.js.map → format-cX7wzEp2.js.map} +1 -1
- package/dist/_chunks/{interception-BOoWmLUA.js → interception-Cey5DCGr.js} +129 -77
- package/dist/_chunks/interception-Cey5DCGr.js.map +1 -0
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/{request-context-DIkVh_jG.js → request-context-rju2rbga.js} +97 -69
- package/dist/_chunks/request-context-rju2rbga.js.map +1 -0
- package/dist/_chunks/segment-context-CyaM1mrD.js +72 -0
- package/dist/_chunks/segment-context-CyaM1mrD.js.map +1 -0
- package/dist/_chunks/stale-reload-BSSym1MJ.js +64 -0
- package/dist/_chunks/stale-reload-BSSym1MJ.js.map +1 -0
- package/dist/_chunks/{tracing-Cwn7697K.js → tracing-VYETCQsg.js} +17 -3
- package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-VYETCQsg.js.map} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-wEXY2JQB.js} +1 -1
- package/dist/_chunks/use-query-states-wEXY2JQB.js.map +1 -0
- package/dist/_chunks/wrappers-BaG1bnM3.js +63 -0
- package/dist/_chunks/wrappers-BaG1bnM3.js.map +1 -0
- 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/fast-hash.d.ts +22 -0
- package/dist/cache/fast-hash.d.ts.map +1 -0
- package/dist/cache/index.d.ts +5 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +90 -20
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/register-cached-function.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts +18 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +10 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -125
- 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 +2 -2
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +433 -252
- package/dist/client/index.js.map +1 -1
- package/dist/client/link-pending-store.d.ts +78 -0
- package/dist/client/link-pending-store.d.ts.map +1 -0
- package/dist/client/link.d.ts +23 -9
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +2 -2
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/router.d.ts +25 -3
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +36 -2
- 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-context.d.ts +1 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-merger.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/stale-reload.d.ts +15 -0
- package/dist/client/stale-reload.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +1 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/transition-root.d.ts +1 -1
- package/dist/client/transition-root.d.ts.map +1 -1
- package/dist/client/use-params.d.ts +3 -3
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +1 -1
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/codec.d.ts +21 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/cookies/define-cookie.d.ts +34 -13
- 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 +127 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +665 -242
- package/dist/index.js.map +1 -1
- package/dist/params/define.d.ts +100 -0
- package/dist/params/define.d.ts.map +1 -0
- package/dist/params/index.d.ts +8 -0
- package/dist/params/index.d.ts.map +1 -0
- package/dist/params/index.js +4 -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 +9 -2
- 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 +1 -1
- 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.js +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- 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/search-params/codecs.d.ts +1 -1
- package/dist/search-params/define.d.ts +159 -0
- package/dist/search-params/define.d.ts.map +1 -0
- package/dist/search-params/index.d.ts +4 -5
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +4 -474
- package/dist/search-params/registry.d.ts +1 -1
- package/dist/search-params/wrappers.d.ts +53 -0
- package/dist/search-params/wrappers.d.ts.map +1 -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.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 +1 -1
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +25 -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-renderer.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 +4 -0
- 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 +4 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1977 -1648
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +25 -7
- package/dist/server/logger.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/pipeline-interception.d.ts +1 -1
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +20 -6
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +30 -3
- 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 +65 -38
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +7 -0
- 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 +10 -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/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 +10 -0
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +20 -13
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -3
- 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/server/waituntil-bridge.d.ts.map +1 -1
- 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/navigation-client.d.ts +1 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +1 -1
- package/dist/shims/navigation.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 +17 -17
- package/src/adapters/compress-module.ts +24 -4
- package/src/adapters/nitro.ts +58 -9
- package/src/cache/fast-hash.ts +34 -0
- package/src/cache/index.ts +5 -2
- package/src/cache/register-cached-function.ts +7 -3
- package/src/cache/singleflight.ts +62 -4
- package/src/cache/timber-cache.ts +40 -29
- package/src/cli.ts +0 -0
- package/src/client/browser-entry.ts +151 -99
- package/src/client/error-boundary.tsx +18 -1
- package/src/client/error-reconstituter.tsx +65 -0
- package/src/client/form.tsx +2 -2
- package/src/client/index.ts +10 -1
- package/src/client/link-pending-store.ts +136 -0
- package/src/client/link.tsx +137 -22
- package/src/client/navigation-context.ts +6 -5
- package/src/client/router.ts +117 -60
- package/src/client/rsc-fetch.ts +90 -2
- package/src/client/segment-cache.ts +1 -1
- package/src/client/segment-context.ts +6 -1
- package/src/client/segment-merger.ts +2 -8
- package/src/client/segment-outlet.tsx +86 -0
- package/src/client/stale-reload.ts +73 -6
- package/src/client/top-loader.tsx +10 -9
- package/src/client/transition-root.tsx +20 -2
- package/src/client/use-params.ts +4 -4
- package/src/client/use-query-states.ts +2 -2
- package/src/codec.ts +21 -0
- package/src/cookies/define-cookie.ts +71 -20
- package/src/fonts/css.ts +2 -1
- package/src/index.ts +297 -85
- package/src/params/define.ts +327 -0
- package/src/params/index.ts +28 -0
- package/src/plugins/adapter-build.ts +8 -2
- package/src/plugins/build-manifest.ts +13 -2
- package/src/plugins/build-report.ts +3 -3
- package/src/plugins/cache-transform.ts +1 -1
- package/src/plugins/client-chunks.ts +65 -0
- package/src/plugins/content.ts +1 -1
- package/src/plugins/dev-browser-logs.ts +284 -0
- package/src/plugins/dev-error-overlay.ts +70 -1
- package/src/plugins/dev-logs.ts +1 -1
- package/src/plugins/dev-server.ts +41 -7
- package/src/plugins/entries.ts +6 -8
- package/src/plugins/fonts.ts +102 -55
- 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 +69 -31
- package/src/plugins/static-build.ts +10 -6
- package/src/routing/codegen.ts +109 -88
- package/src/routing/scanner.ts +86 -7
- 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 +1 -1
- package/src/search-params/define.ts +518 -0
- package/src/search-params/index.ts +12 -18
- package/src/search-params/registry.ts +1 -1
- package/src/search-params/wrappers.ts +85 -0
- package/src/server/access-gate.tsx +40 -9
- package/src/server/action-client.ts +8 -2
- package/src/server/action-encryption.ts +144 -0
- package/src/server/action-handler.ts +20 -3
- package/src/server/actions.ts +1 -1
- package/src/server/als-registry.ts +25 -4
- 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-renderer.ts +5 -3
- package/src/server/early-hints.ts +36 -15
- package/src/server/error-boundary-wrapper.ts +58 -15
- package/src/server/fallback-error.ts +29 -14
- 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 +277 -117
- package/src/server/index.ts +9 -4
- package/src/server/logger.ts +44 -36
- package/src/server/node-stream-transforms.ts +509 -0
- package/src/server/pipeline-interception.ts +1 -1
- package/src/server/pipeline.ts +148 -41
- package/src/server/primitives.ts +47 -5
- package/src/server/render-timeout.ts +108 -0
- package/src/server/request-context.ts +125 -119
- package/src/server/route-element-builder.ts +107 -115
- package/src/server/route-handler.ts +2 -1
- package/src/server/route-matcher.ts +9 -2
- package/src/server/rsc-entry/api-handler.ts +8 -8
- package/src/server/rsc-entry/error-renderer.ts +286 -81
- package/src/server/rsc-entry/helpers.ts +134 -5
- package/src/server/rsc-entry/index.ts +177 -76
- package/src/server/rsc-entry/rsc-payload.ts +91 -18
- package/src/server/rsc-entry/rsc-stream.ts +74 -18
- package/src/server/rsc-entry/ssr-bridge.ts +2 -2
- package/src/server/rsc-entry/ssr-renderer.ts +152 -34
- package/src/server/slot-resolver.ts +231 -220
- 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 +23 -0
- package/src/server/tree-builder.ts +92 -58
- package/src/server/types.ts +1 -3
- package/src/server/version-skew.ts +104 -0
- package/src/server/waituntil-bridge.ts +4 -1
- package/src/shared/merge-search-params.ts +55 -0
- package/src/shims/font-google.ts +1 -1
- package/src/shims/navigation-client.ts +1 -1
- package/src/shims/navigation.ts +2 -1
- 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/interception-BOoWmLUA.js.map +0 -1
- package/dist/_chunks/request-context-DIkVh_jG.js.map +0 -1
- package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
- package/dist/_chunks/ssr-data-MjmprTmO.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/client/error-boundary.js.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/cookies/index.js.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/create.d.ts +0 -106
- package/dist/search-params/create.d.ts.map +0 -1
- package/dist/search-params/index.js.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 -53
- package/dist/server/response-cache.d.ts.map +0 -1
- package/src/client/link-status-provider.tsx +0 -30
- 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 -277
|
@@ -17,6 +17,7 @@ import { DenySignal, RedirectSignal } from './primitives.js';
|
|
|
17
17
|
import type { AccessGateProps, SlotAccessGateProps, ReactElement } from './tree-builder.js';
|
|
18
18
|
import { withSpan, setSpanAttribute } from './tracing.js';
|
|
19
19
|
import { isDebug } from './debug.js';
|
|
20
|
+
import { rawSegmentParams } from './request-context.js';
|
|
20
21
|
|
|
21
22
|
// ─── AccessGate ─────────────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -35,7 +36,7 @@ import { isDebug } from './debug.js';
|
|
|
35
36
|
* gets the same data by calling the same cached functions (React.cache dedup).
|
|
36
37
|
*/
|
|
37
38
|
export function AccessGate(props: AccessGateProps): ReactElement | Promise<ReactElement> {
|
|
38
|
-
const { accessFn,
|
|
39
|
+
const { accessFn, segmentName, verdict, children } = props;
|
|
39
40
|
|
|
40
41
|
// Fast path: replay pre-computed verdict from the pre-render pass.
|
|
41
42
|
// This is synchronous — Suspense boundaries cannot interfere with the
|
|
@@ -52,7 +53,7 @@ export function AccessGate(props: AccessGateProps): ReactElement | Promise<React
|
|
|
52
53
|
|
|
53
54
|
// Fallback: call accessFn directly (used by tree-builder.ts which
|
|
54
55
|
// doesn't run a pre-render pass, and for backward compat).
|
|
55
|
-
return accessGateFallback(accessFn,
|
|
56
|
+
return accessGateFallback(accessFn, segmentName, children);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/**
|
|
@@ -61,14 +62,13 @@ export function AccessGate(props: AccessGateProps): ReactElement | Promise<React
|
|
|
61
62
|
*/
|
|
62
63
|
async function accessGateFallback(
|
|
63
64
|
accessFn: AccessGateProps['accessFn'],
|
|
64
|
-
params: AccessGateProps['params'],
|
|
65
|
-
searchParams: AccessGateProps['searchParams'],
|
|
66
65
|
segmentName: AccessGateProps['segmentName'],
|
|
67
66
|
children: ReactElement
|
|
68
67
|
): Promise<ReactElement> {
|
|
69
68
|
await withSpan('timber.access', { 'timber.segment': segmentName ?? 'unknown' }, async () => {
|
|
70
69
|
try {
|
|
71
|
-
|
|
70
|
+
const params = await rawSegmentParams();
|
|
71
|
+
await accessFn({ params });
|
|
72
72
|
await setSpanAttribute('timber.result', 'pass');
|
|
73
73
|
} catch (error: unknown) {
|
|
74
74
|
if (error instanceof DenySignal) {
|
|
@@ -96,18 +96,28 @@ async function accessGateFallback(
|
|
|
96
96
|
* The HTTP status code is unaffected — slot denial is a UI concern, not
|
|
97
97
|
* a protocol concern. The parent layout and sibling slots still render.
|
|
98
98
|
*
|
|
99
|
+
* DeniedComponent is passed instead of a pre-built element so that
|
|
100
|
+
* DenySignal.data can be forwarded as the dangerouslyPassData prop
|
|
101
|
+
* and the slot name can be passed as the slot prop. See TIM-488.
|
|
102
|
+
*
|
|
99
103
|
* redirect() in slot access.ts is a dev-mode error — redirecting from a
|
|
100
104
|
* slot doesn't make architectural sense.
|
|
101
105
|
*/
|
|
102
106
|
export async function SlotAccessGate(props: SlotAccessGateProps): Promise<ReactElement> {
|
|
103
|
-
const { accessFn,
|
|
107
|
+
const { accessFn, DeniedComponent, slotName, createElement, defaultFallback, children } = props;
|
|
104
108
|
|
|
105
109
|
try {
|
|
106
|
-
|
|
110
|
+
const params = await rawSegmentParams();
|
|
111
|
+
await accessFn({ params });
|
|
107
112
|
} catch (error: unknown) {
|
|
108
113
|
// DenySignal → graceful degradation (denied.tsx → default.tsx → null)
|
|
114
|
+
// Build the denied element dynamically so DenySignal.data is forwarded.
|
|
109
115
|
if (error instanceof DenySignal) {
|
|
110
|
-
return
|
|
116
|
+
return (
|
|
117
|
+
buildDeniedFallback(DeniedComponent, slotName, error.data, createElement) ??
|
|
118
|
+
defaultFallback ??
|
|
119
|
+
null
|
|
120
|
+
);
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
// RedirectSignal in slot access → dev-mode error.
|
|
@@ -123,7 +133,11 @@ export async function SlotAccessGate(props: SlotAccessGateProps): Promise<ReactE
|
|
|
123
133
|
);
|
|
124
134
|
}
|
|
125
135
|
// In production, treat as a deny — render fallback rather than crash.
|
|
126
|
-
return
|
|
136
|
+
return (
|
|
137
|
+
buildDeniedFallback(DeniedComponent, slotName, undefined, createElement) ??
|
|
138
|
+
defaultFallback ??
|
|
139
|
+
null
|
|
140
|
+
);
|
|
127
141
|
}
|
|
128
142
|
|
|
129
143
|
// Unhandled error — re-throw so error boundaries can catch it.
|
|
@@ -141,3 +155,20 @@ export async function SlotAccessGate(props: SlotAccessGateProps): Promise<ReactE
|
|
|
141
155
|
// Access passed — render slot content.
|
|
142
156
|
return children;
|
|
143
157
|
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build the denied fallback element dynamically with DenySignal data.
|
|
161
|
+
* Returns null if no DeniedComponent is available.
|
|
162
|
+
*/
|
|
163
|
+
function buildDeniedFallback(
|
|
164
|
+
DeniedComponent: SlotAccessGateProps['DeniedComponent'],
|
|
165
|
+
slotName: string,
|
|
166
|
+
data: unknown,
|
|
167
|
+
createElement: SlotAccessGateProps['createElement']
|
|
168
|
+
): ReactElement | null {
|
|
169
|
+
if (!DeniedComponent) return null;
|
|
170
|
+
return createElement(DeniedComponent, {
|
|
171
|
+
slot: slotName,
|
|
172
|
+
dangerouslyPassData: data,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -183,7 +183,7 @@ async function runActionMiddleware<TCtx>(
|
|
|
183
183
|
|
|
184
184
|
// Re-export parseFormData for use throughout the framework
|
|
185
185
|
import { parseFormData } from './form-data.js';
|
|
186
|
-
import { formatSize } from '
|
|
186
|
+
import { formatSize } from '../utils/format.js';
|
|
187
187
|
import { isDebug, isDevMode } from './debug.js';
|
|
188
188
|
|
|
189
189
|
/**
|
|
@@ -295,8 +295,14 @@ export function createActionClient<TCtx = Record<string, never>>(
|
|
|
295
295
|
// Determine input — either FormData (from useActionState) or direct arg
|
|
296
296
|
let rawInput: unknown;
|
|
297
297
|
if (args.length === 2 && args[1] instanceof FormData) {
|
|
298
|
-
// Called as (prevState, formData) by React useActionState
|
|
298
|
+
// Called as (prevState, formData) by React useActionState (with-JS path)
|
|
299
299
|
rawInput = schema ? parseFormData(args[1]) : args[1];
|
|
300
|
+
} else if (args.length === 1 && args[0] instanceof FormData) {
|
|
301
|
+
// No-JS path: React's decodeAction binds FormData as the sole argument.
|
|
302
|
+
// The form POSTs without JavaScript, decodeAction resolves the server
|
|
303
|
+
// reference and binds the FormData, then executeAction calls fn() with
|
|
304
|
+
// no additional args — so the bound FormData arrives as args[0].
|
|
305
|
+
rawInput = schema ? parseFormData(args[0]) : args[0];
|
|
300
306
|
} else {
|
|
301
307
|
// Direct call: action(input)
|
|
302
308
|
rawInput = args[0];
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server action bound args encryption utilities.
|
|
3
|
+
*
|
|
4
|
+
* Provides key management for the RSC plugin's built-in bound args encryption.
|
|
5
|
+
* The RSC plugin (@vitejs/plugin-rsc) handles the actual encrypt/decrypt via
|
|
6
|
+
* AES-256-GCM — this module handles:
|
|
7
|
+
*
|
|
8
|
+
* 1. Key sourcing: auto-generated at build time (embedded in bundle), overridable
|
|
9
|
+
* via env var for cross-build key sharing (rolling/blue-green deployments)
|
|
10
|
+
* 2. Build-time key expression generation for the RSC plugin's `defineEncryptionKey`
|
|
11
|
+
*
|
|
12
|
+
* Encryption is always on in production. In dev mode, it's on by default
|
|
13
|
+
* (matching the RSC plugin's behavior) but can be disabled for debugging.
|
|
14
|
+
*
|
|
15
|
+
* ## Known Security Considerations
|
|
16
|
+
*
|
|
17
|
+
* 1. **defineEncryptionKey is a raw JS expression.** The RSC plugin inlines it
|
|
18
|
+
* verbatim into generated code. We only emit the hardcoded string
|
|
19
|
+
* `process.env.TIMBER_ACTIONS_ENCRYPTION_KEY` — never user-controlled input.
|
|
20
|
+
* If this function is ever extended to accept configurable env var names,
|
|
21
|
+
* the expression MUST be validated against a safe pattern.
|
|
22
|
+
*
|
|
23
|
+
* 2. **Key material lives in GC-visible JS strings.** `atob()` decodes the key
|
|
24
|
+
* into a regular JavaScript string on the V8 heap. JavaScript has no
|
|
25
|
+
* `SecureString` or memory-zeroing primitive — this is an inherent platform
|
|
26
|
+
* limitation. Acceptable for web server use; would need review for FIPS.
|
|
27
|
+
*
|
|
28
|
+
* 3. **TIMBER_ACTIONS_ENCRYPTION_KEY must be set at both build time and runtime.**
|
|
29
|
+
* At build time, we validate the key format and emit a runtime expression.
|
|
30
|
+
* If the env var is present at build time but missing at runtime, the server
|
|
31
|
+
* will crash on first action invocation with an opaque `atob(undefined)` error.
|
|
32
|
+
* If the env var is present at runtime but was absent at build time, the RSC
|
|
33
|
+
* plugin will have generated its own key and the env var is silently ignored.
|
|
34
|
+
*
|
|
35
|
+
* See design/08-forms-and-actions.md §"Security"
|
|
36
|
+
* See design/13-security.md
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// ─── Types ────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/** User-facing configuration for action bound args encryption. */
|
|
42
|
+
export interface ActionEncryptionConfig {
|
|
43
|
+
/**
|
|
44
|
+
* Disable encryption in dev mode for easier debugging.
|
|
45
|
+
* Has no effect in production — encryption is always enabled.
|
|
46
|
+
* Default: false (encryption is on in dev too).
|
|
47
|
+
*/
|
|
48
|
+
disableInDev?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Key Resolution ───────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Regex for safe `defineEncryptionKey` expressions.
|
|
55
|
+
*
|
|
56
|
+
* The RSC plugin inlines this expression verbatim into generated JavaScript.
|
|
57
|
+
* We restrict it to `process.env.<UPPER_SNAKE_CASE>` to prevent code injection.
|
|
58
|
+
* See "Known Security Considerations" at the top of this file.
|
|
59
|
+
*/
|
|
60
|
+
const SAFE_KEY_EXPR = /^process\.env\.[A-Z_][A-Z0-9_]*$/;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build the `defineEncryptionKey` expression for the RSC plugin.
|
|
64
|
+
*
|
|
65
|
+
* The RSC plugin accepts a JavaScript expression string that will be
|
|
66
|
+
* inlined into the encryption runtime module. At runtime, this expression
|
|
67
|
+
* must evaluate to the base64-encoded encryption key.
|
|
68
|
+
*
|
|
69
|
+
* Priority:
|
|
70
|
+
* 1. `TIMBER_ACTIONS_ENCRYPTION_KEY` env var (for cross-build key sharing
|
|
71
|
+
* in rolling/blue-green deployments)
|
|
72
|
+
* 2. Auto-generated at build time (RSC plugin default — embedded in bundle,
|
|
73
|
+
* consistent across all instances of the same build)
|
|
74
|
+
*
|
|
75
|
+
* For env var keys, we generate a runtime expression that reads the env var.
|
|
76
|
+
* For auto-generated keys, we return undefined and let the RSC plugin handle it.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveEncryptionKeyExpression(): string | undefined {
|
|
79
|
+
// Check for env var override — used for cross-build key sharing where
|
|
80
|
+
// multiple builds must agree on the same encryption key.
|
|
81
|
+
const envKey = process.env.TIMBER_ACTIONS_ENCRYPTION_KEY;
|
|
82
|
+
if (envKey) {
|
|
83
|
+
// Validate the key format (must be base64-encoded 32-byte key)
|
|
84
|
+
validateKeyFormat(envKey);
|
|
85
|
+
|
|
86
|
+
// Return a runtime expression that reads the env var at startup.
|
|
87
|
+
// This ensures the key is read at runtime, not embedded in the build.
|
|
88
|
+
const expr = 'process.env.TIMBER_ACTIONS_ENCRYPTION_KEY';
|
|
89
|
+
|
|
90
|
+
// Defense-in-depth: validate the expression matches our safe pattern.
|
|
91
|
+
// This is redundant today (hardcoded string), but protects against
|
|
92
|
+
// future refactors that might make the expression configurable.
|
|
93
|
+
if (!SAFE_KEY_EXPR.test(expr)) {
|
|
94
|
+
throw new Error(`Unsafe encryption key expression: ${expr}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return expr;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// No override — let the RSC plugin auto-generate a per-build key
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Determine whether action encryption should be enabled.
|
|
106
|
+
*
|
|
107
|
+
* Encryption is always enabled in production. In dev mode, it's enabled
|
|
108
|
+
* by default but can be disabled via config for debugging.
|
|
109
|
+
*/
|
|
110
|
+
export function shouldEnableEncryption(isDev: boolean, config?: ActionEncryptionConfig): boolean {
|
|
111
|
+
if (!isDev) return true; // Always on in production
|
|
112
|
+
if (config?.disableInDev) return false; // Opt-out in dev
|
|
113
|
+
return true; // On by default in dev too
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Key Validation ───────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validate that a key string is a valid base64-encoded 256-bit key.
|
|
120
|
+
* Throws a descriptive error if the key is malformed.
|
|
121
|
+
*/
|
|
122
|
+
export function validateKeyFormat(key: string): void {
|
|
123
|
+
// Decode base64 and check length (32 bytes = 256 bits)
|
|
124
|
+
try {
|
|
125
|
+
const decoded = atob(key);
|
|
126
|
+
const bytes = decoded.length;
|
|
127
|
+
if (bytes !== 32) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`TIMBER_ACTIONS_ENCRYPTION_KEY must be a base64-encoded 256-bit (32-byte) key. ` +
|
|
130
|
+
`Got ${bytes} bytes. Generate one with: ` +
|
|
131
|
+
`node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof Error && error.message.includes('TIMBER_ACTIONS_ENCRYPTION_KEY')) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
throw new Error(
|
|
139
|
+
`TIMBER_ACTIONS_ENCRYPTION_KEY is not valid base64. ` +
|
|
140
|
+
`Generate a key with: ` +
|
|
141
|
+
`node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
decodeReply,
|
|
19
19
|
decodeAction,
|
|
20
20
|
renderToReadableStream,
|
|
21
|
-
} from '
|
|
21
|
+
} from '../rsc-runtime/rsc.js';
|
|
22
22
|
|
|
23
23
|
import { validateCsrf, type CsrfConfig } from './csrf.js';
|
|
24
24
|
import { executeAction, type RevalidateRenderer } from './actions.js';
|
|
@@ -31,6 +31,8 @@ import { handleActionError } from './action-client.js';
|
|
|
31
31
|
import { enforceBodyLimits, enforceFieldLimit, type BodyLimitsConfig } from './body-limits.js';
|
|
32
32
|
import { parseFormData } from './form-data.js';
|
|
33
33
|
import type { FormFlashData } from './form-flash.js';
|
|
34
|
+
import { checkVersionSkew, applyReloadHeaders } from './version-skew.js';
|
|
35
|
+
import { logActionError } from './logger.js';
|
|
34
36
|
|
|
35
37
|
// ─── Types ────────────────────────────────────────────────────────────────
|
|
36
38
|
|
|
@@ -90,6 +92,21 @@ export async function handleActionRequest(
|
|
|
90
92
|
req: Request,
|
|
91
93
|
config: ActionDispatchConfig
|
|
92
94
|
): Promise<Response | FormRerender | null> {
|
|
95
|
+
// Version skew detection — reject actions from stale clients (TIM-446).
|
|
96
|
+
// On mismatch, return a structured RSC error response that the client
|
|
97
|
+
// handles by showing a brief "App updated" message and reloading.
|
|
98
|
+
const skewCheck = checkVersionSkew(req);
|
|
99
|
+
if (!skewCheck.ok) {
|
|
100
|
+
const reloadHeaders = new Headers({
|
|
101
|
+
'Content-Type': RSC_CONTENT_TYPE,
|
|
102
|
+
});
|
|
103
|
+
applyReloadHeaders(reloadHeaders);
|
|
104
|
+
// Return the reload signal as an RSC stream so createFromFetch can
|
|
105
|
+
// decode it. The client checks X-Timber-Reload before processing.
|
|
106
|
+
const rscStream = renderToReadableStream({ _versionSkew: true });
|
|
107
|
+
return new Response(rscStream, { status: 200, headers: reloadHeaders });
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
// CSRF validation — reject cross-origin mutation requests.
|
|
94
111
|
const csrfResult = validateCsrf(req, config.csrf);
|
|
95
112
|
if (!csrfResult.ok) {
|
|
@@ -177,7 +194,7 @@ async function handleRscAction(
|
|
|
177
194
|
});
|
|
178
195
|
} catch (error) {
|
|
179
196
|
// Log full error server-side for debugging
|
|
180
|
-
|
|
197
|
+
logActionError({ method: req.method, path: new URL(req.url).pathname, error });
|
|
181
198
|
|
|
182
199
|
// Return structured error response — ActionError gets its code/data,
|
|
183
200
|
// unexpected errors get sanitized { code: 'INTERNAL_ERROR' }
|
|
@@ -293,7 +310,7 @@ async function handleFormAction(
|
|
|
293
310
|
renderer: config.revalidateRenderer,
|
|
294
311
|
});
|
|
295
312
|
} catch (error) {
|
|
296
|
-
|
|
313
|
+
logActionError({ method: req.method, path: new URL(req.url).pathname, error });
|
|
297
314
|
|
|
298
315
|
// Return the error as flash data for re-render.
|
|
299
316
|
// handleActionError produces { serverError } for ActionErrors
|
package/src/server/actions.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See design/08-forms-and-actions.md
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type { CacheHandler } from '
|
|
17
|
+
import type { CacheHandler } from '../cache/index';
|
|
18
18
|
import { RedirectSignal } from './primitives';
|
|
19
19
|
import { withSpan } from './tracing';
|
|
20
20
|
import { revalidationAls, type RevalidationState } from './als-registry.js';
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
24
|
+
import type { DebugComponentEntry } from './rsc-entry/helpers.js';
|
|
24
25
|
|
|
25
26
|
// ─── Request Context ──────────────────────────────────────────────────────
|
|
26
27
|
// Used by: request-context.ts (headers(), cookies(), searchParams())
|
|
@@ -39,17 +40,37 @@ export interface RequestContextStore {
|
|
|
39
40
|
/** Original (pre-overlay) frozen headers, kept for overlay merging. */
|
|
40
41
|
originalHeaders: Headers;
|
|
41
42
|
/**
|
|
42
|
-
* Promise resolving to the
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* Promise resolving to the raw URLSearchParams for the current request.
|
|
44
|
+
* To get typed parsed params, import a search params definition and
|
|
45
|
+
* call `.parse(searchParams())`.
|
|
45
46
|
*/
|
|
46
|
-
searchParamsPromise: Promise<URLSearchParams
|
|
47
|
+
searchParamsPromise: Promise<URLSearchParams>;
|
|
48
|
+
/**
|
|
49
|
+
* Raw search string from the request URL (e.g. "?foo=bar&baz=1").
|
|
50
|
+
* Available synchronously for use in `redirect()` with `preserveSearchParams`.
|
|
51
|
+
*/
|
|
52
|
+
searchString: string;
|
|
53
|
+
/**
|
|
54
|
+
* Promise resolving to the coerced segment params for the current request.
|
|
55
|
+
* Set by the pipeline after route matching and param coercion, before
|
|
56
|
+
* middleware and rendering. Pages and layouts read params via
|
|
57
|
+
* `rawSegmentParams()` instead of receiving them as a prop.
|
|
58
|
+
*
|
|
59
|
+
* See design/07-routing.md §"params.ts — Convention File for Typed Params"
|
|
60
|
+
*/
|
|
61
|
+
segmentParamsPromise?: Promise<Record<string, string | string[]>>;
|
|
47
62
|
/** Outgoing Set-Cookie entries (name → serialized value + options). Last write wins. */
|
|
48
63
|
cookieJar: Map<string, CookieEntry>;
|
|
49
64
|
/** Whether the response has flushed (headers committed). */
|
|
50
65
|
flushed: boolean;
|
|
51
66
|
/** Whether the current context allows cookie mutation. */
|
|
52
67
|
mutableContext: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Dev-only: getter for the current request's RSC debug components.
|
|
70
|
+
* Set by renderRoute() so onPipelineError can include component tree
|
|
71
|
+
* context for render-phase errors without module-level shared state.
|
|
72
|
+
*/
|
|
73
|
+
debugComponentsGetter?: () => DebugComponentEntry[];
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
/** A single outgoing cookie entry in the cookie jar. */
|
|
@@ -85,6 +85,12 @@ export function collectRouteCss(segments: SegmentWithFiles[], manifest: BuildMan
|
|
|
85
85
|
* via injectHead() before </head>.
|
|
86
86
|
*/
|
|
87
87
|
export function buildCssLinkTags(cssUrls: string[]): string {
|
|
88
|
+
// Emit <link rel="stylesheet"> tags as a fallback for platforms where
|
|
89
|
+
// React's Float system (via @vitejs/plugin-rsc preinit with
|
|
90
|
+
// data-precedence) doesn't handle CSS injection. In practice, Float
|
|
91
|
+
// deduplicates and these may be dropped. No preload hints — Float
|
|
92
|
+
// already starts the fetch via preinit(), and redundant preloads
|
|
93
|
+
// cause "ignored due to unknown as/type" browser warnings.
|
|
88
94
|
return cssUrls.map((url) => `<link rel="stylesheet" href="${url}">`).join('');
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -95,10 +101,10 @@ export function buildCssLinkTags(cssUrls: string[]): string {
|
|
|
95
101
|
* into 103 Early Hints responses. This avoids platform-specific 103
|
|
96
102
|
* sending code.
|
|
97
103
|
*
|
|
98
|
-
* Example output: `</assets/root.css>;
|
|
104
|
+
* Example output: `</assets/root.css>; as=style; rel=preload, </assets/page.css>; as=style; rel=preload`
|
|
99
105
|
*/
|
|
100
106
|
export function buildLinkHeaders(cssUrls: string[]): string {
|
|
101
|
-
return cssUrls.map((url) => `<${url}>;
|
|
107
|
+
return cssUrls.map((url) => `<${url}>; as=style; rel=preload`).join(', ');
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
// ─── Font utilities ──────────────────────────────────────────────────────
|
|
@@ -153,10 +159,10 @@ export function buildFontPreloadTags(fonts: ManifestFontEntry[]): string {
|
|
|
153
159
|
*
|
|
154
160
|
* Cloudflare CDN converts Link headers with rel=preload into 103 Early Hints.
|
|
155
161
|
*
|
|
156
|
-
* Example: `</fonts/inter.woff2>;
|
|
162
|
+
* Example: `</fonts/inter.woff2>; as=font; rel=preload; crossorigin`
|
|
157
163
|
*/
|
|
158
164
|
export function buildFontLinkHeaders(fonts: ManifestFontEntry[]): string {
|
|
159
|
-
return fonts.map((f) => `<${f.href}>;
|
|
165
|
+
return fonts.map((f) => `<${f.href}>; as=font; rel=preload; crossorigin`).join(', ');
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
// ─── JS chunk utilities ──────────────────────────────────────────────────
|
package/src/server/compress.ts
CHANGED
|
@@ -160,15 +160,33 @@ export function compressResponse(request: Request, response: Response): Response
|
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
// ─── Gzip (
|
|
163
|
+
// ─── Gzip (node:zlib with Z_SYNC_FLUSH) ──────────────────────────────────
|
|
164
|
+
//
|
|
165
|
+
// Uses node:zlib's createGzip with Z_SYNC_FLUSH so each chunk is flushed
|
|
166
|
+
// to the output immediately. The Web Platform CompressionStream API buffers
|
|
167
|
+
// internally and does NOT flush per-chunk — this kills streaming because
|
|
168
|
+
// the browser doesn't receive the HTML shell until the gzip stream closes
|
|
169
|
+
// (i.e. after all Suspense boundaries resolve).
|
|
170
|
+
//
|
|
171
|
+
// Z_SYNC_FLUSH adds ~2–5% size overhead vs Z_NO_FLUSH but preserves
|
|
172
|
+
// correct streaming behavior: the shell renders instantly, Suspense
|
|
173
|
+
// fallbacks are visible immediately, and streamed content appears
|
|
174
|
+
// progressively.
|
|
175
|
+
|
|
176
|
+
import { createGzip, constants } from 'node:zlib';
|
|
177
|
+
import { Readable } from 'node:stream';
|
|
164
178
|
|
|
165
179
|
/**
|
|
166
|
-
* Compress a ReadableStream with gzip
|
|
167
|
-
*
|
|
180
|
+
* Compress a ReadableStream with gzip, flushing each chunk immediately.
|
|
181
|
+
*
|
|
182
|
+
* Uses node:zlib's createGzip with Z_SYNC_FLUSH to ensure each HTML chunk
|
|
183
|
+
* (shell, Suspense resolution, RSC payload) is delivered to the browser
|
|
184
|
+
* as soon as it's available — preserving streaming semantics.
|
|
168
185
|
*/
|
|
169
186
|
function compressWithGzip(body: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
187
|
+
const gzip = createGzip({ flush: constants.Z_SYNC_FLUSH });
|
|
188
|
+
const nodeInput = Readable.fromWeb(body as import('stream/web').ReadableStream);
|
|
189
|
+
nodeInput.pipe(gzip);
|
|
190
|
+
|
|
191
|
+
return Readable.toWeb(gzip) as ReadableStream<Uint8Array>;
|
|
174
192
|
}
|
package/src/server/debug.ts
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
*
|
|
49
49
|
* This is the ONLY function that should gate client-visible dev behavior:
|
|
50
50
|
* - Dev error pages with stack traces
|
|
51
|
-
* -
|
|
51
|
+
* - Server-Timing mode default (`'detailed'` in dev, `'total'` in prod)
|
|
52
52
|
* - Error messages in action `INTERNAL_ERROR` payloads
|
|
53
53
|
* - Pipeline error handler wiring (Vite overlay)
|
|
54
54
|
*
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DefaultLogger — human-readable stderr logging when no custom logger is configured.
|
|
3
|
+
*
|
|
4
|
+
* Ships as the fallback so production deployments always have error visibility,
|
|
5
|
+
* even without an `instrumentation.ts` logger export. Output is one line per
|
|
6
|
+
* event, designed for `fly logs`, `kubectl logs`, Cloudflare dashboard tails, etc.
|
|
7
|
+
*
|
|
8
|
+
* Format:
|
|
9
|
+
* [timber] ERROR message key=value key=value trace_id=4bf92f35
|
|
10
|
+
* [timber] WARN message key=value key=value trace_id=4bf92f35
|
|
11
|
+
* [timber] INFO message method=GET path=/dashboard status=200 durationMs=43 trace_id=4bf92f35
|
|
12
|
+
*
|
|
13
|
+
* Behavior:
|
|
14
|
+
* - Suppressed entirely in dev mode (dev logging handles all output)
|
|
15
|
+
* - `debug` suppressed unless TIMBER_DEBUG is set
|
|
16
|
+
* - Replaced entirely when a custom logger is set via `setLogger()`
|
|
17
|
+
*
|
|
18
|
+
* See design/17-logging.md §"DefaultLogger"
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { isDevMode, isDebug } from './debug.js';
|
|
22
|
+
import { formatSsrError } from './error-formatter.js';
|
|
23
|
+
import type { TimberLogger } from './logger.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Format data fields as `key=value` pairs for human-readable output.
|
|
27
|
+
* - `error` key is serialized via formatSsrError for stack trace cleanup
|
|
28
|
+
* - `trace_id` is truncated to 8 chars for readability (full ID in OTEL)
|
|
29
|
+
* - Other values are stringified inline
|
|
30
|
+
*/
|
|
31
|
+
function formatDataFields(data?: Record<string, unknown>): string {
|
|
32
|
+
if (!data) return '';
|
|
33
|
+
|
|
34
|
+
const parts: string[] = [];
|
|
35
|
+
let traceId: string | undefined;
|
|
36
|
+
|
|
37
|
+
for (const [key, value] of Object.entries(data)) {
|
|
38
|
+
if (key === 'trace_id') {
|
|
39
|
+
// Defer trace_id to the end
|
|
40
|
+
traceId = typeof value === 'string' ? value : String(value);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (key === 'error') {
|
|
44
|
+
// Serialize errors with formatSsrError for clean output
|
|
45
|
+
parts.push(`error=${formatSsrError(value)}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (value === undefined || value === null) continue;
|
|
49
|
+
parts.push(`${key}=${value}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// trace_id always last, truncated to 8 chars for readability
|
|
53
|
+
if (traceId) {
|
|
54
|
+
parts.push(`trace_id=${traceId.slice(0, 8)}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return parts.length > 0 ? ' ' + parts.join(' ') : '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Pad level string to fixed width for alignment. */
|
|
61
|
+
function padLevel(level: string): string {
|
|
62
|
+
return level.padEnd(5);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createDefaultLogger(): TimberLogger {
|
|
66
|
+
return {
|
|
67
|
+
error(msg: string, data?: Record<string, unknown>): void {
|
|
68
|
+
// Errors are ALWAYS logged, including dev mode. Suppressing errors
|
|
69
|
+
// in dev causes silent 500s with no stack trace, making route.ts
|
|
70
|
+
// and render errors impossible to debug. See TIM-555.
|
|
71
|
+
const fields = formatDataFields(data);
|
|
72
|
+
process.stderr.write(`[timber] ${padLevel('ERROR')} ${msg}${fields}\n`);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
warn(msg: string, data?: Record<string, unknown>): void {
|
|
76
|
+
// Warnings are always logged — same rationale as errors.
|
|
77
|
+
const fields = formatDataFields(data);
|
|
78
|
+
process.stderr.write(`[timber] ${padLevel('WARN')} ${msg}${fields}\n`);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
info(msg: string, data?: Record<string, unknown>): void {
|
|
82
|
+
// info is suppressed by default — per-request lines are too noisy
|
|
83
|
+
// without a custom logger. Enable with TIMBER_DEBUG.
|
|
84
|
+
if (isDevMode()) return;
|
|
85
|
+
if (!isDebug()) return;
|
|
86
|
+
const fields = formatDataFields(data);
|
|
87
|
+
process.stderr.write(`[timber] ${padLevel('INFO')} ${msg}${fields}\n`);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
debug(msg: string, data?: Record<string, unknown>): void {
|
|
91
|
+
// debug is suppressed in dev (dev logger handles it) and in
|
|
92
|
+
// production unless TIMBER_DEBUG is explicitly set.
|
|
93
|
+
if (isDevMode()) return;
|
|
94
|
+
if (!isDebug()) return;
|
|
95
|
+
const fields = formatDataFields(data);
|
|
96
|
+
process.stderr.write(`[timber] ${padLevel('DEBUG')} ${msg}${fields}\n`);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { createElement } from 'react';
|
|
19
|
-
import { renderToReadableStream } from '
|
|
19
|
+
import { renderToReadableStream } from '../rsc-runtime/rsc.js';
|
|
20
20
|
|
|
21
21
|
import { DenySignal } from './primitives.js';
|
|
22
22
|
import { logRenderError } from './logger.js';
|
|
@@ -26,8 +26,10 @@ import { resolveManifestStatusFile } from './manifest-status-resolver.js';
|
|
|
26
26
|
import type { ManifestSegmentNode } from './route-matcher.js';
|
|
27
27
|
import type { RouteMatch } from './pipeline.js';
|
|
28
28
|
import type { NavContext } from './ssr-entry.js';
|
|
29
|
+
import { flightInitScript } from './flight-scripts.js';
|
|
29
30
|
import type { ClientBootstrapConfig } from './html-injectors.js';
|
|
30
31
|
import type { Metadata } from './types.js';
|
|
32
|
+
import { teeWithErrorPropagation } from './stream-utils.js';
|
|
31
33
|
|
|
32
34
|
/** RSC content type for client navigation payload requests. */
|
|
33
35
|
const RSC_CONTENT_TYPE = 'text/x-component';
|
|
@@ -170,7 +172,7 @@ export async function renderDenyPage(
|
|
|
170
172
|
debugChannel: createDebugChannelSink(),
|
|
171
173
|
});
|
|
172
174
|
|
|
173
|
-
const [ssrStream, inlineStream] = rscStream
|
|
175
|
+
const [ssrStream, inlineStream] = teeWithErrorPropagation(rscStream);
|
|
174
176
|
|
|
175
177
|
const navContext: NavContext = {
|
|
176
178
|
pathname: new URL(req.url).pathname,
|
|
@@ -178,7 +180,7 @@ export async function renderDenyPage(
|
|
|
178
180
|
searchParams: Object.fromEntries(new URL(req.url).searchParams),
|
|
179
181
|
statusCode: deny.status,
|
|
180
182
|
responseHeaders,
|
|
181
|
-
headHtml,
|
|
183
|
+
headHtml: headHtml + flightInitScript(),
|
|
182
184
|
bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
|
|
183
185
|
rscStream: inlineStream,
|
|
184
186
|
};
|