@timber-js/app 0.2.0-alpha.201 → 0.2.0-alpha.202
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/client/browser-dev.d.ts +5 -1
- package/dist/client/browser-dev.d.ts.map +1 -1
- package/dist/index.js +217 -58
- package/dist/index.js.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/slot-subtree-contain.d.ts +1 -1
- package/dist/server/slot-subtree-contain.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/client/browser-dev.ts +35 -3
- package/src/server/slot-resolver.ts +29 -7
- package/src/server/slot-subtree-contain.ts +13 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/server/slot-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AASH,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAQ9D,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAI5E,KAAK,eAAe,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,KAAK,CAAC,YAAY,CAAC;AAgClE;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,UAAU,EACjB,CAAC,EAAE,eAAe,EAClB,YAAY,CAAC,EAAE,mBAAmB,EAClC,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/server/slot-resolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -50,5 +50,5 @@ import { type ReactNode } from 'react';
|
|
|
50
50
|
* the walk must leave alone: a slot layout passes the `children` element it
|
|
51
51
|
* received, so the page beneath it is not wrapped a second time.
|
|
52
52
|
*/
|
|
53
|
-
export declare function containSlotSubtree(tree: ReactNode, fallback: ReactNode, opaque?: ReactNode): ReactNode;
|
|
53
|
+
export declare function containSlotSubtree(tree: ReactNode, fallback: ReactNode, opaque?: ReactNode, slotName?: string): ReactNode;
|
|
54
54
|
//# sourceMappingURL=slot-subtree-contain.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slot-subtree-contain.d.ts","sourceRoot":"","sources":["../../src/server/slot-subtree-contain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EAAsD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"slot-subtree-contain.d.ts","sourceRoot":"","sources":["../../src/server/slot-subtree-contain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EAAsD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AA4M3F;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,SAAS,EACnB,MAAM,CAAC,EAAE,SAAS,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,SAAS,CAEX"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.202",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
"@opentelemetry/sdk-trace-base": "^2.11.0",
|
|
153
153
|
"cookie": "^2.0.1",
|
|
154
154
|
"jsonc-parser": "^3.3.1",
|
|
155
|
-
"magic-string": "^1.
|
|
155
|
+
"magic-string": "^1.4.1",
|
|
156
156
|
"nitro": "3.0.260903-beta",
|
|
157
157
|
"oxc-transform-react": "^0.150.0",
|
|
158
158
|
"srvx": "^1.0.5"
|
|
@@ -235,8 +235,40 @@ export function hideCompilingOverlay(): void {
|
|
|
235
235
|
*
|
|
236
236
|
* The server receives 'timber:client-error' events, and echoes them
|
|
237
237
|
* back as Vite '{ type: "error" }' payloads to trigger the overlay.
|
|
238
|
+
*
|
|
239
|
+
* Errors that fire before the Vite HMR WebSocket has connected are
|
|
240
|
+
* buffered and flushed once the connection is established, so
|
|
241
|
+
* `hot.send()` never throws `SendBeforeConnectError`. See TIM-1509.
|
|
238
242
|
*/
|
|
239
|
-
export function setupClientErrorForwarding(hot:
|
|
243
|
+
export function setupClientErrorForwarding(hot: HotInterface): void {
|
|
244
|
+
// Track WS connection state ourselves because Vite's HMRClient.send()
|
|
245
|
+
// is fire-and-forget: it catches transport rejections internally via
|
|
246
|
+
// .catch(err => logger.error(err)), so a pre-connect send doesn't throw
|
|
247
|
+
// into our code — it just logs a noisy SendBeforeConnectError. Buffering
|
|
248
|
+
// here prevents that noise entirely.
|
|
249
|
+
//
|
|
250
|
+
// The vite:ws:connect listener always fires AFTER registration because
|
|
251
|
+
// WebSocket.onopen is a macrotask and this code runs synchronously during
|
|
252
|
+
// module execution (setupHmr → setupClientErrorForwarding in browser-entry).
|
|
253
|
+
let connected = false;
|
|
254
|
+
const buffer: Array<{ event: string; data: unknown }> = [];
|
|
255
|
+
|
|
256
|
+
function send(event: string, data: unknown): void {
|
|
257
|
+
if (connected) {
|
|
258
|
+
hot.send(event, data);
|
|
259
|
+
} else {
|
|
260
|
+
buffer.push({ event, data });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
hot.on('vite:ws:connect', () => {
|
|
265
|
+
connected = true;
|
|
266
|
+
for (const queued of buffer) {
|
|
267
|
+
hot.send(queued.event, queued.data);
|
|
268
|
+
}
|
|
269
|
+
buffer.length = 0;
|
|
270
|
+
});
|
|
271
|
+
|
|
240
272
|
window.addEventListener('error', (event: ErrorEvent) => {
|
|
241
273
|
// Skip errors without useful information
|
|
242
274
|
if (!event.error && !event.message) return;
|
|
@@ -248,7 +280,7 @@ export function setupClientErrorForwarding(hot: Pick<HotInterface, 'send'>): voi
|
|
|
248
280
|
if (isFrameworkSignalError(event.error)) return;
|
|
249
281
|
|
|
250
282
|
const error = event.error;
|
|
251
|
-
|
|
283
|
+
send('timber:client-error', {
|
|
252
284
|
message: error?.message ?? event.message,
|
|
253
285
|
stack: error?.stack ?? '',
|
|
254
286
|
componentStack: error?.componentStack ?? null,
|
|
@@ -266,7 +298,7 @@ export function setupClientErrorForwarding(hot: Pick<HotInterface, 'send'>): voi
|
|
|
266
298
|
const message = reason instanceof Error ? reason.message : String(reason);
|
|
267
299
|
const stack = reason instanceof Error ? (reason.stack ?? '') : '';
|
|
268
300
|
|
|
269
|
-
|
|
301
|
+
send('timber:client-error', {
|
|
270
302
|
message,
|
|
271
303
|
stack,
|
|
272
304
|
componentStack: null,
|
|
@@ -156,7 +156,12 @@ export async function resolveSlotElement(
|
|
|
156
156
|
const rendered = await scope(() =>
|
|
157
157
|
(SlotPage as (props: Record<string, unknown>) => unknown)(props)
|
|
158
158
|
);
|
|
159
|
-
return containSlotSubtree(
|
|
159
|
+
return containSlotSubtree(
|
|
160
|
+
rendered as React.ReactNode,
|
|
161
|
+
denyFallback,
|
|
162
|
+
undefined,
|
|
163
|
+
slotNode.segmentName
|
|
164
|
+
);
|
|
160
165
|
} catch (error) {
|
|
161
166
|
// RedirectSignal must propagate — the pipeline handles redirects
|
|
162
167
|
// at the top level. Swallowing it here would silently return
|
|
@@ -189,12 +194,20 @@ export async function resolveSlotElement(
|
|
|
189
194
|
element,
|
|
190
195
|
h,
|
|
191
196
|
scope,
|
|
192
|
-
denyFallback
|
|
197
|
+
denyFallback,
|
|
198
|
+
slotNode.segmentName
|
|
193
199
|
);
|
|
194
200
|
|
|
195
201
|
// Wrap with slot root's layout — INSIDE the access gate, so the layout
|
|
196
202
|
// server component never executes when access.ts denies. See TIM-1074.
|
|
197
|
-
element = await wrapWithLayout(
|
|
203
|
+
element = await wrapWithLayout(
|
|
204
|
+
slotNode,
|
|
205
|
+
element,
|
|
206
|
+
h,
|
|
207
|
+
scope,
|
|
208
|
+
denyFallback,
|
|
209
|
+
slotNode.segmentName
|
|
210
|
+
);
|
|
198
211
|
|
|
199
212
|
// Wrap in SlotAccessGate if slot root has access.ts. The gate is
|
|
200
213
|
// OUTSIDE the layout, matching the segment model in
|
|
@@ -290,7 +303,8 @@ async function wrapWithIntermediateSegments(
|
|
|
290
303
|
element: React.ReactElement,
|
|
291
304
|
h: CreateElementFn,
|
|
292
305
|
scope?: SlotParamScope,
|
|
293
|
-
denyFallback?: React.ReactNode
|
|
306
|
+
denyFallback?: React.ReactNode,
|
|
307
|
+
slotName?: string
|
|
294
308
|
): Promise<React.ReactElement> {
|
|
295
309
|
for (let i = chain.length - 1; i > 0; i--) {
|
|
296
310
|
const seg = chain[i];
|
|
@@ -300,7 +314,7 @@ async function wrapWithIntermediateSegments(
|
|
|
300
314
|
errorBoundaryComponent: TimberErrorBoundary,
|
|
301
315
|
isSlotBoundary: true,
|
|
302
316
|
});
|
|
303
|
-
element = await wrapWithLayout(seg, element, h, scope, denyFallback);
|
|
317
|
+
element = await wrapWithLayout(seg, element, h, scope, denyFallback, slotName);
|
|
304
318
|
element = await wrapWithAccessGate(seg, element, h, scope);
|
|
305
319
|
}
|
|
306
320
|
return element;
|
|
@@ -341,10 +355,16 @@ function ScopedSlotLayout(props: {
|
|
|
341
355
|
layout: (props: Record<string, unknown>) => unknown;
|
|
342
356
|
scope: SlotParamScope;
|
|
343
357
|
denyFallback: React.ReactNode;
|
|
358
|
+
slotName: string | undefined;
|
|
344
359
|
children: React.ReactElement;
|
|
345
360
|
}): unknown {
|
|
346
361
|
const rendered = props.scope(() => props.layout({ children: props.children }));
|
|
347
|
-
return containSlotSubtree(
|
|
362
|
+
return containSlotSubtree(
|
|
363
|
+
rendered as React.ReactNode,
|
|
364
|
+
props.denyFallback,
|
|
365
|
+
props.children,
|
|
366
|
+
props.slotName
|
|
367
|
+
);
|
|
348
368
|
}
|
|
349
369
|
|
|
350
370
|
/**
|
|
@@ -362,7 +382,8 @@ async function wrapWithLayout(
|
|
|
362
382
|
element: React.ReactElement,
|
|
363
383
|
h: CreateElementFn,
|
|
364
384
|
scope?: SlotParamScope,
|
|
365
|
-
denyFallback: React.ReactNode = null
|
|
385
|
+
denyFallback: React.ReactNode = null,
|
|
386
|
+
slotName?: string
|
|
366
387
|
): Promise<React.ReactElement> {
|
|
367
388
|
if (!node.layout) return element;
|
|
368
389
|
const Layout = await loadComponent(node.layout);
|
|
@@ -373,6 +394,7 @@ async function wrapWithLayout(
|
|
|
373
394
|
layout: Layout as (props: Record<string, unknown>) => unknown,
|
|
374
395
|
scope,
|
|
375
396
|
denyFallback,
|
|
397
|
+
slotName,
|
|
376
398
|
children: element,
|
|
377
399
|
});
|
|
378
400
|
}
|
|
@@ -65,6 +65,10 @@ interface Containment {
|
|
|
65
65
|
* so the level below is not wrapped (and re-walked) a second time.
|
|
66
66
|
*/
|
|
67
67
|
opaque: unknown;
|
|
68
|
+
/** Slot name (e.g. `@artists`) used as a key prefix for wrapped elements. */
|
|
69
|
+
slotName?: string;
|
|
70
|
+
/** Monotonic counter for disambiguating same-name sibling components. */
|
|
71
|
+
wrapCount: number;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
function isThenable(value: unknown): value is Thenable<unknown> {
|
|
@@ -156,7 +160,7 @@ function ContainedSlotComponent(props: {
|
|
|
156
160
|
fallback: ReactNode;
|
|
157
161
|
}): ReactNode | Promise<ReactNode> {
|
|
158
162
|
const { component: Component, componentProps, fallback } = props;
|
|
159
|
-
const containment: Containment = { fallback, opaque: undefined };
|
|
163
|
+
const containment: Containment = { fallback, opaque: undefined, wrapCount: 0 };
|
|
160
164
|
let result: unknown;
|
|
161
165
|
try {
|
|
162
166
|
result = Component(componentProps);
|
|
@@ -204,12 +208,16 @@ function walk(node: unknown, c: Containment, seen: WeakSet<object>): unknown {
|
|
|
204
208
|
const { type, key, props } = node;
|
|
205
209
|
const component = type === ContainedSlotComponent ? undefined : serverComponentOf(type);
|
|
206
210
|
if (component) {
|
|
211
|
+
const effectiveKey = key ?? (c.slotName ? `${c.slotName}:${c.wrapCount++}` : null);
|
|
207
212
|
const wrapped = {
|
|
208
213
|
component,
|
|
209
214
|
componentProps: props as Record<string, unknown>,
|
|
210
215
|
fallback: c.fallback,
|
|
211
216
|
};
|
|
212
|
-
return createElement(
|
|
217
|
+
return createElement(
|
|
218
|
+
ContainedSlotComponent,
|
|
219
|
+
effectiveKey === null ? wrapped : { ...wrapped, key: effectiveKey }
|
|
220
|
+
);
|
|
213
221
|
}
|
|
214
222
|
// Host elements, fragments, Suspense, client references, providers, and
|
|
215
223
|
// wrappers this walk already made: keep the type, contain whatever server
|
|
@@ -249,7 +257,8 @@ function walk(node: unknown, c: Containment, seen: WeakSet<object>): unknown {
|
|
|
249
257
|
export function containSlotSubtree(
|
|
250
258
|
tree: ReactNode,
|
|
251
259
|
fallback: ReactNode,
|
|
252
|
-
opaque?: ReactNode
|
|
260
|
+
opaque?: ReactNode,
|
|
261
|
+
slotName?: string
|
|
253
262
|
): ReactNode {
|
|
254
|
-
return walk(tree, { fallback, opaque }, new WeakSet()) as ReactNode;
|
|
263
|
+
return walk(tree, { fallback, opaque, slotName, wrapCount: 0 }, new WeakSet()) as ReactNode;
|
|
255
264
|
}
|