@timber-js/app 0.2.0-alpha.34 → 0.2.0-alpha.35
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/{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-B3Gypr3D.js → debug-ECi_61pb.js} +1 -1
- package/dist/_chunks/{debug-B3Gypr3D.js.map → debug-ECi_61pb.js.map} +1 -1
- package/dist/_chunks/define-cookie-w5GWm_bL.js +93 -0
- package/dist/_chunks/define-cookie-w5GWm_bL.js.map +1 -0
- package/dist/_chunks/error-boundary-TYEQJZ1-.js +211 -0
- package/dist/_chunks/error-boundary-TYEQJZ1-.js.map +1 -0
- package/dist/_chunks/{format-RyoGQL74.js → format-cX7wzEp2.js} +2 -2
- package/dist/_chunks/{format-RyoGQL74.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-BQUC8PHn.js → request-context-CZz_T0Bc.js} +40 -71
- package/dist/_chunks/request-context-CZz_T0Bc.js.map +1 -0
- package/dist/_chunks/segment-context-Dpq2XOKg.js +34 -0
- package/dist/_chunks/segment-context-Dpq2XOKg.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-CemImE6h.js → tracing-BPyIzIdu.js} +2 -2
- package/dist/_chunks/{tracing-CemImE6h.js.map → tracing-BPyIzIdu.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-C1SN725w.js +331 -0
- package/dist/_chunks/wrappers-C1SN725w.js.map +1 -0
- package/dist/cache/index.js +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 +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +193 -90
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts +8 -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/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/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 -81
- package/dist/index.d.ts +87 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +346 -210
- 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/entries.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/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-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 +4 -4
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +2 -2
- 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/form-data.d.ts +29 -0
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1819 -1629
- package/dist/server/index.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +28 -40
- 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-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/index.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +3 -0
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +12 -8
- 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/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 +12 -8
- package/src/client/browser-entry.ts +55 -13
- package/src/client/error-boundary.tsx +18 -1
- package/src/client/index.ts +9 -1
- package/src/client/link.tsx +9 -9
- package/src/client/navigation-context.ts +2 -2
- package/src/client/router.ts +102 -55
- package/src/client/rsc-fetch.ts +63 -2
- package/src/client/segment-cache.ts +1 -1
- package/src/client/stale-reload.ts +28 -0
- package/src/client/top-loader.tsx +2 -2
- 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/index.ts +255 -65
- 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/entries.ts +3 -6
- 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/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 +38 -8
- package/src/server/action-encryption.ts +144 -0
- package/src/server/action-handler.ts +16 -0
- package/src/server/als-registry.ts +4 -4
- package/src/server/build-manifest.ts +4 -4
- 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/form-data.ts +76 -0
- package/src/server/html-injectors.ts +42 -26
- package/src/server/index.ts +2 -4
- package/src/server/node-stream-transforms.ts +68 -41
- package/src/server/pipeline.ts +98 -26
- package/src/server/request-context.ts +49 -124
- package/src/server/route-element-builder.ts +102 -99
- package/src/server/route-matcher.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +3 -2
- package/src/server/rsc-entry/index.ts +26 -11
- package/src/server/rsc-entry/rsc-payload.ts +2 -2
- package/src/server/rsc-entry/ssr-renderer.ts +4 -4
- package/src/server/slot-resolver.ts +204 -206
- package/src/server/ssr-entry.ts +3 -1
- package/src/server/ssr-render.ts +3 -0
- package/src/server/tree-builder.ts +84 -48
- package/src/server/types.ts +1 -3
- package/src/server/version-skew.ts +104 -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/interception-BOoWmLUA.js.map +0 -1
- package/dist/_chunks/request-context-BQUC8PHn.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/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
|
@@ -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,62 @@ 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({ params }) { ... }`
|
|
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
|
+
async function extractMetadata(
|
|
124
|
+
mod: Record<string, unknown>,
|
|
125
|
+
segment: ManifestSegmentNode,
|
|
126
|
+
paramsPromise: Promise<Record<string, string | string[]>>
|
|
127
|
+
): Promise<Metadata | null> {
|
|
128
|
+
if (typeof mod.metadata === 'function') {
|
|
129
|
+
type MetadataFn = (props: Record<string, unknown>) => Promise<Metadata>;
|
|
130
|
+
return (
|
|
131
|
+
(await withSpan(
|
|
132
|
+
'timber.metadata',
|
|
133
|
+
{ 'timber.segment': segment.segmentName ?? segment.urlPath },
|
|
134
|
+
() => (mod.metadata as MetadataFn)({ params: paramsPromise })
|
|
135
|
+
)) ?? null
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (mod.metadata) {
|
|
139
|
+
return mod.metadata as Metadata;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Extract `deferSuspenseFor` from a module and return the maximum
|
|
146
|
+
* of the current value and the module's value.
|
|
147
|
+
*/
|
|
148
|
+
function extractDeferSuspenseFor(mod: Record<string, unknown>, current: number): number {
|
|
149
|
+
if (typeof mod.deferSuspenseFor === 'number' && mod.deferSuspenseFor > current) {
|
|
150
|
+
return mod.deferSuspenseFor;
|
|
151
|
+
}
|
|
152
|
+
return current;
|
|
153
|
+
}
|
|
154
|
+
|
|
87
155
|
// ─── Builder ──────────────────────────────────────────────────────────────
|
|
88
156
|
|
|
89
157
|
/**
|
|
@@ -126,87 +194,34 @@ export async function buildRouteElement(
|
|
|
126
194
|
segment,
|
|
127
195
|
});
|
|
128
196
|
}
|
|
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;
|
|
197
|
+
|
|
198
|
+
// Param coercion is handled in the pipeline (Stage 2c) before
|
|
199
|
+
// middleware and rendering. See coerceSegmentParams() in pipeline.ts.
|
|
200
|
+
|
|
201
|
+
rejectLegacyGenerateMetadata(mod, segment.layout.filePath ?? segment.urlPath);
|
|
202
|
+
const layoutMetadata = await extractMetadata(mod, segment, paramsPromise);
|
|
203
|
+
if (layoutMetadata) {
|
|
204
|
+
metadataEntries.push({ metadata: layoutMetadata, isPage: false });
|
|
158
205
|
}
|
|
206
|
+
deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
|
|
159
207
|
}
|
|
160
208
|
|
|
161
209
|
// Load page (leaf segment only)
|
|
162
210
|
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
211
|
const mod = (await segment.page.load()) as Record<string, unknown>;
|
|
212
|
+
|
|
213
|
+
// Param coercion is handled in the pipeline (Stage 2c) before
|
|
214
|
+
// middleware and rendering. See coerceSegmentParams() in pipeline.ts.
|
|
215
|
+
|
|
177
216
|
if (mod.default) {
|
|
178
217
|
PageComponent = mod.default as (...args: unknown[]) => unknown;
|
|
179
218
|
}
|
|
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;
|
|
219
|
+
rejectLegacyGenerateMetadata(mod, segment.page.filePath ?? segment.urlPath);
|
|
220
|
+
const pageMetadata = await extractMetadata(mod, segment, paramsPromise);
|
|
221
|
+
if (pageMetadata) {
|
|
222
|
+
metadataEntries.push({ metadata: pageMetadata, isPage: true });
|
|
209
223
|
}
|
|
224
|
+
deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
|
|
210
225
|
}
|
|
211
226
|
}
|
|
212
227
|
|
|
@@ -227,7 +242,7 @@ export async function buildRouteElement(
|
|
|
227
242
|
if (segment.access) {
|
|
228
243
|
const accessMod = (await segment.access.load()) as Record<string, unknown>;
|
|
229
244
|
const accessFn = accessMod.default as
|
|
230
|
-
| ((ctx: { params: Record<string, string | string[]
|
|
245
|
+
| ((ctx: { params: Record<string, string | string[]> }) => unknown)
|
|
231
246
|
| undefined;
|
|
232
247
|
if (accessFn) {
|
|
233
248
|
try {
|
|
@@ -236,7 +251,7 @@ export async function buildRouteElement(
|
|
|
236
251
|
{ 'timber.segment': segment.segmentName ?? 'unknown' },
|
|
237
252
|
async () => {
|
|
238
253
|
try {
|
|
239
|
-
await accessFn({ params: match.params
|
|
254
|
+
await accessFn({ params: match.params });
|
|
240
255
|
await setSpanAttribute('timber.result', 'pass');
|
|
241
256
|
accessVerdicts.set(si, 'pass');
|
|
242
257
|
} catch (error) {
|
|
@@ -304,7 +319,6 @@ export async function buildRouteElement(
|
|
|
304
319
|
|
|
305
320
|
let element = h(TracedPage, {
|
|
306
321
|
params: paramsPromise,
|
|
307
|
-
searchParams: {},
|
|
308
322
|
});
|
|
309
323
|
|
|
310
324
|
// Build a lookup of layout components by segment for O(1) access.
|
|
@@ -380,13 +394,12 @@ export async function buildRouteElement(
|
|
|
380
394
|
if (segment.access) {
|
|
381
395
|
const accessMod = (await segment.access.load()) as Record<string, unknown>;
|
|
382
396
|
const accessFn = accessMod.default as
|
|
383
|
-
| ((ctx: { params: Record<string, string | string[]
|
|
397
|
+
| ((ctx: { params: Record<string, string | string[]> }) => unknown)
|
|
384
398
|
| undefined;
|
|
385
399
|
if (accessFn) {
|
|
386
400
|
element = h(AccessGate, {
|
|
387
401
|
accessFn,
|
|
388
402
|
params: match.params,
|
|
389
|
-
searchParams: {},
|
|
390
403
|
segmentName: segment.segmentName,
|
|
391
404
|
verdict: accessVerdicts.get(i),
|
|
392
405
|
children: element,
|
|
@@ -412,31 +425,22 @@ export async function buildRouteElement(
|
|
|
412
425
|
const segmentPath = segment.urlPath.split('/');
|
|
413
426
|
const parallelRouteKeys = Object.keys(segment.slots ?? {});
|
|
414
427
|
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
// from the root "layout /".
|
|
419
|
-
const segmentForSpan = segment;
|
|
420
|
-
const layoutComponentForSpan = layoutComponent;
|
|
421
|
-
const segmentLabel =
|
|
422
|
-
segmentForSpan.segmentType === 'group'
|
|
423
|
-
? `${segmentForSpan.urlPath === '/' ? '' : segmentForSpan.urlPath}/${segmentForSpan.segmentName}`
|
|
424
|
-
: segmentForSpan.urlPath;
|
|
425
|
-
const TracedLayout = async (props: Record<string, unknown>) => {
|
|
426
|
-
return withSpan('timber.layout', { 'timber.segment': segmentLabel }, () =>
|
|
427
|
-
(layoutComponentForSpan as (props: Record<string, unknown>) => unknown)(props)
|
|
428
|
-
);
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
// segmentId uniquely identifies this segment for client-side element
|
|
432
|
-
// caching. For route groups, urlPath is shared with the parent (both "/"),
|
|
433
|
-
// so we include the group name to distinguish them. Without this, the
|
|
434
|
-
// segment merger's element cache would conflate root and group elements.
|
|
428
|
+
// For route groups, urlPath is shared with the parent (both "/"),
|
|
429
|
+
// so include the group name to distinguish them. Used for both OTEL
|
|
430
|
+
// span labels and client-side element caching (segmentId).
|
|
435
431
|
const segmentId =
|
|
436
432
|
segment.segmentType === 'group'
|
|
437
433
|
? `${segment.urlPath === '/' ? '' : segment.urlPath}/${segment.segmentName}`
|
|
438
434
|
: segment.urlPath;
|
|
439
435
|
|
|
436
|
+
// Wrap the layout component in an OTEL span
|
|
437
|
+
const layoutComponentRef = layoutComponent;
|
|
438
|
+
const TracedLayout = async (props: Record<string, unknown>) => {
|
|
439
|
+
return withSpan('timber.layout', { 'timber.segment': segmentId }, () =>
|
|
440
|
+
(layoutComponentRef as (props: Record<string, unknown>) => unknown)(props)
|
|
441
|
+
);
|
|
442
|
+
};
|
|
443
|
+
|
|
440
444
|
element = h(SegmentProvider, {
|
|
441
445
|
segments: segmentPath,
|
|
442
446
|
segmentId,
|
|
@@ -444,7 +448,6 @@ export async function buildRouteElement(
|
|
|
444
448
|
children: h(TracedLayout, {
|
|
445
449
|
...slotProps,
|
|
446
450
|
params: paramsPromise,
|
|
447
|
-
searchParams: {},
|
|
448
451
|
children: element,
|
|
449
452
|
}),
|
|
450
453
|
});
|
|
@@ -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
|
|
|
@@ -15,7 +15,8 @@ import type { ClientBootstrapConfig } from '#/server/html-injectors.js';
|
|
|
15
15
|
import { renderDenyPage } from '#/server/deny-renderer.js';
|
|
16
16
|
import type { LayoutEntry } from '#/server/deny-renderer.js';
|
|
17
17
|
import type { NavContext } from '#/server/ssr-entry.js';
|
|
18
|
-
import { createDebugChannelSink
|
|
18
|
+
import { createDebugChannelSink } from './helpers.js';
|
|
19
|
+
import { getCookiesForSsr } from '#/server/request-context.js';
|
|
19
20
|
import { callSsr } from './ssr-bridge.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -127,7 +128,7 @@ export async function renderErrorPage(
|
|
|
127
128
|
headHtml: '',
|
|
128
129
|
bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
|
|
129
130
|
rscStream: inlineStream,
|
|
130
|
-
cookies:
|
|
131
|
+
cookies: getCookiesForSsr(),
|
|
131
132
|
};
|
|
132
133
|
|
|
133
134
|
return callSsr(ssrStream, navContext);
|
|
@@ -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';
|
|
@@ -112,13 +116,15 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
112
116
|
// See design/17-logging.md §"register() — Server Startup"
|
|
113
117
|
await loadInstrumentation(loadUserInstrumentation);
|
|
114
118
|
|
|
115
|
-
// Initialize
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
// Initialize deployment ID for version skew detection (TIM-446).
|
|
120
|
+
// The manifest init module sets globalThis.__TIMBER_DEPLOYMENT_ID__ at startup.
|
|
121
|
+
// In dev mode this is undefined — skew checks are skipped.
|
|
122
|
+
const deploymentId = (globalThis as Record<string, unknown>).__TIMBER_DEPLOYMENT_ID__ as
|
|
123
|
+
| string
|
|
118
124
|
| undefined;
|
|
119
|
-
if (
|
|
120
|
-
const {
|
|
121
|
-
|
|
125
|
+
if (deploymentId) {
|
|
126
|
+
const { setDeploymentId } = await import('#/server/version-skew.js');
|
|
127
|
+
setDeploymentId(deploymentId);
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
const matchRoute = createRouteMatcher(manifest);
|
|
@@ -211,7 +217,8 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
211
217
|
responseHeaders,
|
|
212
218
|
clientBootstrap,
|
|
213
219
|
clientJsDisabled,
|
|
214
|
-
interception
|
|
220
|
+
interception,
|
|
221
|
+
manifest.root
|
|
215
222
|
);
|
|
216
223
|
},
|
|
217
224
|
renderNoMatch: async (req: Request, responseHeaders: Headers) => {
|
|
@@ -304,7 +311,8 @@ async function renderRoute(
|
|
|
304
311
|
responseHeaders: Headers,
|
|
305
312
|
clientBootstrap: ClientBootstrapConfig,
|
|
306
313
|
clientJsDisabled: boolean,
|
|
307
|
-
interception?: InterceptionContext
|
|
314
|
+
interception?: InterceptionContext,
|
|
315
|
+
rootSegment?: ManifestSegmentNode
|
|
308
316
|
): Promise<Response> {
|
|
309
317
|
const segments = match.segments as unknown as ManifestSegmentNode[];
|
|
310
318
|
const leaf = segments[segments.length - 1];
|
|
@@ -359,9 +367,16 @@ async function renderRoute(
|
|
|
359
367
|
return buildRedirectResponse(_req, signal, responseHeaders);
|
|
360
368
|
}
|
|
361
369
|
}
|
|
362
|
-
//
|
|
370
|
+
// Param coercion failed — render the custom 404 page (status files / not-found).
|
|
371
|
+
// Previously returned a bare Response(null, { status: 404 }) which bypassed
|
|
372
|
+
// custom not-found pages. Now routes through renderNoMatchPage so apps with
|
|
373
|
+
// 404.tsx / not-found status files render their custom page.
|
|
374
|
+
if (error instanceof ParamCoercionError) {
|
|
375
|
+
return renderNoMatchPage(_req, rootSegment!, responseHeaders, clientBootstrap);
|
|
376
|
+
}
|
|
377
|
+
// No PageComponent found — same treatment as param coercion: render custom 404.
|
|
363
378
|
if (error instanceof Error && error.message.startsWith('No page component')) {
|
|
364
|
-
return
|
|
379
|
+
return renderNoMatchPage(_req, rootSegment!, responseHeaders, clientBootstrap);
|
|
365
380
|
}
|
|
366
381
|
throw error;
|
|
367
382
|
}
|
|
@@ -155,8 +155,8 @@ export async function buildRscPayloadResponse(
|
|
|
155
155
|
responseHeaders.set('X-Timber-Skipped-Segments', JSON.stringify(skippedSegments));
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
// Send route params so the client can populate
|
|
159
|
-
// SPA navigation. Without this,
|
|
158
|
+
// Send route params so the client can populate useSegmentParams() after
|
|
159
|
+
// SPA navigation. Without this, useSegmentParams() returns {}.
|
|
160
160
|
if (Object.keys(match.params).length > 0) {
|
|
161
161
|
responseHeaders.set('X-Timber-Params', JSON.stringify(match.params));
|
|
162
162
|
}
|
|
@@ -26,8 +26,8 @@ import {
|
|
|
26
26
|
buildSegmentInfo,
|
|
27
27
|
createDebugChannelSink,
|
|
28
28
|
isAbortError,
|
|
29
|
-
parseCookiesFromHeader,
|
|
30
29
|
} from './helpers.js';
|
|
30
|
+
import { getCookiesForSsr } from '#/server/request-context.js';
|
|
31
31
|
import { renderErrorPage } from './error-renderer.js';
|
|
32
32
|
import { callSsr } from './ssr-bridge.js';
|
|
33
33
|
import type { RenderSignals } from './rsc-stream.js';
|
|
@@ -92,8 +92,8 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
92
92
|
? ''
|
|
93
93
|
: `<script>self.__timber_segments=${JSON.stringify(buildSegmentInfo(segments, layoutComponents))}</script>`;
|
|
94
94
|
|
|
95
|
-
// Embed route params in HTML so
|
|
96
|
-
// Without this,
|
|
95
|
+
// Embed route params in HTML so useSegmentParams() works on initial hydration.
|
|
96
|
+
// Without this, useSegmentParams() returns {} until the first client navigation.
|
|
97
97
|
const paramsScript =
|
|
98
98
|
clientJsDisabled || Object.keys(match.params).length === 0
|
|
99
99
|
? ''
|
|
@@ -111,7 +111,7 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
111
111
|
rscStream: clientJsDisabled ? undefined : inlineStream,
|
|
112
112
|
deferSuspenseFor: deferSuspenseFor > 0 ? deferSuspenseFor : undefined,
|
|
113
113
|
signal: req.signal,
|
|
114
|
-
cookies:
|
|
114
|
+
cookies: getCookiesForSsr(),
|
|
115
115
|
};
|
|
116
116
|
|
|
117
117
|
// Helper: check if render-phase signals were captured and return the
|