@timber-js/app 0.2.0-alpha.158 → 0.2.0-alpha.159
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{actions-BA5tIpVu.js → actions-CBbP1hHO.js} +3 -3
- package/dist/_chunks/{actions-BA5tIpVu.js.map → actions-CBbP1hHO.js.map} +1 -1
- package/dist/_chunks/{build-output-helper-C8q4Zpf5.js → build-output-helper-DF5uScqS.js} +4 -1
- package/dist/_chunks/{build-output-helper-C8q4Zpf5.js.map → build-output-helper-DF5uScqS.js.map} +1 -1
- package/dist/_chunks/cache-api-DsWEYwVZ.js +1120 -0
- package/dist/_chunks/cache-api-DsWEYwVZ.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-DcmQrzJG.js → cli-schema-sync-Bivx4Rn7.js} +4 -3
- package/dist/_chunks/{cli-schema-sync-DcmQrzJG.js.map → cli-schema-sync-Bivx4Rn7.js.map} +1 -1
- package/dist/_chunks/dist-BA3u1z3W.js +423 -0
- package/dist/_chunks/dist-BA3u1z3W.js.map +1 -0
- package/dist/_chunks/{logger-C5v1IMuh.js → logger-CrRmZMlo.js} +81 -56
- package/dist/_chunks/logger-CrRmZMlo.js.map +1 -0
- package/dist/_chunks/{plugin-context-DUl3dnCF.js → plugin-context-DeAxFRMq.js} +3 -36
- package/dist/_chunks/plugin-context-DeAxFRMq.js.map +1 -0
- package/dist/_chunks/sizeof-lXffQ1Vr.js +35 -0
- package/dist/_chunks/sizeof-lXffQ1Vr.js.map +1 -0
- package/dist/_chunks/{walkers-D5jCGPum.js → walkers-DXbJkeCY.js} +217 -19
- package/dist/_chunks/walkers-DXbJkeCY.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare.d.ts +28 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +143 -9
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/nitro.js +1 -1
- package/dist/cache/cache-api.d.ts +2 -0
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/handler-store.d.ts +2 -0
- package/dist/cache/handler-store.d.ts.map +1 -1
- package/dist/cache/index.d.ts +2 -0
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +3 -186
- package/dist/cache/redis-handler.d.ts +17 -5
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts +1 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/store.d.ts +53 -0
- package/dist/cache/store.d.ts.map +1 -0
- package/dist/cache/stores/cloudflare-kv.d.ts +38 -0
- package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -0
- package/dist/cache/stores/cloudflare-kv.js +29 -0
- package/dist/cache/stores/cloudflare-kv.js.map +1 -0
- package/dist/cache/stores/memory.d.ts +42 -0
- package/dist/cache/stores/memory.d.ts.map +1 -0
- package/dist/cache/stores/memory.js +90 -0
- package/dist/cache/stores/memory.js.map +1 -0
- package/dist/cache/stores/redis.d.ts +69 -0
- package/dist/cache/stores/redis.d.ts.map +1 -0
- package/dist/cache/stores/redis.js +35 -0
- package/dist/cache/stores/redis.js.map +1 -0
- package/dist/cache/stores/vercel.d.ts +36 -0
- package/dist/cache/stores/vercel.d.ts.map +1 -0
- package/dist/cache/stores/vercel.js +33 -0
- package/dist/cache/stores/vercel.js.map +1 -0
- package/dist/cache/tag-aware-handler.d.ts +51 -0
- package/dist/cache/tag-aware-handler.d.ts.map +1 -0
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cdn/cloudflare-purge.d.ts +23 -0
- package/dist/cdn/cloudflare-purge.d.ts.map +1 -0
- package/dist/cdn/fastly-purge.d.ts +25 -0
- package/dist/cdn/fastly-purge.d.ts.map +1 -0
- package/dist/cdn/headers.d.ts +82 -0
- package/dist/cdn/headers.d.ts.map +1 -0
- package/dist/cdn/index.d.ts +5 -0
- package/dist/cdn/index.d.ts.map +1 -0
- package/dist/cdn/purge-store.d.ts +23 -0
- package/dist/cdn/purge-store.d.ts.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/history.d.ts +0 -3
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +0 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +11 -86
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-api.d.ts +1 -3
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/router.d.ts +1 -4
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +1 -11
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +1 -3
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/config-types.d.ts +3 -1
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/index.js +15 -11
- package/dist/cookies/index.js.map +1 -1
- package/dist/dev-tools/browser-logs.d.ts +28 -72
- package/dist/dev-tools/browser-logs.d.ts.map +1 -1
- package/dist/dev-tools/index.d.ts +1 -1
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/dev-tools/logger.d.ts.map +1 -1
- package/dist/dev-tools/terminal.d.ts.map +1 -1
- package/dist/fonts/transform.d.ts +4 -0
- package/dist/fonts/transform.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1269 -359
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +36 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +21 -0
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +5 -14
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/callsite-ast.d.ts +95 -0
- package/dist/plugins/callsite-ast.d.ts.map +1 -0
- package/dist/plugins/callsite-jsx.d.ts +35 -0
- package/dist/plugins/callsite-jsx.d.ts.map +1 -0
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/prebuilt-capture.d.ts +60 -0
- package/dist/plugins/prebuilt-capture.d.ts.map +1 -0
- package/dist/plugins/prebuilt.d.ts +82 -0
- package/dist/plugins/prebuilt.d.ts.map +1 -0
- package/dist/plugins/prerender-sugar.d.ts +42 -0
- package/dist/plugins/prerender-sugar.d.ts.map +1 -0
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/export-detect.d.ts +56 -0
- package/dist/routing/export-detect.d.ts.map +1 -0
- package/dist/routing/index.js +2 -2
- package/dist/routing/manifest-codegen.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/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +0 -3
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +7 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/cookie-parsing.d.ts +15 -1
- package/dist/server/cookie-parsing.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +1 -0
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.d.ts +6 -1
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +563 -74
- package/dist/server/internal.js.map +1 -1
- package/dist/server/prebuilt/cache-key.d.ts +58 -0
- package/dist/server/prebuilt/cache-key.d.ts.map +1 -0
- package/dist/server/prebuilt/capture-state.d.ts +57 -0
- package/dist/server/prebuilt/capture-state.d.ts.map +1 -0
- package/dist/server/prebuilt/manifest-tag-index.d.ts +27 -0
- package/dist/server/prebuilt/manifest-tag-index.d.ts.map +1 -0
- package/dist/server/prebuilt/overlay.d.ts +100 -0
- package/dist/server/prebuilt/overlay.d.ts.map +1 -0
- package/dist/server/prebuilt/payload-source.d.ts +75 -0
- package/dist/server/prebuilt/payload-source.d.ts.map +1 -0
- package/dist/server/prebuilt/synthetic-store.d.ts +64 -0
- package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -0
- package/dist/server/prebuilt-builder.d.ts +110 -0
- package/dist/server/prebuilt-builder.d.ts.map +1 -0
- package/dist/server/prebuilt-runtime.d.ts +101 -0
- package/dist/server/prebuilt-runtime.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +10 -8
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +0 -5
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +10 -0
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +3 -4
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/sitemap-generator.d.ts +6 -6
- package/dist/server/sitemap-generator.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +37 -3
- package/dist/server/tracing.d.ts.map +1 -1
- package/docs/api/32-api-cache.mdx +10 -4
- package/docs/learn/00-introduction.mdx +8 -13
- package/docs/learn/02-pages-and-layouts.mdx +9 -0
- package/docs/learn/06-forms-and-actions.mdx +0 -4
- package/docs/learn/07-typed-routes.mdx +127 -0
- package/docs/learn/09-caching.mdx +12 -1
- package/docs/learn/10-middleware.mdx +1 -0
- package/docs/learn/11-error-handling.mdx +9 -0
- package/docs/learn/13-configuration.mdx +28 -0
- package/docs/learn/14-deploying.mdx +52 -0
- package/docs/more/06-instrumentation.mdx +27 -1
- package/docs/more/07-security.mdx +8 -2
- package/docs/more/08-developer-experience.mdx +134 -0
- package/package.json +31 -2
- package/src/adapters/build-output-helper.ts +10 -0
- package/src/adapters/cloudflare.ts +178 -4
- package/src/cache/cache-api.ts +60 -0
- package/src/cache/handler-store.ts +8 -0
- package/src/cache/index.ts +4 -0
- package/src/cache/redis-handler.ts +25 -13
- package/src/cache/singleflight.ts +11 -11
- package/src/cache/store.ts +71 -0
- package/src/cache/stores/cloudflare-kv.ts +52 -0
- package/src/cache/stores/memory.ts +125 -0
- package/src/cache/stores/redis.ts +97 -0
- package/src/cache/stores/vercel.ts +59 -0
- package/src/cache/tag-aware-handler.ts +233 -0
- package/src/cache/timber-cache.ts +17 -7
- package/src/cdn/cloudflare-purge.ts +50 -0
- package/src/cdn/fastly-purge.ts +63 -0
- package/src/cdn/headers.ts +127 -0
- package/src/cdn/index.ts +16 -0
- package/src/cdn/purge-store.ts +34 -0
- package/src/client/browser-entry/action-dispatch.ts +1 -10
- package/src/client/browser-entry/post-hydration.ts +0 -1
- package/src/client/browser-entry/router-init.ts +0 -5
- package/src/client/history.ts +0 -4
- package/src/client/navigation-api.ts +5 -11
- package/src/client/router.ts +9 -43
- package/src/client/rsc-fetch.ts +5 -27
- package/src/client/segment-cache.ts +1 -4
- package/src/client/use-cookie.ts +18 -12
- package/src/config-types.ts +3 -1
- package/src/config-validation.ts +1 -0
- package/src/dev-tools/browser-logs.ts +37 -273
- package/src/dev-tools/index.ts +2 -2
- package/src/dev-tools/instrumentation.ts +1 -1
- package/src/dev-tools/logger.ts +33 -51
- package/src/dev-tools/terminal.ts +21 -22
- package/src/fonts/transform.ts +4 -43
- package/src/index.ts +16 -3
- package/src/plugin-context.ts +36 -0
- package/src/plugins/build-report.ts +133 -1
- package/src/plugins/cache.ts +32 -131
- package/src/plugins/callsite-ast.ts +167 -0
- package/src/plugins/callsite-jsx.ts +243 -0
- package/src/plugins/entries.ts +23 -4
- package/src/plugins/fonts.ts +16 -2
- package/src/plugins/prebuilt-capture.ts +208 -0
- package/src/plugins/prebuilt.ts +673 -0
- package/src/plugins/prerender-sugar.ts +329 -0
- package/src/plugins/server-bundle.ts +12 -0
- package/src/plugins/shims.ts +9 -0
- package/src/plugins/static-build.ts +0 -1
- package/src/routing/codegen.ts +1 -21
- package/src/routing/convention-lint.ts +85 -49
- package/src/routing/export-detect.ts +304 -0
- package/src/routing/manifest-codegen.ts +2 -11
- package/src/rsc-runtime/rsc.ts +7 -0
- package/src/server/action-handler.ts +11 -27
- package/src/server/actions.ts +3 -4
- package/src/server/als-registry.ts +7 -0
- package/src/server/cookie-parsing.ts +75 -59
- package/src/server/deny-renderer.ts +7 -15
- package/src/server/html-injectors.ts +11 -3
- package/src/server/internal.ts +17 -1
- package/src/server/prebuilt/cache-key.ts +224 -0
- package/src/server/prebuilt/capture-state.ts +89 -0
- package/src/server/prebuilt/manifest-tag-index.ts +46 -0
- package/src/server/prebuilt/overlay.ts +256 -0
- package/src/server/prebuilt/payload-source.ts +192 -0
- package/src/server/prebuilt/synthetic-store.ts +108 -0
- package/src/server/prebuilt-builder.ts +640 -0
- package/src/server/prebuilt-runtime.ts +706 -0
- package/src/server/route-element-builder.ts +40 -12
- package/src/server/rsc-entry/helpers.ts +0 -17
- package/src/server/rsc-entry/index.ts +83 -0
- package/src/server/rsc-entry/render-route.ts +6 -33
- package/src/server/rsc-entry/revalidate-renderer.ts +16 -11
- package/src/server/rsc-entry/rsc-payload.ts +5 -11
- package/src/server/sitemap-generator.ts +16 -10
- package/src/server/tracing.ts +88 -3
- package/dist/_chunks/cache-api-Cbyh3RW4.js +0 -401
- package/dist/_chunks/cache-api-Cbyh3RW4.js.map +0 -1
- package/dist/_chunks/logger-C5v1IMuh.js.map +0 -1
- package/dist/_chunks/plugin-context-DUl3dnCF.js.map +0 -1
- package/dist/_chunks/validation-CnKhGJsp.js +0 -113
- package/dist/_chunks/validation-CnKhGJsp.js.map +0 -1
- package/dist/_chunks/walkers-D5jCGPum.js.map +0 -1
- package/dist/cache/index.js.map +0 -1
- package/dist/client/head.d.ts +0 -16
- package/dist/client/head.d.ts.map +0 -1
- package/dist/client/navigation-api-types.d.ts +0 -90
- package/dist/client/navigation-api-types.d.ts.map +0 -1
- package/dist/server/client-module-map.d.ts +0 -47
- package/dist/server/client-module-map.d.ts.map +0 -1
- package/dist/shared/head-header.d.ts +0 -54
- package/dist/shared/head-header.d.ts.map +0 -1
- package/src/client/head.ts +0 -61
- package/src/client/navigation-api-types.ts +0 -112
- package/src/server/client-module-map.ts +0 -58
- package/src/shared/head-header.ts +0 -69
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* See design/02-rendering-pipeline.md, design/04-authorization.md
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { createElement } from 'react';
|
|
18
|
+
import { createElement, Fragment } from 'react';
|
|
19
19
|
import { randomUUID } from 'node:crypto';
|
|
20
20
|
|
|
21
21
|
import { withSpan } from './tracing.js';
|
|
@@ -48,6 +48,28 @@ import { SegmentOutlet } from '../client/segment-outlet.js';
|
|
|
48
48
|
import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.js';
|
|
49
49
|
import type { InterceptionContext } from './pipeline.js';
|
|
50
50
|
import { shouldSkipSegment } from './state-tree-diff.js';
|
|
51
|
+
import type { HeadElement } from './metadata.js';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Convert HeadElement descriptors to React elements for Float hoisting.
|
|
55
|
+
*
|
|
56
|
+
* React 19 Float hoists <title>, <meta>, and <link> rendered anywhere in
|
|
57
|
+
* the tree into <head> during SSR, and manages them in document.head
|
|
58
|
+
* during client-side rendering. This replaces the X-Timber-Head header
|
|
59
|
+
* side-channel and the imperative client DOM applier.
|
|
60
|
+
*/
|
|
61
|
+
export function headElementsToReact(elements: HeadElement[]): React.ReactElement {
|
|
62
|
+
const children: React.ReactElement[] = [];
|
|
63
|
+
for (let i = 0; i < elements.length; i++) {
|
|
64
|
+
const el = elements[i];
|
|
65
|
+
if (el.tag === 'title' && el.content !== undefined) {
|
|
66
|
+
children.push(createElement('title', { key: `t${i}` }, el.content));
|
|
67
|
+
} else if (el.attrs) {
|
|
68
|
+
children.push(createElement(el.tag, { key: `h${i}`, ...el.attrs }));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return createElement(Fragment, null, ...children);
|
|
72
|
+
}
|
|
51
73
|
import { loadModule } from './safe-load.js';
|
|
52
74
|
|
|
53
75
|
/**
|
|
@@ -119,13 +141,6 @@ export class ParamCoercionError extends Error {
|
|
|
119
141
|
|
|
120
142
|
// ─── Types ────────────────────────────────────────────────────────────────
|
|
121
143
|
|
|
122
|
-
/** Head element for client-side metadata updates. */
|
|
123
|
-
export interface HeadElement {
|
|
124
|
-
tag: string;
|
|
125
|
-
content?: string;
|
|
126
|
-
attrs?: Record<string, string | null>;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
144
|
/** Layout entry with component and segment. */
|
|
130
145
|
export interface LayoutComponentEntry {
|
|
131
146
|
component: (...args: unknown[]) => unknown;
|
|
@@ -136,8 +151,6 @@ export interface LayoutComponentEntry {
|
|
|
136
151
|
export interface RouteElementResult {
|
|
137
152
|
/** The React element tree (page wrapped in layouts, access gates, error boundaries). */
|
|
138
153
|
element: React.ReactElement;
|
|
139
|
-
/** Resolved head elements for metadata. */
|
|
140
|
-
headElements: HeadElement[];
|
|
141
154
|
/** Layout components loaded along the segment chain. */
|
|
142
155
|
layoutComponents: LayoutComponentEntry[];
|
|
143
156
|
/** Segments from the route match. */
|
|
@@ -358,7 +371,12 @@ export async function buildRouteElement(
|
|
|
358
371
|
}
|
|
359
372
|
}
|
|
360
373
|
|
|
361
|
-
// Resolve metadata
|
|
374
|
+
// Resolve metadata and convert to React elements that React 19 Float
|
|
375
|
+
// hoists into <head> during both SSR and client rendering. This replaces
|
|
376
|
+
// the previous HeadElement descriptor system (string splice for SSR,
|
|
377
|
+
// X-Timber-Head header for RSC nav, imperative DOM updates on client).
|
|
378
|
+
// React manages <title>/<meta>/<link> lifecycle across navigations.
|
|
379
|
+
// See design/16-metadata.md, TIM-1151.
|
|
362
380
|
const resolvedMetadata = resolveMetadata(metadataEntries);
|
|
363
381
|
const headElements = renderMetadataToElements(resolvedMetadata);
|
|
364
382
|
|
|
@@ -423,6 +441,11 @@ export async function buildRouteElement(
|
|
|
423
441
|
}
|
|
424
442
|
}
|
|
425
443
|
|
|
444
|
+
// Convert HeadElement descriptors to React elements for Float hoisting.
|
|
445
|
+
// Placed at the page segment level so they're always re-rendered on
|
|
446
|
+
// SPA navigation (sync layouts may be skipped by segment tree diffing).
|
|
447
|
+
const metadataElement = headElementsToReact(headElements);
|
|
448
|
+
|
|
426
449
|
// Build element tree: page wrapped in layouts (innermost to outermost)
|
|
427
450
|
const h = createElement as (...args: unknown[]) => React.ReactElement;
|
|
428
451
|
|
|
@@ -464,6 +487,12 @@ export async function buildRouteElement(
|
|
|
464
487
|
element = h(TracedPage, {});
|
|
465
488
|
}
|
|
466
489
|
|
|
490
|
+
// Wrap page + metadata in a Fragment so Float-hoisted elements are at
|
|
491
|
+
// the page segment level (always re-rendered, never skipped by segment
|
|
492
|
+
// tree diffing). React 19 Float hoists the <title>/<meta>/<link>
|
|
493
|
+
// children into <head> regardless of where they appear in the tree.
|
|
494
|
+
element = h(Fragment, null, metadataElement, element);
|
|
495
|
+
|
|
467
496
|
// Build a lookup of layout components by segment for O(1) access.
|
|
468
497
|
const layoutBySegment = new Map(
|
|
469
498
|
layoutComponents.map(({ component, segment }) => [segment, component])
|
|
@@ -715,7 +744,6 @@ export async function buildRouteElement(
|
|
|
715
744
|
|
|
716
745
|
return {
|
|
717
746
|
element,
|
|
718
|
-
headElements: headElements as HeadElement[],
|
|
719
747
|
layoutComponents,
|
|
720
748
|
segments,
|
|
721
749
|
deferSuspenseFor,
|
|
@@ -229,20 +229,3 @@ export { isAbortError } from '../render-utils.js';
|
|
|
229
229
|
|
|
230
230
|
// escapeHtml is now imported from server/utils/escape-html.ts
|
|
231
231
|
export { escapeHtml } from '../utils/escape-html.js';
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Parse a Cookie header string into a name→value Map.
|
|
235
|
-
* Used to populate NavContext.cookies for SSR hooks (useCookie).
|
|
236
|
-
*/
|
|
237
|
-
export function parseCookiesFromHeader(header: string): Map<string, string> {
|
|
238
|
-
const map = new Map<string, string>();
|
|
239
|
-
if (!header) return map;
|
|
240
|
-
for (const pair of header.split(';')) {
|
|
241
|
-
const eqIndex = pair.indexOf('=');
|
|
242
|
-
if (eqIndex === -1) continue;
|
|
243
|
-
const name = pair.slice(0, eqIndex).trim();
|
|
244
|
-
const value = pair.slice(eqIndex + 1).trim();
|
|
245
|
-
if (name) map.set(name, value);
|
|
246
|
-
}
|
|
247
|
-
return map;
|
|
248
|
-
}
|
|
@@ -30,6 +30,8 @@ import buildManifest from 'virtual:timber-build-manifest';
|
|
|
30
30
|
import loadUserInstrumentation from 'virtual:timber-instrumentation';
|
|
31
31
|
// @ts-expect-error — virtual module provided by timber-entries plugin
|
|
32
32
|
import loadCacheHandler from 'virtual:timber-cache-handler';
|
|
33
|
+
// @ts-expect-error — virtual module provided by timber-entries plugin
|
|
34
|
+
import { loadCdnPurgeHandler } from 'virtual:timber-cache-handler';
|
|
33
35
|
// @ts-expect-error — virtual module provided by timber-routing plugin
|
|
34
36
|
import globalSchemaCodecs from 'virtual:timber-schema';
|
|
35
37
|
|
|
@@ -61,6 +63,9 @@ import { callSsr } from './ssr-bridge.js';
|
|
|
61
63
|
import { isDebug, isDevMode, setDebugFromConfig } from '../debug.js';
|
|
62
64
|
import { setSourceMapCallback } from '../dev-source-map.js';
|
|
63
65
|
import { requestContextAls } from '../als-registry.js';
|
|
66
|
+
import { capturePrebuiltPayloads } from '../prebuilt-builder.js';
|
|
67
|
+
import { setPrebuiltPayloadSource } from '../prebuilt/payload-source.js';
|
|
68
|
+
import { setRscFunctions, setComponentRefreshTimeout } from '../prebuilt-runtime.js';
|
|
64
69
|
import { createAutoSitemapHandler } from '../sitemap-handler.js';
|
|
65
70
|
|
|
66
71
|
/**
|
|
@@ -130,6 +135,13 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
130
135
|
setCacheHandler(userCacheHandler as Parameters<typeof setCacheHandler>[0]);
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
// Wire the CDN purge handler so revalidateTag triggers CDN purge (TIM-1166).
|
|
139
|
+
const userCdnPurge = await loadCdnPurgeHandler();
|
|
140
|
+
if (userCdnPurge) {
|
|
141
|
+
const { setCdnPurgeHandler } = await import('../../cdn/purge-store.js');
|
|
142
|
+
setCdnPurgeHandler(userCdnPurge as Parameters<typeof setCdnPurgeHandler>[0]);
|
|
143
|
+
}
|
|
144
|
+
|
|
133
145
|
// Propagate renderTimeoutMs into the default singleflight so cache calls
|
|
134
146
|
// without a per-call timeoutMs still get deadlock protection (TIM-1032).
|
|
135
147
|
const renderTimeoutMs = (runtimeConfig as Record<string, unknown>).renderTimeoutMs as
|
|
@@ -138,8 +150,63 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
138
150
|
if (renderTimeoutMs) {
|
|
139
151
|
const { setDefaultSingleflightTimeout } = await import('../../cache/timber-cache.js');
|
|
140
152
|
setDefaultSingleflightTimeout(renderTimeoutMs);
|
|
153
|
+
setComponentRefreshTimeout(renderTimeoutMs);
|
|
141
154
|
}
|
|
142
155
|
|
|
156
|
+
// Wire the prebuilt payload source so cache.component wrappers can look
|
|
157
|
+
// up cached flight payloads at request time (TIM-1122). The source is
|
|
158
|
+
// platform-aware: Node/Nitro reads from disk; Cloudflare reads from
|
|
159
|
+
// static assets via globalThis.__TIMBER_PREBUILT_SOURCE__ (set by the
|
|
160
|
+
// generated _worker.js before this module loads).
|
|
161
|
+
// In dev mode there are no prebuilt artifacts — source stays null and
|
|
162
|
+
// all cache.component wrappers render dynamically.
|
|
163
|
+
if (!isDevMode()) {
|
|
164
|
+
const platformSource = (globalThis as Record<string, unknown>).__TIMBER_PREBUILT_SOURCE__ as
|
|
165
|
+
| import('../prebuilt/payload-source.js').PrebuiltPayloadSource
|
|
166
|
+
| undefined;
|
|
167
|
+
if (platformSource) {
|
|
168
|
+
setPrebuiltPayloadSource(platformSource);
|
|
169
|
+
} else {
|
|
170
|
+
// Default: Node/fs-based source — prebuilt/ is alongside rsc/ in the
|
|
171
|
+
// adapter output directory, so resolve relative to this entry.
|
|
172
|
+
try {
|
|
173
|
+
const { readFile: fsReadFile } = await import('node:fs/promises');
|
|
174
|
+
const { join: pathJoin, dirname } = await import('node:path');
|
|
175
|
+
const { fileURLToPath } = await import('node:url');
|
|
176
|
+
const entryDir = dirname(fileURLToPath(import.meta.url));
|
|
177
|
+
const prebuiltDir = pathJoin(entryDir, '..', 'prebuilt');
|
|
178
|
+
setPrebuiltPayloadSource({
|
|
179
|
+
async loadManifest() {
|
|
180
|
+
try {
|
|
181
|
+
const data = await fsReadFile(pathJoin(prebuiltDir, 'manifest.json'), 'utf-8');
|
|
182
|
+
return JSON.parse(data);
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
async readPayload(file) {
|
|
188
|
+
try {
|
|
189
|
+
return await fsReadFile(pathJoin(prebuiltDir, file));
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
} catch {
|
|
196
|
+
// node:fs not available (e.g. edge runtimes without the global
|
|
197
|
+
// override) — source stays null, wrappers render dynamically.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Wire RSC flight functions so prebuilt-runtime can revive cached payloads
|
|
203
|
+
// and capture re-renders without a dynamic import (TIM-1165).
|
|
204
|
+
const rscMod = await import('../../rsc-runtime/rsc.js');
|
|
205
|
+
setRscFunctions({
|
|
206
|
+
createFromReadableStream: rscMod.createFromReadableStream,
|
|
207
|
+
renderToReadableStream: rscMod.renderToReadableStream,
|
|
208
|
+
});
|
|
209
|
+
|
|
143
210
|
const matchRoute = createRouteMatcher(manifest);
|
|
144
211
|
const matchMetadataRoute = createMetadataRouteMatcher(manifest);
|
|
145
212
|
|
|
@@ -161,6 +228,7 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
161
228
|
const clientBootstrap = buildClientScripts({
|
|
162
229
|
...runtimeConfig,
|
|
163
230
|
clientJavascript: clientJsConfig ?? { disabled: false, enableHMRInDev: false },
|
|
231
|
+
devBrowserLogs: (runtimeConfig as Record<string, unknown>).devBrowserLogs as string | undefined,
|
|
164
232
|
buildManifest: buildManifest as BuildManifest,
|
|
165
233
|
});
|
|
166
234
|
|
|
@@ -465,6 +533,21 @@ export { runWithEarlyHintsSender } from '../early-hints-sender.js';
|
|
|
465
533
|
// waitUntil support via ALS. See design/11-platform.md §"waitUntil()".
|
|
466
534
|
export { runWithWaitUntil } from '../waituntil-bridge.js';
|
|
467
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Post-build flight capture entry point (TIM-1119). The capture pass must
|
|
538
|
+
* run inside the built RSC bundle — the prebuilt component registry, the
|
|
539
|
+
* ALS instances, and the flight renderer are module instances in THIS
|
|
540
|
+
* bundle's graph; an outside import of the framework package would get
|
|
541
|
+
* fresh, empty copies. The timber-prebuilt plugin imports the built
|
|
542
|
+
* `rsc/index.js` after the 5-step build and calls this export.
|
|
543
|
+
* See design/44-render-at-build-time.md §Build Phase.
|
|
544
|
+
*/
|
|
545
|
+
export function __timber_capturePrebuilt(
|
|
546
|
+
options: import('../prebuilt-builder.js').CaptureOptions
|
|
547
|
+
): Promise<import('../prebuilt-builder.js').CaptureSummary> {
|
|
548
|
+
return capturePrebuiltPayloads(routeManifest, options);
|
|
549
|
+
}
|
|
550
|
+
|
|
468
551
|
const { handler: _handler, pipelineConfig: _pipelineConfig } = await createRequestHandler(
|
|
469
552
|
routeManifest,
|
|
470
553
|
config
|
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
collectRouteFonts,
|
|
28
28
|
collectRouteModulepreloads,
|
|
29
29
|
} from '../build-manifest.js';
|
|
30
|
-
import { isDevMode } from '../debug.js';
|
|
31
30
|
import type { LayoutEntry } from '../deny-renderer.js';
|
|
32
31
|
import { renderDenyPage, renderDenyPageAsRsc } from '../deny-renderer.js';
|
|
33
32
|
import type { ClientBootstrapConfig } from '../html-injectors.js';
|
|
@@ -40,12 +39,7 @@ import { parseClientStateTree } from '../state-tree-diff.js';
|
|
|
40
39
|
|
|
41
40
|
import { handleApiRoute } from './api-handler.js';
|
|
42
41
|
import { renderErrorPage, renderNoMatchPage } from './error-renderer.js';
|
|
43
|
-
import {
|
|
44
|
-
buildRedirectResponse,
|
|
45
|
-
createDebugChannelSink,
|
|
46
|
-
escapeHtml,
|
|
47
|
-
isRscPayloadRequest,
|
|
48
|
-
} from './helpers.js';
|
|
42
|
+
import { buildRedirectResponse, createDebugChannelSink, isRscPayloadRequest } from './helpers.js';
|
|
49
43
|
import { buildRscPayloadResponse } from './rsc-payload.js';
|
|
50
44
|
import { renderRscStream } from './rsc-stream.js';
|
|
51
45
|
import { renderSsrResponse } from './ssr-renderer.js';
|
|
@@ -139,8 +133,7 @@ export async function renderRoute(
|
|
|
139
133
|
desc: 'build element tree',
|
|
140
134
|
});
|
|
141
135
|
|
|
142
|
-
const { element,
|
|
143
|
-
routeResult;
|
|
136
|
+
const { element, layoutComponents, deferSuspenseFor, skippedSegments } = routeResult;
|
|
144
137
|
|
|
145
138
|
// Build head HTML for injection into the SSR output.
|
|
146
139
|
// Collects CSS, fonts, and modulepreload from the build manifest for matched segments.
|
|
@@ -186,29 +179,10 @@ export async function renderRoute(
|
|
|
186
179
|
}
|
|
187
180
|
}
|
|
188
181
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const attrs = Object.entries(el.attrs)
|
|
194
|
-
.filter(([, v]) => v != null)
|
|
195
|
-
.map(([k, v]) => `${k}="${escapeHtml(v as string)}"`)
|
|
196
|
-
.join(' ');
|
|
197
|
-
headHtml += `<${el.tag} ${attrs}>`;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// In dev mode, inject the browser log forwarding script so console
|
|
202
|
-
// errors/warnings from the browser appear in the server terminal.
|
|
203
|
-
// Set by timber-dev-browser-logs plugin via globalThis (TIM-575).
|
|
204
|
-
if (isDevMode()) {
|
|
205
|
-
const devLogScript = (globalThis as Record<string, unknown>).__timber_dev_browser_log_script as
|
|
206
|
-
| string
|
|
207
|
-
| undefined;
|
|
208
|
-
if (devLogScript) {
|
|
209
|
-
headHtml += devLogScript;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
182
|
+
// Metadata tags (<title>, <meta>, <link>) are now rendered as React
|
|
183
|
+
// elements in the route element tree — React 19 Float hoists them into
|
|
184
|
+
// <head> during SSR and manages them during client rendering. No string
|
|
185
|
+
// splice needed. See TIM-1151.
|
|
212
186
|
|
|
213
187
|
// Render to RSC Flight stream with signal tracking.
|
|
214
188
|
const _rscStart = performance.now();
|
|
@@ -286,7 +260,6 @@ export async function renderRoute(
|
|
|
286
260
|
signals,
|
|
287
261
|
segments,
|
|
288
262
|
layoutComponents,
|
|
289
|
-
headElements,
|
|
290
263
|
match,
|
|
291
264
|
responseHeaders,
|
|
292
265
|
skippedSegments
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* design/13-security.md §"Server actions silently bypass middleware".
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import { stringifyCookie } from 'cookie';
|
|
30
|
+
|
|
29
31
|
import type { BuildManifest } from '../build-manifest.js';
|
|
30
32
|
import { canonicalize } from '../canonicalize.js';
|
|
31
33
|
import { getCookiesForSsr, seedRequestCookies } from '../cookie-context.js';
|
|
@@ -116,18 +118,22 @@ export function validateRevalidatePath(path: unknown): PathValidation {
|
|
|
116
118
|
* Serialize a cookie map into a `Cookie:` request header value
|
|
117
119
|
* (`name1=value1; name2=value2`).
|
|
118
120
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
121
|
+
* The RYW map holds decoded logical values (decodeURIComponent has
|
|
122
|
+
* already run), so we use the default encode (encodeURIComponent) to
|
|
123
|
+
* re-encode for the wire — mirroring getCookieJar().toString().
|
|
124
|
+
*
|
|
125
|
+
* Total — never throws. Malformed inbound cookie names (e.g. spaces)
|
|
126
|
+
* survive parseCookie but make stringifyCookie throw, so we fall back
|
|
127
|
+
* to a manual join to stay total over arbitrary request headers.
|
|
124
128
|
*/
|
|
125
129
|
function serializeCookieMap(cookies: Map<string, string>): string {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
try {
|
|
131
|
+
return stringifyCookie(Object.fromEntries(cookies));
|
|
132
|
+
} catch {
|
|
133
|
+
return Array.from(cookies.entries())
|
|
134
|
+
.map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
|
|
135
|
+
.join('; ');
|
|
136
|
+
}
|
|
131
137
|
}
|
|
132
138
|
|
|
133
139
|
/** Dependencies for the revalidate renderer factory. */
|
|
@@ -337,7 +343,6 @@ export function createRevalidateRendererFactory(
|
|
|
337
343
|
);
|
|
338
344
|
return {
|
|
339
345
|
element: routeResult.element,
|
|
340
|
-
headElements: routeResult.headElements,
|
|
341
346
|
};
|
|
342
347
|
};
|
|
343
348
|
};
|
|
@@ -5,16 +5,14 @@
|
|
|
5
5
|
* returned directly without SSR HTML rendering. The client decodes it via
|
|
6
6
|
* `createFromFetch` and renders into the hydrated React root.
|
|
7
7
|
*
|
|
8
|
-
* Design docs: 19-client-navigation.md §"RSC Payload Handling"
|
|
9
|
-
* 16-metadata.md §"Head Elements"
|
|
8
|
+
* Design docs: 19-client-navigation.md §"RSC Payload Handling"
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
11
|
import type { LayoutEntry } from '../deny-renderer.js';
|
|
13
12
|
import { renderDenyPageAsRsc } from '../deny-renderer.js';
|
|
14
13
|
import type { RouteMatch } from '../pipeline.js';
|
|
15
|
-
import { encodeHeadHeader, HEAD_HEADER } from '../../shared/head-header.js';
|
|
16
14
|
import type { RedirectSignal } from '../primitives.js';
|
|
17
|
-
import type {
|
|
15
|
+
import type { LayoutComponentEntry } from '../route-element-builder.js';
|
|
18
16
|
import type { ManifestSegmentNode } from '../route-matcher.js';
|
|
19
17
|
|
|
20
18
|
import {
|
|
@@ -40,7 +38,6 @@ export async function buildRscPayloadResponse(
|
|
|
40
38
|
signals: RenderSignals,
|
|
41
39
|
segments: ManifestSegmentNode[],
|
|
42
40
|
layoutComponents: LayoutComponentEntry[],
|
|
43
|
-
headElements: HeadElement[],
|
|
44
41
|
match: RouteMatch,
|
|
45
42
|
responseHeaders: Headers,
|
|
46
43
|
skippedSegments?: string[]
|
|
@@ -175,12 +172,9 @@ export async function buildRscPayloadResponse(
|
|
|
175
172
|
|
|
176
173
|
responseHeaders.set('content-type', `${RSC_CONTENT_TYPE}; charset=utf-8`);
|
|
177
174
|
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
if (encodedHead !== null) {
|
|
182
|
-
responseHeaders.set(HEAD_HEADER, encodedHead);
|
|
183
|
-
}
|
|
175
|
+
// Metadata (<title>/<meta>/<link>) now rides the RSC Flight payload as
|
|
176
|
+
// React elements — React 19 Float manages them in document.head during
|
|
177
|
+
// client rendering. No X-Timber-Head header needed. See TIM-1151.
|
|
184
178
|
|
|
185
179
|
// Send segment metadata so the client can populate its segment cache
|
|
186
180
|
// for state tree diffing on subsequent navigations.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
8
8
|
* - Collects all static page routes automatically
|
|
9
|
-
* - Calls `
|
|
9
|
+
* - Calls `generateStaticSegmentParams()` on dynamic routes to enumerate URLs
|
|
10
10
|
* - Skips API routes (route.ts), layout-only segments, slots, intercepting routes
|
|
11
11
|
* - Skips auth-protected routes (access.ts in segment chain) by default
|
|
12
12
|
* - Automatic pagination: sitemap index when > 50,000 URLs
|
|
@@ -42,12 +42,12 @@ export interface SitemapEntry {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* A route discovered during tree walk that may need async resolution
|
|
45
|
-
* (dynamic routes with
|
|
45
|
+
* (dynamic routes with generateStaticSegmentParams).
|
|
46
46
|
*/
|
|
47
47
|
export interface SitemapRoute {
|
|
48
48
|
/** The URL path pattern (e.g., '/products/[id]'). */
|
|
49
49
|
urlPath: string;
|
|
50
|
-
/** Whether this route has dynamic segments that need
|
|
50
|
+
/** Whether this route has dynamic segments that need generateStaticSegmentParams. */
|
|
51
51
|
isDynamic: boolean;
|
|
52
52
|
/** Loader for the page module (from route manifest). */
|
|
53
53
|
pageLoader?: { load: () => Promise<unknown>; filePath: string };
|
|
@@ -78,7 +78,7 @@ export const URLS_PER_SITEMAP = 50_000;
|
|
|
78
78
|
* Walk the route tree and collect all routes eligible for the sitemap.
|
|
79
79
|
*
|
|
80
80
|
* Pure synchronous function — no module loading. Returns routes that may
|
|
81
|
-
* need async resolution (dynamic routes with
|
|
81
|
+
* need async resolution (dynamic routes with generateStaticSegmentParams).
|
|
82
82
|
*
|
|
83
83
|
* @param root - The root segment node from the route tree
|
|
84
84
|
* @param config - Sitemap configuration
|
|
@@ -159,7 +159,7 @@ export function hasDynamicSegments(urlPath: string): boolean {
|
|
|
159
159
|
* Resolve sitemap routes into concrete URLs.
|
|
160
160
|
*
|
|
161
161
|
* For static routes, the URL is derived directly from the path.
|
|
162
|
-
* For dynamic routes, calls `
|
|
162
|
+
* For dynamic routes, calls `generateStaticSegmentParams()` from the page module.
|
|
163
163
|
*
|
|
164
164
|
* @param routes - Routes collected by collectSitemapRoutes
|
|
165
165
|
* @param config - Sitemap configuration
|
|
@@ -184,12 +184,18 @@ export async function resolveSitemapUrls(
|
|
|
184
184
|
priority: config.defaultPriority,
|
|
185
185
|
});
|
|
186
186
|
} else if (loadModule && route.pageLoader) {
|
|
187
|
-
// Dynamic route — try to call
|
|
187
|
+
// Dynamic route — try to call generateStaticSegmentParams
|
|
188
188
|
try {
|
|
189
189
|
const mod = await loadModule(route.pageLoader);
|
|
190
|
-
|
|
190
|
+
let generateFn = mod.generateStaticSegmentParams;
|
|
191
|
+
if (typeof generateFn !== 'function' && typeof mod.generateStaticParams === 'function') {
|
|
192
|
+
generateFn = mod.generateStaticParams;
|
|
193
|
+
console.warn(
|
|
194
|
+
`[timber] ${route.pageLoader.filePath}: generateStaticParams is deprecated — rename to generateStaticSegmentParams`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
191
197
|
if (typeof generateFn !== 'function') {
|
|
192
|
-
// No
|
|
198
|
+
// No generateStaticSegmentParams export — skip this dynamic route
|
|
193
199
|
continue;
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -208,7 +214,7 @@ export async function resolveSitemapUrls(
|
|
|
208
214
|
// Log and skip — don't fail the entire sitemap for one bad route
|
|
209
215
|
const filePath = route.pageLoader.filePath;
|
|
210
216
|
console.warn(
|
|
211
|
-
`[timber] sitemap: failed to call
|
|
217
|
+
`[timber] sitemap: failed to call generateStaticSegmentParams for ${filePath}:`,
|
|
212
218
|
error instanceof Error ? error.message : error
|
|
213
219
|
);
|
|
214
220
|
}
|
|
@@ -286,7 +292,7 @@ export function resolvePathWithParams(
|
|
|
286
292
|
*
|
|
287
293
|
* @param root - The root segment node from the route tree
|
|
288
294
|
* @param config - Sitemap configuration
|
|
289
|
-
* @param loadModule - Optional async function to load page modules (for
|
|
295
|
+
* @param loadModule - Optional async function to load page modules (for generateStaticSegmentParams)
|
|
290
296
|
* @returns The sitemap result (single or index with pages)
|
|
291
297
|
*/
|
|
292
298
|
export async function generateSitemap(
|
package/src/server/tracing.ts
CHANGED
|
@@ -163,6 +163,50 @@ export async function initDevTracing(
|
|
|
163
163
|
globals[DEV_TRACING_INIT_KEY] = true;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// ─── Platform Tracer ─────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* A native platform span. Mirrors the subset of the Cloudflare Workers
|
|
170
|
+
* `Span` API that timber uses. See design/17-logging.md §"Cloudflare Native
|
|
171
|
+
* Spans" and TIM-1135.
|
|
172
|
+
*/
|
|
173
|
+
export interface PlatformSpan {
|
|
174
|
+
setAttribute(key: string, value: string | number | boolean): void;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Adapter-provided native tracer. Callback-scoped like Cloudflare's
|
|
179
|
+
* `tracing.enterSpan()` — the span starts when the callback is invoked and
|
|
180
|
+
* ends when it returns or its promise settles. Nesting follows the
|
|
181
|
+
* platform's async context.
|
|
182
|
+
*
|
|
183
|
+
* When registered, withSpan() wraps every framework span in a native span
|
|
184
|
+
* *in addition to* the OTEL emission — dual emission, so external OTEL
|
|
185
|
+
* collectors keep working alongside the platform's native trace view.
|
|
186
|
+
*
|
|
187
|
+
* Register via setPlatformTracer(), or by writing the
|
|
188
|
+
* Symbol.for('timber:platform-tracer') key on globalThis from
|
|
189
|
+
* adapter-generated entry code (what the Cloudflare _worker.js does).
|
|
190
|
+
*/
|
|
191
|
+
export interface PlatformTracer {
|
|
192
|
+
enterSpan<T>(name: string, fn: (span: PlatformSpan) => T): T;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// globalThis + Symbol.for so a tracer registered by adapter-generated entry
|
|
196
|
+
// code (a separate module instance) is visible in both the RSC and SSR
|
|
197
|
+
// environments — same pattern as the cf-bindings ALS.
|
|
198
|
+
const PLATFORM_TRACER_KEY = Symbol.for('timber:platform-tracer');
|
|
199
|
+
|
|
200
|
+
/** Register (or clear) the native platform tracer for this runtime. */
|
|
201
|
+
export function setPlatformTracer(tracer: PlatformTracer | undefined): void {
|
|
202
|
+
(globalThis as Record<symbol, unknown>)[PLATFORM_TRACER_KEY] = tracer;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The registered native platform tracer, if any. */
|
|
206
|
+
export function getPlatformTracer(): PlatformTracer | undefined {
|
|
207
|
+
return (globalThis as Record<symbol, unknown>)[PLATFORM_TRACER_KEY] as PlatformTracer | undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
166
210
|
// ─── OTEL Span Helpers ───────────────────────────────────────────────────
|
|
167
211
|
|
|
168
212
|
/**
|
|
@@ -205,19 +249,54 @@ export async function getTracer(): Promise<import('@opentelemetry/api').Tracer |
|
|
|
205
249
|
}
|
|
206
250
|
|
|
207
251
|
/**
|
|
208
|
-
* Run a function within
|
|
209
|
-
*
|
|
252
|
+
* Run a function within a framework span. Composes two emission channels:
|
|
253
|
+
*
|
|
254
|
+
* - **Native platform span** — when an adapter registered a PlatformTracer
|
|
255
|
+
* (Cloudflare Workers), the fn is wrapped in platformTracer.enterSpan()
|
|
256
|
+
* so it appears in the platform's native trace view.
|
|
257
|
+
* - **OTEL span** — when an OTEL SDK is active, the fn also runs inside an
|
|
258
|
+
* OTEL span (dual emission). No SDK and no platform tracer = zero overhead.
|
|
210
259
|
*
|
|
211
260
|
* Automatically:
|
|
212
261
|
* - Creates the span as a child of the current context
|
|
213
262
|
* - Updates the ALS span ID for log–trace correlation
|
|
214
263
|
* - Ends the span when the function completes
|
|
215
|
-
* - Records exceptions on error
|
|
264
|
+
* - Records exceptions on error (OTEL channel)
|
|
216
265
|
*/
|
|
217
266
|
export async function withSpan<T>(
|
|
218
267
|
name: string,
|
|
219
268
|
attributes: Record<string, string | number | boolean>,
|
|
220
269
|
fn: () => T | Promise<T>
|
|
270
|
+
): Promise<T> {
|
|
271
|
+
const platformTracer = getPlatformTracer();
|
|
272
|
+
if (!platformTracer) {
|
|
273
|
+
return runOtelSpan(name, attributes, fn);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Native platform span wraps the OTEL emission (dual emission). The
|
|
277
|
+
// innermost platform span is tracked on the trace store so
|
|
278
|
+
// setSpanAttribute() can reach it after creation — the platform API has
|
|
279
|
+
// no getActiveSpan() equivalent.
|
|
280
|
+
return platformTracer.enterSpan(name, async (span) => {
|
|
281
|
+
for (const key of Object.keys(attributes)) {
|
|
282
|
+
span.setAttribute(key, attributes[key]);
|
|
283
|
+
}
|
|
284
|
+
const store = traceAls.getStore();
|
|
285
|
+
const prevPlatformSpan = store?.platformSpan;
|
|
286
|
+
if (store) store.platformSpan = span;
|
|
287
|
+
try {
|
|
288
|
+
return await runOtelSpan(name, attributes, fn);
|
|
289
|
+
} finally {
|
|
290
|
+
if (store) store.platformSpan = prevPlatformSpan;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** The OTEL half of withSpan() — no-op passthrough when no SDK is active. */
|
|
296
|
+
async function runOtelSpan<T>(
|
|
297
|
+
name: string,
|
|
298
|
+
attributes: Record<string, string | number | boolean>,
|
|
299
|
+
fn: () => T | Promise<T>
|
|
221
300
|
): Promise<T> {
|
|
222
301
|
const tracer = await getTracer();
|
|
223
302
|
if (!tracer) {
|
|
@@ -253,6 +332,12 @@ export async function setSpanAttribute(
|
|
|
253
332
|
key: string,
|
|
254
333
|
value: string | number | boolean
|
|
255
334
|
): Promise<void> {
|
|
335
|
+
// Forward to the innermost active native platform span, if any.
|
|
336
|
+
const platformSpan = traceAls.getStore()?.platformSpan;
|
|
337
|
+
if (platformSpan) {
|
|
338
|
+
platformSpan.setAttribute(key, value);
|
|
339
|
+
}
|
|
340
|
+
|
|
256
341
|
const api = await getOtelApi();
|
|
257
342
|
if (!api) return;
|
|
258
343
|
|