@barefootjs/cli 0.5.1 → 0.5.3

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.
@@ -12,6 +12,7 @@
12
12
 
13
13
  ## Reactivity
14
14
 
15
+ - [batch](https://barefootjs.dev/docs/reactivity/batch.md): Groups multiple signal writes so dependent effects and memos run once, after all writes complete.
15
16
  - [createEffect](https://barefootjs.dev/docs/reactivity/create-effect.md): Runs a function and re-runs it whenever its tracked signal dependencies change.
16
17
  - [createMemo](https://barefootjs.dev/docs/reactivity/create-memo.md): Creates a cached derived value that recomputes only when its dependencies change.
17
18
  - [createSignal](https://barefootjs.dev/docs/reactivity/create-signal.md): Creates a reactive getter/setter pair for managing state.
@@ -0,0 +1,135 @@
1
+ ---
2
+ title: batch
3
+ description: Groups multiple signal writes so dependent effects and memos run once, after all writes complete.
4
+ ---
5
+
6
+ # batch
7
+
8
+ Groups multiple signal writes so that dependent effects and memos run **once**,
9
+ after all the writes inside the batch complete — instead of once per write.
10
+
11
+ ```tsx
12
+ import { batch } from '@barefootjs/client'
13
+
14
+ batch<T>(fn: () => T): T
15
+ ```
16
+
17
+ Returns the value produced by `fn`.
18
+
19
+ ## Default behavior (no batch)
20
+
21
+ BarefootJS propagates updates **synchronously**: each setter call immediately
22
+ re-runs every subscriber. This keeps reads-after-writes predictable — after a
23
+ setter returns, derived memos, effects, and the DOM already reflect the new value.
24
+
25
+ The cost is that writing N signals that share a subscriber re-runs that
26
+ subscriber N times, and the subscriber briefly observes intermediate states
27
+ where some signals are updated and others are not:
28
+
29
+ ```tsx
30
+ const [x, setX] = createSignal(40)
31
+ const [y, setY] = createSignal(60)
32
+
33
+ createEffect(() => {
34
+ // depends on both x and y
35
+ send({ x: x(), y: y() })
36
+ })
37
+
38
+ setX(70) // effect runs — observes x=70, y=60 (intermediate)
39
+ setY(30) // effect runs again — observes x=70, y=30
40
+ ```
41
+
42
+ Beyond its initial run on creation, the effect ran twice more — once per write —
43
+ and saw a transient `x=70, y=60` state.
44
+
45
+ ## With batch
46
+
47
+ ```tsx
48
+ batch(() => {
49
+ setX(70)
50
+ setY(30)
51
+ })
52
+ // effect runs once, observing x=70, y=30
53
+ ```
54
+
55
+ Inside `batch`, writes are collected and dependent subscribers are de-duplicated,
56
+ so each runs **exactly once** after the batch ends — and never observes an
57
+ intermediate, half-updated state.
58
+
59
+ ## When to use
60
+
61
+ When a single handler updates several signals that feed shared effects/memos,
62
+ `batch` collapses the work into one update pass — and keeps the subscriber from
63
+ running while a cross-field invariant is temporarily broken:
64
+
65
+ ```tsx
66
+ const reset = () => {
67
+ batch(() => {
68
+ setName('')
69
+ setEmail('')
70
+ setAge(0)
71
+ // ...20 more fields
72
+ })
73
+ // every subscriber ran once, not once-per-field
74
+ }
75
+ ```
76
+
77
+ ## Caveats
78
+
79
+ ### Derived values are stale *inside* the batch
80
+
81
+ `batch` defers the work that recomputes derived values. Plain signal reads return
82
+ the new value immediately, but **memos and effect-driven values stay stale until
83
+ the batch ends**:
84
+
85
+ ```tsx
86
+ const [n, setN] = createSignal(1)
87
+ const doubled = createMemo(() => n() * 2)
88
+
89
+ batch(() => {
90
+ setN(10)
91
+ n() // 10 — plain signal read is fresh
92
+ doubled() // 2 — STALE; the memo hasn't recomputed yet
93
+ })
94
+ doubled() // 20 — recomputed after the batch ends
95
+ ```
96
+
97
+ If you need the recomputed value, read it after the batch.
98
+
99
+ ### `await` escapes the batch
100
+
101
+ `batch` only covers the **synchronous** portion of `fn`. Wrapping an async
102
+ function in `batch` groups only the writes before the first `await` — everything
103
+ after runs ungrouped, and the promise `batch` returns is easy to leave floating.
104
+
105
+ Instead, wrap each synchronous group of writes in its own `batch`, with `await`
106
+ between the groups:
107
+
108
+ ```tsx
109
+ const onSubmit = async () => {
110
+ batch(() => {
111
+ setLoading(true)
112
+ setError(null)
113
+ })
114
+
115
+ try {
116
+ const result = await save()
117
+ batch(() => {
118
+ setLoading(false)
119
+ setResult(result)
120
+ })
121
+ } catch (err) {
122
+ batch(() => {
123
+ setLoading(false)
124
+ setError(err)
125
+ })
126
+ }
127
+ }
128
+ ```
129
+
130
+ ## Note
131
+
132
+ `batch` is an **opt-in** optimization. Forgetting it is never a correctness bug —
133
+ code still works, just with extra subscriber runs. Reach for `batch` when a
134
+ handler writes many signals that share subscribers, or when an effect must not
135
+ observe a partially-updated state.
@@ -9,7 +9,7 @@ All reactive primitives are imported from `@barefootjs/client`:
9
9
 
10
10
  ```tsx
11
11
  "use client"
12
- import { createSignal, createEffect, createMemo, onMount, onCleanup, untrack } from '@barefootjs/client'
12
+ import { createSignal, createEffect, createMemo, onMount, onCleanup, untrack, batch } from '@barefootjs/client'
13
13
  ```
14
14
 
15
15
  ## API Reference
@@ -22,6 +22,7 @@ import { createSignal, createEffect, createMemo, onMount, onCleanup, untrack } f
22
22
  | [`onMount`](./reactivity/on-mount.md) | Run once on component initialization |
23
23
  | [`onCleanup`](./reactivity/on-cleanup.md) | Register cleanup for effects and lifecycle |
24
24
  | [`untrack`](./reactivity/untrack.md) | Read signals without tracking dependencies |
25
+ | [`batch`](./reactivity/batch.md) | Group signal writes so subscribers run once |
25
26
 
26
27
  ## Guides
27
28
 
package/dist/index.js CHANGED
@@ -518,6 +518,19 @@ function buildChainedArrayExpr(elem) {
518
518
  chainOrder: elem.chainOrder
519
519
  });
520
520
  }
521
+ function loopOffsetTerms(offset) {
522
+ if (!offset) return [];
523
+ const terms = [];
524
+ if (offset.staticCount) terms.push(String(offset.staticCount));
525
+ terms.push(...offset.dynamicTerms);
526
+ return terms;
527
+ }
528
+ function buildLoopChildIndexExpr(indexParam, offset) {
529
+ return [indexParam, ...loopOffsetTerms(offset)].join(" + ");
530
+ }
531
+ function buildLoopChildIndexSubtraction(offset) {
532
+ return loopOffsetTerms(offset).map((term) => ` - ${term}`).join("");
533
+ }
521
534
  function toDomEventName(eventName) {
522
535
  return jsxToDomEventMap[eventName] ?? eventName;
523
536
  }
@@ -1274,16 +1287,28 @@ function maybeHoistedScopeAttr(inHoistedChildren, node) {
1274
1287
  return inHoistedChildren && node.needsScope ? `${BF_SCOPE}="${BF_PARENT_SCOPE_PLACEHOLDER}"` : null;
1275
1288
  }
1276
1289
  function templateAttrExpr(attrName, valExpr, presenceOrUndefined) {
1290
+ if (attrName === "dangerouslySetInnerHTML") return "";
1277
1291
  if (isBooleanAttr(attrName) || presenceOrUndefined) {
1278
1292
  return `\${${valExpr} ? '${attrName}' : ''}`;
1279
1293
  }
1280
1294
  if (attrName === "style") {
1281
- return `\${((v) => v != null ? 'style="' + v + '"' : '')(styleToCss(${valExpr}))}`;
1295
+ return `\${((v) => v != null ? 'style="' + ${escapeAttrValueExpr("v")} + '"' : '')(styleToCss(${valExpr}))}`;
1282
1296
  }
1283
1297
  if (attrName === "data-key" || attrName.startsWith("data-key-")) {
1284
1298
  return `${attrName}="\${${valExpr}}"`;
1285
1299
  }
1286
- return `\${(${valExpr}) != null ? '${attrName}="' + (${valExpr}) + '"' : ''}`;
1300
+ return `\${(${valExpr}) != null ? '${attrName}="' + ${escapeAttrValueExpr(valExpr)} + '"' : ''}`;
1301
+ }
1302
+ function escapeAttrValueExpr(valExpr) {
1303
+ return `escapeAttr(${valExpr})`;
1304
+ }
1305
+ function escapeTextSlotExpr(innerExpr) {
1306
+ return `escapeText(${innerExpr})`;
1307
+ }
1308
+ function dangerouslyHtmlChildren(attrs, toExpr) {
1309
+ const attr = attrs.find((a) => a.name === "dangerouslySetInnerHTML");
1310
+ if (!attr || attr.value.kind !== "expression") return null;
1311
+ return `\${((${toExpr(attr.value)}) ?? {}).__html ?? ''}`;
1287
1312
  }
1288
1313
  function transformKeyValue(value, transformExpr) {
1289
1314
  switch (value.kind) {
@@ -1328,6 +1353,7 @@ function renderTemplateAttrPart(attr, attrName, wrap, restSpreadNames) {
1328
1353
  function isMergeableAttr(a, ctx2) {
1329
1354
  if (ctx2.honorClientOnly && a.clientOnly) return false;
1330
1355
  if (a.name === "key") return false;
1356
+ if (a.name === "dangerouslySetInnerHTML") return false;
1331
1357
  const v = a.value;
1332
1358
  if (v.kind === "jsx-children") return false;
1333
1359
  if (v.kind === "boolean-shorthand") return false;
@@ -1415,7 +1441,7 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
1415
1441
  }
1416
1442
  const attrs = attrParts.join(" ");
1417
1443
  const childrenRecurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, insideLoop, false);
1418
- const children = node.children.map(childrenRecurse).join("");
1444
+ const children = dangerouslyHtmlChildren(node.attrs, (v) => wrapExpr(v.expr)) ?? node.children.map(childrenRecurse).join("");
1419
1445
  if (children || !VOID_ELEMENTS.has(node.tag)) {
1420
1446
  return `<${node.tag}${attrs ? " " + attrs : ""}>${children}</${node.tag}>`;
1421
1447
  }
@@ -1426,7 +1452,9 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
1426
1452
  case "expression":
1427
1453
  if (node.expr === "null" || node.expr === "undefined") return "";
1428
1454
  if (node.slotId) {
1429
- return `<!--bf:${node.slotId}-->\${${wrapInterpolation(wrapExpr(node.expr))}}<!--/-->`;
1455
+ const inner = wrapInterpolation(wrapExpr(node.expr));
1456
+ const slotted = branchSlotsVar ? inner : escapeTextSlotExpr(inner);
1457
+ return `<!--bf:${node.slotId}-->\${${slotted}}<!--/-->`;
1430
1458
  }
1431
1459
  return `\${${wrapInterpolation(wrapExpr(node.expr))}}`;
1432
1460
  case "conditional": {
@@ -1522,7 +1550,7 @@ function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParam
1522
1550
  attrParts.push(`bf="${node.slotId}"`);
1523
1551
  }
1524
1552
  const attrs = attrParts.join(" ");
1525
- const children = node.children.map(recurse).join("");
1553
+ const children = dangerouslyHtmlChildren(node.attrs, (v) => wrapExpr(v.expr)) ?? node.children.map(recurse).join("");
1526
1554
  if (children || !VOID_ELEMENTS.has(node.tag)) {
1527
1555
  return `<${node.tag}${attrs ? " " + attrs : ""}>${children}</${node.tag}>`;
1528
1556
  }
@@ -1533,7 +1561,7 @@ function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParam
1533
1561
  case "expression":
1534
1562
  if (node.expr === "null" || node.expr === "undefined") return "";
1535
1563
  if (node.slotId) {
1536
- return `<!--bf:${node.slotId}-->\${${wrapExpr(node.expr)}}<!--/-->`;
1564
+ return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(wrapExpr(node.expr))}}<!--/-->`;
1537
1565
  }
1538
1566
  return `\${${wrapExpr(node.expr)}}`;
1539
1567
  case "conditional": {
@@ -1752,7 +1780,7 @@ function irToComponentTemplateWithOpts(node, opts) {
1752
1780
  attrParts.push(`bf="${node.slotId}"`);
1753
1781
  }
1754
1782
  const attrs = attrParts.join(" ");
1755
- const children = node.children.map(childrenRecurse).join("");
1783
+ const children = dangerouslyHtmlChildren(node.attrs, (v) => transformExpr(v.expr, v.templateExpr)) ?? node.children.map(childrenRecurse).join("");
1756
1784
  if (children || !VOID_ELEMENTS.has(node.tag)) {
1757
1785
  return `<${node.tag}${attrs ? " " + attrs : ""}>${children}</${node.tag}>`;
1758
1786
  }
@@ -1763,7 +1791,7 @@ function irToComponentTemplateWithOpts(node, opts) {
1763
1791
  case "expression":
1764
1792
  if (node.expr === "null" || node.expr === "undefined") return "";
1765
1793
  if (node.slotId) {
1766
- return `<!--bf:${node.slotId}-->\${${transformExpr(node.expr, node.templateExpr)}}<!--/-->`;
1794
+ return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(transformExpr(node.expr, node.templateExpr))}}<!--/-->`;
1767
1795
  }
1768
1796
  return `\${${transformExpr(node.expr, node.templateExpr)}}`;
1769
1797
  case "conditional": {
@@ -1966,7 +1994,7 @@ function generateCsrTemplateWithOpts(node, opts) {
1966
1994
  attrParts.push(`bf="${node.slotId}"`);
1967
1995
  }
1968
1996
  const attrs = attrParts.join(" ");
1969
- const children = node.children.map(childrenRecurse).join("");
1997
+ const children = dangerouslyHtmlChildren(node.attrs, (v) => transformExpr(v.expr, v.templateExpr)) ?? node.children.map(childrenRecurse).join("");
1970
1998
  if (children || !VOID_ELEMENTS.has(node.tag)) {
1971
1999
  return `<${node.tag}${attrs ? " " + attrs : ""}>${children}</${node.tag}>`;
1972
2000
  }
@@ -1983,7 +2011,7 @@ function generateCsrTemplateWithOpts(node, opts) {
1983
2011
  const transformed = transformExpr(node.expr, node.templateExpr);
1984
2012
  const expr = transformed === UNSAFE_TEMPLATE_EXPR ? "''" : transformed;
1985
2013
  if (node.slotId) {
1986
- return `<!--bf:${node.slotId}-->\${${expr}}<!--/-->`;
2014
+ return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(expr)}}<!--/-->`;
1987
2015
  }
1988
2016
  return `\${${expr}}`;
1989
2017
  }
@@ -2450,6 +2478,24 @@ function getSourceLocation(node, sourceFile, filePath) {
2450
2478
  }
2451
2479
  };
2452
2480
  }
2481
+ function membersToProperties(members, sourceFile) {
2482
+ return members.filter(ts6.isPropertySignature).map((member) => ({
2483
+ name: propertyNameText(member.name, sourceFile),
2484
+ type: typeNodeToTypeInfo(member.type, sourceFile) ?? {
2485
+ kind: "unknown",
2486
+ raw: "unknown"
2487
+ },
2488
+ optional: !!member.questionToken,
2489
+ readonly: !!member.modifiers?.some(
2490
+ (m) => m.kind === ts6.SyntaxKind.ReadonlyKeyword
2491
+ )
2492
+ }));
2493
+ }
2494
+ function propertyNameText(name, sourceFile) {
2495
+ if (!name) return "";
2496
+ if (ts6.isStringLiteral(name) || ts6.isNumericLiteral(name)) return name.text;
2497
+ return name.getText(sourceFile);
2498
+ }
2453
2499
  function typeNodeToTypeInfo(typeNode, sourceFile) {
2454
2500
  if (!typeNode) return null;
2455
2501
  const raw = typeNode.getText(sourceFile);
@@ -2488,20 +2534,21 @@ function typeNodeToTypeInfo(typeNode, sourceFile) {
2488
2534
  return {
2489
2535
  kind: "object",
2490
2536
  raw,
2491
- properties: typeNode.members.filter(ts6.isPropertySignature).map((member) => ({
2492
- name: member.name?.getText(sourceFile) ?? "",
2493
- type: typeNodeToTypeInfo(member.type, sourceFile) ?? {
2494
- kind: "unknown",
2495
- raw: "unknown"
2496
- },
2497
- optional: !!member.questionToken,
2498
- readonly: !!member.modifiers?.some(
2499
- (m) => m.kind === ts6.SyntaxKind.ReadonlyKeyword
2500
- )
2501
- }))
2537
+ properties: membersToProperties(typeNode.members, sourceFile)
2502
2538
  };
2503
2539
  }
2504
2540
  if (ts6.isTypeReferenceNode(typeNode)) {
2541
+ const refName = ts6.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : "";
2542
+ if ((refName === "Array" || refName === "ReadonlyArray") && typeNode.typeArguments?.length === 1) {
2543
+ return {
2544
+ kind: "array",
2545
+ raw,
2546
+ elementType: typeNodeToTypeInfo(typeNode.typeArguments[0], sourceFile) ?? {
2547
+ kind: "unknown",
2548
+ raw: "unknown"
2549
+ }
2550
+ };
2551
+ }
2505
2552
  return {
2506
2553
  kind: "interface",
2507
2554
  raw
@@ -3679,14 +3726,17 @@ function collectInterfaceDefinition(node, ctx2) {
3679
3726
  kind: "interface",
3680
3727
  name: node.name.text,
3681
3728
  definition: node.getText(ctx2.sourceFile),
3729
+ properties: membersToProperties(node.members, ctx2.sourceFile),
3682
3730
  loc: getSourceLocation(node, ctx2.sourceFile, ctx2.filePath)
3683
3731
  });
3684
3732
  }
3685
3733
  function collectTypeAliasDefinition(node, ctx2) {
3734
+ const properties = ts7.isTypeLiteralNode(node.type) ? membersToProperties(node.type.members, ctx2.sourceFile) : void 0;
3686
3735
  ctx2.typeDefinitions.push({
3687
3736
  kind: "type",
3688
3737
  name: node.name.text,
3689
3738
  definition: node.getText(ctx2.sourceFile),
3739
+ properties,
3690
3740
  loc: getSourceLocation(node, ctx2.sourceFile, ctx2.filePath)
3691
3741
  });
3692
3742
  }
@@ -5882,7 +5932,7 @@ function checkSupport(expr) {
5882
5932
  return {
5883
5933
  supported: false,
5884
5934
  level: "L5_UNSUPPORTED",
5885
- reason: `Higher-order method '${methodName}()' requires client-side evaluation. Use @client directive or pre-compute in Go.`
5935
+ reason: `Method '${methodName}()' has no template lowering and requires client-side evaluation. Wrap the expression in /* @client */ to defer it to hydration, or pre-compute the value before rendering.`
5886
5936
  };
5887
5937
  }
5888
5938
  }
@@ -6172,7 +6222,7 @@ var init_expression_parser = __esm({
6172
6222
  "some",
6173
6223
  "forEach",
6174
6224
  "flatMap",
6175
- "flat"
6225
+ "flat",
6176
6226
  // #1448 Tier A — Array methods. Each method PR adds the lowering
6177
6227
  // (typically a new `array-method` variant or runtime helper) and
6178
6228
  // removes its row here. See packages/adapter-tests/fixtures/methods/.
@@ -6202,6 +6252,34 @@ var init_expression_parser = __esm({
6202
6252
  // `bf_lower` / `bf_upper` (Go) and Perl's native `lc` / `uc` (Mojo).
6203
6253
  // `trim` lowers via the `array-method` IR + `bf_trim` (Go) and a
6204
6254
  // Perl regex strip (Mojo).
6255
+ //
6256
+ // #1448 follow-up — String methods that have NO lowering yet. These
6257
+ // were previously absent from this gate, so `isSupported` reported
6258
+ // them "supported" and the adapters emitted a raw method call
6259
+ // (`{{.Name.StartsWith "a"}}` on Go, `$name->{startsWith}('a')` on
6260
+ // Mojo) with no build diagnostic — a silent footgun that only
6261
+ // surfaced as a crash at template-render time. Listing them here
6262
+ // makes the build fail loudly with BF101 (the same treatment the
6263
+ // unsupported array methods above get), pointing users at the
6264
+ // `/* @client */` escape hatch. Each name drops off as its lowering
6265
+ // lands. See #1448 "Unsupported string methods" Tier B / Tier C.
6266
+ "split",
6267
+ "startsWith",
6268
+ "endsWith",
6269
+ "replace",
6270
+ "replaceAll",
6271
+ "repeat",
6272
+ "padStart",
6273
+ "padEnd",
6274
+ "charAt",
6275
+ "charCodeAt",
6276
+ "codePointAt",
6277
+ "normalize",
6278
+ "substring",
6279
+ "substr",
6280
+ "match",
6281
+ "matchAll",
6282
+ "search"
6205
6283
  ]);
6206
6284
  }
6207
6285
  });
@@ -8260,11 +8338,11 @@ function transformMapCall(node, ctx2, isClientOnly = false, method = "map") {
8260
8338
  if (stmt === returnStmt) break;
8261
8339
  const js = ctx2.getJS(stmt);
8262
8340
  const tjs = ctx2.getTemplateJS(stmt);
8263
- const ts20 = stmt.getText(ctx2.sourceFile);
8341
+ const ts21 = stmt.getText(ctx2.sourceFile);
8264
8342
  preambleStmts.push(js.endsWith(";") ? js : js + ";");
8265
8343
  templatePreambleStmts.push(tjs.endsWith(";") ? tjs : tjs + ";");
8266
- typedPreambleStmts.push(ts20.endsWith(";") ? ts20 : ts20 + ";");
8267
- if (js !== ts20) hasTypeDiff = true;
8344
+ typedPreambleStmts.push(ts21.endsWith(";") ? ts21 : ts21 + ";");
8345
+ if (js !== ts21) hasTypeDiff = true;
8268
8346
  if (js !== tjs) hasTemplateDiff = true;
8269
8347
  }
8270
8348
  if (preambleStmts.length > 0) {
@@ -9540,29 +9618,100 @@ var init_reactivity = __esm({
9540
9618
  });
9541
9619
 
9542
9620
  // ../jsx/src/ir-to-client-js/collect-elements.ts
9543
- function producesDomChild(node) {
9544
- return node.type === "element" || node.type === "component" || node.type === "provider" || node.type === "async" || node.type === "text" || node.type === "expression" && !node.reactive || node.type === "conditional";
9621
+ function domElementCount(node) {
9622
+ switch (node.type) {
9623
+ case "element":
9624
+ case "component":
9625
+ case "provider":
9626
+ case "async":
9627
+ return 1;
9628
+ case "text":
9629
+ return 0;
9630
+ case "expression":
9631
+ return EMPTY_RENDER_EXPRS.has(node.expr.trim()) ? 0 : null;
9632
+ case "loop":
9633
+ if (node.bodyIsItemConditional || node.method === "flatMap") return null;
9634
+ return `(${buildLoopChainExpr({
9635
+ base: node.array,
9636
+ sortComparator: node.sortComparator,
9637
+ filterPredicate: node.filterPredicate,
9638
+ chainOrder: node.chainOrder
9639
+ })}).length`;
9640
+ case "conditional": {
9641
+ const t = domElementCount(node.whenTrue);
9642
+ const f = domElementCount(node.whenFalse);
9643
+ if (t === null || f === null) return null;
9644
+ if (typeof t === "number" && typeof f === "number" && t === f) return t;
9645
+ return `(${node.condition} ? ${t} : ${f})`;
9646
+ }
9647
+ case "fragment":
9648
+ return sumElementCounts(node.children);
9649
+ default:
9650
+ return null;
9651
+ }
9652
+ }
9653
+ function sumElementCounts(nodes) {
9654
+ let staticCount = 0;
9655
+ const dynamic = [];
9656
+ for (const n of nodes) {
9657
+ const c = domElementCount(n);
9658
+ if (c === null) return null;
9659
+ if (typeof c === "number") staticCount += c;
9660
+ else dynamic.push(c);
9661
+ }
9662
+ if (dynamic.length === 0) return staticCount;
9663
+ const parts = staticCount > 0 ? [String(staticCount), ...dynamic] : dynamic;
9664
+ return parts.length === 1 ? parts[0] : `(${parts.join(" + ")})`;
9665
+ }
9666
+ function legacyElementCount(node) {
9667
+ return node.type === "element" || node.type === "component" || node.type === "provider" || node.type === "async" || node.type === "text" || node.type === "expression" && !node.reactive || node.type === "conditional" ? 1 : 0;
9545
9668
  }
9546
9669
  function computeLoopSiblingOffsets(root) {
9547
9670
  const offsets = /* @__PURE__ */ new Map();
9548
- walkIR(root, null, {
9549
- element: ({ node: el, descend }) => {
9550
- let nonLoopCount = 0;
9551
- for (const child of el.children) {
9552
- if (child.type === "loop") {
9553
- if (nonLoopCount > 0) offsets.set(child, nonLoopCount);
9554
- } else if (producesDomChild(child)) {
9555
- nonLoopCount++;
9671
+ const recordRun = (children, preceding) => {
9672
+ for (const child of children) {
9673
+ if (child.type === "loop") {
9674
+ if (preceding.length > 0 && !offsets.has(child)) {
9675
+ offsets.set(child, [...preceding]);
9556
9676
  }
9677
+ preceding.push(child);
9678
+ } else if (child.type === "fragment" || child.type === "provider" || child.type === "async") {
9679
+ recordRun(child.children, preceding);
9680
+ } else {
9681
+ preceding.push(child);
9557
9682
  }
9558
- descend();
9559
9683
  }
9560
- // All container kinds (fragment / component / provider / async / loop /
9561
- // conditional / if-statement) rely on walkIR's default descent with the
9562
- // same scope. Leaves (text / expression / slot) are no-ops.
9684
+ };
9685
+ const containerVisit = ({ node, descend }) => {
9686
+ recordRun(node.children, []);
9687
+ descend();
9688
+ };
9689
+ walkIR(root, null, {
9690
+ element: containerVisit,
9691
+ component: containerVisit,
9692
+ fragment: containerVisit,
9693
+ provider: containerVisit,
9694
+ async: containerVisit
9695
+ // `loop` / `conditional` / `if-statement` are not flat sibling
9696
+ // containers (their children are item bodies / branches), and leaves
9697
+ // (text / expression / slot) have no children — all rely on walkIR's
9698
+ // default descent with the same scope.
9563
9699
  });
9564
9700
  return offsets;
9565
9701
  }
9702
+ function resolveLoopOffset(preceding) {
9703
+ if (!preceding || preceding.length === 0) return void 0;
9704
+ let staticCount = 0;
9705
+ const dynamicTerms = [];
9706
+ for (const node of preceding) {
9707
+ const c = domElementCount(node);
9708
+ if (c === null) staticCount += legacyElementCount(node);
9709
+ else if (typeof c === "number") staticCount += c;
9710
+ else dynamicTerms.push(c);
9711
+ }
9712
+ if (staticCount === 0 && dynamicTerms.length === 0) return void 0;
9713
+ return { staticCount, dynamicTerms };
9714
+ }
9566
9715
  function collectInnerLoops(nodes, siblingOffsets, outerLoopParam, ctx2, options) {
9567
9716
  const result = [];
9568
9717
  const flat = options?.flatBranchMode === true;
@@ -9648,7 +9797,7 @@ function collectInnerLoops(nodes, siblingOffsets, outerLoopParam, ctx2, options)
9648
9797
  refsOuterParam: refsOuter,
9649
9798
  childComponents,
9650
9799
  insideConditional: !flat && scope.insideCond ? true : void 0,
9651
- siblingOffset: flat ? void 0 : siblingOffsets.get(n) || void 0,
9800
+ offset: flat ? void 0 : resolveLoopOffset(siblingOffsets.get(n)),
9652
9801
  bindings
9653
9802
  });
9654
9803
  if (!flat) {
@@ -9858,7 +10007,7 @@ function collectElements(node, ctx2, siblingOffsets, insideConditional = false)
9858
10007
  isStaticArray: l.isStaticArray,
9859
10008
  useElementReconciliation,
9860
10009
  innerLoops: useElementReconciliation || l.isStaticArray && innerLoops?.length ? innerLoops : void 0,
9861
- siblingOffset: siblingOffsets.get(l) || void 0,
10010
+ offset: resolveLoopOffset(siblingOffsets.get(l)),
9862
10011
  filterPredicate: l.filterPredicate ? {
9863
10012
  param: l.filterPredicate.param,
9864
10013
  raw: l.filterPredicate.raw
@@ -10159,7 +10308,7 @@ function summarizeLoopChildBranch(node, ctx2, siblingOffsets, loopParam, loopPar
10159
10308
  events: collectConditionalBranchEvents(node)
10160
10309
  };
10161
10310
  }
10162
- var branchInnerLoopOptions;
10311
+ var EMPTY_RENDER_EXPRS, branchInnerLoopOptions;
10163
10312
  var init_collect_elements = __esm({
10164
10313
  "../jsx/src/ir-to-client-js/collect-elements.ts"() {
10165
10314
  "use strict";
@@ -10169,6 +10318,8 @@ var init_collect_elements = __esm({
10169
10318
  init_html_template();
10170
10319
  init_prop_handling();
10171
10320
  init_walker();
10321
+ init_loop_chain();
10322
+ EMPTY_RENDER_EXPRS = /* @__PURE__ */ new Set(["null", "undefined", "false", "''", '""', "``"]);
10172
10323
  branchInnerLoopOptions = {
10173
10324
  collectItemBindings: true,
10174
10325
  templateDepth: 1,
@@ -10728,6 +10879,8 @@ var init_imports = __esm({
10728
10879
  "splitProps",
10729
10880
  "spreadAttrs",
10730
10881
  "styleToCss",
10882
+ "escapeAttr",
10883
+ "escapeText",
10731
10884
  "qsa",
10732
10885
  "qsaItem",
10733
10886
  "qsaChildScope",
@@ -12565,7 +12718,7 @@ function buildOuterNestedPlan(elem, comp) {
12565
12718
  arrayExpr: elem.array,
12566
12719
  param: elem.param,
12567
12720
  indexParam,
12568
- offsetExpr: elem.siblingOffset ? `${indexParam} + ${elem.siblingOffset}` : indexParam,
12721
+ offsetExpr: buildLoopChildIndexExpr(indexParam, elem.offset),
12569
12722
  outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
12570
12723
  propsExpr: buildStaticPropsExpr(comp.props)
12571
12724
  };
@@ -12583,12 +12736,12 @@ function buildInnerLoopNestedPlan(elem, innerLoop, innerComps) {
12583
12736
  outerArrayExpr: elem.array,
12584
12737
  outerParam: elem.param,
12585
12738
  outerIndexParam,
12586
- outerOffsetExpr: elem.siblingOffset ? `${outerIndexParam} + ${elem.siblingOffset}` : outerIndexParam,
12739
+ outerOffsetExpr: buildLoopChildIndexExpr(outerIndexParam, elem.offset),
12587
12740
  outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
12588
12741
  innerContainerSlotId: innerLoop.containerSlotId ?? null,
12589
12742
  innerArrayExpr: innerLoop.array,
12590
12743
  innerParam: innerLoop.param,
12591
- innerOffsetExpr: innerLoop.siblingOffset ? `__innerIdx + ${innerLoop.siblingOffset}` : "__innerIdx",
12744
+ innerOffsetExpr: buildLoopChildIndexExpr("__innerIdx", innerLoop.offset),
12592
12745
  innerPreludeStatements: innerLoop.mapPreamble ? [innerLoop.mapPreamble] : [],
12593
12746
  depth: innerLoop.depth,
12594
12747
  comps
@@ -13405,7 +13558,7 @@ function buildStaticArrayDelegationPlan(elem) {
13405
13558
  arrayExpr: buildChainedArrayExpr(elem),
13406
13559
  param: elem.param,
13407
13560
  mapPreamble: elem.mapPreamble ?? null,
13408
- siblingOffset: elem.siblingOffset ?? null
13561
+ offset: elem.offset ?? null
13409
13562
  }
13410
13563
  };
13411
13564
  }
@@ -13559,6 +13712,11 @@ var init_build_insert = __esm({
13559
13712
  // ../jsx/src/ir-to-client-js/emit-reactive.ts
13560
13713
  function emitAttrUpdate(target, attrName, expression, meta) {
13561
13714
  const htmlName = toHTMLAttrName(attrName);
13715
+ if (attrName === "dangerouslySetInnerHTML" || htmlName === "dangerouslySetInnerHTML") {
13716
+ return [
13717
+ `{ const __v = ${expression}; ${target}.innerHTML = __v != null && __v.__html != null ? String(__v.__html) : '' }`
13718
+ ];
13719
+ }
13562
13720
  if (htmlName === "style") {
13563
13721
  return [
13564
13722
  `{ const __v = styleToCss(${expression}); if (__v != null) ${target}.setAttribute('style', __v); else ${target}.removeAttribute('style') }`
@@ -14622,11 +14780,11 @@ function emitDynamicIndexLookup(ls, ev, handlerCall, lookup) {
14622
14780
  ls.push(` }`);
14623
14781
  }
14624
14782
  function emitStaticIndexLookup(ls, ev, handlerCall, lookup, containerVar) {
14625
- const { arrayExpr, param, mapPreamble, siblingOffset } = lookup;
14783
+ const { arrayExpr, param, mapPreamble, offset } = lookup;
14626
14784
  ls.push(` let __el = ${varSlotId(ev.childSlotId)}El`);
14627
14785
  ls.push(` while (__el.parentElement && __el.parentElement !== ${containerVar}) __el = __el.parentElement`);
14628
14786
  ls.push(` if (__el.parentElement === ${containerVar}) {`);
14629
- const idxOffset = siblingOffset ? ` - ${siblingOffset}` : "";
14787
+ const idxOffset = buildLoopChildIndexSubtraction(offset ?? void 0);
14630
14788
  ls.push(` const __idx = Array.from(${containerVar}.children).indexOf(__el)${idxOffset}`);
14631
14789
  ls.push(` const ${param} = ${arrayExpr}[__idx]`);
14632
14790
  if (mapPreamble) ls.push(` ${mapPreamble}`);
@@ -14954,7 +15112,7 @@ function buildStaticLoopPlan(elem, unsafeLocalNames) {
14954
15112
  }
14955
15113
  }
14956
15114
  const indexParam = elem.index || "__idx";
14957
- const childIndexExpr = elem.siblingOffset ? `${indexParam} + ${elem.siblingOffset}` : indexParam;
15115
+ const childIndexExpr = buildLoopChildIndexExpr(indexParam, elem.offset);
14958
15116
  return {
14959
15117
  kind: "static",
14960
15118
  containerVar: `_${varSlotId(elem.slotId)}`,
@@ -17350,6 +17508,7 @@ var init_attr_value_emitter = __esm({
17350
17508
  });
17351
17509
 
17352
17510
  // ../jsx/src/combine-client-js.ts
17511
+ import ts18 from "typescript";
17353
17512
  function combineParentChildClientJs(files) {
17354
17513
  const result = /* @__PURE__ */ new Map();
17355
17514
  const lookup = /* @__PURE__ */ new Map();
@@ -17406,30 +17565,48 @@ function combineParentChildClientJs(files) {
17406
17565
  return result;
17407
17566
  }
17408
17567
  function parseAndMerge(content, importsBySource, otherImports, codeSections) {
17409
- const codeLines = [];
17410
- for (const line of content.split("\n")) {
17411
- if (line.startsWith("import ")) {
17412
- if (line.includes("@bf-child:")) continue;
17413
- const match = line.match(/^import \{ ([^}]+) \} from ['"]([^'"]+)['"]/);
17414
- if (match) {
17415
- const names = match[1].split(",").map((n) => n.trim());
17416
- const source = match[2];
17417
- if (!importsBySource.has(source)) {
17418
- importsBySource.set(source, /* @__PURE__ */ new Set());
17419
- }
17420
- for (const name of names) {
17421
- importsBySource.get(source).add(name);
17422
- }
17423
- } else {
17424
- if (!otherImports.includes(line)) {
17425
- otherImports.push(line);
17426
- }
17568
+ const sourceFile = ts18.createSourceFile(
17569
+ "combine.js",
17570
+ content,
17571
+ ts18.ScriptTarget.Latest,
17572
+ /*setParentNodes*/
17573
+ false,
17574
+ ts18.ScriptKind.JS
17575
+ );
17576
+ const importSpans = [];
17577
+ for (const stmt of sourceFile.statements) {
17578
+ if (!ts18.isImportDeclaration(stmt)) continue;
17579
+ const start = stmt.getStart(sourceFile);
17580
+ const end = stmt.getEnd();
17581
+ importSpans.push([start, end]);
17582
+ const stmtText = content.slice(start, end);
17583
+ if (stmtText.includes("@bf-child:")) continue;
17584
+ const clause = stmt.importClause;
17585
+ const bindings = clause?.namedBindings;
17586
+ const specifier = ts18.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : "";
17587
+ if (clause && !clause.name && bindings && ts18.isNamedImports(bindings)) {
17588
+ if (!importsBySource.has(specifier)) {
17589
+ importsBySource.set(specifier, /* @__PURE__ */ new Set());
17590
+ }
17591
+ const set = importsBySource.get(specifier);
17592
+ for (const el of bindings.elements) {
17593
+ const name = el.propertyName ? `${el.propertyName.text} as ${el.name.text}` : el.name.text;
17594
+ set.add(name);
17427
17595
  }
17428
17596
  } else {
17429
- codeLines.push(line);
17597
+ if (!otherImports.includes(stmtText)) {
17598
+ otherImports.push(stmtText);
17599
+ }
17430
17600
  }
17431
17601
  }
17432
- const code = codeLines.join("\n").trim();
17602
+ let code = "";
17603
+ let cursor = 0;
17604
+ for (const [start, end] of importSpans) {
17605
+ code += content.slice(cursor, start);
17606
+ cursor = end;
17607
+ }
17608
+ code += content.slice(cursor);
17609
+ code = code.trim();
17433
17610
  if (code) {
17434
17611
  codeSections.push(code);
17435
17612
  }
@@ -18862,7 +19039,7 @@ var init_runtime = __esm({
18862
19039
 
18863
19040
  // src/lib/resolve-imports.ts
18864
19041
  import { dirname as dirname2, resolve as resolve2 } from "node:path";
18865
- import ts18 from "typescript";
19042
+ import ts19 from "typescript";
18866
19043
  function shapeFromDecl(decl) {
18867
19044
  const clause = decl.importClause;
18868
19045
  if (!clause) return null;
@@ -18872,7 +19049,7 @@ function shapeFromDecl(decl) {
18872
19049
  }
18873
19050
  const bindings = clause.namedBindings;
18874
19051
  if (bindings) {
18875
- if (ts18.isNamespaceImport(bindings)) {
19052
+ if (ts19.isNamespaceImport(bindings)) {
18876
19053
  shape.namespace = bindings.name.text;
18877
19054
  } else {
18878
19055
  for (const el of bindings.elements) {
@@ -18886,38 +19063,38 @@ function shapeFromDecl(decl) {
18886
19063
  }
18887
19064
  function collectExportedNames(source) {
18888
19065
  const names = /* @__PURE__ */ new Set();
18889
- const sourceFile = ts18.createSourceFile(
19066
+ const sourceFile = ts19.createSourceFile(
18890
19067
  "mod.ts",
18891
19068
  source,
18892
- ts18.ScriptTarget.Latest,
19069
+ ts19.ScriptTarget.Latest,
18893
19070
  /*setParents*/
18894
19071
  false,
18895
- ts18.ScriptKind.TS
19072
+ ts19.ScriptKind.TS
18896
19073
  );
18897
19074
  function hasExport(node) {
18898
- if (!ts18.canHaveModifiers(node)) return false;
18899
- const mods = ts18.getModifiers(node);
18900
- return mods?.some((m) => m.kind === ts18.SyntaxKind.ExportKeyword) ?? false;
19075
+ if (!ts19.canHaveModifiers(node)) return false;
19076
+ const mods = ts19.getModifiers(node);
19077
+ return mods?.some((m) => m.kind === ts19.SyntaxKind.ExportKeyword) ?? false;
18901
19078
  }
18902
19079
  function collectFromBindingName(name) {
18903
- if (ts18.isIdentifier(name)) {
19080
+ if (ts19.isIdentifier(name)) {
18904
19081
  names.add(name.text);
18905
19082
  return;
18906
19083
  }
18907
19084
  for (const el of name.elements) {
18908
- if (ts18.isBindingElement(el)) collectFromBindingName(el.name);
19085
+ if (ts19.isBindingElement(el)) collectFromBindingName(el.name);
18909
19086
  }
18910
19087
  }
18911
19088
  for (const stmt of sourceFile.statements) {
18912
- if (ts18.isVariableStatement(stmt) && hasExport(stmt)) {
19089
+ if (ts19.isVariableStatement(stmt) && hasExport(stmt)) {
18913
19090
  for (const d of stmt.declarationList.declarations) {
18914
19091
  collectFromBindingName(d.name);
18915
19092
  }
18916
- } else if (ts18.isFunctionDeclaration(stmt) && hasExport(stmt) && stmt.name) {
19093
+ } else if (ts19.isFunctionDeclaration(stmt) && hasExport(stmt) && stmt.name) {
18917
19094
  names.add(stmt.name.text);
18918
- } else if (ts18.isClassDeclaration(stmt) && hasExport(stmt) && stmt.name) {
19095
+ } else if (ts19.isClassDeclaration(stmt) && hasExport(stmt) && stmt.name) {
18919
19096
  names.add(stmt.name.text);
18920
- } else if (ts18.isExportDeclaration(stmt) && !stmt.moduleSpecifier && stmt.exportClause && ts18.isNamedExports(stmt.exportClause)) {
19097
+ } else if (ts19.isExportDeclaration(stmt) && !stmt.moduleSpecifier && stmt.exportClause && ts19.isNamedExports(stmt.exportClause)) {
18921
19098
  if (stmt.isTypeOnly) continue;
18922
19099
  for (const el of stmt.exportClause.elements) {
18923
19100
  if (el.isTypeOnly) continue;
@@ -18928,16 +19105,16 @@ function collectExportedNames(source) {
18928
19105
  return [...names];
18929
19106
  }
18930
19107
  function hasUseClientDirective(source) {
18931
- const sourceFile = ts18.createSourceFile(
19108
+ const sourceFile = ts19.createSourceFile(
18932
19109
  "check.tsx",
18933
19110
  source,
18934
- ts18.ScriptTarget.Latest,
19111
+ ts19.ScriptTarget.Latest,
18935
19112
  /*setParents*/
18936
19113
  false,
18937
- ts18.ScriptKind.TSX
19114
+ ts19.ScriptKind.TSX
18938
19115
  );
18939
19116
  for (const stmt of sourceFile.statements) {
18940
- if (!ts18.isExpressionStatement(stmt) || !ts18.isStringLiteral(stmt.expression)) {
19117
+ if (!ts19.isExpressionStatement(stmt) || !ts19.isStringLiteral(stmt.expression)) {
18941
19118
  return false;
18942
19119
  }
18943
19120
  if (stmt.expression.text === "use client") return true;
@@ -18946,53 +19123,53 @@ function hasUseClientDirective(source) {
18946
19123
  }
18947
19124
  function collectTopLevelBindings(source) {
18948
19125
  const names = /* @__PURE__ */ new Set();
18949
- const sourceFile = ts18.createSourceFile(
19126
+ const sourceFile = ts19.createSourceFile(
18950
19127
  "bundle.ts",
18951
19128
  source,
18952
- ts18.ScriptTarget.Latest,
19129
+ ts19.ScriptTarget.Latest,
18953
19130
  /*setParents*/
18954
19131
  false,
18955
- ts18.ScriptKind.TS
19132
+ ts19.ScriptKind.TS
18956
19133
  );
18957
19134
  function collectFromBindingName(name) {
18958
- if (ts18.isIdentifier(name)) {
19135
+ if (ts19.isIdentifier(name)) {
18959
19136
  names.add(name.text);
18960
19137
  return;
18961
19138
  }
18962
19139
  for (const el of name.elements) {
18963
- if (ts18.isBindingElement(el)) collectFromBindingName(el.name);
19140
+ if (ts19.isBindingElement(el)) collectFromBindingName(el.name);
18964
19141
  }
18965
19142
  }
18966
19143
  for (const stmt of sourceFile.statements) {
18967
- if (ts18.isVariableStatement(stmt)) {
19144
+ if (ts19.isVariableStatement(stmt)) {
18968
19145
  for (const d of stmt.declarationList.declarations) {
18969
19146
  collectFromBindingName(d.name);
18970
19147
  }
18971
- } else if (ts18.isFunctionDeclaration(stmt) && stmt.name) {
19148
+ } else if (ts19.isFunctionDeclaration(stmt) && stmt.name) {
18972
19149
  names.add(stmt.name.text);
18973
- } else if (ts18.isClassDeclaration(stmt) && stmt.name) {
19150
+ } else if (ts19.isClassDeclaration(stmt) && stmt.name) {
18974
19151
  names.add(stmt.name.text);
18975
19152
  }
18976
19153
  }
18977
19154
  return names;
18978
19155
  }
18979
19156
  function stripImportsAndExports(body) {
18980
- const sourceFile = ts18.createSourceFile(
19157
+ const sourceFile = ts19.createSourceFile(
18981
19158
  "body.ts",
18982
19159
  body,
18983
- ts18.ScriptTarget.Latest,
19160
+ ts19.ScriptTarget.Latest,
18984
19161
  /*setParents*/
18985
19162
  false,
18986
- ts18.ScriptKind.TS
19163
+ ts19.ScriptKind.TS
18987
19164
  );
18988
19165
  const spans = [];
18989
19166
  const hoistedImports = [];
18990
19167
  for (const stmt of sourceFile.statements) {
18991
- if (ts18.isImportDeclaration(stmt)) {
19168
+ if (ts19.isImportDeclaration(stmt)) {
18992
19169
  const start = stmt.getStart(sourceFile);
18993
19170
  const end = stmt.getEnd();
18994
19171
  const specifier = stmt.moduleSpecifier;
18995
- if (ts18.isStringLiteral(specifier)) {
19172
+ if (ts19.isStringLiteral(specifier)) {
18996
19173
  const path23 = specifier.text;
18997
19174
  const isRelative = path23.startsWith("./") || path23.startsWith("../");
18998
19175
  if (!isRelative) {
@@ -19002,24 +19179,24 @@ function stripImportsAndExports(body) {
19002
19179
  spans.push([start, end]);
19003
19180
  continue;
19004
19181
  }
19005
- if (ts18.isExportDeclaration(stmt)) {
19182
+ if (ts19.isExportDeclaration(stmt)) {
19006
19183
  spans.push([stmt.getStart(sourceFile), stmt.getEnd()]);
19007
19184
  continue;
19008
19185
  }
19009
- if (ts18.isExportAssignment(stmt)) {
19010
- const exportKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts18.SyntaxKind.ExportKeyword);
19011
- const defaultKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts18.SyntaxKind.DefaultKeyword);
19012
- const equalsKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts18.SyntaxKind.EqualsToken);
19186
+ if (ts19.isExportAssignment(stmt)) {
19187
+ const exportKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts19.SyntaxKind.ExportKeyword);
19188
+ const defaultKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts19.SyntaxKind.DefaultKeyword);
19189
+ const equalsKw = stmt.getChildren(sourceFile).find((c) => c.kind === ts19.SyntaxKind.EqualsToken);
19013
19190
  const start = exportKw?.getStart(sourceFile) ?? stmt.getStart(sourceFile);
19014
19191
  const end = (defaultKw ?? equalsKw)?.getEnd() ?? exportKw?.getEnd() ?? stmt.getStart(sourceFile);
19015
19192
  if (end > start) spans.push([start, end]);
19016
19193
  continue;
19017
19194
  }
19018
- if (ts18.canHaveModifiers(stmt)) {
19019
- const mods = ts18.getModifiers(stmt);
19195
+ if (ts19.canHaveModifiers(stmt)) {
19196
+ const mods = ts19.getModifiers(stmt);
19020
19197
  if (!mods) continue;
19021
19198
  for (const mod of mods) {
19022
- if (mod.kind === ts18.SyntaxKind.ExportKeyword) {
19199
+ if (mod.kind === ts19.SyntaxKind.ExportKeyword) {
19023
19200
  const start = mod.getStart(sourceFile);
19024
19201
  let end = mod.getEnd();
19025
19202
  while (end < body.length && /\s/.test(body[end])) end++;
@@ -19128,48 +19305,48 @@ function buildDanglingReferenceMessage(binding, s) {
19128
19305
  function isValueReference(id) {
19129
19306
  const parent = id.parent;
19130
19307
  if (!parent) return false;
19131
- if (ts18.isPropertyAccessExpression(parent) && parent.name === id) return false;
19132
- if (ts18.isPropertyAssignment(parent) && parent.name === id) return false;
19133
- if ((ts18.isMethodDeclaration(parent) || ts18.isGetAccessorDeclaration(parent) || ts18.isSetAccessorDeclaration(parent)) && parent.name === id) {
19308
+ if (ts19.isPropertyAccessExpression(parent) && parent.name === id) return false;
19309
+ if (ts19.isPropertyAssignment(parent) && parent.name === id) return false;
19310
+ if ((ts19.isMethodDeclaration(parent) || ts19.isGetAccessorDeclaration(parent) || ts19.isSetAccessorDeclaration(parent)) && parent.name === id) {
19134
19311
  return false;
19135
19312
  }
19136
- if (ts18.isVariableDeclaration(parent) && parent.name === id) return false;
19137
- if (ts18.isFunctionDeclaration(parent) && parent.name === id) return false;
19138
- if (ts18.isFunctionExpression(parent) && parent.name === id) return false;
19139
- if (ts18.isClassDeclaration(parent) && parent.name === id) return false;
19140
- if (ts18.isClassExpression(parent) && parent.name === id) return false;
19141
- if (ts18.isParameter(parent) && parent.name === id) return false;
19142
- if (ts18.isBindingElement(parent) && (parent.name === id || parent.propertyName === id)) return false;
19143
- if (ts18.isLabeledStatement(parent) && parent.label === id) return false;
19144
- if (ts18.isBreakOrContinueStatement(parent) && parent.label === id) return false;
19145
- if (ts18.isImportSpecifier(parent) && (parent.name === id || parent.propertyName === id)) return false;
19146
- if (ts18.isExportSpecifier(parent) && (parent.name === id || parent.propertyName === id)) return false;
19147
- if (ts18.isImportClause(parent) && parent.name === id) return false;
19148
- if (ts18.isNamespaceImport(parent) && parent.name === id) return false;
19149
- if (ts18.isQualifiedName(parent) && parent.right === id) return false;
19313
+ if (ts19.isVariableDeclaration(parent) && parent.name === id) return false;
19314
+ if (ts19.isFunctionDeclaration(parent) && parent.name === id) return false;
19315
+ if (ts19.isFunctionExpression(parent) && parent.name === id) return false;
19316
+ if (ts19.isClassDeclaration(parent) && parent.name === id) return false;
19317
+ if (ts19.isClassExpression(parent) && parent.name === id) return false;
19318
+ if (ts19.isParameter(parent) && parent.name === id) return false;
19319
+ if (ts19.isBindingElement(parent) && (parent.name === id || parent.propertyName === id)) return false;
19320
+ if (ts19.isLabeledStatement(parent) && parent.label === id) return false;
19321
+ if (ts19.isBreakOrContinueStatement(parent) && parent.label === id) return false;
19322
+ if (ts19.isImportSpecifier(parent) && (parent.name === id || parent.propertyName === id)) return false;
19323
+ if (ts19.isExportSpecifier(parent) && (parent.name === id || parent.propertyName === id)) return false;
19324
+ if (ts19.isImportClause(parent) && parent.name === id) return false;
19325
+ if (ts19.isNamespaceImport(parent) && parent.name === id) return false;
19326
+ if (ts19.isQualifiedName(parent) && parent.right === id) return false;
19150
19327
  return true;
19151
19328
  }
19152
19329
  function detectStrippedReferences(bundleSource, stripped) {
19153
19330
  if (stripped.length === 0) return [];
19154
19331
  let sf;
19155
19332
  try {
19156
- sf = ts18.createSourceFile(
19333
+ sf = ts19.createSourceFile(
19157
19334
  "bundle.js",
19158
19335
  bundleSource,
19159
- ts18.ScriptTarget.Latest,
19336
+ ts19.ScriptTarget.Latest,
19160
19337
  /*setParents*/
19161
19338
  true,
19162
- ts18.ScriptKind.JS
19339
+ ts19.ScriptKind.JS
19163
19340
  );
19164
19341
  } catch {
19165
19342
  return [];
19166
19343
  }
19167
19344
  const firstReference = /* @__PURE__ */ new Map();
19168
19345
  function visit3(node) {
19169
- if (ts18.isIdentifier(node) && isValueReference(node)) {
19346
+ if (ts19.isIdentifier(node) && isValueReference(node)) {
19170
19347
  if (!firstReference.has(node.text)) firstReference.set(node.text, node);
19171
19348
  }
19172
- ts18.forEachChild(node, visit3);
19349
+ ts19.forEachChild(node, visit3);
19173
19350
  }
19174
19351
  visit3(sf);
19175
19352
  const errors = [];
@@ -19199,18 +19376,18 @@ function detectStrippedReferences(bundleSource, stripped) {
19199
19376
  return errors;
19200
19377
  }
19201
19378
  async function walkAndCollect(content, searchDirs, modules, visiting, loggingPath, stripped, stubDeps, nextId) {
19202
- const sourceFile = ts18.createSourceFile(
19379
+ const sourceFile = ts19.createSourceFile(
19203
19380
  "walk.js",
19204
19381
  content,
19205
- ts18.ScriptTarget.Latest,
19382
+ ts19.ScriptTarget.Latest,
19206
19383
  /*setParents*/
19207
19384
  false,
19208
- ts18.ScriptKind.JS
19385
+ ts19.ScriptKind.JS
19209
19386
  );
19210
19387
  const sites = [];
19211
19388
  for (const stmt of sourceFile.statements) {
19212
- if (!ts18.isImportDeclaration(stmt)) continue;
19213
- if (!ts18.isStringLiteral(stmt.moduleSpecifier)) continue;
19389
+ if (!ts19.isImportDeclaration(stmt)) continue;
19390
+ if (!ts19.isStringLiteral(stmt.moduleSpecifier)) continue;
19214
19391
  const spec = stmt.moduleSpecifier.text;
19215
19392
  if (!spec.startsWith("./") && !spec.startsWith("../")) continue;
19216
19393
  const start = stmt.getStart(sourceFile);
@@ -19662,7 +19839,7 @@ var init_assets_ignore = __esm({
19662
19839
  });
19663
19840
 
19664
19841
  // src/lib/build.ts
19665
- import ts19 from "typescript";
19842
+ import ts20 from "typescript";
19666
19843
  import { mkdir, readdir, stat, unlink } from "node:fs/promises";
19667
19844
  import { resolve as resolve6, basename, relative as relative2, dirname as dirname3, isAbsolute as isAbsolute2 } from "node:path";
19668
19845
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -20119,12 +20296,7 @@ async function build(config, options = {}) {
20119
20296
  try {
20120
20297
  let content = await readText(filePath);
20121
20298
  const before = content;
20122
- if (content.includes("@barefootjs/client")) {
20123
- content = content.replace(
20124
- /from ['"]@barefootjs\/client(?:\/[^'"]*)?['"]/g,
20125
- `from '${rel}'`
20126
- );
20127
- }
20299
+ content = rewriteBarefootClientSpecifiers(content, rel);
20128
20300
  content = mergeDuplicateNamedImports(content);
20129
20301
  if (content !== before && await writeIfChanged(filePath, content)) {
20130
20302
  anyOutputChanged = true;
@@ -20213,7 +20385,7 @@ async function build(config, options = {}) {
20213
20385
  };
20214
20386
  }
20215
20387
  function extractBareImports(code) {
20216
- const { importedFiles } = ts19.preProcessFile(code, true, true);
20388
+ const { importedFiles } = ts20.preProcessFile(code, true, true);
20217
20389
  const specifiers = /* @__PURE__ */ new Set();
20218
20390
  for (const { fileName } of importedFiles) {
20219
20391
  if (!fileName.startsWith(".") && !fileName.startsWith("/") && !fileName.includes("://")) {
@@ -20278,13 +20450,82 @@ function effectiveOutName(tplPath, entryBaseNoExt) {
20278
20450
  const entryDir = entryBaseNoExt.includes("/") ? entryBaseNoExt.slice(0, entryBaseNoExt.lastIndexOf("/")) : "";
20279
20451
  return entryDir ? `${entryDir}/${bn}` : bn;
20280
20452
  }
20453
+ function topLevelImportLines(content) {
20454
+ const lines = /* @__PURE__ */ new Set();
20455
+ const sourceFile = ts20.createSourceFile(
20456
+ "merge.js",
20457
+ content,
20458
+ ts20.ScriptTarget.Latest,
20459
+ /*setParentNodes*/
20460
+ true,
20461
+ ts20.ScriptKind.JS
20462
+ );
20463
+ for (const stmt of sourceFile.statements) {
20464
+ if (ts20.isImportDeclaration(stmt)) {
20465
+ const { line } = sourceFile.getLineAndCharacterOfPosition(stmt.getStart(sourceFile));
20466
+ lines.add(line);
20467
+ }
20468
+ }
20469
+ return lines;
20470
+ }
20471
+ function rewriteBarefootClientSpecifiers(content, rel) {
20472
+ if (!content.includes("@barefootjs/client")) return content;
20473
+ const sourceFile = ts20.createSourceFile(
20474
+ "client.js",
20475
+ content,
20476
+ ts20.ScriptTarget.Latest,
20477
+ /*setParentNodes*/
20478
+ true,
20479
+ ts20.ScriptKind.JS
20480
+ );
20481
+ const isBarefootClient = (s) => s === "@barefootjs/client" || s.startsWith("@barefootjs/client/");
20482
+ const spans = [];
20483
+ const visit3 = (node) => {
20484
+ if (ts20.isImportDeclaration(node) || ts20.isExportDeclaration(node)) {
20485
+ const ms = node.moduleSpecifier;
20486
+ if (ms && ts20.isStringLiteral(ms) && isBarefootClient(ms.text)) {
20487
+ spans.push([ms.getStart(sourceFile), ms.getEnd()]);
20488
+ }
20489
+ } else if (ts20.isCallExpression(node) && node.expression.kind === ts20.SyntaxKind.ImportKeyword) {
20490
+ const arg = node.arguments[0];
20491
+ if (arg && ts20.isStringLiteral(arg) && isBarefootClient(arg.text)) {
20492
+ spans.push([arg.getStart(sourceFile), arg.getEnd()]);
20493
+ }
20494
+ }
20495
+ ts20.forEachChild(node, visit3);
20496
+ };
20497
+ visit3(sourceFile);
20498
+ if (spans.length === 0) return content;
20499
+ spans.sort((a, b) => b[0] - a[0]);
20500
+ let out = content;
20501
+ for (const [start, end] of spans) {
20502
+ out = out.slice(0, start) + `'${rel}'` + out.slice(end);
20503
+ }
20504
+ return out;
20505
+ }
20281
20506
  function mergeDuplicateNamedImports(content) {
20282
20507
  const lines = content.split("\n");
20283
20508
  const namedImportRe = /^import\s+\{\s*([^}]+)\s*\}\s+from\s+(['"])([^'"]+)\2\s*;?\s*$/;
20284
20509
  const bySource = /* @__PURE__ */ new Map();
20285
20510
  const dropIndices = /* @__PURE__ */ new Set();
20286
20511
  let changed = false;
20512
+ {
20513
+ const seen = /* @__PURE__ */ new Set();
20514
+ let possibleDuplicate = false;
20515
+ for (const line of lines) {
20516
+ const m = line.match(namedImportRe);
20517
+ if (!m) continue;
20518
+ if (seen.has(m[3])) {
20519
+ possibleDuplicate = true;
20520
+ break;
20521
+ }
20522
+ seen.add(m[3]);
20523
+ }
20524
+ if (!possibleDuplicate) return content;
20525
+ }
20526
+ const realImportLines = topLevelImportLines(content);
20287
20527
  lines.forEach((line, idx) => {
20528
+ if (!realImportLines.has(idx)) return;
20288
20529
  const m = line.match(namedImportRe);
20289
20530
  if (!m) return;
20290
20531
  const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
@@ -23788,7 +24029,13 @@ export default createConfig({
23788
24029
  }
23789
24030
  },
23790
24031
  "include": ["**/*.ts", "**/*.tsx"],
23791
- "exclude": ["node_modules", "dist/components"]
24032
+ // Do NOT exclude dist/components here. The server imports the compiled
24033
+ // SSR templates from there (see the @/components/* paths above), and
24034
+ // tsx applies the JSX transform per-file honouring this include/
24035
+ // exclude \u2014 an excluded .tsx loses jsxImportSource and falls back to
24036
+ // the classic React runtime, so SSR throws "ReferenceError: React is
24037
+ // not defined" at the first render. They must stay in scope.
24038
+ "exclude": ["node_modules"]
23792
24039
  }
23793
24040
  `;
23794
24041
  HONO_NODE_ADAPTER = {
@@ -27508,9 +27755,9 @@ function findProjectConfig(startDir) {
27508
27755
  let dir = path.resolve(startDir);
27509
27756
  const { root: fsRoot } = path.parse(dir);
27510
27757
  while (true) {
27511
- const ts20 = path.join(dir, "barefoot.config.ts");
27512
- if (existsSync2(ts20)) {
27513
- return { dir, tsConfigPath: ts20 };
27758
+ const ts21 = path.join(dir, "barefoot.config.ts");
27759
+ if (existsSync2(ts21)) {
27760
+ return { dir, tsConfigPath: ts21 };
27514
27761
  }
27515
27762
  if (dir === fsRoot) return null;
27516
27763
  dir = path.dirname(dir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "CLI for agent-driven UI component discovery and scaffolding",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "typescript": "^5.0.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@barefootjs/jsx": "0.5.1",
34
+ "@barefootjs/jsx": "0.5.3",
35
35
  "@types/node": "^22.0.0"
36
36
  }
37
37
  }