@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.
@@ -1 +1 @@
1
- {"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/server/slot-resolver.ts"],"names":[],"mappings":"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"}
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;AAoM3F;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,SAAS,EACnB,MAAM,CAAC,EAAE,SAAS,GACjB,SAAS,CAEX"}
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.201",
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.3.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: Pick<HotInterface, 'send'>): void {
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
- hot.send('timber:client-error', {
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
- hot.send('timber:client-error', {
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(rendered as React.ReactNode, denyFallback);
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(slotNode, element, h, scope, denyFallback);
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(rendered as React.ReactNode, props.denyFallback, props.children);
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(ContainedSlotComponent, key === null ? wrapped : { ...wrapped, key });
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
  }