@timber-js/app 0.2.0-alpha.4 → 0.2.0-alpha.40
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-ECi_61pb.js +108 -0
- package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
- package/dist/_chunks/define-cookie-BmKbSyp0.js +93 -0
- package/dist/_chunks/define-cookie-BmKbSyp0.js.map +1 -0
- package/dist/_chunks/error-boundary-BAN3751q.js +211 -0
- package/dist/_chunks/error-boundary-BAN3751q.js.map +1 -0
- package/dist/_chunks/{format-CwdaB0_2.js → format-cX7wzEp2.js} +2 -2
- package/dist/_chunks/{format-CwdaB0_2.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-CZJi4CuK.js → request-context-BxYIJM24.js} +93 -69
- package/dist/_chunks/request-context-BxYIJM24.js.map +1 -0
- package/dist/_chunks/segment-context-C6byCyZU.js +69 -0
- package/dist/_chunks/segment-context-C6byCyZU.js.map +1 -0
- package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
- package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
- package/dist/_chunks/{tracing-Cwn7697K.js → tracing-CuXiCP5p.js} +17 -3
- package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-CuXiCP5p.js.map} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
- package/dist/_chunks/wrappers-C6J0nNji.js +331 -0
- package/dist/_chunks/wrappers-C6J0nNji.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 +88 -18
- 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.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/fonts/local.d.ts +4 -2
- package/dist/fonts/local.d.ts.map +1 -1
- package/dist/index.d.ts +112 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +635 -233
- 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 +104 -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 +7 -0
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +9 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/mdx.d.ts +6 -0
- package/dist/plugins/mdx.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 +153 -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 +3 -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 +46 -15
- package/dist/server/debug.d.ts.map +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 +78 -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 +5 -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 +1975 -1649
- 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 +77 -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/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 +34 -26
- package/src/cli.ts +0 -0
- package/src/client/browser-entry.ts +94 -90
- 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/fonts/local.ts +7 -3
- 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 +12 -11
- package/src/plugins/fonts.ts +171 -19
- package/src/plugins/mdx.ts +9 -5
- 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 +504 -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 +14 -5
- 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 +55 -17
- 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 +152 -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 +103 -66
- package/src/server/index.ts +9 -4
- package/src/server/logger.ts +38 -35
- package/src/server/node-stream-transforms.ts +381 -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 +112 -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 +125 -49
- package/src/server/rsc-entry/rsc-payload.ts +52 -12
- package/src/server/rsc-entry/rsc-stream.ts +33 -8
- package/src/server/rsc-entry/ssr-renderer.ts +40 -13
- package/src/server/slot-resolver.ts +199 -210
- package/src/server/ssr-entry.ts +169 -17
- package/src/server/ssr-render.ts +266 -67
- 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-B4WUeqJ-.js +0 -75
- package/dist/_chunks/debug-B4WUeqJ-.js.map +0 -1
- package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
- package/dist/_chunks/request-context-CZJi4CuK.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
|
@@ -28,12 +28,24 @@ import { DenySignal, RedirectSignal } from './primitives.js';
|
|
|
28
28
|
import { AccessGate } from './access-gate.js';
|
|
29
29
|
import { resolveSlotElement } from './slot-resolver.js';
|
|
30
30
|
import { SegmentProvider } from '#/client/segment-context.js';
|
|
31
|
-
|
|
32
|
-
import type { SearchParamsDefinition } from '#/search-params/create.js';
|
|
31
|
+
|
|
33
32
|
import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.js';
|
|
34
33
|
import type { InterceptionContext } from './pipeline.js';
|
|
35
34
|
import { shouldSkipSegment } from './state-tree-diff.js';
|
|
36
35
|
|
|
36
|
+
// ─── Param Coercion Error ─────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Thrown when a defineSegmentParams codec's parse() fails.
|
|
40
|
+
* The pipeline catches this and responds with 404.
|
|
41
|
+
*/
|
|
42
|
+
export class ParamCoercionError extends Error {
|
|
43
|
+
constructor(message: string) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'ParamCoercionError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
// ─── Types ────────────────────────────────────────────────────────────────
|
|
38
50
|
|
|
39
51
|
/** Head element for client-side metadata updates. */
|
|
@@ -84,6 +96,64 @@ export class RouteSignalWithContext extends Error {
|
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
98
|
|
|
99
|
+
// ─── Module Processing Helpers ─────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reject the legacy `generateMetadata` export with a helpful migration message.
|
|
103
|
+
* Throws if the module exports `generateMetadata` instead of `metadata`.
|
|
104
|
+
*/
|
|
105
|
+
function rejectLegacyGenerateMetadata(mod: Record<string, unknown>, filePath: string): void {
|
|
106
|
+
if ('generateMetadata' in mod) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`${filePath}: "generateMetadata" is not a valid export. ` +
|
|
109
|
+
`Export an async function named "metadata" instead.\n\n` +
|
|
110
|
+
` // Before\n` +
|
|
111
|
+
` export async function generateMetadata({ params }) { ... }\n\n` +
|
|
112
|
+
` // After\n` +
|
|
113
|
+
` export async function metadata() { ... }`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extract and resolve metadata from a module (layout or page).
|
|
120
|
+
* Handles both static metadata objects and async metadata functions.
|
|
121
|
+
* Returns the resolved Metadata, or null if none exported.
|
|
122
|
+
*
|
|
123
|
+
* Metadata functions no longer receive { params } — they access params
|
|
124
|
+
* via rawSegmentParams() from ALS, same as page/layout components.
|
|
125
|
+
*/
|
|
126
|
+
async function extractMetadata(
|
|
127
|
+
mod: Record<string, unknown>,
|
|
128
|
+
segment: ManifestSegmentNode
|
|
129
|
+
): Promise<Metadata | null> {
|
|
130
|
+
if (typeof mod.metadata === 'function') {
|
|
131
|
+
type MetadataFn = () => Promise<Metadata>;
|
|
132
|
+
return (
|
|
133
|
+
(await withSpan(
|
|
134
|
+
'timber.metadata',
|
|
135
|
+
{ 'timber.segment': segment.segmentName ?? segment.urlPath },
|
|
136
|
+
() => (mod.metadata as MetadataFn)()
|
|
137
|
+
)) ?? null
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (mod.metadata) {
|
|
141
|
+
return mod.metadata as Metadata;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Extract `deferSuspenseFor` from a module and return the maximum
|
|
148
|
+
* of the current value and the module's value.
|
|
149
|
+
*/
|
|
150
|
+
function extractDeferSuspenseFor(mod: Record<string, unknown>, current: number): number {
|
|
151
|
+
if (typeof mod.deferSuspenseFor === 'number' && mod.deferSuspenseFor > current) {
|
|
152
|
+
return mod.deferSuspenseFor;
|
|
153
|
+
}
|
|
154
|
+
return current;
|
|
155
|
+
}
|
|
156
|
+
|
|
87
157
|
// ─── Builder ──────────────────────────────────────────────────────────────
|
|
88
158
|
|
|
89
159
|
/**
|
|
@@ -104,9 +174,6 @@ export async function buildRouteElement(
|
|
|
104
174
|
): Promise<RouteElementResult> {
|
|
105
175
|
const segments = match.segments as unknown as ManifestSegmentNode[];
|
|
106
176
|
|
|
107
|
-
// Params are passed as a Promise to match Next.js 15+ convention.
|
|
108
|
-
const paramsPromise = Promise.resolve(match.params);
|
|
109
|
-
|
|
110
177
|
// Load all modules along the segment chain
|
|
111
178
|
const metadataEntries: Array<{ metadata: Metadata; isPage: boolean }> = [];
|
|
112
179
|
const layoutComponents: LayoutComponentEntry[] = [];
|
|
@@ -126,87 +193,34 @@ export async function buildRouteElement(
|
|
|
126
193
|
segment,
|
|
127
194
|
});
|
|
128
195
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
` // After\n` +
|
|
138
|
-
` export async function metadata({ params }) { ... }`
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
// Unified metadata export: static object or async function
|
|
142
|
-
if (typeof mod.metadata === 'function') {
|
|
143
|
-
type MetadataFn = (props: Record<string, unknown>) => Promise<Metadata>;
|
|
144
|
-
const generated = await withSpan(
|
|
145
|
-
'timber.metadata',
|
|
146
|
-
{ 'timber.segment': segment.segmentName ?? segment.urlPath },
|
|
147
|
-
() => (mod.metadata as MetadataFn)({ params: paramsPromise })
|
|
148
|
-
);
|
|
149
|
-
if (generated) {
|
|
150
|
-
metadataEntries.push({ metadata: generated, isPage: false });
|
|
151
|
-
}
|
|
152
|
-
} else if (mod.metadata) {
|
|
153
|
-
metadataEntries.push({ metadata: mod.metadata as Metadata, isPage: false });
|
|
154
|
-
}
|
|
155
|
-
// deferSuspenseFor hold window — max across all segments
|
|
156
|
-
if (typeof mod.deferSuspenseFor === 'number' && mod.deferSuspenseFor > deferSuspenseFor) {
|
|
157
|
-
deferSuspenseFor = mod.deferSuspenseFor;
|
|
196
|
+
|
|
197
|
+
// Param coercion is handled in the pipeline (Stage 2c) before
|
|
198
|
+
// middleware and rendering. See coerceSegmentParams() in pipeline.ts.
|
|
199
|
+
|
|
200
|
+
rejectLegacyGenerateMetadata(mod, segment.layout.filePath ?? segment.urlPath);
|
|
201
|
+
const layoutMetadata = await extractMetadata(mod, segment);
|
|
202
|
+
if (layoutMetadata) {
|
|
203
|
+
metadataEntries.push({ metadata: layoutMetadata, isPage: false });
|
|
158
204
|
}
|
|
205
|
+
deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
|
|
159
206
|
}
|
|
160
207
|
|
|
161
208
|
// Load page (leaf segment only)
|
|
162
209
|
if (isLeaf && segment.page) {
|
|
163
|
-
// Load and apply search-params.ts definition before rendering so
|
|
164
|
-
// searchParams() from @timber-js/app/server returns parsed typed values.
|
|
165
|
-
if (segment.searchParams) {
|
|
166
|
-
const spMod = (await segment.searchParams.load()) as {
|
|
167
|
-
default?: SearchParamsDefinition<Record<string, unknown>>;
|
|
168
|
-
};
|
|
169
|
-
if (spMod.default) {
|
|
170
|
-
const rawSearchParams = new URL(req.url).searchParams;
|
|
171
|
-
const parsed = spMod.default.parse(rawSearchParams);
|
|
172
|
-
setParsedSearchParams(parsed);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
210
|
const mod = (await segment.page.load()) as Record<string, unknown>;
|
|
211
|
+
|
|
212
|
+
// Param coercion is handled in the pipeline (Stage 2c) before
|
|
213
|
+
// middleware and rendering. See coerceSegmentParams() in pipeline.ts.
|
|
214
|
+
|
|
177
215
|
if (mod.default) {
|
|
178
216
|
PageComponent = mod.default as (...args: unknown[]) => unknown;
|
|
179
217
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
`${filePath}: "generateMetadata" is not a valid export. ` +
|
|
185
|
-
`Export an async function named "metadata" instead.\n\n` +
|
|
186
|
-
` // Before\n` +
|
|
187
|
-
` export async function generateMetadata({ params }) { ... }\n\n` +
|
|
188
|
-
` // After\n` +
|
|
189
|
-
` export async function metadata({ params }) { ... }`
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
// Unified metadata export: static object or async function
|
|
193
|
-
if (typeof mod.metadata === 'function') {
|
|
194
|
-
type MetadataFn = (props: Record<string, unknown>) => Promise<Metadata>;
|
|
195
|
-
const generated = await withSpan(
|
|
196
|
-
'timber.metadata',
|
|
197
|
-
{ 'timber.segment': segment.segmentName ?? segment.urlPath },
|
|
198
|
-
() => (mod.metadata as MetadataFn)({ params: paramsPromise })
|
|
199
|
-
);
|
|
200
|
-
if (generated) {
|
|
201
|
-
metadataEntries.push({ metadata: generated, isPage: true });
|
|
202
|
-
}
|
|
203
|
-
} else if (mod.metadata) {
|
|
204
|
-
metadataEntries.push({ metadata: mod.metadata as Metadata, isPage: true });
|
|
205
|
-
}
|
|
206
|
-
// deferSuspenseFor hold window — max across all segments
|
|
207
|
-
if (typeof mod.deferSuspenseFor === 'number' && mod.deferSuspenseFor > deferSuspenseFor) {
|
|
208
|
-
deferSuspenseFor = mod.deferSuspenseFor;
|
|
218
|
+
rejectLegacyGenerateMetadata(mod, segment.page.filePath ?? segment.urlPath);
|
|
219
|
+
const pageMetadata = await extractMetadata(mod, segment);
|
|
220
|
+
if (pageMetadata) {
|
|
221
|
+
metadataEntries.push({ metadata: pageMetadata, isPage: true });
|
|
209
222
|
}
|
|
223
|
+
deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
|
|
210
224
|
}
|
|
211
225
|
}
|
|
212
226
|
|
|
@@ -227,7 +241,7 @@ export async function buildRouteElement(
|
|
|
227
241
|
if (segment.access) {
|
|
228
242
|
const accessMod = (await segment.access.load()) as Record<string, unknown>;
|
|
229
243
|
const accessFn = accessMod.default as
|
|
230
|
-
| ((ctx: { params: Record<string, string | string[]
|
|
244
|
+
| ((ctx: { params: Record<string, string | string[]> }) => unknown)
|
|
231
245
|
| undefined;
|
|
232
246
|
if (accessFn) {
|
|
233
247
|
try {
|
|
@@ -236,7 +250,7 @@ export async function buildRouteElement(
|
|
|
236
250
|
{ 'timber.segment': segment.segmentName ?? 'unknown' },
|
|
237
251
|
async () => {
|
|
238
252
|
try {
|
|
239
|
-
await accessFn({ params: match.params
|
|
253
|
+
await accessFn({ params: match.params });
|
|
240
254
|
await setSpanAttribute('timber.result', 'pass');
|
|
241
255
|
accessVerdicts.set(si, 'pass');
|
|
242
256
|
} catch (error) {
|
|
@@ -302,10 +316,7 @@ export async function buildRouteElement(
|
|
|
302
316
|
);
|
|
303
317
|
};
|
|
304
318
|
|
|
305
|
-
let element = h(TracedPage, {
|
|
306
|
-
params: paramsPromise,
|
|
307
|
-
searchParams: {},
|
|
308
|
-
});
|
|
319
|
+
let element = h(TracedPage, {});
|
|
309
320
|
|
|
310
321
|
// Build a lookup of layout components by segment for O(1) access.
|
|
311
322
|
const layoutBySegment = new Map(
|
|
@@ -352,12 +363,7 @@ export async function buildRouteElement(
|
|
|
352
363
|
// same urlPath (e.g., /(marketing) and /(app) both have "/"),
|
|
353
364
|
// which would cause the wrong cached layout to be reused
|
|
354
365
|
const skip =
|
|
355
|
-
shouldSkipSegment(
|
|
356
|
-
segment.urlPath,
|
|
357
|
-
layoutComponent,
|
|
358
|
-
isLeaf,
|
|
359
|
-
clientStateTree ?? null
|
|
360
|
-
) &&
|
|
366
|
+
shouldSkipSegment(segment.urlPath, layoutComponent, isLeaf, clientStateTree ?? null) &&
|
|
361
367
|
hasRenderedLayoutBelow &&
|
|
362
368
|
segment.segmentType !== 'group';
|
|
363
369
|
|
|
@@ -385,13 +391,11 @@ export async function buildRouteElement(
|
|
|
385
391
|
if (segment.access) {
|
|
386
392
|
const accessMod = (await segment.access.load()) as Record<string, unknown>;
|
|
387
393
|
const accessFn = accessMod.default as
|
|
388
|
-
| ((ctx: { params: Record<string, string | string[]
|
|
394
|
+
| ((ctx: { params: Record<string, string | string[]> }) => unknown)
|
|
389
395
|
| undefined;
|
|
390
396
|
if (accessFn) {
|
|
391
397
|
element = h(AccessGate, {
|
|
392
398
|
accessFn,
|
|
393
|
-
params: match.params,
|
|
394
|
-
searchParams: {},
|
|
395
399
|
segmentName: segment.segmentName,
|
|
396
400
|
verdict: accessVerdicts.get(i),
|
|
397
401
|
children: element,
|
|
@@ -408,7 +412,6 @@ export async function buildRouteElement(
|
|
|
408
412
|
slotProps[slotName] = await resolveSlotElement(
|
|
409
413
|
slotNode as ManifestSegmentNode,
|
|
410
414
|
match,
|
|
411
|
-
paramsPromise,
|
|
412
415
|
h,
|
|
413
416
|
interception
|
|
414
417
|
);
|
|
@@ -417,39 +420,28 @@ export async function buildRouteElement(
|
|
|
417
420
|
const segmentPath = segment.urlPath.split('/');
|
|
418
421
|
const parallelRouteKeys = Object.keys(segment.slots ?? {});
|
|
419
422
|
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
// from the root "layout /".
|
|
424
|
-
const segmentForSpan = segment;
|
|
425
|
-
const layoutComponentForSpan = layoutComponent;
|
|
426
|
-
const segmentLabel =
|
|
427
|
-
segmentForSpan.segmentType === 'group'
|
|
428
|
-
? `${segmentForSpan.urlPath === '/' ? '' : segmentForSpan.urlPath}/${segmentForSpan.segmentName}`
|
|
429
|
-
: segmentForSpan.urlPath;
|
|
430
|
-
const TracedLayout = async (props: Record<string, unknown>) => {
|
|
431
|
-
return withSpan('timber.layout', { 'timber.segment': segmentLabel }, () =>
|
|
432
|
-
(layoutComponentForSpan as (props: Record<string, unknown>) => unknown)(props)
|
|
433
|
-
);
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
// segmentId uniquely identifies this segment for client-side element
|
|
437
|
-
// caching. For route groups, urlPath is shared with the parent (both "/"),
|
|
438
|
-
// so we include the group name to distinguish them. Without this, the
|
|
439
|
-
// segment merger's element cache would conflate root and group elements.
|
|
423
|
+
// For route groups, urlPath is shared with the parent (both "/"),
|
|
424
|
+
// so include the group name to distinguish them. Used for both OTEL
|
|
425
|
+
// span labels and client-side element caching (segmentId).
|
|
440
426
|
const segmentId =
|
|
441
427
|
segment.segmentType === 'group'
|
|
442
428
|
? `${segment.urlPath === '/' ? '' : segment.urlPath}/${segment.segmentName}`
|
|
443
429
|
: segment.urlPath;
|
|
444
430
|
|
|
431
|
+
// Wrap the layout component in an OTEL span
|
|
432
|
+
const layoutComponentRef = layoutComponent;
|
|
433
|
+
const TracedLayout = async (props: Record<string, unknown>) => {
|
|
434
|
+
return withSpan('timber.layout', { 'timber.segment': segmentId }, () =>
|
|
435
|
+
(layoutComponentRef as (props: Record<string, unknown>) => unknown)(props)
|
|
436
|
+
);
|
|
437
|
+
};
|
|
438
|
+
|
|
445
439
|
element = h(SegmentProvider, {
|
|
446
440
|
segments: segmentPath,
|
|
447
441
|
segmentId,
|
|
448
442
|
parallelRouteKeys,
|
|
449
443
|
children: h(TracedLayout, {
|
|
450
444
|
...slotProps,
|
|
451
|
-
params: paramsPromise,
|
|
452
|
-
searchParams: {},
|
|
453
445
|
children: element,
|
|
454
446
|
}),
|
|
455
447
|
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { RouteContext } from './types.js';
|
|
12
|
+
import { logRouteError } from './logger.js';
|
|
12
13
|
|
|
13
14
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
14
15
|
|
|
@@ -122,7 +123,7 @@ async function runHandler(handler: RouteHandler, ctx: RouteContext): Promise<Res
|
|
|
122
123
|
const res = await handler(ctx);
|
|
123
124
|
return mergeResponseHeaders(res, ctx.headers);
|
|
124
125
|
} catch (error) {
|
|
125
|
-
|
|
126
|
+
logRouteError({ method: ctx.req.method, path: new URL(ctx.req.url).pathname, error });
|
|
126
127
|
return new Response(null, { status: 500 });
|
|
127
128
|
}
|
|
128
129
|
}
|
|
@@ -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
|