@timber-js/app 0.2.0-alpha.33 → 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/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.js +5 -2
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/index.js +1 -2
- package/dist/cache/index.js.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 +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 -83
- package/dist/index.d.ts +87 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +356 -215
- 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/adapters/compress-module.ts +5 -2
- package/src/client/browser-entry.ts +94 -85
- 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/compress.ts +25 -7
- 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 +91 -46
- 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
|
@@ -48,8 +48,6 @@ export interface TreeBuilderConfig {
|
|
|
48
48
|
segments: SegmentNode[];
|
|
49
49
|
/** Route params extracted by the matcher (catch-all segments produce string[]). */
|
|
50
50
|
params: Record<string, string | string[]>;
|
|
51
|
-
/** Parsed search params (typed or URLSearchParams). */
|
|
52
|
-
searchParams: unknown;
|
|
53
51
|
/** Loads a route file's module. */
|
|
54
52
|
loadModule: ModuleLoader;
|
|
55
53
|
/** React.createElement or equivalent. */
|
|
@@ -77,9 +75,8 @@ export interface TreeBuilderConfig {
|
|
|
77
75
|
* (backward compat for tree-builder.ts which doesn't run a pre-render pass).
|
|
78
76
|
*/
|
|
79
77
|
export interface AccessGateProps {
|
|
80
|
-
accessFn: (ctx: { params: Record<string, string | string[]
|
|
78
|
+
accessFn: (ctx: { params: Record<string, string | string[]> }) => unknown;
|
|
81
79
|
params: Record<string, string | string[]>;
|
|
82
|
-
searchParams: unknown;
|
|
83
80
|
/** Segment name for dev logging (e.g. "authenticated", "dashboard"). */
|
|
84
81
|
segmentName?: string;
|
|
85
82
|
/**
|
|
@@ -98,12 +95,20 @@ export interface AccessGateProps {
|
|
|
98
95
|
/**
|
|
99
96
|
* Framework-injected slot access gate component.
|
|
100
97
|
* On denial, renders denied.tsx → default.tsx → null instead of failing the page.
|
|
98
|
+
*
|
|
99
|
+
* DeniedComponent is passed instead of a pre-built element so that
|
|
100
|
+
* SlotAccessGate can forward DenySignal.data as dangerouslyPassData
|
|
101
|
+
* and slotName as the slot prop after catching the signal.
|
|
101
102
|
*/
|
|
102
103
|
export interface SlotAccessGateProps {
|
|
103
|
-
accessFn: (ctx: { params: Record<string, string | string[]
|
|
104
|
+
accessFn: (ctx: { params: Record<string, string | string[]> }) => unknown;
|
|
104
105
|
params: Record<string, string | string[]>;
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
/** The denied.tsx component (not a pre-built element). null if no denied.tsx exists. */
|
|
107
|
+
DeniedComponent: ((...args: unknown[]) => unknown) | null;
|
|
108
|
+
/** Slot directory name without @ prefix (e.g. "admin", "sidebar"). */
|
|
109
|
+
slotName: string;
|
|
110
|
+
/** createElement function for building elements dynamically. */
|
|
111
|
+
createElement: CreateElement;
|
|
107
112
|
defaultFallback: ReactElement | null;
|
|
108
113
|
children: ReactElement;
|
|
109
114
|
}
|
|
@@ -113,7 +118,8 @@ export interface SlotAccessGateProps {
|
|
|
113
118
|
* Wraps content with status-code error boundary handling.
|
|
114
119
|
*/
|
|
115
120
|
export interface ErrorBoundaryProps {
|
|
116
|
-
fallbackComponent
|
|
121
|
+
fallbackComponent?: ReactElement | null;
|
|
122
|
+
fallbackElement?: ReactElement | null;
|
|
117
123
|
status?: number;
|
|
118
124
|
children: ReactElement;
|
|
119
125
|
}
|
|
@@ -143,8 +149,7 @@ export interface TreeBuildResult {
|
|
|
143
149
|
* Parallel slots are resolved at each layout level and composed as named props.
|
|
144
150
|
*/
|
|
145
151
|
export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeBuildResult> {
|
|
146
|
-
const { segments, params,
|
|
147
|
-
config;
|
|
152
|
+
const { segments, params, loadModule, createElement, errorBoundaryComponent } = config;
|
|
148
153
|
|
|
149
154
|
if (segments.length === 0) {
|
|
150
155
|
throw new Error('[timber] buildElementTree: empty segment chain');
|
|
@@ -168,8 +173,8 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
|
|
|
168
173
|
);
|
|
169
174
|
}
|
|
170
175
|
|
|
171
|
-
// Build the page element with params
|
|
172
|
-
let element: ReactElement = createElement(PageComponent, { params
|
|
176
|
+
// Build the page element with params prop
|
|
177
|
+
let element: ReactElement = createElement(PageComponent, { params });
|
|
173
178
|
|
|
174
179
|
// Build tree bottom-up: wrap page, then walk segments from leaf to root
|
|
175
180
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
@@ -191,7 +196,6 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
|
|
|
191
196
|
element = createElement('timber:access-gate', {
|
|
192
197
|
accessFn,
|
|
193
198
|
params,
|
|
194
|
-
searchParams,
|
|
195
199
|
segmentName: segment.segmentName,
|
|
196
200
|
children: element,
|
|
197
201
|
} satisfies AccessGateProps);
|
|
@@ -212,7 +216,6 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
|
|
|
212
216
|
slotProps[slotName] = await buildSlotElement(
|
|
213
217
|
slotNode,
|
|
214
218
|
params,
|
|
215
|
-
searchParams,
|
|
216
219
|
loadModule,
|
|
217
220
|
createElement,
|
|
218
221
|
errorBoundaryComponent
|
|
@@ -223,7 +226,6 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
|
|
|
223
226
|
element = createElement(LayoutComponent, {
|
|
224
227
|
...slotProps,
|
|
225
228
|
params,
|
|
226
|
-
searchParams,
|
|
227
229
|
children: element,
|
|
228
230
|
});
|
|
229
231
|
}
|
|
@@ -244,7 +246,6 @@ export async function buildElementTree(config: TreeBuilderConfig): Promise<TreeB
|
|
|
244
246
|
async function buildSlotElement(
|
|
245
247
|
slotNode: SegmentNode,
|
|
246
248
|
params: Record<string, string | string[]>,
|
|
247
|
-
searchParams: unknown,
|
|
248
249
|
loadModule: ModuleLoader,
|
|
249
250
|
createElement: CreateElement,
|
|
250
251
|
errorBoundaryComponent: unknown
|
|
@@ -261,10 +262,10 @@ async function buildSlotElement(
|
|
|
261
262
|
|
|
262
263
|
// If no page, render default.tsx or null
|
|
263
264
|
if (!PageComponent) {
|
|
264
|
-
return DefaultComponent ? createElement(DefaultComponent, { params
|
|
265
|
+
return DefaultComponent ? createElement(DefaultComponent, { params }) : null;
|
|
265
266
|
}
|
|
266
267
|
|
|
267
|
-
let element: ReactElement = createElement(PageComponent, { params
|
|
268
|
+
let element: ReactElement = createElement(PageComponent, { params });
|
|
268
269
|
|
|
269
270
|
// Wrap in error boundaries
|
|
270
271
|
element = await wrapWithErrorBoundaries(
|
|
@@ -280,27 +281,20 @@ async function buildSlotElement(
|
|
|
280
281
|
const accessModule = await loadModule(slotNode.access);
|
|
281
282
|
const accessFn = accessModule.default as SlotAccessGateProps['accessFn'];
|
|
282
283
|
|
|
283
|
-
// Load denied.tsx
|
|
284
|
+
// Load denied.tsx — pass component (not pre-built element) so
|
|
285
|
+
// SlotAccessGate can forward DenySignal.data dynamically. See TIM-488.
|
|
284
286
|
const deniedModule = slotNode.denied ? await loadModule(slotNode.denied) : null;
|
|
285
|
-
const DeniedComponent =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const deniedFallback = DeniedComponent
|
|
290
|
-
? createElement(DeniedComponent, {
|
|
291
|
-
slot: slotNode.segmentName.replace(/^@/, ''),
|
|
292
|
-
dangerouslyPassData: undefined,
|
|
293
|
-
})
|
|
294
|
-
: null;
|
|
295
|
-
const defaultFallback = DefaultComponent
|
|
296
|
-
? createElement(DefaultComponent, { params, searchParams })
|
|
297
|
-
: null;
|
|
287
|
+
const DeniedComponent =
|
|
288
|
+
(deniedModule?.default as ((...args: unknown[]) => ReactElement) | undefined) ?? null;
|
|
289
|
+
|
|
290
|
+
const defaultFallback = DefaultComponent ? createElement(DefaultComponent, { params }) : null;
|
|
298
291
|
|
|
299
292
|
element = createElement('timber:slot-access-gate', {
|
|
300
293
|
accessFn,
|
|
301
294
|
params,
|
|
302
|
-
|
|
303
|
-
|
|
295
|
+
DeniedComponent,
|
|
296
|
+
slotName: slotNode.segmentName.replace(/^@/, ''),
|
|
297
|
+
createElement,
|
|
304
298
|
defaultFallback,
|
|
305
299
|
children: element,
|
|
306
300
|
} satisfies SlotAccessGateProps);
|
|
@@ -311,6 +305,19 @@ async function buildSlotElement(
|
|
|
311
305
|
|
|
312
306
|
// ─── Error Boundary Wrapping ─────────────────────────────────────────────────
|
|
313
307
|
|
|
308
|
+
/** MDX/markdown extensions — these are server components that cannot be passed as function props. */
|
|
309
|
+
const MDX_EXTENSIONS = new Set(['mdx', 'md']);
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check if a route file is an MDX/markdown file based on its extension.
|
|
313
|
+
* MDX components are server components by default and cannot cross the
|
|
314
|
+
* RSC→client boundary as function props. They must be pre-rendered as
|
|
315
|
+
* elements and passed as fallbackElement instead of fallbackComponent.
|
|
316
|
+
*/
|
|
317
|
+
function isMdxFile(file: RouteFile): boolean {
|
|
318
|
+
return MDX_EXTENSIONS.has(file.extension);
|
|
319
|
+
}
|
|
320
|
+
|
|
314
321
|
/**
|
|
315
322
|
* Wrap an element with error boundaries from a segment's status-code files.
|
|
316
323
|
*
|
|
@@ -320,6 +327,12 @@ async function buildSlotElement(
|
|
|
320
327
|
* 3. error.tsx (general error boundary)
|
|
321
328
|
*
|
|
322
329
|
* This creates the fallback chain described in design/10-error-handling.md.
|
|
330
|
+
*
|
|
331
|
+
* MDX status files are server components and cannot be passed as function
|
|
332
|
+
* props to TimberErrorBoundary (a 'use client' component). Instead, they
|
|
333
|
+
* are pre-rendered as elements and passed as fallbackElement. The error
|
|
334
|
+
* boundary renders the element directly when an error is caught.
|
|
335
|
+
* See TIM-503.
|
|
323
336
|
*/
|
|
324
337
|
async function wrapWithErrorBoundaries(
|
|
325
338
|
segment: SegmentNode,
|
|
@@ -340,11 +353,18 @@ async function wrapWithErrorBoundaries(
|
|
|
340
353
|
const mod = await loadModule(file);
|
|
341
354
|
const Component = mod.default;
|
|
342
355
|
if (Component) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
356
|
+
const boundaryProps = isMdxFile(file)
|
|
357
|
+
? ({
|
|
358
|
+
fallbackElement: createElement(Component, { status }),
|
|
359
|
+
status,
|
|
360
|
+
children: element,
|
|
361
|
+
} satisfies ErrorBoundaryProps)
|
|
362
|
+
: ({
|
|
363
|
+
fallbackComponent: Component,
|
|
364
|
+
status,
|
|
365
|
+
children: element,
|
|
366
|
+
} satisfies ErrorBoundaryProps);
|
|
367
|
+
element = createElement(errorBoundaryComponent, boundaryProps);
|
|
348
368
|
}
|
|
349
369
|
}
|
|
350
370
|
}
|
|
@@ -356,25 +376,41 @@ async function wrapWithErrorBoundaries(
|
|
|
356
376
|
const mod = await loadModule(file);
|
|
357
377
|
const Component = mod.default;
|
|
358
378
|
if (Component) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
379
|
+
const categoryStatus = key === '4xx' ? 400 : 500;
|
|
380
|
+
const boundaryProps = isMdxFile(file)
|
|
381
|
+
? ({
|
|
382
|
+
fallbackElement: createElement(Component, {}),
|
|
383
|
+
status: categoryStatus,
|
|
384
|
+
children: element,
|
|
385
|
+
} satisfies ErrorBoundaryProps)
|
|
386
|
+
: ({
|
|
387
|
+
fallbackComponent: Component,
|
|
388
|
+
status: categoryStatus,
|
|
389
|
+
children: element,
|
|
390
|
+
} satisfies ErrorBoundaryProps);
|
|
391
|
+
element = createElement(errorBoundaryComponent, boundaryProps);
|
|
364
392
|
}
|
|
365
393
|
}
|
|
366
394
|
}
|
|
367
395
|
}
|
|
368
396
|
|
|
369
397
|
// Wrap with error.tsx (outermost — catches anything not matched by status files)
|
|
398
|
+
// Note: error.tsx/error.mdx receives { error, digest, reset } props.
|
|
399
|
+
// MDX error files are pre-rendered without those props (they're static content).
|
|
370
400
|
if (segment.error) {
|
|
371
401
|
const errorModule = await loadModule(segment.error);
|
|
372
402
|
const ErrorComponent = errorModule.default;
|
|
373
403
|
if (ErrorComponent) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
404
|
+
const boundaryProps = isMdxFile(segment.error)
|
|
405
|
+
? ({
|
|
406
|
+
fallbackElement: createElement(ErrorComponent, {}),
|
|
407
|
+
children: element,
|
|
408
|
+
} satisfies ErrorBoundaryProps)
|
|
409
|
+
: ({
|
|
410
|
+
fallbackComponent: ErrorComponent,
|
|
411
|
+
children: element,
|
|
412
|
+
} satisfies ErrorBoundaryProps);
|
|
413
|
+
element = createElement(errorBoundaryComponent, boundaryProps);
|
|
378
414
|
}
|
|
379
415
|
}
|
|
380
416
|
|
package/src/server/types.ts
CHANGED
|
@@ -22,8 +22,7 @@ export interface MiddlewareContext {
|
|
|
22
22
|
req: Request;
|
|
23
23
|
requestHeaders: Headers;
|
|
24
24
|
headers: Headers;
|
|
25
|
-
|
|
26
|
-
searchParams: unknown;
|
|
25
|
+
segmentParams: Record<string, string | string[]>;
|
|
27
26
|
/** Declare early hints for critical resources. Appends Link headers. */
|
|
28
27
|
earlyHints: (hints: EarlyHint[]) => void;
|
|
29
28
|
}
|
|
@@ -37,7 +36,6 @@ export interface RouteContext {
|
|
|
37
36
|
|
|
38
37
|
export interface AccessContext {
|
|
39
38
|
params: Record<string, string | string[]>;
|
|
40
|
-
searchParams: unknown;
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
export interface Metadata {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version Skew Detection — graceful recovery when stale clients hit new deployments.
|
|
3
|
+
*
|
|
4
|
+
* When a new version of the app is deployed, clients with open tabs still have
|
|
5
|
+
* the old JavaScript bundle. Without version skew handling, these stale clients
|
|
6
|
+
* will experience:
|
|
7
|
+
*
|
|
8
|
+
* 1. Server action calls that crash (action IDs are content-hashed)
|
|
9
|
+
* 2. Chunk load failures (old filenames gone from CDN)
|
|
10
|
+
* 3. RSC payload mismatches (component references differ between builds)
|
|
11
|
+
*
|
|
12
|
+
* This module implements deployment ID comparison:
|
|
13
|
+
* - A per-build deployment ID is generated at build time (see build-manifest.ts)
|
|
14
|
+
* - The client sends it via `X-Timber-Deployment-Id` header on every RSC/action request
|
|
15
|
+
* - The server compares it against the current build's ID
|
|
16
|
+
* - On mismatch: signal the client to reload (not crash)
|
|
17
|
+
*
|
|
18
|
+
* The deployment ID is always-on in production. Dev mode skips the check
|
|
19
|
+
* (HMR handles code updates without full reloads).
|
|
20
|
+
*
|
|
21
|
+
* See design/25-production-deployments.md, TIM-446
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// ─── Constants ───────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Header sent by the client with every RSC/action request. */
|
|
27
|
+
export const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';
|
|
28
|
+
|
|
29
|
+
/** Response header that signals the client to do a full page reload. */
|
|
30
|
+
export const RELOAD_HEADER = 'X-Timber-Reload';
|
|
31
|
+
|
|
32
|
+
// ─── Deployment ID ───────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The current build's deployment ID. Set at startup from the manifest init
|
|
36
|
+
* module (globalThis.__TIMBER_DEPLOYMENT_ID__). Null in dev mode.
|
|
37
|
+
*/
|
|
38
|
+
let currentDeploymentId: string | null = null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Set the current deployment ID. Called once at server startup from the
|
|
42
|
+
* manifest init module. In dev mode this is never called (deployment ID
|
|
43
|
+
* checks are skipped).
|
|
44
|
+
*/
|
|
45
|
+
export function setDeploymentId(id: string): void {
|
|
46
|
+
currentDeploymentId = id;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the current deployment ID. Returns null in dev mode.
|
|
51
|
+
*/
|
|
52
|
+
export function getDeploymentId(): string | null {
|
|
53
|
+
return currentDeploymentId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Skew Detection ──────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/** Result of a version skew check. */
|
|
59
|
+
export interface SkewCheckResult {
|
|
60
|
+
/** Whether the client's deployment ID matches the server's. */
|
|
61
|
+
ok: boolean;
|
|
62
|
+
/** The client's deployment ID (null if header not sent — e.g., initial page load). */
|
|
63
|
+
clientId: string | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a request's deployment ID matches the current build.
|
|
68
|
+
*
|
|
69
|
+
* Returns `{ ok: true }` when:
|
|
70
|
+
* - Dev mode (no deployment ID set — HMR handles updates)
|
|
71
|
+
* - No deployment ID header (initial page load, non-RSC request)
|
|
72
|
+
* - Deployment IDs match
|
|
73
|
+
*
|
|
74
|
+
* Returns `{ ok: false }` when:
|
|
75
|
+
* - Client sends a deployment ID that differs from the current build
|
|
76
|
+
*/
|
|
77
|
+
export function checkVersionSkew(req: Request): SkewCheckResult {
|
|
78
|
+
// Dev mode — no deployment ID checks (HMR handles updates)
|
|
79
|
+
if (!currentDeploymentId) {
|
|
80
|
+
return { ok: true, clientId: null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const clientId = req.headers.get(DEPLOYMENT_ID_HEADER);
|
|
84
|
+
|
|
85
|
+
// No header — initial page load or non-RSC request. Always OK.
|
|
86
|
+
if (!clientId) {
|
|
87
|
+
return { ok: true, clientId: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Compare deployment IDs
|
|
91
|
+
if (clientId === currentDeploymentId) {
|
|
92
|
+
return { ok: true, clientId };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { ok: false, clientId };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Apply version skew reload headers to a response.
|
|
100
|
+
* Sets X-Timber-Reload: 1 to signal the client to do a full page reload.
|
|
101
|
+
*/
|
|
102
|
+
export function applyReloadHeaders(headers: Headers): void {
|
|
103
|
+
headers.set(RELOAD_HEADER, '1');
|
|
104
|
+
}
|
package/src/shims/navigation.ts
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny typed state machine utility.
|
|
3
|
+
*
|
|
4
|
+
* Enforces discriminated-union states, typed transitions with runtime
|
|
5
|
+
* guards, subscribe for external store integration, and assertPhase
|
|
6
|
+
* for function entry guards.
|
|
7
|
+
*
|
|
8
|
+
* Designed for 3–5 state consumers (stream transforms, client navigation,
|
|
9
|
+
* build phase sequencing). No async, no hierarchy, no history.
|
|
10
|
+
*
|
|
11
|
+
* Performance: send() is one object lookup + one function call.
|
|
12
|
+
* Equivalent cost to a boolean check after V8 inlining.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** A state machine instance with typed state and events. */
|
|
16
|
+
export interface Machine<TState extends { phase: string }, TEvent extends { type: string }> {
|
|
17
|
+
/** Current state (discriminated union — narrowed by phase). */
|
|
18
|
+
readonly state: TState;
|
|
19
|
+
|
|
20
|
+
/** Transition with runtime guard. Throws on invalid source+event pair. */
|
|
21
|
+
send(event: TEvent): void;
|
|
22
|
+
|
|
23
|
+
/** Subscribe to state changes. Returns unsubscribe function. */
|
|
24
|
+
subscribe(listener: (state: TState) => void): () => void;
|
|
25
|
+
|
|
26
|
+
/** Throw if not in the expected phase. Entry guard for functions. */
|
|
27
|
+
assertPhase<P extends TState['phase']>(phase: P): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Transition map: `transitions[phase][eventType]` returns the next state.
|
|
32
|
+
*
|
|
33
|
+
* Each handler receives the current state (narrowed by phase context)
|
|
34
|
+
* and the event, returning the new state.
|
|
35
|
+
*/
|
|
36
|
+
export type TransitionMap<TState extends { phase: string }, TEvent extends { type: string }> = {
|
|
37
|
+
[P in TState['phase']]?: {
|
|
38
|
+
[E in TEvent['type']]?: (
|
|
39
|
+
state: Extract<TState, { phase: P }>,
|
|
40
|
+
event: Extract<TEvent, { type: E }>
|
|
41
|
+
) => TState;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface MachineConfig<TState extends { phase: string }, TEvent extends { type: string }> {
|
|
46
|
+
initial: TState;
|
|
47
|
+
transitions: TransitionMap<TState, TEvent>;
|
|
48
|
+
/** Fires after every valid transition. Use for side effects. */
|
|
49
|
+
onTransition?: (prev: TState, next: TState, event: TEvent) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a state machine from a config.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* type State = { phase: 'idle' } | { phase: 'running'; pid: number };
|
|
58
|
+
* type Event = { type: 'START'; pid: number } | { type: 'STOP' };
|
|
59
|
+
*
|
|
60
|
+
* const m = createMachine<State, Event>({
|
|
61
|
+
* initial: { phase: 'idle' },
|
|
62
|
+
* transitions: {
|
|
63
|
+
* idle: { START: (_s, e) => ({ phase: 'running', pid: e.pid }) },
|
|
64
|
+
* running: { STOP: () => ({ phase: 'idle' }) },
|
|
65
|
+
* },
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function createMachine<TState extends { phase: string }, TEvent extends { type: string }>(
|
|
70
|
+
config: MachineConfig<TState, TEvent>
|
|
71
|
+
): Machine<TState, TEvent> {
|
|
72
|
+
let current: TState = config.initial;
|
|
73
|
+
const listeners = new Set<(state: TState) => void>();
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
get state() {
|
|
77
|
+
return current;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
send(event: TEvent): void {
|
|
81
|
+
const phaseHandlers = config.transitions[current.phase as TState['phase']];
|
|
82
|
+
const handler = phaseHandlers?.[event.type as TEvent['type']] as
|
|
83
|
+
| ((state: TState, event: TEvent) => TState)
|
|
84
|
+
| undefined;
|
|
85
|
+
|
|
86
|
+
if (!handler) {
|
|
87
|
+
throw new Error(`[state-machine] Invalid transition: ${current.phase} + ${event.type}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const prev = current;
|
|
91
|
+
current = handler(prev, event);
|
|
92
|
+
|
|
93
|
+
config.onTransition?.(prev, current, event);
|
|
94
|
+
|
|
95
|
+
for (const listener of listeners) {
|
|
96
|
+
listener(current);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
subscribe(listener: (state: TState) => void): () => void {
|
|
101
|
+
listeners.add(listener);
|
|
102
|
+
return () => listeners.delete(listener);
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
assertPhase<P extends TState['phase']>(phase: P): void {
|
|
106
|
+
if (current.phase !== phase) {
|
|
107
|
+
throw new Error(`[state-machine] Expected phase "${phase}", got "${current.phase}"`);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"als-registry-B7DbZ2hS.js","names":[],"sources":["../../src/server/als-registry.ts"],"sourcesContent":["/**\n * Centralized AsyncLocalStorage registry for server-side per-request state.\n *\n * ALL ALS instances used by the server framework live here. Individual\n * modules (request-context.ts, tracing.ts, actions.ts, etc.) import from\n * this registry and re-export public accessor functions.\n *\n * Why: ALS instances require singleton semantics — if two copies of the\n * same ALS exist (one from a relative import, one from a barrel import),\n * one module writes to its copy and another reads from an empty copy.\n * Centralizing ALS creation in a single module eliminates this class of bug.\n *\n * The `timber-shims` plugin ensures `@timber-js/app/server` resolves to\n * src/ in RSC and SSR environments, so all import paths converge here.\n *\n * DO NOT create ALS instances outside this file. If you need a new ALS,\n * add it here and import from `./als-registry.js` in the consuming module.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\".\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\n\n// ─── Request Context ──────────────────────────────────────────────────────\n// Used by: request-context.ts (headers(), cookies(), searchParams())\n// Design doc: design/04-authorization.md\n\n/** @internal — import via request-context.ts public API */\nexport const requestContextAls = new AsyncLocalStorage<RequestContextStore>();\n\nexport interface RequestContextStore {\n /** Incoming request headers (read-only view). */\n headers: Headers;\n /** Raw cookie header string, parsed lazily into a Map on first access. */\n cookieHeader: string;\n /** Lazily-parsed cookie map (mutable — reflects write-overlay from set()). */\n parsedCookies?: Map<string, string>;\n /** Original (pre-overlay) frozen headers, kept for overlay merging. */\n originalHeaders: Headers;\n /**\n * Promise resolving to the route's typed search params (when search-params.ts\n * exists) or to the raw URLSearchParams. Stored as a Promise so the framework\n * can later support partial pre-rendering where param resolution is deferred.\n */\n searchParamsPromise: Promise<URLSearchParams | Record<string, unknown>>;\n /** Outgoing Set-Cookie entries (name → serialized value + options). Last write wins. */\n cookieJar: Map<string, CookieEntry>;\n /** Whether the response has flushed (headers committed). */\n flushed: boolean;\n /** Whether the current context allows cookie mutation. */\n mutableContext: boolean;\n}\n\n/** A single outgoing cookie entry in the cookie jar. */\nexport interface CookieEntry {\n name: string;\n value: string;\n options: import('./request-context.js').CookieOptions;\n}\n\n// ─── Tracing ──────────────────────────────────────────────────────────────\n// Used by: tracing.ts (traceId(), spanId())\n// Design doc: design/17-logging.md\n\nexport interface TraceStore {\n /** 32-char lowercase hex trace ID (OTEL or UUID fallback). */\n traceId: string;\n /** OTEL span ID if available, undefined otherwise. */\n spanId?: string;\n}\n\n/** @internal — import via tracing.ts public API */\nexport const traceAls = new AsyncLocalStorage<TraceStore>();\n\n// ─── Server-Timing ────────────────────────────────────────────────────────\n// Used by: server-timing.ts (recordTiming(), withTiming())\n// Design doc: (dev-only performance instrumentation)\n\nexport interface TimingStore {\n entries: import('./server-timing.js').TimingEntry[];\n}\n\n/** @internal — import via server-timing.ts public API */\nexport const timingAls = new AsyncLocalStorage<TimingStore>();\n\n// ─── Revalidation ─────────────────────────────────────────────────────────\n// Used by: actions.ts (revalidatePath(), revalidateTag())\n// Design doc: design/08-forms-and-actions.md\n\nexport interface RevalidationState {\n /** Paths to re-render (populated by revalidatePath calls). */\n paths: string[];\n /** Tags to invalidate (populated by revalidateTag calls). */\n tags: string[];\n}\n\n/** @internal — import via actions.ts public API */\nexport const revalidationAls = new AsyncLocalStorage<RevalidationState>();\n\n// ─── Form Flash ───────────────────────────────────────────────────────────\n// Used by: form-flash.ts (getFormFlash())\n// Design doc: design/08-forms-and-actions.md §\"No-JS Error Round-Trip\"\n\n/** @internal — import via form-flash.ts public API */\nexport const formFlashAls = new AsyncLocalStorage<import('./form-flash.js').FormFlashData>();\n\n// ─── Early Hints Sender ──────────────────────────────────────────────────\n// Used by: early-hints-sender.ts (sendEarlyHints103())\n// Design doc: design/02-rendering-pipeline.md §\"Early Hints (103)\"\n\n/** Function that sends Link header values as a 103 Early Hints response. */\nexport type EarlyHintsSenderFn = (links: string[]) => void;\n\n/** @internal — import via early-hints-sender.ts public API */\nexport const earlyHintsSenderAls = new AsyncLocalStorage<EarlyHintsSenderFn>();\n\n// ─── waitUntil Bridge ────────────────────────────────────────────────────\n// Used by: waituntil-bridge.ts (waitUntil())\n// Design doc: design/11-platform.md §\"waitUntil()\"\n\n/** Function that extends the request lifecycle with a background promise. */\nexport type WaitUntilFn = (promise: Promise<unknown>) => void;\n\n/** @internal — import via waituntil-bridge.ts public API */\nexport const waitUntilAls = new AsyncLocalStorage<WaitUntilFn>();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAa,oBAAoB,IAAI,mBAAwC;;AA4C7E,IAAa,WAAW,IAAI,mBAA+B;;AAW3D,IAAa,YAAY,IAAI,mBAAgC;;AAc7D,IAAa,kBAAkB,IAAI,mBAAsC;;AAOzE,IAAa,eAAe,IAAI,mBAA4D;;AAU5F,IAAa,sBAAsB,IAAI,mBAAuC;;AAU9E,IAAa,eAAe,IAAI,mBAAgC"}
|