@timber-js/app 0.2.0-alpha.5 → 0.2.0-alpha.50
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-TK8C1M3x.js +279 -0
- package/dist/_chunks/define-TK8C1M3x.js.map +1 -0
- package/dist/_chunks/define-cookie-k9btcEfI.js +93 -0
- package/dist/_chunks/define-cookie-k9btcEfI.js.map +1 -0
- package/dist/_chunks/error-boundary-B9vT_YK_.js +211 -0
- package/dist/_chunks/error-boundary-B9vT_YK_.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-D2djYaIm.js} +112 -77
- package/dist/_chunks/interception-D2djYaIm.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-0h-6Voad.js} +95 -69
- package/dist/_chunks/request-context-0h-6Voad.js.map +1 -0
- package/dist/_chunks/segment-context-DBn-nrMN.js +69 -0
- package/dist/_chunks/segment-context-DBn-nrMN.js.map +1 -0
- package/dist/_chunks/stale-reload-4L-_skC7.js +47 -0
- package/dist/_chunks/stale-reload-4L-_skC7.js.map +1 -0
- package/dist/_chunks/{tracing-Cwn7697K.js → tracing-JI4cYUdz.js} +17 -3
- package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-JI4cYUdz.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-D5KaffOK.js.map → use-query-states-wEXY2JQB.js.map} +1 -1
- package/dist/_chunks/wrappers-C9XPg7-U.js +63 -0
- package/dist/_chunks/wrappers-C9XPg7-U.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/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +213 -93
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts +22 -8
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +2 -2
- package/dist/client/router.d.ts +25 -3
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +23 -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/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 +2 -2
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +1 -1
- package/dist/codec.d.ts +21 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/cookies/define-cookie.d.ts +33 -12
- 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 +112 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +467 -246
- package/dist/index.js.map +1 -1
- package/dist/params/define.d.ts +76 -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 +105 -0
- package/dist/params/index.js.map +1 -0
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.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/dev-error-overlay.d.ts +26 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +7 -0
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +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 +6 -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/als-registry.d.ts +18 -4
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +2 -2
- 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/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 +39 -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 +1974 -1648
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +24 -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.d.ts +7 -4
- 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 +2 -2
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +46 -3
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +6 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- 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/tracing.d.ts +10 -0
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +19 -12
- 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/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 -14
- 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 +133 -93
- package/src/client/error-boundary.tsx +18 -1
- package/src/client/index.ts +10 -1
- package/src/client/link.tsx +78 -19
- package/src/client/navigation-context.ts +2 -2
- package/src/client/router.ts +105 -60
- package/src/client/rsc-fetch.ts +63 -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/stale-reload.ts +32 -6
- package/src/client/top-loader.tsx +10 -9
- package/src/client/transition-root.tsx +7 -1
- package/src/client/use-params.ts +3 -3
- package/src/client/use-query-states.ts +1 -1
- package/src/codec.ts +21 -0
- package/src/cookies/define-cookie.ts +69 -18
- package/src/fonts/css.ts +2 -1
- package/src/index.ts +280 -85
- package/src/params/define.ts +260 -0
- package/src/params/index.ts +28 -0
- package/src/plugins/adapter-build.ts +6 -0
- package/src/plugins/build-manifest.ts +11 -0
- package/src/plugins/client-chunks.ts +65 -0
- package/src/plugins/dev-error-overlay.ts +70 -1
- package/src/plugins/dev-server.ts +38 -4
- package/src/plugins/entries.ts +5 -7
- package/src/plugins/fonts.ts +93 -42
- package/src/plugins/routing.ts +40 -14
- package/src/plugins/server-bundle.ts +32 -1
- package/src/plugins/shims.ts +1 -1
- package/src/plugins/static-build.ts +8 -4
- package/src/routing/codegen.ts +109 -88
- package/src/routing/scanner.ts +55 -6
- package/src/routing/status-file-lint.ts +2 -1
- package/src/routing/types.ts +7 -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 +7 -1
- package/src/server/action-encryption.ts +144 -0
- package/src/server/action-handler.ts +19 -2
- package/src/server/als-registry.ts +18 -4
- package/src/server/build-manifest.ts +4 -4
- package/src/server/compress.ts +25 -7
- package/src/server/debug.ts +1 -1
- package/src/server/default-logger.ts +98 -0
- package/src/server/deny-renderer.ts +2 -1
- package/src/server/early-hints.ts +36 -15
- package/src/server/error-boundary-wrapper.ts +57 -14
- package/src/server/flight-injection-state.ts +113 -0
- package/src/server/flight-scripts.ts +59 -0
- package/src/server/flush.ts +2 -1
- package/src/server/form-data.ts +76 -0
- package/src/server/html-injectors.ts +261 -117
- package/src/server/index.ts +9 -4
- package/src/server/logger.ts +38 -35
- package/src/server/node-stream-transforms.ts +504 -0
- package/src/server/pipeline.ts +131 -39
- package/src/server/primitives.ts +47 -5
- package/src/server/render-timeout.ts +108 -0
- package/src/server/request-context.ts +119 -119
- package/src/server/route-element-builder.ts +106 -114
- package/src/server/route-handler.ts +2 -1
- package/src/server/route-matcher.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +5 -3
- package/src/server/rsc-entry/helpers.ts +122 -3
- package/src/server/rsc-entry/index.ts +108 -43
- package/src/server/rsc-entry/rsc-payload.ts +52 -12
- package/src/server/rsc-entry/rsc-stream.ts +49 -12
- package/src/server/rsc-entry/ssr-renderer.ts +40 -13
- package/src/server/slot-resolver.ts +222 -217
- package/src/server/ssr-entry.ts +209 -30
- package/src/server/ssr-render.ts +289 -67
- package/src/server/ssr-wrappers.tsx +139 -0
- package/src/server/tracing.ts +23 -0
- package/src/server/tree-builder.ts +91 -57
- 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 +48 -0
- package/src/shims/navigation-client.ts +1 -1
- package/src/shims/navigation.ts +1 -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/client/error-boundary.js.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/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
|
@@ -50,14 +50,14 @@ export interface ManifestSegmentNode {
|
|
|
50
50
|
middleware?: ManifestFile;
|
|
51
51
|
access?: ManifestFile;
|
|
52
52
|
route?: ManifestFile;
|
|
53
|
+
/** params.ts — isomorphic convention file for segmentParams + searchParams definitions. */
|
|
54
|
+
params?: ManifestFile;
|
|
53
55
|
error?: ManifestFile;
|
|
54
56
|
default?: ManifestFile;
|
|
55
57
|
denied?: ManifestFile;
|
|
56
|
-
searchParams?: ManifestFile;
|
|
57
58
|
statusFiles?: Record<string, ManifestFile>;
|
|
58
59
|
jsonStatusFiles?: Record<string, ManifestFile>;
|
|
59
60
|
legacyStatusFiles?: Record<string, ManifestFile>;
|
|
60
|
-
prerender?: ManifestFile;
|
|
61
61
|
/** Metadata route files (sitemap.ts, robots.ts, icon.tsx, etc.) keyed by base name */
|
|
62
62
|
metadataRoutes?: Record<string, ManifestFile>;
|
|
63
63
|
|
|
@@ -12,10 +12,12 @@ import { logRenderError } from '#/server/logger.js';
|
|
|
12
12
|
import type { ManifestSegmentNode } from '#/server/route-matcher.js';
|
|
13
13
|
import { DenySignal, RenderError } from '#/server/primitives.js';
|
|
14
14
|
import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
|
|
15
|
+
import { flightInitScript } from '#/server/flight-scripts.js';
|
|
15
16
|
import { renderDenyPage } from '#/server/deny-renderer.js';
|
|
16
17
|
import type { LayoutEntry } from '#/server/deny-renderer.js';
|
|
17
18
|
import type { NavContext } from '#/server/ssr-entry.js';
|
|
18
|
-
import { createDebugChannelSink
|
|
19
|
+
import { createDebugChannelSink } from './helpers.js';
|
|
20
|
+
import { getCookiesForSsr } from '#/server/request-context.js';
|
|
19
21
|
import { callSsr } from './ssr-bridge.js';
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -124,10 +126,10 @@ export async function renderErrorPage(
|
|
|
124
126
|
searchParams: Object.fromEntries(new URL(req.url).searchParams),
|
|
125
127
|
statusCode: status,
|
|
126
128
|
responseHeaders,
|
|
127
|
-
headHtml:
|
|
129
|
+
headHtml: flightInitScript(),
|
|
128
130
|
bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
|
|
129
131
|
rscStream: inlineStream,
|
|
130
|
-
cookies:
|
|
132
|
+
cookies: getCookiesForSsr(),
|
|
131
133
|
};
|
|
132
134
|
|
|
133
135
|
return callSsr(ssrStream, navContext);
|
|
@@ -20,9 +20,6 @@ export const RSC_CONTENT_TYPE = 'text/x-component';
|
|
|
20
20
|
* stream that we drain and discard.
|
|
21
21
|
*
|
|
22
22
|
* See design/13-security.md §"Server component source leak"
|
|
23
|
-
*
|
|
24
|
-
* TODO: In the future, expose this debug data to the browser in dev mode
|
|
25
|
-
* for inline error overlays (e.g. component stack traces).
|
|
26
23
|
*/
|
|
27
24
|
export function createDebugChannelSink(): { readable: ReadableStream; writable: WritableStream } {
|
|
28
25
|
const sink = new TransformStream();
|
|
@@ -34,6 +31,128 @@ export function createDebugChannelSink(): { readable: ReadableStream; writable:
|
|
|
34
31
|
};
|
|
35
32
|
}
|
|
36
33
|
|
|
34
|
+
// ─── Debug Channel Collector (dev mode only) ────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parsed component debug info extracted from the Flight debug channel.
|
|
38
|
+
*
|
|
39
|
+
* Contains only component names, environment labels, and stack frames —
|
|
40
|
+
* never source code or props. See design/13-security.md §"Server source
|
|
41
|
+
* never reaches the client".
|
|
42
|
+
*/
|
|
43
|
+
export interface DebugComponentEntry {
|
|
44
|
+
name: string;
|
|
45
|
+
env: string | null;
|
|
46
|
+
key: string | null;
|
|
47
|
+
stack: unknown[] | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A debug channel that collects Flight debug rows instead of discarding them.
|
|
52
|
+
*
|
|
53
|
+
* Used in dev mode to capture server component tree information for the
|
|
54
|
+
* Vite error overlay. The collector provides the same `{ readable, writable }`
|
|
55
|
+
* shape as the discard sink, plus methods to retrieve collected data.
|
|
56
|
+
*
|
|
57
|
+
* Security: only component names, environments, and stack frames are
|
|
58
|
+
* extracted — props and source code are stripped. In production builds,
|
|
59
|
+
* use `createDebugChannelSink()` instead (this function is never called).
|
|
60
|
+
*/
|
|
61
|
+
export interface DebugChannelCollector {
|
|
62
|
+
readable: ReadableStream;
|
|
63
|
+
writable: WritableStream;
|
|
64
|
+
/** Get the raw collected text from the debug channel. */
|
|
65
|
+
getCollectedText(): string;
|
|
66
|
+
/** Get parsed component entries (names, stacks — no props or source). */
|
|
67
|
+
getComponents(): DebugComponentEntry[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createDebugChannelCollector(): DebugChannelCollector {
|
|
71
|
+
const chunks: string[] = [];
|
|
72
|
+
const decoder = new TextDecoder();
|
|
73
|
+
|
|
74
|
+
const sink = new TransformStream();
|
|
75
|
+
|
|
76
|
+
// Collect chunks from the readable side instead of discarding them.
|
|
77
|
+
sink.readable
|
|
78
|
+
.pipeTo(
|
|
79
|
+
new WritableStream({
|
|
80
|
+
write(chunk: Uint8Array) {
|
|
81
|
+
chunks.push(decoder.decode(chunk, { stream: true }));
|
|
82
|
+
},
|
|
83
|
+
close() {
|
|
84
|
+
// Flush any remaining bytes in the decoder
|
|
85
|
+
const remaining = decoder.decode();
|
|
86
|
+
if (remaining) chunks.push(remaining);
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
)
|
|
90
|
+
.catch(() => {
|
|
91
|
+
// Stream abort — request cancelled. Not an error.
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
readable: new ReadableStream(), // no commands to send to Flight
|
|
96
|
+
writable: sink.writable,
|
|
97
|
+
getCollectedText() {
|
|
98
|
+
return chunks.join('');
|
|
99
|
+
},
|
|
100
|
+
getComponents() {
|
|
101
|
+
return parseDebugRows(chunks.join(''));
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parse React Flight debug rows into component entries.
|
|
108
|
+
*
|
|
109
|
+
* The Flight debug channel writes rows in `hexId:json\n` format. Each row
|
|
110
|
+
* with a JSON object containing a `name` field is a component debug info
|
|
111
|
+
* entry. Rows without `name` (timing rows, reference rows like `D"$id"`)
|
|
112
|
+
* are skipped.
|
|
113
|
+
*
|
|
114
|
+
* Security: `props` are explicitly stripped from parsed entries — they may
|
|
115
|
+
* contain rendered output or user data. Only `name`, `env`, `key`, and
|
|
116
|
+
* `stack` are retained.
|
|
117
|
+
*/
|
|
118
|
+
export function parseDebugRows(text: string): DebugComponentEntry[] {
|
|
119
|
+
if (!text) return [];
|
|
120
|
+
|
|
121
|
+
const entries: DebugComponentEntry[] = [];
|
|
122
|
+
const lines = text.split('\n');
|
|
123
|
+
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
if (!line) continue;
|
|
126
|
+
|
|
127
|
+
// Flight row format: hexId:payload
|
|
128
|
+
const colonIdx = line.indexOf(':');
|
|
129
|
+
if (colonIdx === -1) continue;
|
|
130
|
+
|
|
131
|
+
const payload = line.slice(colonIdx + 1);
|
|
132
|
+
// Skip non-JSON payloads (e.g., D"$a" reference rows)
|
|
133
|
+
if (!payload.startsWith('{')) continue;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(payload);
|
|
137
|
+
if (typeof parsed !== 'object' || parsed === null) continue;
|
|
138
|
+
if (typeof parsed.name !== 'string') continue;
|
|
139
|
+
|
|
140
|
+
// Strip props — may contain source-derived data or user data.
|
|
141
|
+
// Only retain: name, env, key, stack.
|
|
142
|
+
entries.push({
|
|
143
|
+
name: parsed.name,
|
|
144
|
+
env: parsed.env ?? null,
|
|
145
|
+
key: parsed.key ?? null,
|
|
146
|
+
stack: Array.isArray(parsed.stack) ? parsed.stack : null,
|
|
147
|
+
});
|
|
148
|
+
} catch {
|
|
149
|
+
// Malformed JSON — skip this row
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return entries;
|
|
154
|
+
}
|
|
155
|
+
|
|
37
156
|
/**
|
|
38
157
|
* Build segment metadata for the X-Timber-Segments response header.
|
|
39
158
|
* Describes the rendered segment chain with async status, enabling
|
|
@@ -47,7 +47,11 @@ import { buildClientScripts } from '#/server/html-injectors.js';
|
|
|
47
47
|
import type { InterceptionContext, PipelineConfig, RouteMatch } from '#/server/pipeline.js';
|
|
48
48
|
import { createPipeline } from '#/server/pipeline.js';
|
|
49
49
|
import { DenySignal, RedirectSignal } from '#/server/primitives.js';
|
|
50
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
buildRouteElement,
|
|
52
|
+
RouteSignalWithContext,
|
|
53
|
+
ParamCoercionError,
|
|
54
|
+
} from '#/server/route-element-builder.js';
|
|
51
55
|
import type { ManifestSegmentNode } from '#/server/route-matcher.js';
|
|
52
56
|
import { createMetadataRouteMatcher, createRouteMatcher } from '#/server/route-matcher.js';
|
|
53
57
|
import { initDevTracing } from '#/server/tracing.js';
|
|
@@ -61,33 +65,62 @@ import {
|
|
|
61
65
|
createDebugChannelSink,
|
|
62
66
|
escapeHtml,
|
|
63
67
|
isRscPayloadRequest,
|
|
68
|
+
type DebugComponentEntry,
|
|
64
69
|
} from './helpers.js';
|
|
65
70
|
import { parseClientStateTree } from '#/server/state-tree-diff.js';
|
|
66
|
-
import {
|
|
67
|
-
createResponseCache,
|
|
68
|
-
resolveResponseCacheConfig,
|
|
69
|
-
type ResponseCache,
|
|
70
|
-
} from '#/server/response-cache.js';
|
|
71
71
|
import { buildRscPayloadResponse } from './rsc-payload.js';
|
|
72
72
|
import { renderRscStream } from './rsc-stream.js';
|
|
73
73
|
import { renderSsrResponse } from './ssr-renderer.js';
|
|
74
74
|
import { callSsr } from './ssr-bridge.js';
|
|
75
75
|
import { isDebug, isDevMode, setDebugFromConfig } from '#/server/debug.js';
|
|
76
|
+
import { recordTiming } from '#/server/server-timing.js';
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the Server-Timing mode from timber.config.ts.
|
|
80
|
+
*
|
|
81
|
+
* If the user set `serverTiming` explicitly, use that value.
|
|
82
|
+
* Otherwise: `'detailed'` in dev, `'total'` in production.
|
|
83
|
+
*/
|
|
84
|
+
function resolveServerTimingMode(
|
|
85
|
+
config: Record<string, unknown>,
|
|
86
|
+
isDev: boolean
|
|
87
|
+
): 'detailed' | 'total' | false {
|
|
88
|
+
const userValue = config.serverTiming as 'detailed' | 'total' | false | undefined;
|
|
89
|
+
if (userValue !== undefined) return userValue;
|
|
90
|
+
return isDev ? 'detailed' : 'total';
|
|
91
|
+
}
|
|
76
92
|
|
|
77
93
|
// Dev-only pipeline error handler, set by the dev server after import.
|
|
78
94
|
// In production this is always undefined — no overhead.
|
|
79
|
-
|
|
95
|
+
// The third argument provides RSC debug component data (from the Flight
|
|
96
|
+
// debug channel) when available — used by the error overlay to show the
|
|
97
|
+
// server component tree context for render errors.
|
|
98
|
+
let _devPipelineErrorHandler:
|
|
99
|
+
| ((error: Error, phase: string, debugComponents?: DebugComponentEntry[]) => void)
|
|
100
|
+
| undefined;
|
|
80
101
|
|
|
81
102
|
/**
|
|
82
103
|
* Set the dev pipeline error handler.
|
|
83
104
|
*
|
|
84
105
|
* Called by the dev server after importing this module to wire pipeline
|
|
85
106
|
* errors into the Vite browser error overlay. No-op in production.
|
|
107
|
+
*
|
|
108
|
+
* The handler receives an optional third argument with RSC debug component
|
|
109
|
+
* info — component names, environments, and stack frames from the Flight
|
|
110
|
+
* debug channel. This is only populated for render-phase errors.
|
|
86
111
|
*/
|
|
87
|
-
export function setDevPipelineErrorHandler(
|
|
112
|
+
export function setDevPipelineErrorHandler(
|
|
113
|
+
handler: (error: Error, phase: string, debugComponents?: DebugComponentEntry[]) => void
|
|
114
|
+
): void {
|
|
88
115
|
_devPipelineErrorHandler = handler;
|
|
89
116
|
}
|
|
90
117
|
|
|
118
|
+
// Dev-only: holds a getter for the current request's RSC debug components.
|
|
119
|
+
// Updated on each renderRscStream call so the onPipelineError callback can
|
|
120
|
+
// include component tree context for render-phase errors. This is request-
|
|
121
|
+
// scoped by convention — each renderRoute call sets it before returning.
|
|
122
|
+
let _lastDebugComponentsGetter: (() => DebugComponentEntry[]) | undefined;
|
|
123
|
+
|
|
91
124
|
/**
|
|
92
125
|
* Create the RSC request handler from the route manifest.
|
|
93
126
|
*
|
|
@@ -101,13 +134,15 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
101
134
|
// See design/17-logging.md §"register() — Server Startup"
|
|
102
135
|
await loadInstrumentation(loadUserInstrumentation);
|
|
103
136
|
|
|
104
|
-
// Initialize
|
|
105
|
-
|
|
106
|
-
|
|
137
|
+
// Initialize deployment ID for version skew detection (TIM-446).
|
|
138
|
+
// The manifest init module sets globalThis.__TIMBER_DEPLOYMENT_ID__ at startup.
|
|
139
|
+
// In dev mode this is undefined — skew checks are skipped.
|
|
140
|
+
const deploymentId = (globalThis as Record<string, unknown>).__TIMBER_DEPLOYMENT_ID__ as
|
|
141
|
+
| string
|
|
107
142
|
| undefined;
|
|
108
|
-
if (
|
|
109
|
-
const {
|
|
110
|
-
|
|
143
|
+
if (deploymentId) {
|
|
144
|
+
const { setDeploymentId } = await import('#/server/version-skew.js');
|
|
145
|
+
setDeploymentId(deploymentId);
|
|
111
146
|
}
|
|
112
147
|
|
|
113
148
|
const matchRoute = createRouteMatcher(manifest);
|
|
@@ -164,17 +199,6 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
164
199
|
|
|
165
200
|
const typedBuildManifest = buildManifest as BuildManifest;
|
|
166
201
|
|
|
167
|
-
// Initialize response-level caching and singleflight deduplication.
|
|
168
|
-
// See design/31-benchmarking.md for performance motivation.
|
|
169
|
-
const responseCacheRaw = (runtimeConfig as Record<string, unknown>).responseCache as
|
|
170
|
-
| { maxSize?: number; ttlMs?: number; publicOnly?: boolean }
|
|
171
|
-
| false
|
|
172
|
-
| undefined;
|
|
173
|
-
const responseCacheConfig = resolveResponseCacheConfig(responseCacheRaw);
|
|
174
|
-
const responseCache: ResponseCache | null = responseCacheConfig
|
|
175
|
-
? createResponseCache(responseCacheConfig)
|
|
176
|
-
: null;
|
|
177
|
-
|
|
178
202
|
const pipelineConfig: PipelineConfig = {
|
|
179
203
|
proxyLoader: manifest.proxy?.load,
|
|
180
204
|
matchRoute,
|
|
@@ -205,17 +229,15 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
205
229
|
_requestHeaderOverlay: Headers,
|
|
206
230
|
interception?: InterceptionContext
|
|
207
231
|
) => {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
return doRender();
|
|
232
|
+
return renderRoute(
|
|
233
|
+
req,
|
|
234
|
+
match,
|
|
235
|
+
responseHeaders,
|
|
236
|
+
clientBootstrap,
|
|
237
|
+
clientJsDisabled,
|
|
238
|
+
interception,
|
|
239
|
+
manifest.root
|
|
240
|
+
);
|
|
219
241
|
},
|
|
220
242
|
renderNoMatch: async (req: Request, responseHeaders: Headers) => {
|
|
221
243
|
return renderNoMatchPage(req, manifest.root, responseHeaders, clientBootstrap);
|
|
@@ -224,10 +246,18 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
224
246
|
// Slow request threshold from timber.config.ts. Default 3000ms, 0 to disable.
|
|
225
247
|
// See design/17-logging.md §"slowRequestMs"
|
|
226
248
|
slowRequestMs: (runtimeConfig as Record<string, unknown>).slowRequestMs as number | undefined,
|
|
227
|
-
|
|
249
|
+
serverTiming: resolveServerTimingMode(runtimeConfig, isDev),
|
|
228
250
|
onPipelineError: isDev
|
|
229
251
|
? (error: Error, phase: string) => {
|
|
230
|
-
if (_devPipelineErrorHandler)
|
|
252
|
+
if (_devPipelineErrorHandler) {
|
|
253
|
+
// For render-phase errors, include RSC debug component data
|
|
254
|
+
// from the Flight debug channel (if available from the current request).
|
|
255
|
+
const debugComponents =
|
|
256
|
+
phase === 'render' && _lastDebugComponentsGetter
|
|
257
|
+
? _lastDebugComponentsGetter()
|
|
258
|
+
: undefined;
|
|
259
|
+
_devPipelineErrorHandler(error, phase, debugComponents);
|
|
260
|
+
}
|
|
231
261
|
}
|
|
232
262
|
: undefined,
|
|
233
263
|
renderFallbackError: (error, req, responseHeaders) =>
|
|
@@ -307,7 +337,8 @@ async function renderRoute(
|
|
|
307
337
|
responseHeaders: Headers,
|
|
308
338
|
clientBootstrap: ClientBootstrapConfig,
|
|
309
339
|
clientJsDisabled: boolean,
|
|
310
|
-
interception?: InterceptionContext
|
|
340
|
+
interception?: InterceptionContext,
|
|
341
|
+
rootSegment?: ManifestSegmentNode
|
|
311
342
|
): Promise<Response> {
|
|
312
343
|
const segments = match.segments as unknown as ManifestSegmentNode[];
|
|
313
344
|
const leaf = segments[segments.length - 1];
|
|
@@ -329,6 +360,7 @@ async function renderRoute(
|
|
|
329
360
|
// Build the React element tree — loads modules, runs access checks,
|
|
330
361
|
// resolves metadata. DenySignal/RedirectSignal propagate for HTTP handling.
|
|
331
362
|
let routeResult;
|
|
363
|
+
const _buildStart = performance.now();
|
|
332
364
|
try {
|
|
333
365
|
routeResult = await buildRouteElement(_req, match, interception, clientStateTree);
|
|
334
366
|
} catch (error) {
|
|
@@ -361,14 +393,29 @@ async function renderRoute(
|
|
|
361
393
|
return buildRedirectResponse(_req, signal, responseHeaders);
|
|
362
394
|
}
|
|
363
395
|
}
|
|
364
|
-
//
|
|
396
|
+
// Param coercion failed — render the custom 404 page (status files / not-found).
|
|
397
|
+
// Previously returned a bare Response(null, { status: 404 }) which bypassed
|
|
398
|
+
// custom not-found pages. Now routes through renderNoMatchPage so apps with
|
|
399
|
+
// 404.tsx / not-found status files render their custom page.
|
|
400
|
+
if (error instanceof ParamCoercionError) {
|
|
401
|
+
return renderNoMatchPage(_req, rootSegment!, responseHeaders, clientBootstrap);
|
|
402
|
+
}
|
|
403
|
+
// No PageComponent found — same treatment as param coercion: render custom 404.
|
|
365
404
|
if (error instanceof Error && error.message.startsWith('No page component')) {
|
|
366
|
-
return
|
|
405
|
+
return renderNoMatchPage(_req, rootSegment!, responseHeaders, clientBootstrap);
|
|
367
406
|
}
|
|
368
407
|
throw error;
|
|
369
408
|
}
|
|
370
409
|
|
|
371
|
-
const
|
|
410
|
+
const _buildEnd = performance.now();
|
|
411
|
+
recordTiming({
|
|
412
|
+
name: 'build',
|
|
413
|
+
dur: Math.round(_buildEnd - _buildStart),
|
|
414
|
+
desc: 'build element tree',
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const { element, headElements, layoutComponents, deferSuspenseFor, skippedSegments } =
|
|
418
|
+
routeResult;
|
|
372
419
|
|
|
373
420
|
// Build head HTML for injection into the SSR output.
|
|
374
421
|
// Collects CSS, fonts, and modulepreload from the build manifest for matched segments.
|
|
@@ -385,6 +432,14 @@ async function renderRoute(
|
|
|
385
432
|
headHtml += buildCssLinkTags(cssUrls);
|
|
386
433
|
}
|
|
387
434
|
|
|
435
|
+
// Inline font CSS as a <style> tag — @font-face rules and scoped classes.
|
|
436
|
+
// The font CSS is set on globalThis by the transformed font file's
|
|
437
|
+
// side-effect import of virtual:timber-font-css-register.
|
|
438
|
+
const fontCss = (globalThis as Record<string, unknown>).__timber_font_css as string | undefined;
|
|
439
|
+
if (fontCss) {
|
|
440
|
+
headHtml += `<style data-timber-fonts>${fontCss}</style>`;
|
|
441
|
+
}
|
|
442
|
+
|
|
388
443
|
const fontEntries = collectRouteFonts(segments, typedManifest);
|
|
389
444
|
if (fontEntries.length > 0) {
|
|
390
445
|
headHtml += buildFontPreloadTags(fontEntries);
|
|
@@ -411,7 +466,17 @@ async function renderRoute(
|
|
|
411
466
|
}
|
|
412
467
|
|
|
413
468
|
// Render to RSC Flight stream with signal tracking.
|
|
414
|
-
const
|
|
469
|
+
const _rscStart = performance.now();
|
|
470
|
+
const { rscStream, signals, getDebugComponents } = renderRscStream(element, _req);
|
|
471
|
+
|
|
472
|
+
// Store the debug components getter so onPipelineError can include
|
|
473
|
+
// component tree context for render-phase errors (dev mode only).
|
|
474
|
+
_lastDebugComponentsGetter = getDebugComponents;
|
|
475
|
+
recordTiming({
|
|
476
|
+
name: 'rsc-init',
|
|
477
|
+
dur: Math.round(performance.now() - _rscStart),
|
|
478
|
+
desc: 'RSC stream init',
|
|
479
|
+
});
|
|
415
480
|
|
|
416
481
|
// Synchronous redirect — redirect() in access.ts or a non-async component
|
|
417
482
|
// throws during renderToReadableStream creation. Return HTTP redirect.
|
|
@@ -45,18 +45,45 @@ export async function buildRscPayloadResponse(
|
|
|
45
45
|
skippedSegments?: string[]
|
|
46
46
|
): Promise<Response> {
|
|
47
47
|
// Read the first chunk from the RSC stream before committing headers.
|
|
48
|
+
// Race the first read against signal detection — if an async component
|
|
49
|
+
// throws a RedirectSignal or DenySignal, the onError callback fires
|
|
50
|
+
// signals.onSignal() and we can react immediately without waiting for
|
|
51
|
+
// the full macrotask queue.
|
|
52
|
+
//
|
|
53
|
+
// The rejection chain for an async-wrapped page component:
|
|
54
|
+
// 1. PageComponent throws RedirectSignal
|
|
55
|
+
// 2. withSpan catches and re-throws (microtask 1)
|
|
56
|
+
// 3. TracedPage promise rejects (microtask 2)
|
|
57
|
+
// 4. React Flight rejection handler → onError (microtask 3+)
|
|
58
|
+
//
|
|
59
|
+
// Promise.race reacts the instant onError fires, eliminating the
|
|
60
|
+
// per-request setTimeout(0) macrotask delay for the common case
|
|
61
|
+
// (no signal). A 50ms ceiling timeout guards against edge cases
|
|
62
|
+
// where onError never fires.
|
|
48
63
|
const reader = rscStream.getReader();
|
|
49
|
-
const
|
|
64
|
+
const signalDetected = new Promise<void>((resolve) => {
|
|
65
|
+
signals.onSignal = resolve;
|
|
66
|
+
});
|
|
50
67
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
type RaceResult =
|
|
69
|
+
| { type: 'data'; chunk: ReadableStreamReadResult<Uint8Array> }
|
|
70
|
+
| { type: 'signal' };
|
|
71
|
+
|
|
72
|
+
const first: RaceResult = await Promise.race([
|
|
73
|
+
reader.read().then((chunk) => ({ type: 'data' as const, chunk })),
|
|
74
|
+
signalDetected.then(() => ({ type: 'signal' as const })),
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
// If data arrived first, still check signals — they may have fired
|
|
78
|
+
// concurrently. Also do a final ceiling timeout check for edge cases
|
|
79
|
+
// where the signal fires just after the first read resolves.
|
|
80
|
+
if (first.type === 'data' && !signals.redirectSignal && !signals.denySignal) {
|
|
81
|
+
// Brief yield to let any in-flight microtask rejections complete.
|
|
82
|
+
await new Promise<void>((r) => setTimeout(r, 0));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Detach the callback — no longer needed after this point.
|
|
86
|
+
signals.onSignal = undefined;
|
|
60
87
|
|
|
61
88
|
// Check for redirect/deny signals detected during initial rendering
|
|
62
89
|
const trackedRedirect = signals.redirectSignal as RedirectSignal | null;
|
|
@@ -75,6 +102,19 @@ export async function buildRscPayloadResponse(
|
|
|
75
102
|
);
|
|
76
103
|
}
|
|
77
104
|
|
|
105
|
+
// Extract the first chunk from the race result.
|
|
106
|
+
// If the signal won the race but neither redirect nor deny was detected
|
|
107
|
+
// (edge case), cancel the reader immediately rather than issuing a bare
|
|
108
|
+
// read() that could hang forever if the RSC stream has stalled.
|
|
109
|
+
// See TIM-519.
|
|
110
|
+
let firstRead: ReadableStreamReadResult<Uint8Array>;
|
|
111
|
+
if (first.type === 'data') {
|
|
112
|
+
firstRead = first.chunk;
|
|
113
|
+
} else {
|
|
114
|
+
await reader.cancel();
|
|
115
|
+
firstRead = { done: true, value: undefined };
|
|
116
|
+
}
|
|
117
|
+
|
|
78
118
|
// Reconstruct the stream: prepend the buffered first chunk,
|
|
79
119
|
// then continue piping from the original reader.
|
|
80
120
|
const patchedStream = new ReadableStream<Uint8Array>({
|
|
@@ -123,8 +163,8 @@ export async function buildRscPayloadResponse(
|
|
|
123
163
|
responseHeaders.set('X-Timber-Skipped-Segments', JSON.stringify(skippedSegments));
|
|
124
164
|
}
|
|
125
165
|
|
|
126
|
-
// Send route params so the client can populate
|
|
127
|
-
// SPA navigation. Without this,
|
|
166
|
+
// Send route params so the client can populate useSegmentParams() after
|
|
167
|
+
// SPA navigation. Without this, useSegmentParams() returns {}.
|
|
128
168
|
if (Object.keys(match.params).length > 0) {
|
|
129
169
|
responseHeaders.set('X-Timber-Params', JSON.stringify(match.params));
|
|
130
170
|
}
|
|
@@ -16,24 +16,38 @@ import { logRenderError } from '#/server/logger.js';
|
|
|
16
16
|
import { DenySignal, RedirectSignal, RenderError } from '#/server/primitives.js';
|
|
17
17
|
import { checkAndWarnRscPropError } from '#/server/rsc-prop-warnings.js';
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
createDebugChannelSink,
|
|
21
|
+
createDebugChannelCollector,
|
|
22
|
+
isAbortError,
|
|
23
|
+
type DebugComponentEntry,
|
|
24
|
+
} from './helpers.js';
|
|
20
25
|
import { isDebug } from '#/server/debug.js';
|
|
26
|
+
import { isDevMode } from '#/server/debug.js';
|
|
21
27
|
|
|
22
28
|
/**
|
|
23
29
|
* Mutable signal state captured during RSC rendering.
|
|
24
30
|
*
|
|
25
31
|
* Signals fire asynchronously via `onError` during stream consumption.
|
|
26
32
|
* The first signal of each type wins — subsequent signals are ignored.
|
|
33
|
+
*
|
|
34
|
+
* `onSignal` is an optional callback fired when a DenySignal or
|
|
35
|
+
* RedirectSignal is captured. Consumers use it with Promise.race to
|
|
36
|
+
* react immediately instead of polling with setTimeout/queueMicrotask.
|
|
27
37
|
*/
|
|
28
38
|
export interface RenderSignals {
|
|
29
39
|
denySignal: DenySignal | null;
|
|
30
40
|
redirectSignal: RedirectSignal | null;
|
|
31
41
|
renderError: { error: unknown; status: number } | null;
|
|
42
|
+
/** Callback fired when a redirect or deny signal is captured in onError. */
|
|
43
|
+
onSignal?: () => void;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
export interface RscStreamResult {
|
|
35
47
|
rscStream: ReadableStream<Uint8Array> | undefined;
|
|
36
48
|
signals: RenderSignals;
|
|
49
|
+
/** Dev-only: server component debug info from the Flight debug channel. */
|
|
50
|
+
getDebugComponents?: () => DebugComponentEntry[];
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
/**
|
|
@@ -56,6 +70,10 @@ export function renderRscStream(element: React.ReactElement, req: Request): RscS
|
|
|
56
70
|
|
|
57
71
|
let rscStream: ReadableStream<Uint8Array> | undefined;
|
|
58
72
|
|
|
73
|
+
// In dev mode, collect debug channel data for the error overlay.
|
|
74
|
+
// In production, use the discard sink (no overhead).
|
|
75
|
+
const debugChannel = isDevMode() ? createDebugChannelCollector() : createDebugChannelSink();
|
|
76
|
+
|
|
59
77
|
try {
|
|
60
78
|
rscStream = renderToReadableStream(
|
|
61
79
|
element,
|
|
@@ -67,11 +85,13 @@ export function renderRscStream(element: React.ReactElement, req: Request): RscS
|
|
|
67
85
|
if (isAbortError(error) || req.signal?.aborted) return;
|
|
68
86
|
if (error instanceof DenySignal) {
|
|
69
87
|
signals.denySignal = error;
|
|
88
|
+
signals.onSignal?.();
|
|
70
89
|
// Return structured digest for client-side error boundaries
|
|
71
90
|
return JSON.stringify({ type: 'deny', status: error.status, data: error.data });
|
|
72
91
|
}
|
|
73
92
|
if (error instanceof RedirectSignal) {
|
|
74
93
|
signals.redirectSignal = error;
|
|
94
|
+
signals.onSignal?.();
|
|
75
95
|
return JSON.stringify({
|
|
76
96
|
type: 'redirect',
|
|
77
97
|
location: error.location,
|
|
@@ -98,11 +118,7 @@ export function renderRscStream(element: React.ReactElement, req: Request): RscS
|
|
|
98
118
|
// directive isn't at the very top of the file, or the component is
|
|
99
119
|
// re-exported through a barrel file without 'use client'.
|
|
100
120
|
// See LOCAL-297.
|
|
101
|
-
if (
|
|
102
|
-
isDebug() &&
|
|
103
|
-
error instanceof Error &&
|
|
104
|
-
error.message.includes('Invalid hook call')
|
|
105
|
-
) {
|
|
121
|
+
if (isDebug() && error instanceof Error && error.message.includes('Invalid hook call')) {
|
|
106
122
|
console.error(
|
|
107
123
|
'[timber] A React hook was called during RSC rendering. This usually means a ' +
|
|
108
124
|
"'use client' component is being executed as a server component instead of " +
|
|
@@ -122,13 +138,25 @@ export function renderRscStream(element: React.ReactElement, req: Request): RscS
|
|
|
122
138
|
checkAndWarnRscPropError(error, new URL(req.url).pathname);
|
|
123
139
|
}
|
|
124
140
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
// Log the error but do NOT track it as a page-level render error.
|
|
142
|
+
// If this error is inside a <Suspense> boundary, React will emit
|
|
143
|
+
// an error row in the Flight stream and the Suspense fallback will
|
|
144
|
+
// render on the client. Tracking it as signals.renderError would
|
|
145
|
+
// cause the pipeline to treat the entire page as a 500, even though
|
|
146
|
+
// the shell rendered successfully. See TIM-524.
|
|
147
|
+
//
|
|
148
|
+
// Only track as renderError if no Suspense boundary contains it —
|
|
149
|
+
// React will call onShellError for truly unrecoverable errors.
|
|
129
150
|
logRenderError({ method: req.method, path: new URL(req.url).pathname, error });
|
|
151
|
+
|
|
152
|
+
// Return a digest so React emits a per-row error in the Flight
|
|
153
|
+
// stream instead of leaving the lazy reference unresolved.
|
|
154
|
+
return JSON.stringify({
|
|
155
|
+
type: 'error',
|
|
156
|
+
message: error instanceof Error ? error.message : String(error),
|
|
157
|
+
});
|
|
130
158
|
},
|
|
131
|
-
debugChannel
|
|
159
|
+
debugChannel,
|
|
132
160
|
},
|
|
133
161
|
{
|
|
134
162
|
onClientReference(info: { id: string; name: string; deps: unknown }) {
|
|
@@ -156,5 +184,14 @@ export function renderRscStream(element: React.ReactElement, req: Request): RscS
|
|
|
156
184
|
}
|
|
157
185
|
}
|
|
158
186
|
|
|
159
|
-
return {
|
|
187
|
+
return {
|
|
188
|
+
rscStream,
|
|
189
|
+
signals,
|
|
190
|
+
// Expose the debug channel collector's getComponents in dev mode.
|
|
191
|
+
// The caller can retrieve component tree info when handling errors.
|
|
192
|
+
getDebugComponents:
|
|
193
|
+
'getComponents' in debugChannel
|
|
194
|
+
? (debugChannel as { getComponents: () => DebugComponentEntry[] }).getComponents
|
|
195
|
+
: undefined,
|
|
196
|
+
};
|
|
160
197
|
}
|