@barefootjs/jsx 0.5.2 → 0.6.0

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.
Files changed (53) hide show
  1. package/dist/adapters/parsed-expr-emitter.d.ts +1 -1
  2. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  3. package/dist/combine-client-js.d.ts.map +1 -1
  4. package/dist/expression-parser.d.ts +1 -1
  5. package/dist/expression-parser.d.ts.map +1 -1
  6. package/dist/index.js +330 -70
  7. package/dist/ir-to-client-js/collect-elements.d.ts +26 -14
  8. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  9. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +8 -3
  11. package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/generate-init.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/html-template.d.ts +30 -1
  15. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/imports.d.ts +2 -2
  17. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/phases/provider-and-child-inits.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +3 -3
  20. package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/types.d.ts +36 -4
  22. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/utils.d.ts +19 -1
  24. package/dist/ir-to-client-js/utils.d.ts.map +1 -1
  25. package/package.json +2 -2
  26. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +203 -203
  27. package/src/__tests__/child-components-in-map.test.ts +333 -0
  28. package/src/__tests__/combine-client-js.test.ts +47 -0
  29. package/src/__tests__/dangerously-set-inner-html.test.ts +82 -0
  30. package/src/__tests__/expression-parser.test.ts +167 -13
  31. package/src/__tests__/ir-to-client-js/reactivity.test.ts +1 -0
  32. package/src/__tests__/staged-ir/06-multi-stage-soak.test.ts +18 -3
  33. package/src/__tests__/static-loop-csr-materialize.test.ts +6 -4
  34. package/src/__tests__/text-slot-escaping.test.ts +56 -0
  35. package/src/adapters/parsed-expr-emitter.ts +7 -0
  36. package/src/combine-client-js.ts +66 -22
  37. package/src/expression-parser.ts +200 -17
  38. package/src/ir-to-client-js/collect-elements.ts +170 -32
  39. package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +1 -1
  40. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +2 -1
  41. package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +8 -3
  42. package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +3 -3
  43. package/src/ir-to-client-js/emit-reactive.ts +9 -0
  44. package/src/ir-to-client-js/emit-registration.ts +1 -1
  45. package/src/ir-to-client-js/generate-init.ts +16 -1
  46. package/src/ir-to-client-js/html-template.ts +238 -12
  47. package/src/ir-to-client-js/imports.ts +1 -1
  48. package/src/ir-to-client-js/index.ts +1 -0
  49. package/src/ir-to-client-js/phases/provider-and-child-inits.ts +12 -1
  50. package/src/ir-to-client-js/plan/build-static-array-child-init.ts +4 -8
  51. package/src/ir-to-client-js/plan/static-array-child-init.ts +3 -3
  52. package/src/ir-to-client-js/types.ts +37 -4
  53. package/src/ir-to-client-js/utils.ts +41 -1
package/dist/index.js CHANGED
@@ -273,6 +273,21 @@ function buildChainedArrayExpr(elem) {
273
273
  chainOrder: elem.chainOrder
274
274
  });
275
275
  }
276
+ function loopOffsetTerms(offset) {
277
+ if (!offset)
278
+ return [];
279
+ const terms = [];
280
+ if (offset.staticCount)
281
+ terms.push(String(offset.staticCount));
282
+ terms.push(...offset.dynamicTerms);
283
+ return terms;
284
+ }
285
+ function buildLoopChildIndexExpr(indexParam, offset) {
286
+ return [indexParam, ...loopOffsetTerms(offset)].join(" + ");
287
+ }
288
+ function buildLoopChildIndexSubtraction(offset) {
289
+ return loopOffsetTerms(offset).map((term) => ` - ${term}`).join("");
290
+ }
276
291
  var jsxToDomEventMap = {
277
292
  doubleclick: "dblclick"
278
293
  };
@@ -1036,16 +1051,30 @@ function maybeHoistedScopeAttr(inHoistedChildren, node) {
1036
1051
  }
1037
1052
  var UNSAFE_TEMPLATE_EXPR = "undefined";
1038
1053
  function templateAttrExpr(attrName, valExpr, presenceOrUndefined) {
1054
+ if (attrName === "dangerouslySetInnerHTML")
1055
+ return "";
1039
1056
  if (isBooleanAttr(attrName) || presenceOrUndefined) {
1040
1057
  return `\${${valExpr} ? '${attrName}' : ''}`;
1041
1058
  }
1042
1059
  if (attrName === "style") {
1043
- return `\${((v) => v != null ? 'style="' + v + '"' : '')(styleToCss(${valExpr}))}`;
1060
+ return `\${((v) => v != null ? 'style="' + ${escapeAttrValueExpr("v")} + '"' : '')(styleToCss(${valExpr}))}`;
1044
1061
  }
1045
1062
  if (attrName === "data-key" || attrName.startsWith("data-key-")) {
1046
1063
  return `${attrName}="\${${valExpr}}"`;
1047
1064
  }
1048
- return `\${(${valExpr}) != null ? '${attrName}="' + (${valExpr}) + '"' : ''}`;
1065
+ return `\${(${valExpr}) != null ? '${attrName}="' + ${escapeAttrValueExpr(valExpr)} + '"' : ''}`;
1066
+ }
1067
+ function escapeAttrValueExpr(valExpr) {
1068
+ return `escapeAttr(${valExpr})`;
1069
+ }
1070
+ function escapeTextSlotExpr(innerExpr) {
1071
+ return `escapeText(${innerExpr})`;
1072
+ }
1073
+ function dangerouslyHtmlChildren(attrs, toExpr) {
1074
+ const attr = attrs.find((a) => a.name === "dangerouslySetInnerHTML");
1075
+ if (!attr || attr.value.kind !== "expression")
1076
+ return null;
1077
+ return `\${((${toExpr(attr.value)}) ?? {}).__html ?? ''}`;
1049
1078
  }
1050
1079
  function transformKeyValue(value, transformExpr) {
1051
1080
  switch (value.kind) {
@@ -1093,6 +1122,8 @@ function isMergeableAttr(a, ctx) {
1093
1122
  return false;
1094
1123
  if (a.name === "key")
1095
1124
  return false;
1125
+ if (a.name === "dangerouslySetInnerHTML")
1126
+ return false;
1096
1127
  const v = a.value;
1097
1128
  if (v.kind === "jsx-children")
1098
1129
  return false;
@@ -1191,7 +1222,7 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
1191
1222
  }
1192
1223
  const attrs = attrParts.join(" ");
1193
1224
  const childrenRecurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, insideLoop, false);
1194
- const children = node.children.map(childrenRecurse).join("");
1225
+ const children = dangerouslyHtmlChildren(node.attrs, (v) => wrapExpr(v.expr)) ?? node.children.map(childrenRecurse).join("");
1195
1226
  if (children || !VOID_ELEMENTS.has(node.tag)) {
1196
1227
  return `<${node.tag}${attrs ? " " + attrs : ""}>${children}</${node.tag}>`;
1197
1228
  }
@@ -1203,7 +1234,9 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
1203
1234
  if (node.expr === "null" || node.expr === "undefined")
1204
1235
  return "";
1205
1236
  if (node.slotId) {
1206
- return `<!--bf:${node.slotId}-->\${${wrapInterpolation(wrapExpr(node.expr))}}<!--/-->`;
1237
+ const inner = wrapInterpolation(wrapExpr(node.expr));
1238
+ const slotted = branchSlotsVar ? inner : escapeTextSlotExpr(inner);
1239
+ return `<!--bf:${node.slotId}-->\${${slotted}}<!--/-->`;
1207
1240
  }
1208
1241
  return `\${${wrapInterpolation(wrapExpr(node.expr))}}`;
1209
1242
  case "conditional": {
@@ -1301,7 +1334,7 @@ function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParam
1301
1334
  attrParts.push(`bf="${node.slotId}"`);
1302
1335
  }
1303
1336
  const attrs = attrParts.join(" ");
1304
- const children = node.children.map(recurse).join("");
1337
+ const children = dangerouslyHtmlChildren(node.attrs, (v) => wrapExpr(v.expr)) ?? node.children.map(recurse).join("");
1305
1338
  if (children || !VOID_ELEMENTS.has(node.tag)) {
1306
1339
  return `<${node.tag}${attrs ? " " + attrs : ""}>${children}</${node.tag}>`;
1307
1340
  }
@@ -1313,7 +1346,7 @@ function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParam
1313
1346
  if (node.expr === "null" || node.expr === "undefined")
1314
1347
  return "";
1315
1348
  if (node.slotId) {
1316
- return `<!--bf:${node.slotId}-->\${${wrapExpr(node.expr)}}<!--/-->`;
1349
+ return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(wrapExpr(node.expr))}}<!--/-->`;
1317
1350
  }
1318
1351
  return `\${${wrapExpr(node.expr)}}`;
1319
1352
  case "conditional": {
@@ -1529,7 +1562,7 @@ function irToComponentTemplateWithOpts(node, opts) {
1529
1562
  attrParts.push(`bf="${node.slotId}"`);
1530
1563
  }
1531
1564
  const attrs = attrParts.join(" ");
1532
- const children = node.children.map(childrenRecurse).join("");
1565
+ const children = dangerouslyHtmlChildren(node.attrs, (v) => transformExpr(v.expr, v.templateExpr)) ?? node.children.map(childrenRecurse).join("");
1533
1566
  if (children || !VOID_ELEMENTS.has(node.tag)) {
1534
1567
  return `<${node.tag}${attrs ? " " + attrs : ""}>${children}</${node.tag}>`;
1535
1568
  }
@@ -1541,7 +1574,7 @@ function irToComponentTemplateWithOpts(node, opts) {
1541
1574
  if (node.expr === "null" || node.expr === "undefined")
1542
1575
  return "";
1543
1576
  if (node.slotId) {
1544
- return `<!--bf:${node.slotId}-->\${${transformExpr(node.expr, node.templateExpr)}}<!--/-->`;
1577
+ return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(transformExpr(node.expr, node.templateExpr))}}<!--/-->`;
1545
1578
  }
1546
1579
  return `\${${transformExpr(node.expr, node.templateExpr)}}`;
1547
1580
  case "conditional": {
@@ -1677,7 +1710,19 @@ function canGenerateStaticTemplate(node, propNames, inlinableConstants, unsafeLo
1677
1710
  return assertNever(node);
1678
1711
  }
1679
1712
  }
1680
- function generateCsrTemplate(node, inlinableConstants, ctx, insideLoop, restSpreadNames, propsObjectName, unsafeLocalNames) {
1713
+ function generateCsrTemplate(node, inlinableConstants, ctx, insideLoop, restSpreadNames, propsObjectName, unsafeLocalNames, deferredChildSlots) {
1714
+ const base = buildSignalMemoEnv(ctx.signals, ctx.memos, propsObjectName ?? null);
1715
+ const csrEnv = { substitutions: new Map(base.substitutions), propsObjectName: base.propsObjectName };
1716
+ if (inlinableConstants) {
1717
+ for (const [name, value] of inlinableConstants) {
1718
+ if (!csrEnv.substitutions.has(name)) {
1719
+ csrEnv.substitutions.set(name, { kind: "identifier", replacement: value, freeIdentifiers: new Set });
1720
+ }
1721
+ }
1722
+ }
1723
+ return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames, deferredChildSlots, loopDepth: -1 });
1724
+ }
1725
+ function buildCsrEnvForCtx(ctx, inlinableConstants, propsObjectName) {
1681
1726
  const base = buildSignalMemoEnv(ctx.signals, ctx.memos, propsObjectName ?? null);
1682
1727
  const csrEnv = { substitutions: new Map(base.substitutions), propsObjectName: base.propsObjectName };
1683
1728
  if (inlinableConstants) {
@@ -1687,7 +1732,71 @@ function generateCsrTemplate(node, inlinableConstants, ctx, insideLoop, restSpre
1687
1732
  }
1688
1733
  }
1689
1734
  }
1690
- return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames, loopDepth: -1 });
1735
+ return csrEnv;
1736
+ }
1737
+ function propResolvesUnsafe(prop, env, unsafeLocalNames) {
1738
+ if (unsafeLocalNames.size === 0)
1739
+ return false;
1740
+ let source;
1741
+ switch (prop.value.kind) {
1742
+ case "expression":
1743
+ case "spread":
1744
+ source = prop.value.expr;
1745
+ break;
1746
+ case "template":
1747
+ source = attrValueToString(prop.value, { useTemplate: true }) ?? undefined;
1748
+ break;
1749
+ default:
1750
+ return false;
1751
+ }
1752
+ if (!source)
1753
+ return false;
1754
+ const { freeIdentifiers } = csrSubstitute(source, env);
1755
+ return setIntersects(freeIdentifiers, unsafeLocalNames);
1756
+ }
1757
+ function computeDeferredChildSlots(node, ctx, inlinableConstants, unsafeLocalNames, propsObjectName) {
1758
+ const deferred = new Set;
1759
+ if (!unsafeLocalNames || unsafeLocalNames.size === 0)
1760
+ return deferred;
1761
+ const env = buildCsrEnvForCtx(ctx, inlinableConstants, propsObjectName);
1762
+ const visit = (n) => {
1763
+ switch (n.type) {
1764
+ case "component": {
1765
+ if (n.name === "Portal") {
1766
+ n.children.forEach(visit);
1767
+ return;
1768
+ }
1769
+ if (n.slotId) {
1770
+ const dropped = n.props.some((p) => {
1771
+ if (p.name === "..." || p.name.startsWith("...") || p.name === "key")
1772
+ return false;
1773
+ if (p.name.startsWith("on") && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase())
1774
+ return false;
1775
+ if (p.clientOnly)
1776
+ return false;
1777
+ return propResolvesUnsafe(p, env, unsafeLocalNames);
1778
+ });
1779
+ if (dropped)
1780
+ deferred.add(n.slotId);
1781
+ }
1782
+ return;
1783
+ }
1784
+ case "element":
1785
+ n.children.forEach(visit);
1786
+ return;
1787
+ case "fragment":
1788
+ n.children.forEach(visit);
1789
+ return;
1790
+ case "conditional":
1791
+ return;
1792
+ case "loop":
1793
+ return;
1794
+ default:
1795
+ return;
1796
+ }
1797
+ };
1798
+ visit(node);
1799
+ return deferred;
1691
1800
  }
1692
1801
  function generateCsrTemplateWithOpts(node, opts) {
1693
1802
  const { restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames, loopDepth = 0 } = opts;
@@ -1755,7 +1864,7 @@ function generateCsrTemplateWithOpts(node, opts) {
1755
1864
  attrParts.push(`bf="${node.slotId}"`);
1756
1865
  }
1757
1866
  const attrs = attrParts.join(" ");
1758
- const children = node.children.map(childrenRecurse).join("");
1867
+ const children = dangerouslyHtmlChildren(node.attrs, (v) => transformExpr(v.expr, v.templateExpr)) ?? node.children.map(childrenRecurse).join("");
1759
1868
  if (children || !VOID_ELEMENTS.has(node.tag)) {
1760
1869
  return `<${node.tag}${attrs ? " " + attrs : ""}>${children}</${node.tag}>`;
1761
1870
  }
@@ -1773,7 +1882,7 @@ function generateCsrTemplateWithOpts(node, opts) {
1773
1882
  const transformed = transformExpr(node.expr, node.templateExpr);
1774
1883
  const expr = transformed === UNSAFE_TEMPLATE_EXPR ? "''" : transformed;
1775
1884
  if (node.slotId) {
1776
- return `<!--bf:${node.slotId}-->\${${expr}}<!--/-->`;
1885
+ return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(expr)}}<!--/-->`;
1777
1886
  }
1778
1887
  return `\${${expr}}`;
1779
1888
  }
@@ -1793,6 +1902,9 @@ function generateCsrTemplateWithOpts(node, opts) {
1793
1902
  if (node.name === "Portal") {
1794
1903
  return node.children.map(recurse).join("");
1795
1904
  }
1905
+ if (node.slotId && opts.deferredChildSlots?.has(node.slotId)) {
1906
+ return `<div ${DATA_BF_PH}="${node.slotId}"></div>`;
1907
+ }
1796
1908
  const propsEntries = node.props.filter((p) => p.name !== "..." && !p.name.startsWith("...") && p.name !== "key").filter((p) => !(p.name.startsWith("on") && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase())).map((p) => {
1797
1909
  if (p.clientOnly)
1798
1910
  return null;
@@ -5228,14 +5340,7 @@ var UNSUPPORTED_METHODS = new Set([
5228
5340
  "forEach",
5229
5341
  "flatMap",
5230
5342
  "flat",
5231
- "split",
5232
- "startsWith",
5233
- "endsWith",
5234
- "replace",
5235
5343
  "replaceAll",
5236
- "repeat",
5237
- "padStart",
5238
- "padEnd",
5239
5344
  "charAt",
5240
5345
  "charCodeAt",
5241
5346
  "codePointAt",
@@ -5246,6 +5351,12 @@ var UNSUPPORTED_METHODS = new Set([
5246
5351
  "matchAll",
5247
5352
  "search"
5248
5353
  ]);
5354
+ var LOWERED_ARRAY_METHODS = new Set([
5355
+ "includes",
5356
+ "indexOf",
5357
+ "lastIndexOf",
5358
+ "concat"
5359
+ ]);
5249
5360
  function parseExpression(expr) {
5250
5361
  const trimmed = expr.trim();
5251
5362
  if (!trimmed) {
@@ -5306,7 +5417,7 @@ function convertNode(node, raw) {
5306
5417
  }
5307
5418
  }
5308
5419
  if (callee.kind === "member" && !callee.computed) {
5309
- if (callee.property === "join" && args.length === 1) {
5420
+ if (callee.property === "join") {
5310
5421
  return { kind: "array-method", method: "join", object: callee.object, args };
5311
5422
  }
5312
5423
  if (callee.property === "includes" && args.length === 1) {
@@ -5315,27 +5426,77 @@ function convertNode(node, raw) {
5315
5426
  if ((callee.property === "indexOf" || callee.property === "lastIndexOf") && args.length === 1) {
5316
5427
  return { kind: "array-method", method: callee.property, object: callee.object, args };
5317
5428
  }
5318
- if (callee.property === "at" && args.length === 1) {
5429
+ if (callee.property === "at") {
5319
5430
  return { kind: "array-method", method: "at", object: callee.object, args };
5320
5431
  }
5321
- if (callee.property === "concat" && args.length === 1) {
5432
+ if (callee.property === "concat" && args.length <= 1) {
5322
5433
  return { kind: "array-method", method: "concat", object: callee.object, args };
5323
5434
  }
5324
- if (callee.property === "slice" && (args.length === 1 || args.length === 2)) {
5435
+ if (callee.property === "slice") {
5325
5436
  return { kind: "array-method", method: "slice", object: callee.object, args };
5326
5437
  }
5327
- if ((callee.property === "reverse" || callee.property === "toReversed") && args.length === 0) {
5438
+ if (callee.property === "reverse" || callee.property === "toReversed") {
5328
5439
  return { kind: "array-method", method: callee.property, object: callee.object, args };
5329
5440
  }
5330
- if (callee.property === "toLowerCase" && args.length === 0) {
5441
+ if (callee.property === "toLowerCase") {
5331
5442
  return { kind: "array-method", method: "toLowerCase", object: callee.object, args };
5332
5443
  }
5333
- if (callee.property === "toUpperCase" && args.length === 0) {
5444
+ if (callee.property === "toUpperCase") {
5334
5445
  return { kind: "array-method", method: "toUpperCase", object: callee.object, args };
5335
5446
  }
5336
- if (callee.property === "trim" && args.length === 0) {
5447
+ if (callee.property === "trim") {
5337
5448
  return { kind: "array-method", method: "trim", object: callee.object, args };
5338
5449
  }
5450
+ if (callee.property === "split") {
5451
+ return { kind: "array-method", method: "split", object: callee.object, args };
5452
+ }
5453
+ if (LOWERED_ARRAY_METHODS.has(callee.property)) {
5454
+ const argName = callee.property === "concat" ? "other" : "x";
5455
+ const detail = callee.property === "concat" ? "the variadic `.concat(a, b, …)` form" : `\`.${callee.property}(…)\` with ${args.length} argument(s)`;
5456
+ return {
5457
+ kind: "unsupported",
5458
+ raw,
5459
+ reason: `${detail} is not yet lowered to the Go/Mojo template adapters. Use the single-argument \`.${callee.property}(${argName})\` form, or pre-compute the value before the template.`
5460
+ };
5461
+ }
5462
+ if (callee.property === "startsWith" || callee.property === "endsWith") {
5463
+ if (args.length === 0) {
5464
+ return {
5465
+ kind: "unsupported",
5466
+ raw,
5467
+ reason: `\`.${callee.property}()\` with no search string is not lowered — JS coerces the missing argument to the string "undefined", a degenerate result. Pass an explicit search string, or pre-compute the value before the template.`
5468
+ };
5469
+ }
5470
+ return { kind: "array-method", method: callee.property, object: callee.object, args };
5471
+ }
5472
+ if (callee.property === "replace") {
5473
+ if (args.length < 2) {
5474
+ return {
5475
+ kind: "unsupported",
5476
+ raw,
5477
+ reason: `\`.replace(${args.length === 0 ? "" : "pattern"})\` needs both a pattern and a replacement — JS coerces the missing argument to the string "undefined", a degenerate result. Pass both arguments, or pre-compute the value before the template.`
5478
+ };
5479
+ }
5480
+ const patternNode = node.arguments[0];
5481
+ if (patternNode && ts8.isRegularExpressionLiteral(patternNode)) {
5482
+ return {
5483
+ kind: "unsupported",
5484
+ raw,
5485
+ reason: "String.prototype.replace supports only a string pattern + string replacement (the regex form is deferred); use a string pattern or wrap the expression in /* @client */"
5486
+ };
5487
+ }
5488
+ const badArg = args[0].kind === "unsupported" ? args[0] : args[1].kind === "unsupported" ? args[1] : undefined;
5489
+ if (badArg && badArg.kind === "unsupported") {
5490
+ return { kind: "unsupported", raw, reason: badArg.reason };
5491
+ }
5492
+ return { kind: "array-method", method: "replace", object: callee.object, args };
5493
+ }
5494
+ if (callee.property === "repeat") {
5495
+ return { kind: "array-method", method: "repeat", object: callee.object, args };
5496
+ }
5497
+ if (callee.property === "padStart" || callee.property === "padEnd") {
5498
+ return { kind: "array-method", method: callee.property, object: callee.object, args };
5499
+ }
5339
5500
  if ((callee.property === "sort" || callee.property === "toSorted") && node.arguments.length === 1) {
5340
5501
  const comparator = extractSortComparatorFromTS(node.arguments[0], callee.property);
5341
5502
  if (comparator) {
@@ -9710,24 +9871,80 @@ function collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings)
9710
9871
  }
9711
9872
 
9712
9873
  // src/ir-to-client-js/collect-elements.ts
9713
- function producesDomChild(node) {
9714
- return node.type === "element" || node.type === "component" || node.type === "provider" || node.type === "async" || node.type === "text" || node.type === "expression" && !node.reactive || node.type === "conditional";
9874
+ var EMPTY_RENDER_EXPRS = new Set(["null", "undefined", "false", "''", '""', "``"]);
9875
+ function domElementCount(node) {
9876
+ switch (node.type) {
9877
+ case "element":
9878
+ case "component":
9879
+ case "provider":
9880
+ case "async":
9881
+ return 1;
9882
+ case "text":
9883
+ return 0;
9884
+ case "expression":
9885
+ return EMPTY_RENDER_EXPRS.has(node.expr.trim()) ? 0 : null;
9886
+ case "loop":
9887
+ if (node.bodyIsItemConditional || node.method === "flatMap")
9888
+ return null;
9889
+ return `(${buildLoopChainExpr({
9890
+ base: node.array,
9891
+ sortComparator: node.sortComparator,
9892
+ filterPredicate: node.filterPredicate,
9893
+ chainOrder: node.chainOrder
9894
+ })}).length`;
9895
+ case "conditional": {
9896
+ const t = domElementCount(node.whenTrue);
9897
+ const f = domElementCount(node.whenFalse);
9898
+ if (t === null || f === null)
9899
+ return null;
9900
+ if (typeof t === "number" && typeof f === "number" && t === f)
9901
+ return t;
9902
+ return `(${node.condition} ? ${t} : ${f})`;
9903
+ }
9904
+ case "fragment":
9905
+ return sumElementCounts(node.children);
9906
+ default:
9907
+ return null;
9908
+ }
9909
+ }
9910
+ function sumElementCounts(nodes) {
9911
+ let staticCount = 0;
9912
+ const dynamic = [];
9913
+ for (const n of nodes) {
9914
+ const c = domElementCount(n);
9915
+ if (c === null)
9916
+ return null;
9917
+ if (typeof c === "number")
9918
+ staticCount += c;
9919
+ else
9920
+ dynamic.push(c);
9921
+ }
9922
+ if (dynamic.length === 0)
9923
+ return staticCount;
9924
+ const parts = staticCount > 0 ? [String(staticCount), ...dynamic] : dynamic;
9925
+ return parts.length === 1 ? parts[0] : `(${parts.join(" + ")})`;
9926
+ }
9927
+ function legacyElementCount(node) {
9928
+ 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;
9715
9929
  }
9716
9930
  function computeLoopSiblingOffsets(root) {
9717
9931
  const offsets = new Map;
9718
- const recordChildren = (children) => {
9719
- let nonLoopCount = 0;
9932
+ const recordRun = (children, preceding) => {
9720
9933
  for (const child of children) {
9721
9934
  if (child.type === "loop") {
9722
- if (nonLoopCount > 0)
9723
- offsets.set(child, nonLoopCount);
9724
- } else if (producesDomChild(child)) {
9725
- nonLoopCount++;
9935
+ if (preceding.length > 0 && !offsets.has(child)) {
9936
+ offsets.set(child, [...preceding]);
9937
+ }
9938
+ preceding.push(child);
9939
+ } else if (child.type === "fragment" || child.type === "provider" || child.type === "async") {
9940
+ recordRun(child.children, preceding);
9941
+ } else {
9942
+ preceding.push(child);
9726
9943
  }
9727
9944
  }
9728
9945
  };
9729
9946
  const containerVisit = ({ node, descend }) => {
9730
- recordChildren(node.children);
9947
+ recordRun(node.children, []);
9731
9948
  descend();
9732
9949
  };
9733
9950
  walkIR(root, null, {
@@ -9739,6 +9956,24 @@ function computeLoopSiblingOffsets(root) {
9739
9956
  });
9740
9957
  return offsets;
9741
9958
  }
9959
+ function resolveLoopOffset(preceding) {
9960
+ if (!preceding || preceding.length === 0)
9961
+ return;
9962
+ let staticCount = 0;
9963
+ const dynamicTerms = [];
9964
+ for (const node of preceding) {
9965
+ const c = domElementCount(node);
9966
+ if (c === null)
9967
+ staticCount += legacyElementCount(node);
9968
+ else if (typeof c === "number")
9969
+ staticCount += c;
9970
+ else
9971
+ dynamicTerms.push(c);
9972
+ }
9973
+ if (staticCount === 0 && dynamicTerms.length === 0)
9974
+ return;
9975
+ return { staticCount, dynamicTerms };
9976
+ }
9742
9977
  var branchInnerLoopOptions = {
9743
9978
  collectItemBindings: true,
9744
9979
  templateDepth: 1,
@@ -9824,7 +10059,7 @@ function collectInnerLoops(nodes, siblingOffsets, outerLoopParam, ctx, options)
9824
10059
  refsOuterParam: refsOuter,
9825
10060
  childComponents,
9826
10061
  insideConditional: !flat && scope.insideCond ? true : undefined,
9827
- siblingOffset: flat ? undefined : siblingOffsets.get(n) || undefined,
10062
+ offset: flat ? undefined : resolveLoopOffset(siblingOffsets.get(n)),
9828
10063
  bindings
9829
10064
  });
9830
10065
  if (!flat) {
@@ -10042,7 +10277,7 @@ function collectElements(node, ctx, siblingOffsets, insideConditional = false) {
10042
10277
  isStaticArray: l.isStaticArray,
10043
10278
  useElementReconciliation,
10044
10279
  innerLoops: useElementReconciliation || l.isStaticArray && innerLoops?.length ? innerLoops : undefined,
10045
- siblingOffset: siblingOffsets.get(l) || undefined,
10280
+ offset: resolveLoopOffset(siblingOffsets.get(l)),
10046
10281
  filterPredicate: l.filterPredicate ? {
10047
10282
  param: l.filterPredicate.param,
10048
10283
  raw: l.filterPredicate.raw
@@ -10812,6 +11047,8 @@ var RUNTIME_IMPORT_CANDIDATES = [
10812
11047
  "splitProps",
10813
11048
  "spreadAttrs",
10814
11049
  "styleToCss",
11050
+ "escapeAttr",
11051
+ "escapeText",
10815
11052
  "qsa",
10816
11053
  "qsaItem",
10817
11054
  "qsaChildScope",
@@ -11705,7 +11942,7 @@ function emitRegistrationAndHydration(lines, ctx, _ir, graph, inlinability) {
11705
11942
  }
11706
11943
  } else {
11707
11944
  const csrInlinableConstants = csrInlinableConstantsFromCtx(ctx);
11708
- const templateHtml = generateCsrTemplate(_ir.root, csrInlinableConstants, ctx, undefined, restSpreadNames, ctx.propsObjectName, unsafeLocalNames);
11945
+ const templateHtml = generateCsrTemplate(_ir.root, csrInlinableConstants, ctx, undefined, restSpreadNames, ctx.propsObjectName, unsafeLocalNames, ctx.deferredChildSlots);
11709
11946
  if (templateHtml) {
11710
11947
  defParts.push(`template: (${PROPS_PARAM}) => \`${templateHtml}\``);
11711
11948
  }
@@ -12396,8 +12633,13 @@ function emitProviderAndChildInits(lines, ctx) {
12396
12633
  lines.push("");
12397
12634
  lines.push(` // Initialize child components with props`);
12398
12635
  for (const child of ctx.childInits) {
12636
+ const registryName = nameForRegistryRef(child.name);
12637
+ if (child.slotId && ctx.deferredChildSlots.has(child.slotId)) {
12638
+ lines.push(` upsertChild(__scope, '${registryName}', '${child.slotId}', ${child.propsExpr})`);
12639
+ continue;
12640
+ }
12399
12641
  const scopeRef = child.slotId ? `_${varSlotId(child.slotId)}` : "__scope";
12400
- lines.push(` initChild('${nameForRegistryRef(child.name)}', ${scopeRef}, ${child.propsExpr})`);
12642
+ lines.push(` initChild('${registryName}', ${scopeRef}, ${child.propsExpr})`);
12401
12643
  }
12402
12644
  }
12403
12645
  }
@@ -12678,7 +12920,7 @@ function buildOuterNestedPlan(elem, comp) {
12678
12920
  arrayExpr: elem.array,
12679
12921
  param: elem.param,
12680
12922
  indexParam,
12681
- offsetExpr: elem.siblingOffset ? `${indexParam} + ${elem.siblingOffset}` : indexParam,
12923
+ offsetExpr: buildLoopChildIndexExpr(indexParam, elem.offset),
12682
12924
  outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
12683
12925
  propsExpr: buildStaticPropsExpr(comp.props)
12684
12926
  };
@@ -12696,12 +12938,12 @@ function buildInnerLoopNestedPlan(elem, innerLoop, innerComps) {
12696
12938
  outerArrayExpr: elem.array,
12697
12939
  outerParam: elem.param,
12698
12940
  outerIndexParam,
12699
- outerOffsetExpr: elem.siblingOffset ? `${outerIndexParam} + ${elem.siblingOffset}` : outerIndexParam,
12941
+ outerOffsetExpr: buildLoopChildIndexExpr(outerIndexParam, elem.offset),
12700
12942
  outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
12701
12943
  innerContainerSlotId: innerLoop.containerSlotId ?? null,
12702
12944
  innerArrayExpr: innerLoop.array,
12703
12945
  innerParam: innerLoop.param,
12704
- innerOffsetExpr: innerLoop.siblingOffset ? `__innerIdx + ${innerLoop.siblingOffset}` : "__innerIdx",
12946
+ innerOffsetExpr: buildLoopChildIndexExpr("__innerIdx", innerLoop.offset),
12705
12947
  innerPreludeStatements: innerLoop.mapPreamble ? [innerLoop.mapPreamble] : [],
12706
12948
  depth: innerLoop.depth,
12707
12949
  comps
@@ -13463,7 +13705,7 @@ function buildStaticArrayDelegationPlan(elem) {
13463
13705
  arrayExpr: buildChainedArrayExpr(elem),
13464
13706
  param: elem.param,
13465
13707
  mapPreamble: elem.mapPreamble ?? null,
13466
- siblingOffset: elem.siblingOffset ?? null
13708
+ offset: elem.offset ?? null
13467
13709
  }
13468
13710
  };
13469
13711
  }
@@ -13581,6 +13823,11 @@ function buildArmBody(branch, options) {
13581
13823
  // src/ir-to-client-js/emit-reactive.ts
13582
13824
  function emitAttrUpdate(target, attrName, expression, meta) {
13583
13825
  const htmlName = toHtmlAttrName(attrName);
13826
+ if (attrName === "dangerouslySetInnerHTML" || htmlName === "dangerouslySetInnerHTML") {
13827
+ return [
13828
+ `{ const __v = ${expression}; ${target}.innerHTML = __v != null && __v.__html != null ? String(__v.__html) : '' }`
13829
+ ];
13830
+ }
13584
13831
  if (htmlName === "style") {
13585
13832
  return [
13586
13833
  `{ const __v = styleToCss(${expression}); if (__v != null) ${target}.setAttribute('style', __v); else ${target}.removeAttribute('style') }`
@@ -14592,11 +14839,11 @@ function emitDynamicIndexLookup(ls, ev, handlerCall, lookup) {
14592
14839
  ls.push(` }`);
14593
14840
  }
14594
14841
  function emitStaticIndexLookup(ls, ev, handlerCall, lookup, containerVar) {
14595
- const { arrayExpr, param, mapPreamble, siblingOffset } = lookup;
14842
+ const { arrayExpr, param, mapPreamble, offset } = lookup;
14596
14843
  ls.push(` let __el = ${varSlotId(ev.childSlotId)}El`);
14597
14844
  ls.push(` while (__el.parentElement && __el.parentElement !== ${containerVar}) __el = __el.parentElement`);
14598
14845
  ls.push(` if (__el.parentElement === ${containerVar}) {`);
14599
- const idxOffset = siblingOffset ? ` - ${siblingOffset}` : "";
14846
+ const idxOffset = buildLoopChildIndexSubtraction(offset ?? undefined);
14600
14847
  ls.push(` const __idx = Array.from(${containerVar}.children).indexOf(__el)${idxOffset}`);
14601
14848
  ls.push(` const ${param} = ${arrayExpr}[__idx]`);
14602
14849
  if (mapPreamble)
@@ -14873,7 +15120,7 @@ function buildStaticLoopPlan(elem, unsafeLocalNames) {
14873
15120
  }
14874
15121
  }
14875
15122
  const indexParam = elem.index || "__idx";
14876
- const childIndexExpr = elem.siblingOffset ? `${indexParam} + ${elem.siblingOffset}` : indexParam;
15123
+ const childIndexExpr = buildLoopChildIndexExpr(indexParam, elem.offset);
14877
15124
  return {
14878
15125
  kind: "static",
14879
15126
  containerVar: `_${varSlotId(elem.slotId)}`,
@@ -15192,6 +15439,7 @@ function generateInitFunction(ir, ctx, siblingComponents, localImportPrefixes) {
15192
15439
  const classification = classifyLocalDeclarations(ctx, graph);
15193
15440
  const propUsage = computePropUsage(ctx, classification.neededConstants);
15194
15441
  const inlinability = buildInlinableConstants(ctx, graph, ir.root);
15442
+ ctx.deferredChildSlots = computeDeferredChildSlots(ir.root, ctx, csrInlinableConstantsFromCtx(ctx), inlinability.unsafeLocalNames, ctx.propsObjectName);
15195
15443
  const phaseCtx = buildPhaseCtx({
15196
15444
  ctx,
15197
15445
  ir,
@@ -15460,6 +15708,7 @@ function createContext(ir, scope, adapterCapabilities) {
15460
15708
  loopElements: [],
15461
15709
  refElements: [],
15462
15710
  childInits: [],
15711
+ deferredChildSlots: new Set,
15463
15712
  reactiveProps: [],
15464
15713
  reactiveChildProps: [],
15465
15714
  reactiveAttrs: [],
@@ -17161,6 +17410,7 @@ function emitAttrValue(value, emitter, name) {
17161
17410
  }
17162
17411
  }
17163
17412
  // src/combine-client-js.ts
17413
+ import ts18 from "typescript";
17164
17414
  var CHILD_PLACEHOLDER_RE = /import '\/\* @bf-child:(\w+) \*\/'/g;
17165
17415
  function combineParentChildClientJs(files) {
17166
17416
  const result = new Map;
@@ -17217,33 +17467,43 @@ function combineParentChildClientJs(files) {
17217
17467
  return result;
17218
17468
  }
17219
17469
  function parseAndMerge(content, importsBySource, otherImports, codeSections) {
17220
- const codeLines = [];
17221
- for (const line of content.split(`
17222
- `)) {
17223
- if (line.startsWith("import ")) {
17224
- if (line.includes("@bf-child:"))
17225
- continue;
17226
- const match = line.match(/^import \{ ([^}]+) \} from ['"]([^'"]+)['"]/);
17227
- if (match) {
17228
- const names = match[1].split(",").map((n) => n.trim());
17229
- const source = match[2];
17230
- if (!importsBySource.has(source)) {
17231
- importsBySource.set(source, new Set);
17232
- }
17233
- for (const name of names) {
17234
- importsBySource.get(source).add(name);
17235
- }
17236
- } else {
17237
- if (!otherImports.includes(line)) {
17238
- otherImports.push(line);
17239
- }
17470
+ const sourceFile = ts18.createSourceFile("combine.js", content, ts18.ScriptTarget.Latest, false, ts18.ScriptKind.JS);
17471
+ const importSpans = [];
17472
+ for (const stmt of sourceFile.statements) {
17473
+ if (!ts18.isImportDeclaration(stmt))
17474
+ continue;
17475
+ const start = stmt.getStart(sourceFile);
17476
+ const end = stmt.getEnd();
17477
+ importSpans.push([start, end]);
17478
+ const stmtText = content.slice(start, end);
17479
+ if (stmtText.includes("@bf-child:"))
17480
+ continue;
17481
+ const clause = stmt.importClause;
17482
+ const bindings = clause?.namedBindings;
17483
+ const specifier = ts18.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : "";
17484
+ if (clause && !clause.name && bindings && ts18.isNamedImports(bindings)) {
17485
+ if (!importsBySource.has(specifier)) {
17486
+ importsBySource.set(specifier, new Set);
17487
+ }
17488
+ const set = importsBySource.get(specifier);
17489
+ for (const el of bindings.elements) {
17490
+ const name = el.propertyName ? `${el.propertyName.text} as ${el.name.text}` : el.name.text;
17491
+ set.add(name);
17240
17492
  }
17241
17493
  } else {
17242
- codeLines.push(line);
17494
+ if (!otherImports.includes(stmtText)) {
17495
+ otherImports.push(stmtText);
17496
+ }
17243
17497
  }
17244
17498
  }
17245
- const code = codeLines.join(`
17246
- `).trim();
17499
+ let code = "";
17500
+ let cursor = 0;
17501
+ for (const [start, end] of importSpans) {
17502
+ code += content.slice(cursor, start);
17503
+ cursor = end;
17504
+ }
17505
+ code += content.slice(cursor);
17506
+ code = code.trim();
17247
17507
  if (code) {
17248
17508
  codeSections.push(code);
17249
17509
  }