@barefootjs/jsx 0.17.0 → 0.18.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 (88) hide show
  1. package/dist/adapters/env-signal.d.ts +42 -7
  2. package/dist/adapters/env-signal.d.ts.map +1 -1
  3. package/dist/adapters/interface.d.ts +20 -4
  4. package/dist/adapters/interface.d.ts.map +1 -1
  5. package/dist/adapters/parsed-expr-emitter.d.ts +3 -1
  6. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  7. package/dist/analyzer-context.d.ts.map +1 -1
  8. package/dist/analyzer.d.ts.map +1 -1
  9. package/dist/augment-inherited-props.d.ts +19 -0
  10. package/dist/augment-inherited-props.d.ts.map +1 -1
  11. package/dist/compiler.d.ts.map +1 -1
  12. package/dist/expression-parser.d.ts +48 -2
  13. package/dist/expression-parser.d.ts.map +1 -1
  14. package/dist/index.d.ts +24 -5
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1728 -969
  17. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
  21. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
  24. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/emit-registration.d.ts +4 -2
  26. package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/html-template.d.ts +41 -0
  28. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/types.d.ts +22 -1
  30. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  31. package/dist/jsx-to-ir.d.ts.map +1 -1
  32. package/dist/loop-destructure.d.ts +55 -18
  33. package/dist/loop-destructure.d.ts.map +1 -1
  34. package/dist/lowering-registry.d.ts +13 -0
  35. package/dist/lowering-registry.d.ts.map +1 -1
  36. package/dist/relocate.d.ts +28 -0
  37. package/dist/relocate.d.ts.map +1 -1
  38. package/dist/ssr-defaults.d.ts.map +1 -1
  39. package/dist/ssr-seed-plan.d.ts +84 -0
  40. package/dist/ssr-seed-plan.d.ts.map +1 -0
  41. package/dist/types.d.ts +79 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +2 -2
  44. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +284 -12
  45. package/src/__tests__/augment-inherited-props.test.ts +96 -0
  46. package/src/__tests__/compiler-runtime-contract.test.ts +11 -1
  47. package/src/__tests__/compiler-stress-1244.test.ts +13 -4
  48. package/src/__tests__/csr-substitution-safety-divergence.test.ts +137 -0
  49. package/src/__tests__/destructured-map-params.test.ts +11 -1
  50. package/src/__tests__/expression-parser.test.ts +74 -3
  51. package/src/__tests__/free-identifiers.test.ts +55 -0
  52. package/src/__tests__/ir-sort-comparator.test.ts +261 -0
  53. package/src/__tests__/loop-destructure.test.ts +313 -0
  54. package/src/__tests__/loop-hoisted-template.test.ts +235 -0
  55. package/src/__tests__/materialize-getter-calls.test.ts +58 -0
  56. package/src/__tests__/props-destructuring.test.ts +110 -0
  57. package/src/__tests__/serialize-parsed-expr.test.ts +88 -2
  58. package/src/__tests__/ssr-defaults.test.ts +20 -0
  59. package/src/__tests__/ssr-seed-plan.test.ts +212 -0
  60. package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +231 -1
  61. package/src/__tests__/tagged-template-interleave.test.ts +268 -0
  62. package/src/__tests__/unsupported-expression.test.ts +194 -7
  63. package/src/adapters/env-signal.ts +57 -9
  64. package/src/adapters/interface.ts +20 -4
  65. package/src/adapters/parsed-expr-emitter.ts +19 -2
  66. package/src/analyzer-context.ts +20 -0
  67. package/src/analyzer.ts +74 -1
  68. package/src/augment-inherited-props.ts +139 -9
  69. package/src/compiler.ts +10 -1
  70. package/src/expression-parser.ts +421 -50
  71. package/src/index.ts +30 -3
  72. package/src/ir-to-client-js/collect-elements.ts +15 -1
  73. package/src/ir-to-client-js/compute-inlinability.ts +6 -1
  74. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
  75. package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
  76. package/src/ir-to-client-js/control-flow/stringify/loop.ts +30 -8
  77. package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +30 -0
  78. package/src/ir-to-client-js/emit-registration.ts +4 -2
  79. package/src/ir-to-client-js/html-template.ts +198 -1
  80. package/src/ir-to-client-js/index.ts +1 -0
  81. package/src/ir-to-client-js/types.ts +22 -0
  82. package/src/jsx-to-ir.ts +409 -24
  83. package/src/loop-destructure.ts +89 -36
  84. package/src/lowering-registry.ts +16 -0
  85. package/src/relocate.ts +201 -14
  86. package/src/ssr-defaults.ts +34 -32
  87. package/src/ssr-seed-plan.ts +146 -0
  88. package/src/types.ts +76 -0
package/dist/index.js CHANGED
@@ -352,6 +352,7 @@ function tsNodeToParsedExpr(node) {
352
352
  }
353
353
  var CALLBACK_METHODS = new Set([
354
354
  "filter",
355
+ "map",
355
356
  "every",
356
357
  "some",
357
358
  "find",
@@ -440,6 +441,7 @@ function convertNode(node, raw) {
440
441
  if (callee.property === "flat") {
441
442
  const depthNode = node.arguments[0];
442
443
  let flatDepth;
444
+ let depthExpr;
443
445
  if (depthNode === undefined) {
444
446
  flatDepth = 1;
445
447
  } else if (ts2.isIdentifier(depthNode) && depthNode.text === "Infinity") {
@@ -452,16 +454,23 @@ function convertNode(node, raw) {
452
454
  n = -Number(depthNode.operand.text);
453
455
  }
454
456
  if (n === undefined || Number.isNaN(n)) {
455
- return {
456
- kind: "unsupported",
457
- raw,
458
- reason: `\`.flat(depth)\` needs a literal integer or \`Infinity\` depth — a computed depth can't be resolved at template time. Use a literal depth, or pre-compute the value before the template.`
459
- };
457
+ const parsedDepth = convertNode(depthNode, raw);
458
+ if (checkSupport(parsedDepth).supported) {
459
+ depthExpr = parsedDepth;
460
+ flatDepth = 1;
461
+ } else {
462
+ return {
463
+ kind: "unsupported",
464
+ raw,
465
+ reason: `\`.flat(depth)\` needs a literal integer, \`Infinity\`, or a supported dynamic depth expression — this depth can't be resolved. Use a literal depth, a supported expression (prop/signal/arithmetic), or pre-compute the value before the template.`
466
+ };
467
+ }
468
+ } else {
469
+ const truncated = Math.trunc(n);
470
+ flatDepth = truncated < 0 ? 0 : truncated;
460
471
  }
461
- const truncated = Math.trunc(n);
462
- flatDepth = truncated < 0 ? 0 : truncated;
463
472
  }
464
- return { kind: "array-method", method: "flat", object: callee.object, args: [], flatDepth };
473
+ return depthExpr !== undefined ? { kind: "array-method", method: "flat", object: callee.object, args: [], flatDepth, depthExpr } : { kind: "array-method", method: "flat", object: callee.object, args: [], flatDepth };
465
474
  }
466
475
  if (callee.property === "toLowerCase") {
467
476
  return { kind: "array-method", method: "toLowerCase", object: callee.object, args };
@@ -1006,6 +1015,8 @@ function validateRestUsage(expr, restName, excludedTopKeys) {
1006
1015
  walk(e.object);
1007
1016
  for (const a of e.args)
1008
1017
  walk(a);
1018
+ if (e.method === "flat" && e.depthExpr)
1019
+ walk(e.depthExpr);
1009
1020
  return;
1010
1021
  case "literal":
1011
1022
  case "unsupported":
@@ -1091,6 +1102,8 @@ function collectIdentifiers(expr, out) {
1091
1102
  case "array-method":
1092
1103
  collectIdentifiers(expr.object, out);
1093
1104
  expr.args.forEach((e) => collectIdentifiers(e, out));
1105
+ if (expr.method === "flat" && expr.depthExpr)
1106
+ collectIdentifiers(expr.depthExpr, out);
1094
1107
  return;
1095
1108
  case "literal":
1096
1109
  case "regex":
@@ -1148,7 +1161,14 @@ function substituteDestructuredFields(expr, fieldMap, syntheticParam, restName)
1148
1161
  return { kind: "array-literal", elements: e.elements.map(walk) };
1149
1162
  case "array-method":
1150
1163
  if (e.method === "flat") {
1151
- return { kind: "array-method", method: "flat", object: walk(e.object), args: [], flatDepth: e.flatDepth };
1164
+ return {
1165
+ kind: "array-method",
1166
+ method: "flat",
1167
+ object: walk(e.object),
1168
+ args: [],
1169
+ flatDepth: e.flatDepth,
1170
+ ...e.depthExpr ? { depthExpr: walk(e.depthExpr) } : {}
1171
+ };
1152
1172
  }
1153
1173
  return { kind: "array-method", method: e.method, object: walk(e.object), args: e.args.map(walk) };
1154
1174
  case "literal":
@@ -1245,6 +1265,11 @@ function checkSupport(expr) {
1245
1265
  if (!argSupport.supported)
1246
1266
  return argSupport;
1247
1267
  }
1268
+ if (expr.method === "flat" && expr.depthExpr) {
1269
+ const depthSupport = checkSupport(expr.depthExpr);
1270
+ if (!depthSupport.supported)
1271
+ return depthSupport;
1272
+ }
1248
1273
  return { supported: true, level: "L2" };
1249
1274
  }
1250
1275
  case "call": {
@@ -1344,6 +1369,9 @@ function checkSupport(expr) {
1344
1369
  const leftSupport = checkSupport(expr.left);
1345
1370
  if (!leftSupport.supported)
1346
1371
  return leftSupport;
1372
+ if (expr.op === "??" && expr.right.kind === "object-literal" && expr.right.properties.length === 0) {
1373
+ return { supported: true, level: "L4" };
1374
+ }
1347
1375
  const rightSupport = checkSupport(expr.right);
1348
1376
  if (!rightSupport.supported)
1349
1377
  return rightSupport;
@@ -1398,7 +1426,7 @@ function containsHigherOrder(expr) {
1398
1426
  case "array-literal":
1399
1427
  return expr.elements.some(containsHigherOrder);
1400
1428
  case "array-method":
1401
- return containsHigherOrder(expr.object) || expr.args.some(containsHigherOrder);
1429
+ return containsHigherOrder(expr.object) || expr.args.some(containsHigherOrder) || expr.method === "flat" && expr.depthExpr !== undefined && containsHigherOrder(expr.depthExpr);
1402
1430
  default:
1403
1431
  return false;
1404
1432
  }
@@ -1566,7 +1594,10 @@ function usesPerPath(name, expr) {
1566
1594
  case "array-literal":
1567
1595
  return sum(e.elements);
1568
1596
  case "array-method":
1569
- return add(walk(e.object), e.method === "flat" ? { min: 0, max: 0 } : sum(e.args));
1597
+ if (e.method === "flat") {
1598
+ return add(walk(e.object), e.depthExpr ? walk(e.depthExpr) : { min: 0, max: 0 });
1599
+ }
1600
+ return add(walk(e.object), sum(e.args));
1570
1601
  case "object-literal":
1571
1602
  return sum(e.properties.map((p) => p.value));
1572
1603
  case "arrow":
@@ -1621,7 +1652,14 @@ function inlineBinding(expr, name, value) {
1621
1652
  return { kind: "array-literal", elements: e.elements.map((el) => walk(el, enclosing)) };
1622
1653
  case "array-method":
1623
1654
  if (e.method === "flat") {
1624
- return { kind: "array-method", method: "flat", object: walk(e.object, enclosing), args: [], flatDepth: e.flatDepth };
1655
+ return {
1656
+ kind: "array-method",
1657
+ method: "flat",
1658
+ object: walk(e.object, enclosing),
1659
+ args: [],
1660
+ flatDepth: e.flatDepth,
1661
+ ...e.depthExpr ? { depthExpr: walk(e.depthExpr, enclosing) } : {}
1662
+ };
1625
1663
  }
1626
1664
  return { kind: "array-method", method: e.method, object: walk(e.object, enclosing), args: e.args.map((a) => walk(a, enclosing)) };
1627
1665
  case "object-literal":
@@ -1752,6 +1790,9 @@ function exprToString(expr) {
1752
1790
  return `[${expr.elements.map(exprToString).join(", ")}]`;
1753
1791
  case "array-method":
1754
1792
  if (expr.method === "flat") {
1793
+ if (expr.depthExpr) {
1794
+ return `${exprToString(expr.object)}.flat(${exprToString(expr.depthExpr)})`;
1795
+ }
1755
1796
  const d = expr.flatDepth;
1756
1797
  const depthSrc = d === "infinity" ? "Infinity" : String(d);
1757
1798
  return `${exprToString(expr.object)}.flat(${d === 1 ? "" : depthSrc})`;
@@ -1803,6 +1844,9 @@ function stringifyParsedExpr(expr) {
1803
1844
  return `[${expr.elements.map(stringifyParsedExpr).join(", ")}]`;
1804
1845
  case "array-method":
1805
1846
  if (expr.method === "flat") {
1847
+ if (expr.depthExpr) {
1848
+ return `${stringifyParsedExpr(expr.object)}.flat(${stringifyParsedExpr(expr.depthExpr)})`;
1849
+ }
1806
1850
  const d = expr.flatDepth;
1807
1851
  const depthSrc = d === "infinity" ? "Infinity" : String(d);
1808
1852
  return `${stringifyParsedExpr(expr.object)}.flat(${d === 1 ? "" : depthSrc})`;
@@ -1813,66 +1857,195 @@ function stringifyParsedExpr(expr) {
1813
1857
  return expr.raw;
1814
1858
  }
1815
1859
  }
1860
+ function materializeGetterCalls(expr, names) {
1861
+ const rw = (e) => materializeGetterCalls(e, names);
1862
+ switch (expr.kind) {
1863
+ case "call":
1864
+ if (expr.args.length === 0 && expr.callee.kind === "identifier" && names.has(expr.callee.name)) {
1865
+ return { kind: "identifier", name: expr.callee.name };
1866
+ }
1867
+ return { kind: "call", callee: rw(expr.callee), args: expr.args.map(rw) };
1868
+ case "binary":
1869
+ return { kind: "binary", op: expr.op, left: rw(expr.left), right: rw(expr.right) };
1870
+ case "logical":
1871
+ return { kind: "logical", op: expr.op, left: rw(expr.left), right: rw(expr.right) };
1872
+ case "unary":
1873
+ return { kind: "unary", op: expr.op, argument: rw(expr.argument) };
1874
+ case "conditional":
1875
+ return {
1876
+ kind: "conditional",
1877
+ test: rw(expr.test),
1878
+ consequent: rw(expr.consequent),
1879
+ alternate: rw(expr.alternate)
1880
+ };
1881
+ case "member":
1882
+ return { kind: "member", object: rw(expr.object), property: expr.property, computed: expr.computed };
1883
+ case "index-access":
1884
+ return { kind: "index-access", object: rw(expr.object), index: rw(expr.index) };
1885
+ case "template-literal":
1886
+ return {
1887
+ kind: "template-literal",
1888
+ parts: expr.parts.map((p) => p.type === "string" ? p : { type: "expression", expr: rw(p.expr) })
1889
+ };
1890
+ case "array-literal":
1891
+ return { kind: "array-literal", elements: expr.elements.map(rw) };
1892
+ case "array-method":
1893
+ if (expr.method === "flat") {
1894
+ return { ...expr, object: rw(expr.object), ...expr.depthExpr ? { depthExpr: rw(expr.depthExpr) } : {} };
1895
+ }
1896
+ return { ...expr, object: rw(expr.object), args: expr.args.map(rw) };
1897
+ case "object-literal":
1898
+ return {
1899
+ kind: "object-literal",
1900
+ raw: expr.raw,
1901
+ properties: expr.properties.map((p) => ({ ...p, value: rw(p.value) }))
1902
+ };
1903
+ case "arrow":
1904
+ return { kind: "arrow", params: expr.params, body: rw(expr.body) };
1905
+ case "identifier":
1906
+ case "literal":
1907
+ case "regex":
1908
+ case "unsupported":
1909
+ return expr;
1910
+ }
1911
+ }
1816
1912
  function serializeParsedExpr(expr) {
1817
1913
  const node = toEvalNode(expr);
1818
1914
  return node === null ? null : JSON.stringify(node);
1819
1915
  }
1820
1916
  function freeVarsInBody(body, params) {
1821
1917
  const found = new Set;
1822
- const visit = (e) => {
1918
+ const visit = (e, bound) => {
1823
1919
  switch (e.kind) {
1824
1920
  case "identifier":
1825
- if (!params.has(e.name))
1921
+ if (!bound.has(e.name))
1826
1922
  found.add(e.name);
1827
1923
  return;
1828
1924
  case "binary":
1829
1925
  case "logical":
1830
- visit(e.left);
1831
- visit(e.right);
1926
+ visit(e.left, bound);
1927
+ visit(e.right, bound);
1832
1928
  return;
1833
1929
  case "unary":
1834
- visit(e.argument);
1930
+ visit(e.argument, bound);
1835
1931
  return;
1836
1932
  case "conditional":
1837
- visit(e.test);
1838
- visit(e.consequent);
1839
- visit(e.alternate);
1933
+ visit(e.test, bound);
1934
+ visit(e.consequent, bound);
1935
+ visit(e.alternate, bound);
1840
1936
  return;
1841
1937
  case "member":
1842
- visit(e.object);
1938
+ visit(e.object, bound);
1843
1939
  return;
1844
1940
  case "index-access":
1845
- visit(e.object);
1846
- visit(e.index);
1941
+ visit(e.object, bound);
1942
+ visit(e.index, bound);
1847
1943
  return;
1848
1944
  case "call":
1849
1945
  if (evalBuiltinCalleeName(e.callee) === null)
1850
- visit(e.callee);
1851
- e.args.forEach(visit);
1946
+ visit(e.callee, bound);
1947
+ e.args.forEach((a) => visit(a, bound));
1852
1948
  return;
1853
1949
  case "template-literal":
1854
1950
  for (const p of e.parts)
1855
1951
  if (p.type === "expression")
1856
- visit(p.expr);
1952
+ visit(p.expr, bound);
1857
1953
  return;
1858
1954
  case "array-literal":
1859
- e.elements.forEach(visit);
1955
+ e.elements.forEach((el) => visit(el, bound));
1860
1956
  return;
1861
1957
  case "object-literal":
1862
1958
  for (const p of e.properties)
1863
- visit(p.value);
1959
+ visit(p.value, bound);
1864
1960
  return;
1865
- case "literal":
1866
1961
  case "array-method":
1867
- case "arrow":
1962
+ if (e.method === "includes" || e.method === "join") {
1963
+ visit(e.object, bound);
1964
+ e.args.forEach((a) => visit(a, bound));
1965
+ }
1966
+ return;
1967
+ case "arrow": {
1968
+ const inner = e.params.length === 0 ? bound : new Set([...bound, ...e.params]);
1969
+ visit(e.body, inner);
1970
+ return;
1971
+ }
1972
+ case "literal":
1868
1973
  case "regex":
1869
1974
  case "unsupported":
1870
1975
  return;
1871
1976
  }
1872
1977
  };
1873
- visit(body);
1978
+ visit(body, params);
1874
1979
  return [...found].sort();
1875
1980
  }
1981
+ function freeIdentifiers(expr) {
1982
+ const free = new Set;
1983
+ function visit(e, bound) {
1984
+ switch (e.kind) {
1985
+ case "literal":
1986
+ case "regex":
1987
+ return true;
1988
+ case "identifier":
1989
+ if (!bound.has(e.name))
1990
+ free.add(e.name);
1991
+ return true;
1992
+ case "call": {
1993
+ const isBuiltinCallee = evalBuiltinCalleeName(e.callee) !== null;
1994
+ if (!isBuiltinCallee && !visit(e.callee, bound))
1995
+ return false;
1996
+ for (const a of e.args)
1997
+ if (!visit(a, bound))
1998
+ return false;
1999
+ return true;
2000
+ }
2001
+ case "member":
2002
+ return visit(e.object, bound);
2003
+ case "index-access":
2004
+ return visit(e.object, bound) && visit(e.index, bound);
2005
+ case "binary":
2006
+ case "logical":
2007
+ return visit(e.left, bound) && visit(e.right, bound);
2008
+ case "unary":
2009
+ return visit(e.argument, bound);
2010
+ case "conditional":
2011
+ return visit(e.test, bound) && visit(e.consequent, bound) && visit(e.alternate, bound);
2012
+ case "template-literal":
2013
+ for (const p of e.parts) {
2014
+ if (p.type === "expression" && !visit(p.expr, bound))
2015
+ return false;
2016
+ }
2017
+ return true;
2018
+ case "array-literal":
2019
+ for (const el of e.elements)
2020
+ if (!visit(el, bound))
2021
+ return false;
2022
+ return true;
2023
+ case "array-method":
2024
+ if (!visit(e.object, bound))
2025
+ return false;
2026
+ for (const a of e.args)
2027
+ if (!visit(a, bound))
2028
+ return false;
2029
+ if (e.method === "flat" && e.depthExpr && !visit(e.depthExpr, bound))
2030
+ return false;
2031
+ return true;
2032
+ case "object-literal":
2033
+ for (const p of e.properties)
2034
+ if (!visit(p.value, bound))
2035
+ return false;
2036
+ return true;
2037
+ case "arrow": {
2038
+ const inner = new Set(bound);
2039
+ for (const p of e.params)
2040
+ inner.add(p);
2041
+ return visit(e.body, inner);
2042
+ }
2043
+ case "unsupported":
2044
+ return false;
2045
+ }
2046
+ }
2047
+ return visit(expr, new Set) ? free : null;
2048
+ }
1876
2049
  var EVAL_BINARY_OPS = new Set([
1877
2050
  "+",
1878
2051
  "-",
@@ -1950,6 +2123,20 @@ function toEvalNode(e) {
1950
2123
  return object && index ? { kind: "index-access", object, index } : null;
1951
2124
  }
1952
2125
  case "call": {
2126
+ const cb = asCallbackMethodCall(e);
2127
+ if (cb && (cb.method === "map" || cb.method === "filter")) {
2128
+ const object = toEvalNode(cb.object);
2129
+ if (!object)
2130
+ return null;
2131
+ const body = toEvalNode(cb.arrow.body);
2132
+ if (!body)
2133
+ return null;
2134
+ return {
2135
+ kind: "call",
2136
+ callee: { kind: "member", object, property: cb.method, computed: false },
2137
+ args: [{ kind: "arrow", params: cb.arrow.params, body }]
2138
+ };
2139
+ }
1953
2140
  if (evalBuiltinCalleeName(e.callee) === null)
1954
2141
  return null;
1955
2142
  const callee = toEvalNode(e.callee);
@@ -1998,7 +2185,23 @@ function toEvalNode(e) {
1998
2185
  }
1999
2186
  return { kind: "object-literal", properties };
2000
2187
  }
2001
- case "array-method":
2188
+ case "array-method": {
2189
+ if (e.method === "includes" && e.args.length === 1) {
2190
+ const object = toEvalNode(e.object);
2191
+ const arg = toEvalNode(e.args[0]);
2192
+ return object && arg ? { kind: "array-method", method: "includes", object, args: [arg] } : null;
2193
+ }
2194
+ if (e.method === "join" && e.args.length <= 1) {
2195
+ const object = toEvalNode(e.object);
2196
+ if (!object)
2197
+ return null;
2198
+ if (e.args.length === 0)
2199
+ return { kind: "array-method", method: "join", object, args: [] };
2200
+ const sep = toEvalNode(e.args[0]);
2201
+ return sep ? { kind: "array-method", method: "join", object, args: [sep] } : null;
2202
+ }
2203
+ return null;
2204
+ }
2002
2205
  case "arrow":
2003
2206
  case "regex":
2004
2207
  case "unsupported":
@@ -3152,6 +3355,89 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
3152
3355
  return assertNever(node);
3153
3356
  }
3154
3357
  }
3358
+ function buildLoopSkeletonTemplate(node, safe) {
3359
+ switch (node.type) {
3360
+ case "element": {
3361
+ const attrParts = [];
3362
+ for (const a of node.attrs) {
3363
+ if (a.name === "...")
3364
+ return null;
3365
+ if (a.name === "dangerouslySetInnerHTML")
3366
+ return null;
3367
+ if (a.name === "key") {
3368
+ attrParts.push(`${keyAttrName(0)}=""`);
3369
+ continue;
3370
+ }
3371
+ const v = a.value;
3372
+ switch (v.kind) {
3373
+ case "literal":
3374
+ attrParts.push(`${toHtmlAttrName(a.name)}="${v.value}"`);
3375
+ break;
3376
+ case "boolean-attr":
3377
+ attrParts.push(toHtmlAttrName(a.name));
3378
+ break;
3379
+ case "boolean-shorthand":
3380
+ case "jsx-children":
3381
+ break;
3382
+ case "expression":
3383
+ case "template": {
3384
+ const attrKey = node.slotId ? `${node.slotId}::${a.name}` : null;
3385
+ if (!attrKey || !safe.reactiveAttrKeys.has(attrKey))
3386
+ return null;
3387
+ break;
3388
+ }
3389
+ case "spread":
3390
+ return null;
3391
+ }
3392
+ }
3393
+ if (node.slotId)
3394
+ attrParts.push(`bf="${node.slotId}"`);
3395
+ const attrs = attrParts.join(" ");
3396
+ let children = "";
3397
+ for (const child of node.children) {
3398
+ const rendered = buildLoopSkeletonTemplate(child, safe);
3399
+ if (rendered === null)
3400
+ return null;
3401
+ children += rendered;
3402
+ }
3403
+ if (children || !VOID_ELEMENTS.has(node.tag)) {
3404
+ return `<${node.tag}${attrs ? " " + attrs : ""}>${children}</${node.tag}>`;
3405
+ }
3406
+ return `<${node.tag}${attrs ? " " + attrs : ""} />`;
3407
+ }
3408
+ case "text":
3409
+ return node.value;
3410
+ case "expression":
3411
+ if (node.expr === "null" || node.expr === "undefined")
3412
+ return "";
3413
+ if (!node.slotId) {
3414
+ return null;
3415
+ }
3416
+ if (!safe.reactiveTextSlotIds.has(node.slotId))
3417
+ return null;
3418
+ return `<!--bf:${node.slotId}--><!--/-->`;
3419
+ case "fragment": {
3420
+ let out = "";
3421
+ for (const child of node.children) {
3422
+ const rendered = buildLoopSkeletonTemplate(child, safe);
3423
+ if (rendered === null)
3424
+ return null;
3425
+ out += rendered;
3426
+ }
3427
+ return out;
3428
+ }
3429
+ case "conditional":
3430
+ case "component":
3431
+ case "loop":
3432
+ case "if-statement":
3433
+ case "provider":
3434
+ case "async":
3435
+ case "slot":
3436
+ return null;
3437
+ default:
3438
+ return assertNever(node);
3439
+ }
3440
+ }
3155
3441
  function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParams) {
3156
3442
  const recurse = (n) => irToPlaceholderTemplate(n, restSpreadNames, loopDepth, loopParams);
3157
3443
  const wrapExpr = (expr) => wrapExprWithLoopParams(expr, loopParams);
@@ -3551,7 +3837,23 @@ function generateCsrTemplate(node, inlinableConstants, ctx, insideLoop, restSpre
3551
3837
  }
3552
3838
  }
3553
3839
  }
3554
- return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames, deferredChildSlots, loopDepth: -1 });
3840
+ const effectiveUnsafeLocalNames = mergeCsrNullUnsafe(ctx, unsafeLocalNames);
3841
+ return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1 });
3842
+ }
3843
+ function mergeCsrNullUnsafe(ctx, unsafeLocalNames) {
3844
+ let merged = null;
3845
+ let exemptNames = null;
3846
+ for (const [name, entry] of ctx.csrInlinable) {
3847
+ if (entry !== null || unsafeLocalNames?.has(name))
3848
+ continue;
3849
+ exemptNames ??= new Set(ctx.localConstants.filter((c) => c.isJsx || c.systemConstructKind).map((c) => c.name));
3850
+ if (exemptNames.has(name))
3851
+ continue;
3852
+ if (!merged)
3853
+ merged = new Set(unsafeLocalNames ?? []);
3854
+ merged.add(name);
3855
+ }
3856
+ return merged ?? unsafeLocalNames;
3555
3857
  }
3556
3858
  function buildCsrEnvForCtx(ctx, inlinableConstants, propsObjectName) {
3557
3859
  const base = buildSignalMemoEnv(ctx.signals, ctx.memos, propsObjectName ?? null);
@@ -3582,8 +3884,8 @@ function propResolvesUnsafe(prop, env, unsafeLocalNames) {
3582
3884
  }
3583
3885
  if (!source)
3584
3886
  return false;
3585
- const { freeIdentifiers } = csrSubstitute(source, env);
3586
- return setIntersects(freeIdentifiers, unsafeLocalNames);
3887
+ const { freeIdentifiers: freeIdentifiers2 } = csrSubstitute(source, env);
3888
+ return setIntersects(freeIdentifiers2, unsafeLocalNames);
3587
3889
  }
3588
3890
  function computeDeferredChildSlots(node, ctx, inlinableConstants, unsafeLocalNames, propsObjectName) {
3589
3891
  const deferred = new Set;
@@ -3636,8 +3938,8 @@ function generateCsrTemplateWithOpts(node, opts) {
3636
3938
  const source = templateExpr ?? expr;
3637
3939
  if (!source)
3638
3940
  return source;
3639
- const { rewritten, freeIdentifiers } = csrSubstitute(source, env);
3640
- if (unsafeLocalNames && unsafeLocalNames.size > 0 && setIntersects(freeIdentifiers, unsafeLocalNames)) {
3941
+ const { rewritten, freeIdentifiers: freeIdentifiers2 } = csrSubstitute(source, env);
3942
+ if (unsafeLocalNames && unsafeLocalNames.size > 0 && setIntersects(freeIdentifiers2, unsafeLocalNames)) {
3641
3943
  return UNSAFE_TEMPLATE_EXPR;
3642
3944
  }
3643
3945
  return applyPropsRewrite(rewritten, propsObjectName ?? null);
@@ -4139,6 +4441,15 @@ function createAnalyzerContext(sourceFile, filePath) {
4139
4441
  checker: null,
4140
4442
  componentBodyBlock: null,
4141
4443
  getJS(node) {
4444
+ let ownSourceFile;
4445
+ try {
4446
+ ownSourceFile = node.getSourceFile();
4447
+ } catch {
4448
+ ownSourceFile = undefined;
4449
+ }
4450
+ if (ownSourceFile && ownSourceFile !== sourceFile) {
4451
+ return node.getText(ownSourceFile);
4452
+ }
4142
4453
  return reconstructWithoutTypes(node, sourceFile, this.typeExcludeRanges);
4143
4454
  }
4144
4455
  };
@@ -6301,7 +6612,7 @@ function collectConstant(node, ctx, _isModule, declarationKind = "const", isExpo
6301
6612
  const baseValue = `${propsName}.${sourceKey}`;
6302
6613
  const value2 = defaultValueExpr ? `${baseValue} ?? ${defaultValueExpr}` : baseValue;
6303
6614
  const containsArrow2 = el.initializer ? nodeContainsArrow(el.initializer) : false;
6304
- const freeIdentifiers2 = el.initializer ? extractFreeIdentifiersFromNode(el.initializer) : new Set([propsName]);
6615
+ const freeIdentifiers3 = el.initializer ? extractFreeIdentifiersFromNode(el.initializer) : new Set([propsName]);
6305
6616
  ctx.localConstants.push({
6306
6617
  name: localName,
6307
6618
  value: value2,
@@ -6309,7 +6620,7 @@ function collectConstant(node, ctx, _isModule, declarationKind = "const", isExpo
6309
6620
  isExported,
6310
6621
  type: null,
6311
6622
  loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
6312
- freeIdentifiers: freeIdentifiers2,
6623
+ freeIdentifiers: freeIdentifiers3,
6313
6624
  containsArrow: containsArrow2 || undefined,
6314
6625
  origin: { phase: "hydrate", scope: "init", effect: "pure" }
6315
6626
  });
@@ -6393,7 +6704,7 @@ function collectConstant(node, ctx, _isModule, declarationKind = "const", isExpo
6393
6704
  } else if (value) {
6394
6705
  type = inferTypeFromValue(value);
6395
6706
  }
6396
- const freeIdentifiers = node.initializer ? extractFreeIdentifiersFromNode(node.initializer) : undefined;
6707
+ const freeIdentifiers2 = node.initializer ? extractFreeIdentifiersFromNode(node.initializer) : undefined;
6397
6708
  const containsArrow = node.initializer ? nodeContainsArrow(node.initializer) : false;
6398
6709
  const systemConstructKind = node.initializer ? getSystemConstructKind(node.initializer) : undefined;
6399
6710
  let templateValue;
@@ -6442,7 +6753,7 @@ function collectConstant(node, ctx, _isModule, declarationKind = "const", isExpo
6442
6753
  isModule: isModule || undefined,
6443
6754
  type,
6444
6755
  loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
6445
- freeIdentifiers,
6756
+ freeIdentifiers: freeIdentifiers2,
6446
6757
  isJsx,
6447
6758
  isJsxFunction: isJsxFunction || undefined,
6448
6759
  containsArrow: containsArrow || undefined,
@@ -6492,6 +6803,7 @@ function extractProps(param, ctx) {
6492
6803
  loc: getSourceLocation(param, ctx.sourceFile, ctx.filePath),
6493
6804
  hasIgnoreDirective: ignored
6494
6805
  };
6806
+ const memberTypes = param.type ? collectMemberTypes(param.type, ctx) : null;
6495
6807
  for (const element of param.name.elements) {
6496
6808
  if (ts8.isBindingElement(element) && ts8.isIdentifier(element.name)) {
6497
6809
  const localName = element.name.text;
@@ -6500,10 +6812,12 @@ function extractProps(param, ctx) {
6500
6812
  ctx.restPropsName = localName;
6501
6813
  continue;
6502
6814
  }
6815
+ const sourcePropName = element.propertyName && ts8.isIdentifier(element.propertyName) ? element.propertyName.text : localName;
6816
+ const resolvedType = memberTypes?.get(sourcePropName) ?? { kind: "unknown", raw: "unknown" };
6503
6817
  const defaultContainsArrow = element.initializer ? nodeContainsArrow(element.initializer) : false;
6504
6818
  ctx.propsParams.push({
6505
6819
  name: localName,
6506
- type: { kind: "unknown", raw: "unknown" },
6820
+ type: resolvedType,
6507
6821
  optional: !!element.initializer,
6508
6822
  defaultValue,
6509
6823
  defaultContainsArrow: defaultContainsArrow || undefined
@@ -6564,6 +6878,37 @@ function collectKeysFromMembers(members, ctx) {
6564
6878
  }
6565
6879
  return keys;
6566
6880
  }
6881
+ function collectMemberTypes(typeNode, ctx) {
6882
+ const isResolvablePrimitive = (info) => info.kind === "primitive" && (info.primitive === "string" || info.primitive === "number" || info.primitive === "boolean");
6883
+ const fromMembers = (members) => {
6884
+ const map = new Map;
6885
+ for (const member of members) {
6886
+ if (ts8.isPropertySignature(member) && member.name && member.type && !member.questionToken) {
6887
+ const info = typeNodeToTypeInfo(member.type, ctx.sourceFile);
6888
+ if (info && isResolvablePrimitive(info)) {
6889
+ map.set(member.name.getText(ctx.sourceFile), info);
6890
+ }
6891
+ }
6892
+ }
6893
+ return map;
6894
+ };
6895
+ if (ts8.isTypeLiteralNode(typeNode)) {
6896
+ return fromMembers(typeNode.members);
6897
+ }
6898
+ if (ts8.isTypeReferenceNode(typeNode)) {
6899
+ const typeName = typeNode.typeName.getText(ctx.sourceFile);
6900
+ const typeDecl = findTypeDeclaration(typeName, ctx.sourceFile);
6901
+ if (!typeDecl)
6902
+ return null;
6903
+ if (ts8.isInterfaceDeclaration(typeDecl)) {
6904
+ return fromMembers(typeDecl.members);
6905
+ }
6906
+ if (ts8.isTypeAliasDeclaration(typeDecl) && ts8.isTypeLiteralNode(typeDecl.type)) {
6907
+ return fromMembers(typeDecl.type.members);
6908
+ }
6909
+ }
6910
+ return null;
6911
+ }
6567
6912
  function extractPropsFromType(typeNode, ctx) {
6568
6913
  if (ts8.isTypeLiteralNode(typeNode)) {
6569
6914
  extractPropsFromTypeMembers(typeNode.members, ctx);
@@ -7258,58 +7603,171 @@ function pickAttrMetaFromIR(src) {
7258
7603
  };
7259
7604
  }
7260
7605
 
7261
- // src/builtins.ts
7262
- var CLIENT_BUILTIN_SOURCE = "@barefootjs/client";
7263
- function isClientBuiltinName(name) {
7264
- return name === "Async" || name === "Region";
7265
- }
7266
- function stripClientBuiltinImports(imports) {
7267
- const result = [];
7268
- for (const imp of imports) {
7269
- if (imp.source !== CLIENT_BUILTIN_SOURCE || imp.isTypeOnly || imp.specifiers.length === 0) {
7270
- result.push(imp);
7606
+ // src/module-exports.ts
7607
+ function generateModuleExports(ir, extraInlineExported = new Set, rewriteRelativeImport) {
7608
+ const lines = [];
7609
+ for (const constant of ir.metadata.localConstants) {
7610
+ if (!constant.isExported)
7611
+ continue;
7612
+ const keyword = constant.declarationKind ?? "const";
7613
+ if (!constant.value) {
7614
+ lines.push(`export ${keyword} ${constant.name}`);
7271
7615
  continue;
7272
7616
  }
7273
- const kept = imp.specifiers.filter((spec) => spec.isDefault || spec.isNamespace || spec.isTypeOnly || !isClientBuiltinName(spec.name));
7274
- if (kept.length === 0)
7617
+ const value = constant.value.trim();
7618
+ if (/^createContext\b/.test(value) || /^new WeakMap\b/.test(value))
7275
7619
  continue;
7276
- result.push(kept.length === imp.specifiers.length ? imp : { ...imp, specifiers: kept });
7620
+ lines.push(`export ${keyword} ${constant.name} = ${constant.value}`);
7277
7621
  }
7278
- return result;
7279
- }
7280
-
7281
- // src/reactivity-checker.ts
7282
- import ts9 from "typescript";
7283
- var REACTIVE_BRAND = "__reactive";
7284
- function queryType(checker, node) {
7285
- incrementCounter("typeCheckerQueries");
7286
- return checker.getTypeAtLocation(node);
7287
- }
7288
- function isReactiveType(type) {
7289
- return type.getProperty(REACTIVE_BRAND) !== undefined;
7290
- }
7291
- var NOT_REACTIVE = { isReactive: false, reason: { kind: "not-reactive" } };
7292
- function safeGetText(node) {
7293
- try {
7294
- return node.getText();
7295
- } catch {
7296
- return "";
7622
+ for (const func of ir.metadata.localFunctions) {
7623
+ if (!func.isExported)
7624
+ continue;
7625
+ const params = func.typedParams !== undefined ? func.typedParams : func.params.map(formatParamWithType).join(", ");
7626
+ const returnAnnotation = func.typedReturnType ? `: ${func.typedReturnType}` : "";
7627
+ const body = func.typedBody ?? func.body;
7628
+ const asyncKw = func.isAsync ? "async " : "";
7629
+ lines.push(`export ${asyncKw}function ${func.name}(${params})${returnAnnotation} ${body}`);
7297
7630
  }
7298
- }
7299
- function analyze(node, checker) {
7300
- if (ts9.isPropertyAccessExpression(node)) {
7301
- try {
7302
- const type = queryType(checker, node);
7303
- if (isReactiveType(type)) {
7304
- return {
7305
- isReactive: true,
7306
- reason: { kind: "brand", via: "property-access", nodeText: safeGetText(node) }
7307
- };
7308
- }
7309
- } catch {}
7310
- const sub = analyze(node.expression, checker);
7311
- if (sub.isReactive) {
7312
- return {
7631
+ const inlineExported = collectInlineExportedNames(ir);
7632
+ for (const name of extraInlineExported)
7633
+ inlineExported.add(name);
7634
+ for (const block of ir.metadata.namedExports) {
7635
+ const isReexportFrom = block.source !== null;
7636
+ const survivingSpecs = block.specifiers.filter((spec) => {
7637
+ if (isReexportFrom)
7638
+ return true;
7639
+ return !(inlineExported.has(spec.name) && spec.alias == null);
7640
+ });
7641
+ if (survivingSpecs.length === 0)
7642
+ continue;
7643
+ const specText = survivingSpecs.map((s) => {
7644
+ const prefix = s.isTypeOnly ? "type " : "";
7645
+ return s.alias ? `${prefix}${s.name} as ${s.alias}` : `${prefix}${s.name}`;
7646
+ }).join(", ");
7647
+ const typeKw = block.isTypeOnly ? "type " : "";
7648
+ if (isReexportFrom) {
7649
+ const source = rewriteRelativeImport && block.source.startsWith(".") ? rewriteRelativeImport(block.source) : block.source;
7650
+ lines.push(`export ${typeKw}{ ${specText} } from '${source}'`);
7651
+ } else {
7652
+ lines.push(`export ${typeKw}{ ${specText} }`);
7653
+ }
7654
+ }
7655
+ return lines.length > 0 ? lines.join(`
7656
+ `) : null;
7657
+ }
7658
+ function collectInlineExportedNames(ir) {
7659
+ const names = new Set;
7660
+ for (const c of ir.metadata.localConstants) {
7661
+ if (c.isExported)
7662
+ names.add(c.name);
7663
+ }
7664
+ for (const f of ir.metadata.localFunctions) {
7665
+ if (f.isExported)
7666
+ names.add(f.name);
7667
+ }
7668
+ if (ir.metadata.isExported && ir.metadata.componentName) {
7669
+ names.add(ir.metadata.componentName);
7670
+ }
7671
+ return names;
7672
+ }
7673
+ function formatParamWithType(p) {
7674
+ const rest = p.isRest ? "..." : "";
7675
+ const optional = p.optional ? "?" : "";
7676
+ const typeAnnotation = p.type?.raw && p.type.raw !== "unknown" ? `: ${p.type.raw}` : "";
7677
+ const defaultPart = p.defaultValue !== undefined ? ` = ${p.defaultValue}` : "";
7678
+ return `${rest}${p.name}${optional}${typeAnnotation}${defaultPart}`;
7679
+ }
7680
+ function findReachableNames(primaryRefs, declarations) {
7681
+ const allNames = new Set(declarations.map((d) => d.name));
7682
+ const bodyMap = new Map(declarations.map((d) => [d.name, d.body]));
7683
+ const reachable = new Set;
7684
+ const queue = [];
7685
+ for (const name of allNames) {
7686
+ if (new RegExp(`\\b${name}\\b`).test(primaryRefs)) {
7687
+ reachable.add(name);
7688
+ queue.push(name);
7689
+ }
7690
+ }
7691
+ while (queue.length > 0) {
7692
+ const current = queue.shift();
7693
+ const body = bodyMap.get(current) || "";
7694
+ for (const name of allNames) {
7695
+ if (!reachable.has(name) && new RegExp(`\\b${name}\\b`).test(body)) {
7696
+ reachable.add(name);
7697
+ queue.push(name);
7698
+ }
7699
+ }
7700
+ }
7701
+ return reachable;
7702
+ }
7703
+ function extractFunctionParams(value) {
7704
+ const arrowMatch = value.match(/^(?:async\s*)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/);
7705
+ if (arrowMatch) {
7706
+ return arrowMatch[1].split(",").map((p) => p.trim().split(":")[0].split("=")[0].trim()).filter(Boolean).join(", ");
7707
+ }
7708
+ const singleMatch = value.match(/^(?:async\s*)?(\w+)\s*=>/);
7709
+ if (singleMatch) {
7710
+ return singleMatch[1];
7711
+ }
7712
+ const funcMatch = value.match(/^(?:async\s*)?function\s*\w*\s*\(([^)]*)\)/);
7713
+ if (funcMatch) {
7714
+ return funcMatch[1].split(",").map((p) => p.trim().split(":")[0].split("=")[0].trim()).filter(Boolean).join(", ");
7715
+ }
7716
+ return "";
7717
+ }
7718
+
7719
+ // src/builtins.ts
7720
+ var CLIENT_BUILTIN_SOURCE = "@barefootjs/client";
7721
+ function isClientBuiltinName(name) {
7722
+ return name === "Async" || name === "Region";
7723
+ }
7724
+ function stripClientBuiltinImports(imports) {
7725
+ const result = [];
7726
+ for (const imp of imports) {
7727
+ if (imp.source !== CLIENT_BUILTIN_SOURCE || imp.isTypeOnly || imp.specifiers.length === 0) {
7728
+ result.push(imp);
7729
+ continue;
7730
+ }
7731
+ const kept = imp.specifiers.filter((spec) => spec.isDefault || spec.isNamespace || spec.isTypeOnly || !isClientBuiltinName(spec.name));
7732
+ if (kept.length === 0)
7733
+ continue;
7734
+ result.push(kept.length === imp.specifiers.length ? imp : { ...imp, specifiers: kept });
7735
+ }
7736
+ return result;
7737
+ }
7738
+
7739
+ // src/reactivity-checker.ts
7740
+ import ts9 from "typescript";
7741
+ var REACTIVE_BRAND = "__reactive";
7742
+ function queryType(checker, node) {
7743
+ incrementCounter("typeCheckerQueries");
7744
+ return checker.getTypeAtLocation(node);
7745
+ }
7746
+ function isReactiveType(type) {
7747
+ return type.getProperty(REACTIVE_BRAND) !== undefined;
7748
+ }
7749
+ var NOT_REACTIVE = { isReactive: false, reason: { kind: "not-reactive" } };
7750
+ function safeGetText(node) {
7751
+ try {
7752
+ return node.getText();
7753
+ } catch {
7754
+ return "";
7755
+ }
7756
+ }
7757
+ function analyze(node, checker) {
7758
+ if (ts9.isPropertyAccessExpression(node)) {
7759
+ try {
7760
+ const type = queryType(checker, node);
7761
+ if (isReactiveType(type)) {
7762
+ return {
7763
+ isReactive: true,
7764
+ reason: { kind: "brand", via: "property-access", nodeText: safeGetText(node) }
7765
+ };
7766
+ }
7767
+ } catch {}
7768
+ const sub = analyze(node.expression, checker);
7769
+ if (sub.isReactive) {
7770
+ return {
7313
7771
  isReactive: true,
7314
7772
  reason: {
7315
7773
  kind: "child",
@@ -8379,6 +8837,7 @@ function transformExpression(node, ctx) {
8379
8837
  return transformExpressionInner(expr, ctx, node, isClientOnly);
8380
8838
  }
8381
8839
  function transformExpressionInner(expr, ctx, node, isClientOnly) {
8840
+ expr = tryDesugarInterleaveTaggedTemplate(expr, ctx);
8382
8841
  checkBareSignalOrMemoIdentifier(expr, ctx);
8383
8842
  if (ts11.isIdentifier(expr)) {
8384
8843
  const jsxNode = ctx.analyzer.jsxConstants.get(expr.text);
@@ -8920,11 +9379,10 @@ function isIteratorShapeCall(node) {
8920
9379
  return { array: node.expression.expression, shape: name };
8921
9380
  }
8922
9381
  function extractSortComparator(callback, _method, ctx) {
8923
- const unsupported = () => {
8924
- const raw = ctx.getJS(callback);
8925
- return {
8926
- result: null,
8927
- unsupportedReason: `Sort comparator '${raw}' is not a supported shape. Accepted:
9382
+ const outerRaw = ctx.getJS(callback);
9383
+ const unsupported = () => ({
9384
+ result: null,
9385
+ unsupportedReason: `Sort comparator '${outerRaw}' is not a supported shape. Accepted:
8928
9386
  ` + ` (a, b) => a - b
8929
9387
  ` + ` (a, b) => a.field - b.field
8930
9388
  ` + ` (a, b) => a.localeCompare(b)
@@ -8932,15 +9390,25 @@ function extractSortComparator(callback, _method, ctx) {
8932
9390
  ` + ` (a, b) => a.field > b.field ? 1 : a.field < b.field ? -1 : 0
8933
9391
  ` + ` any of the above '||'-chained for multi-key tie-breaks
8934
9392
  ` + `(reverse the operands for descending order).`
8935
- };
8936
- };
8937
- if (!ts11.isArrowFunction(callback) && !ts11.isFunctionExpression(callback)) {
9393
+ });
9394
+ let resolvedNode = callback;
9395
+ if (ts11.isIdentifier(callback)) {
9396
+ const resolved = resolveSortComparatorIdentifier(callback.text, ctx);
9397
+ if (!resolved) {
9398
+ return {
9399
+ result: null,
9400
+ unsupportedReason: `Sort comparator '${outerRaw}' could not be resolved to a local function — ` + `declare it in the same file or inline it.`
9401
+ };
9402
+ }
9403
+ resolvedNode = resolved;
9404
+ }
9405
+ if (!ts11.isArrowFunction(resolvedNode) && !ts11.isFunctionExpression(resolvedNode)) {
8938
9406
  return {
8939
9407
  result: null,
8940
9408
  unsupportedReason: "Sort comparator must be an arrow function or function expression"
8941
9409
  };
8942
9410
  }
8943
- const arrow = tsNodeToParsedExpr(callback);
9411
+ const arrow = tsNodeToParsedExpr(resolvedNode);
8944
9412
  if (arrow.kind !== "arrow" || arrow.params.length !== 2)
8945
9413
  return unsupported();
8946
9414
  if (sortComparatorFromArrow(arrow) === null)
@@ -8954,6 +9422,21 @@ function extractSortComparator(callback, _method, ctx) {
8954
9422
  }
8955
9423
  };
8956
9424
  }
9425
+ function resolveSortComparatorIdentifier(name, ctx) {
9426
+ const constInfo = findLocalConst(name, ctx);
9427
+ const fnInfo = findLocalFunction(name, ctx);
9428
+ if (constInfo && fnInfo)
9429
+ return null;
9430
+ if (constInfo) {
9431
+ const ast = parseConstInitializer(constInfo);
9432
+ return ast && (ts11.isArrowFunction(ast) || ts11.isFunctionExpression(ast)) ? ast : null;
9433
+ }
9434
+ if (fnInfo) {
9435
+ const ast = parseFunctionInfoAsExpr(fnInfo);
9436
+ return ast && (ts11.isArrowFunction(ast) || ts11.isFunctionExpression(ast)) ? ast : null;
9437
+ }
9438
+ return null;
9439
+ }
8957
9440
  function extractFilterPredicate(callback, ctx) {
8958
9441
  if (!ts11.isArrowFunction(callback))
8959
9442
  return { result: null };
@@ -9026,7 +9509,7 @@ function extractLoopParamBindings(pattern) {
9026
9509
  const appendDotAccess = (prefix, key) => {
9027
9510
  return isIdent(key) ? `${prefix}.${key}` : `${prefix}[${JSON.stringify(key)}]`;
9028
9511
  };
9029
- const walk = (p, prefix) => {
9512
+ const walk = (p, prefix, segments) => {
9030
9513
  if (unsupported)
9031
9514
  return;
9032
9515
  if (ts11.isArrayBindingPattern(p)) {
@@ -9043,15 +9526,17 @@ function extractLoopParamBindings(pattern) {
9043
9526
  bindings.push({
9044
9527
  name: el.name.text,
9045
9528
  path: prefix,
9046
- rest: { kind: "array", from: index }
9529
+ rest: { kind: "array", from: index },
9530
+ segments
9047
9531
  });
9048
9532
  return;
9049
9533
  }
9050
9534
  const path = `${prefix}[${index}]`;
9535
+ const nextSegments = [...segments, { kind: "index", index }];
9051
9536
  if (ts11.isIdentifier(el.name)) {
9052
- bindings.push({ name: el.name.text, path });
9537
+ bindings.push({ name: el.name.text, path, segments: nextSegments });
9053
9538
  } else {
9054
- walk(el.name, path);
9539
+ walk(el.name, path, nextSegments);
9055
9540
  }
9056
9541
  }
9057
9542
  return;
@@ -9068,7 +9553,8 @@ function extractLoopParamBindings(pattern) {
9068
9553
  bindings.push({
9069
9554
  name: el.name.text,
9070
9555
  path: prefix,
9071
- rest: { kind: "object", exclude: collectedKeys }
9556
+ rest: { kind: "object", exclude: collectedKeys },
9557
+ segments
9072
9558
  });
9073
9559
  return;
9074
9560
  }
@@ -9091,17 +9577,19 @@ function extractLoopParamBindings(pattern) {
9091
9577
  unsupported = true;
9092
9578
  return;
9093
9579
  }
9094
- collectedKeys.push({ key: keyText, isIdent: isIdent(keyText) });
9580
+ const keyIsIdent = isIdent(keyText);
9581
+ collectedKeys.push({ key: keyText, isIdent: keyIsIdent });
9095
9582
  const path = appendDotAccess(prefix, keyText);
9583
+ const nextSegments = [...segments, { kind: "field", key: keyText, isIdent: keyIsIdent }];
9096
9584
  if (ts11.isIdentifier(el.name)) {
9097
- bindings.push({ name: el.name.text, path });
9585
+ bindings.push({ name: el.name.text, path, segments: nextSegments });
9098
9586
  } else {
9099
- walk(el.name, path);
9587
+ walk(el.name, path, nextSegments);
9100
9588
  }
9101
9589
  }
9102
9590
  };
9103
9591
  if (ts11.isArrayBindingPattern(pattern) || ts11.isObjectBindingPattern(pattern)) {
9104
- walk(pattern, "");
9592
+ walk(pattern, "", []);
9105
9593
  if (unsupported)
9106
9594
  return { unsupported: true };
9107
9595
  return bindings;
@@ -9843,14 +10331,14 @@ function processAttributes(attributes, ctx) {
9843
10331
  clientOnly = true;
9844
10332
  }
9845
10333
  }
9846
- const freeIdentifiers = attrFreeIdentifiers(attr);
10334
+ const freeIdentifiers2 = attrFreeIdentifiers(attr);
9847
10335
  attrs.push({
9848
10336
  name,
9849
10337
  value,
9850
10338
  clientOnly,
9851
10339
  loc: getSourceLocation(attr, ctx.sourceFile, ctx.filePath),
9852
10340
  ...computeReactivityFlags(attr, ctx),
9853
- ...freeIdentifiers !== undefined && { freeIdentifiers }
10341
+ ...freeIdentifiers2 !== undefined && { freeIdentifiers: freeIdentifiers2 }
9854
10342
  });
9855
10343
  }
9856
10344
  return { attrs, events, ref };
@@ -9870,6 +10358,7 @@ function getAttributeValue(attr, ctx) {
9870
10358
  expr = branchInit;
9871
10359
  }
9872
10360
  }
10361
+ expr = tryDesugarInterleaveTaggedTemplate(expr, ctx);
9873
10362
  if (ts11.isAwaitExpression(expr)) {
9874
10363
  ctx.analyzer.errors.push(createError(ErrorCodes.STAGE_AWAIT_IN_TEMPLATE, getSourceLocation(expr, ctx.sourceFile, ctx.filePath)));
9875
10364
  return AttrValueOf.expression("undefined");
@@ -10005,6 +10494,14 @@ function findLocalConst(name, ctx) {
10005
10494
  const pool = fnScoped.length > 0 ? fnScoped : matches;
10006
10495
  return pool[pool.length - 1];
10007
10496
  }
10497
+ function findLocalFunction(name, ctx) {
10498
+ const matches = ctx.analyzer.localFunctions.filter((f) => f.name === name);
10499
+ if (matches.length === 0)
10500
+ return;
10501
+ const fnScoped = matches.filter((f) => !f.isModule);
10502
+ const pool = fnScoped.length > 0 ? fnScoped : matches;
10503
+ return pool[pool.length - 1];
10504
+ }
10008
10505
  function isDynamicTagLocal(name, ctx) {
10009
10506
  if (!hasDynamicTagBinding(name, ctx.sourceFile))
10010
10507
  return false;
@@ -10099,6 +10596,147 @@ function parseConstInitializerImpl(c) {
10099
10596
  function astText(node) {
10100
10597
  return node.getText(node.getSourceFile());
10101
10598
  }
10599
+ var functionInfoExprCache = new WeakMap;
10600
+ function parseFunctionInfoAsExpr(fn) {
10601
+ const cached = functionInfoExprCache.get(fn);
10602
+ if (cached !== undefined)
10603
+ return cached;
10604
+ const result = parseFunctionInfoAsExprImpl(fn);
10605
+ functionInfoExprCache.set(fn, result);
10606
+ return result;
10607
+ }
10608
+ function parseFunctionInfoAsExprImpl(fn) {
10609
+ if (!fn.body)
10610
+ return null;
10611
+ const params = fn.typedParams !== undefined ? fn.typedParams : fn.params.map(formatParamWithType).join(", ");
10612
+ const body = fn.typedBody ?? fn.body;
10613
+ const wrapped = `const __bf_resolve_fn__ = function(${params}) ${body}`;
10614
+ const sf = ts11.createSourceFile("__bf_resolve_fn.ts", wrapped, ts11.ScriptTarget.Latest, true, ts11.ScriptKind.TS);
10615
+ const stmt = sf.statements[0];
10616
+ if (!stmt || !ts11.isVariableStatement(stmt))
10617
+ return null;
10618
+ const decl = stmt.declarationList.declarations[0];
10619
+ if (!decl?.initializer)
10620
+ return null;
10621
+ return decl.initializer;
10622
+ }
10623
+ function tryDesugarInterleaveTaggedTemplate(expr, ctx) {
10624
+ if (!ts11.isTaggedTemplateExpression(expr))
10625
+ return expr;
10626
+ if (!ts11.isIdentifier(expr.tag))
10627
+ return expr;
10628
+ const resolvedTag = resolveInterleaveTagIdentifier(expr.tag.text, ctx);
10629
+ if (!resolvedTag)
10630
+ return expr;
10631
+ if (!isInterleaveTagFunction(resolvedTag))
10632
+ return expr;
10633
+ const rewritten = buildUntaggedTemplateLiteral(expr, ctx);
10634
+ return rewritten ?? expr;
10635
+ }
10636
+ function resolveInterleaveTagIdentifier(name, ctx) {
10637
+ const constInfo = findLocalConst(name, ctx);
10638
+ const fnInfo = findLocalFunction(name, ctx);
10639
+ if (constInfo && fnInfo)
10640
+ return null;
10641
+ if (constInfo) {
10642
+ const ast = parseConstInitializer(constInfo);
10643
+ return ast && (ts11.isArrowFunction(ast) || ts11.isFunctionExpression(ast)) ? ast : null;
10644
+ }
10645
+ if (fnInfo) {
10646
+ const ast = parseFunctionInfoAsExpr(fnInfo);
10647
+ return ast && (ts11.isArrowFunction(ast) || ts11.isFunctionExpression(ast)) ? ast : null;
10648
+ }
10649
+ return null;
10650
+ }
10651
+ function isInterleaveTagFunction(fn) {
10652
+ if (!ts11.isArrowFunction(fn) && !ts11.isFunctionExpression(fn))
10653
+ return false;
10654
+ if (fn.parameters.length !== 2)
10655
+ return false;
10656
+ const [partsParam, argsParam] = fn.parameters;
10657
+ if (!ts11.isIdentifier(partsParam.name) || partsParam.dotDotDotToken)
10658
+ return false;
10659
+ if (!ts11.isIdentifier(argsParam.name) || !argsParam.dotDotDotToken)
10660
+ return false;
10661
+ const parsed = tsNodeToParsedExpr(fn);
10662
+ if (parsed.kind !== "arrow")
10663
+ return false;
10664
+ return isInterleaveReduceCall(parsed.body, partsParam.name.text, argsParam.name.text);
10665
+ }
10666
+ function isInterleaveReduceCall(body, partsName, argsName) {
10667
+ if (body.kind !== "call" || body.args.length !== 2)
10668
+ return false;
10669
+ const { callee, args } = body;
10670
+ if (callee.kind !== "member" || callee.computed || callee.property !== "reduce")
10671
+ return false;
10672
+ if (callee.object.kind !== "identifier" || callee.object.name !== partsName)
10673
+ return false;
10674
+ const [callback, init] = args;
10675
+ if (init.kind !== "literal" || init.literalType !== "string" || init.value !== "")
10676
+ return false;
10677
+ if (callback.kind !== "arrow" || callback.params.length !== 3)
10678
+ return false;
10679
+ const [acc, p, i] = callback.params;
10680
+ return isInterleaveReduceCallbackBody(callback.body, acc, p, i, argsName);
10681
+ }
10682
+ function isInterleaveReduceCallbackBody(body, acc, p, i, argsName) {
10683
+ if (body.kind !== "binary" || body.op !== "+")
10684
+ return false;
10685
+ const { left, right } = body;
10686
+ if (left.kind !== "binary" || left.op !== "+")
10687
+ return false;
10688
+ if (left.left.kind !== "identifier" || left.left.name !== acc)
10689
+ return false;
10690
+ if (left.right.kind !== "identifier" || left.right.name !== p)
10691
+ return false;
10692
+ return isInterleaveSpanExpr(right, i, argsName);
10693
+ }
10694
+ function isInterleaveSpanExpr(expr, i, argsName) {
10695
+ let inner = expr;
10696
+ if (inner.kind === "call" && inner.args.length === 1 && inner.callee.kind === "identifier" && inner.callee.name === "String") {
10697
+ inner = inner.args[0];
10698
+ }
10699
+ if (inner.kind !== "logical" || inner.op !== "??")
10700
+ return false;
10701
+ if (inner.right.kind !== "literal" || inner.right.literalType !== "string" || inner.right.value !== "") {
10702
+ return false;
10703
+ }
10704
+ const idx = inner.left;
10705
+ if (idx.kind !== "index-access")
10706
+ return false;
10707
+ if (idx.object.kind !== "identifier" || idx.object.name !== argsName)
10708
+ return false;
10709
+ if (idx.index.kind !== "identifier" || idx.index.name !== i)
10710
+ return false;
10711
+ return true;
10712
+ }
10713
+ function buildUntaggedTemplateLiteral(node, ctx) {
10714
+ const template = node.template;
10715
+ let text;
10716
+ if (ts11.isNoSubstitutionTemplateLiteral(template)) {
10717
+ text = "`" + (template.rawText ?? template.text) + "`";
10718
+ } else {
10719
+ let body = template.head.rawText ?? template.head.text;
10720
+ for (const span of template.templateSpans) {
10721
+ const spanText = ctx.getJS(span.expression);
10722
+ body += "${(" + spanText + ") ?? ''}";
10723
+ body += span.literal.rawText ?? span.literal.text;
10724
+ }
10725
+ text = "`" + body + "`";
10726
+ }
10727
+ const wrapped = `const __bf_resolve_tagged__ = (${text})`;
10728
+ const sf = ts11.createSourceFile("__bf_resolve_tagged.tsx", wrapped, ts11.ScriptTarget.Latest, true, ts11.ScriptKind.TSX);
10729
+ const stmt = sf.statements[0];
10730
+ if (!stmt || !ts11.isVariableStatement(stmt))
10731
+ return null;
10732
+ const decl = stmt.declarationList.declarations[0];
10733
+ if (!decl?.initializer)
10734
+ return null;
10735
+ const result = ts11.isParenthesizedExpression(decl.initializer) ? decl.initializer.expression : decl.initializer;
10736
+ if (!ts11.isTemplateExpression(result) && !ts11.isNoSubstitutionTemplateLiteral(result))
10737
+ return null;
10738
+ return result;
10739
+ }
10102
10740
  function parseTernary(expr, ctx) {
10103
10741
  const whenTrueValue = getStringValue(expr.whenTrue);
10104
10742
  const whenFalseValue = getStringValue(expr.whenFalse);
@@ -10173,14 +10811,14 @@ function processComponentProps(attributes, ctx) {
10173
10811
  clientOnly = true;
10174
10812
  }
10175
10813
  }
10176
- const freeIdentifiers = attrFreeIdentifiers(attr);
10814
+ const freeIdentifiers2 = attrFreeIdentifiers(attr);
10177
10815
  props.push({
10178
10816
  name,
10179
10817
  value,
10180
10818
  clientOnly,
10181
10819
  loc: getSourceLocation(attr, ctx.sourceFile, ctx.filePath),
10182
10820
  ...computeReactivityFlags(attr, ctx),
10183
- ...freeIdentifiers !== undefined && { freeIdentifiers }
10821
+ ...freeIdentifiers2 !== undefined && { freeIdentifiers: freeIdentifiers2 }
10184
10822
  });
10185
10823
  }
10186
10824
  return props;
@@ -10596,7 +11234,7 @@ function decideWrapForChildProp(expandedValue, ctx, prop) {
10596
11234
  return { wrap: true, reason: "props-access" };
10597
11235
  return decideWrapForAttr(expandedValue, ctx, prop);
10598
11236
  }
10599
- function needsEffectWrapper(expr, ctx, freeIdentifiers) {
11237
+ function needsEffectWrapper(expr, ctx, freeIdentifiers2) {
10600
11238
  for (const signal of ctx.signals) {
10601
11239
  if (new RegExp(`\\b${signal.getter}\\s*\\(`).test(expr)) {
10602
11240
  return true;
@@ -10610,7 +11248,7 @@ function needsEffectWrapper(expr, ctx, freeIdentifiers) {
10610
11248
  for (const prop of ctx.propsParams) {
10611
11249
  if (prop.name === "children")
10612
11250
  continue;
10613
- if (freeIdentifiers ? freeIdentifiers.has(prop.name) : tokenContainsIdent(expr, prop.name)) {
11251
+ if (freeIdentifiers2 ? freeIdentifiers2.has(prop.name) : tokenContainsIdent(expr, prop.name)) {
10614
11252
  return true;
10615
11253
  }
10616
11254
  }
@@ -10621,8 +11259,8 @@ function needsEffectWrapper(expr, ctx, freeIdentifiers) {
10621
11259
  }
10622
11260
  return false;
10623
11261
  }
10624
- function classifyReactivity(expr, ctx, loopParam, loopParamBindings, freeIdentifiers) {
10625
- const has = (name) => freeIdentifiers ? freeIdentifiers.has(name) : tokenContainsIdent(expr, name);
11262
+ function classifyReactivity(expr, ctx, loopParam, loopParamBindings, freeIdentifiers2) {
11263
+ const has = (name) => freeIdentifiers2 ? freeIdentifiers2.has(name) : tokenContainsIdent(expr, name);
10626
11264
  if (loopParamBindings && loopParamBindings.length > 0) {
10627
11265
  for (const b of loopParamBindings) {
10628
11266
  if (has(b.name)) {
@@ -10632,7 +11270,7 @@ function classifyReactivity(expr, ctx, loopParam, loopParamBindings, freeIdentif
10632
11270
  } else if (loopParam && has(loopParam)) {
10633
11271
  return { kind: "loop-param", param: loopParam };
10634
11272
  }
10635
- if (needsEffectWrapper(expr, ctx, freeIdentifiers)) {
11273
+ if (needsEffectWrapper(expr, ctx, freeIdentifiers2)) {
10636
11274
  return { kind: "signal-or-memo-or-prop" };
10637
11275
  }
10638
11276
  return { kind: "none" };
@@ -11214,6 +11852,7 @@ function collectElements(node, ctx, siblingOffsets, insideConditional = false) {
11214
11852
  const { useElementReconciliation, innerLoops } = decideLoopRendering(l, siblingOffsets, ctx);
11215
11853
  let template = "";
11216
11854
  let staticItemTemplate;
11855
+ let skeletonTemplate;
11217
11856
  if (l.childComponent) {
11218
11857
  template = "";
11219
11858
  if (l.isStaticArray && l.children[0]) {
@@ -11224,6 +11863,11 @@ function collectElements(node, ctx, siblingOffsets, insideConditional = false) {
11224
11863
  template = useElementReconciliation ? irToPlaceholderTemplate(l.children[0], buildRestSpreadNames(ctx), 0, loopParamSpec) : irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx), 0, loopParamSpec);
11225
11864
  if (l.isStaticArray) {
11226
11865
  staticItemTemplate = useElementReconciliation ? irToPlaceholderTemplate(l.children[0], buildRestSpreadNames(ctx), 0) : irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx), 0);
11866
+ } else if (!useElementReconciliation && !l.bodyIsMultiRoot && !l.bodyIsItemConditional) {
11867
+ skeletonTemplate = buildLoopSkeletonTemplate(l.children[0], {
11868
+ reactiveAttrKeys: new Set(bindings.reactiveAttrs.map((a) => `${a.childSlotId}::${a.attrName}`)),
11869
+ reactiveTextSlotIds: new Set(bindings.reactiveTexts.map((t) => t.slotId))
11870
+ }) ?? undefined;
11227
11871
  }
11228
11872
  }
11229
11873
  ctx.loopElements.push({
@@ -11241,6 +11885,7 @@ function collectElements(node, ctx, siblingOffsets, insideConditional = false) {
11241
11885
  iterationShape: l.iterationShape,
11242
11886
  template,
11243
11887
  staticItemTemplate,
11888
+ skeletonTemplate,
11244
11889
  childEventHandlers: childHandlers,
11245
11890
  bindings,
11246
11891
  childComponent: l.childComponent,
@@ -12129,31 +12774,70 @@ function collectComponentNames(node) {
12129
12774
 
12130
12775
  // src/relocate.ts
12131
12776
  import ts13 from "typescript";
12132
- function classify(name, env) {
12133
- return env.bindings.get(name) ?? "global";
12777
+
12778
+ // src/lowering-registry.ts
12779
+ var plugins = [];
12780
+ function registerLoweringPlugin(plugin) {
12781
+ const existing = plugins.findIndex((p) => p.name === plugin.name);
12782
+ if (existing >= 0)
12783
+ plugins[existing] = plugin;
12784
+ else
12785
+ plugins.push(plugin);
12134
12786
  }
12135
- function collectFreeRefs(node) {
12136
- const refs = new Map;
12137
- function visit3(n, parent) {
12138
- if (ts13.isIdentifier(n)) {
12139
- if (parent && ts13.isPropertyAccessExpression(parent) && parent.name === n)
12140
- return;
12141
- if (parent && ts13.isPropertyAssignment(parent) && parent.name === n)
12142
- return;
12143
- if (parent && ts13.isShorthandPropertyAssignment(parent) && parent.name === n)
12144
- return;
12145
- const list = refs.get(n.text) ?? [];
12146
- list.push(n);
12147
- refs.set(n.text, list);
12148
- return;
12149
- }
12150
- ts13.forEachChild(n, (child) => visit3(child, n));
12151
- }
12152
- visit3(node);
12153
- return refs;
12787
+ function getLoweringPlugins() {
12788
+ return [...plugins];
12154
12789
  }
12155
- function decideAction(kind, toScope, env, name) {
12156
- if (isVisibleIn(toScope, kind)) {
12790
+ function prepareLoweringMatchers(metadata) {
12791
+ const matchers = [];
12792
+ for (const plugin of plugins) {
12793
+ const matcher = plugin.prepare(metadata);
12794
+ if (matcher)
12795
+ matchers.push(matcher);
12796
+ }
12797
+ return matchers;
12798
+ }
12799
+ function matchLoweringCall(callee, args, metadata) {
12800
+ for (const matcher of prepareLoweringMatchers(metadata)) {
12801
+ const node = matcher(callee, args);
12802
+ if (node)
12803
+ return node;
12804
+ }
12805
+ return null;
12806
+ }
12807
+ function isValidHelperId(helper) {
12808
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(helper);
12809
+ }
12810
+ function __resetLoweringPluginsForTest(next = []) {
12811
+ plugins.length = 0;
12812
+ plugins.push(...next);
12813
+ }
12814
+
12815
+ // src/relocate.ts
12816
+ function classify(name, env) {
12817
+ return env.bindings.get(name) ?? "global";
12818
+ }
12819
+ function collectFreeRefs(node) {
12820
+ const refs = new Map;
12821
+ function visit3(n, parent) {
12822
+ if (ts13.isIdentifier(n)) {
12823
+ if (parent && ts13.isPropertyAccessExpression(parent) && parent.name === n)
12824
+ return;
12825
+ if (parent && ts13.isPropertyAssignment(parent) && parent.name === n)
12826
+ return;
12827
+ if (parent && ts13.isShorthandPropertyAssignment(parent) && parent.name === n)
12828
+ return;
12829
+ const list = refs.get(n.text) ?? [];
12830
+ list.push(n);
12831
+ refs.set(n.text, list);
12832
+ return;
12833
+ }
12834
+ ts13.forEachChild(n, (child) => visit3(child, n));
12835
+ }
12836
+ visit3(node);
12837
+ return refs;
12838
+ }
12839
+ function decideAction(kind, toScope, env, name) {
12840
+ if (isVisibleIn(toScope, kind)) {
12157
12841
  return { action: "pass-through", rewrittenAs: name };
12158
12842
  }
12159
12843
  if (kind === "prop" && toScope === "template") {
@@ -12163,6 +12847,14 @@ function decideAction(kind, toScope, env, name) {
12163
12847
  return { action: "lift-to-prop", rewrittenAs: `${PROPS_PARAM}.${name}` };
12164
12848
  }
12165
12849
  if ((kind === "init-local" || kind === "sub-init-local") && toScope === "template") {
12850
+ const aliasTarget = env.aliasTargets?.get(name);
12851
+ if (aliasTarget !== undefined) {
12852
+ const targetLeftmost = aliasTarget.includes(".") ? aliasTarget.split(".")[0] : aliasTarget;
12853
+ const targetKind = classify(targetLeftmost, env);
12854
+ if (isVisibleIn(toScope, targetKind)) {
12855
+ return { action: "inline", rewrittenAs: aliasTarget };
12856
+ }
12857
+ }
12166
12858
  const inlineForm = env.inlinable.get(name);
12167
12859
  if (inlineForm !== undefined) {
12168
12860
  return { action: "inline", rewrittenAs: inlineForm };
@@ -12242,6 +12934,8 @@ function isInlinableInTemplate(value, env) {
12242
12934
  return { ok: true, rewrittenValue: r.text, decisions: r.decisions };
12243
12935
  }
12244
12936
  function getCalleeIdentifierPath(callee) {
12937
+ if (ts13.isParenthesizedExpression(callee))
12938
+ return getCalleeIdentifierPath(callee.expression);
12245
12939
  if (ts13.isIdentifier(callee))
12246
12940
  return callee.text;
12247
12941
  if (ts13.isPropertyAccessExpression(callee)) {
@@ -12253,6 +12947,8 @@ function getCalleeIdentifierPath(callee) {
12253
12947
  return null;
12254
12948
  }
12255
12949
  function getCalleeLeftmostIdentifier(callee) {
12950
+ if (ts13.isParenthesizedExpression(callee))
12951
+ return getCalleeLeftmostIdentifier(callee.expression);
12256
12952
  if (ts13.isIdentifier(callee))
12257
12953
  return callee.text;
12258
12954
  if (ts13.isPropertyAccessExpression(callee)) {
@@ -12266,20 +12962,39 @@ var REGISTRY_SAFE_BINDING_KINDS = new Set([
12266
12962
  "module-local"
12267
12963
  ]);
12268
12964
  function isCallAcceptedByAdapter(call, env) {
12269
- const name = getCalleeIdentifierPath(call.expression);
12270
- if (name === null)
12965
+ const originalPath = getCalleeIdentifierPath(call.expression);
12966
+ if (originalPath === null)
12271
12967
  return false;
12272
12968
  const leftmost = getCalleeLeftmostIdentifier(call.expression);
12969
+ let resolvedPath = originalPath;
12970
+ let resolvedLeftmost = leftmost;
12273
12971
  if (leftmost !== null) {
12274
- const kind = env.bindings.get(leftmost);
12972
+ const aliasTarget = env.aliasTargets?.get(leftmost);
12973
+ if (aliasTarget !== undefined) {
12974
+ resolvedPath = originalPath === leftmost ? aliasTarget : `${aliasTarget}${originalPath.slice(leftmost.length)}`;
12975
+ resolvedLeftmost = aliasTarget.includes(".") ? aliasTarget.split(".")[0] : aliasTarget;
12976
+ }
12977
+ }
12978
+ if (resolvedLeftmost !== null) {
12979
+ const kind = env.bindings.get(resolvedLeftmost);
12275
12980
  if (kind !== undefined && !REGISTRY_SAFE_BINDING_KINDS.has(kind)) {
12276
12981
  return false;
12277
12982
  }
12278
12983
  }
12279
- if (env.templatePrimitives && env.templatePrimitives[name])
12984
+ if (env.templatePrimitives && env.templatePrimitives[resolvedPath])
12280
12985
  return true;
12281
- if (env.acceptsTemplateCall && env.acceptsTemplateCall(name))
12986
+ if (env.acceptsTemplateCall && env.acceptsTemplateCall(resolvedPath))
12282
12987
  return true;
12988
+ if (env.loweringMatchers && env.loweringMatchers.length > 0) {
12989
+ const parsed = tsNodeToParsedExpr(call);
12990
+ if (parsed.kind === "call") {
12991
+ const calleeForMatch = resolvedPath !== originalPath && !resolvedPath.includes(".") && parsed.callee.kind === "identifier" ? { kind: "identifier", name: resolvedPath } : parsed.callee;
12992
+ for (const matcher of env.loweringMatchers) {
12993
+ if (matcher(calleeForMatch, parsed.args))
12994
+ return true;
12995
+ }
12996
+ }
12997
+ }
12283
12998
  return false;
12284
12999
  }
12285
13000
  function parseExpressionNode(text) {
@@ -12425,6 +13140,7 @@ function buildRelocateEnvFromIR(metadata, options) {
12425
13140
  env.templatePrimitives = options.templatePrimitives;
12426
13141
  if (options?.acceptsTemplateCall)
12427
13142
  env.acceptsTemplateCall = options.acceptsTemplateCall;
13143
+ env.loweringMatchers = prepareLoweringMatchers(metadata);
12428
13144
  return env;
12429
13145
  }
12430
13146
  function buildRelocateEnvFromFields(src) {
@@ -12473,14 +13189,35 @@ function buildRelocateEnvFromFields(src) {
12473
13189
  if (kind === "prop")
12474
13190
  propsForLift.add(name);
12475
13191
  }
13192
+ const aliasTargets = new Map;
13193
+ for (const c of src.localConstants) {
13194
+ const kind = bindings.get(c.name);
13195
+ if (kind !== "init-local" && kind !== "module-local")
13196
+ continue;
13197
+ const target = identifierPathFromParsed(c.parsed);
13198
+ if (target !== null)
13199
+ aliasTargets.set(c.name, target);
13200
+ }
12476
13201
  return {
12477
13202
  bindings,
12478
13203
  inlinable: new Map,
12479
13204
  propsForLift,
12480
13205
  propsObjectName,
12481
- allowFallback: true
13206
+ allowFallback: true,
13207
+ aliasTargets
12482
13208
  };
12483
13209
  }
13210
+ function identifierPathFromParsed(expr) {
13211
+ if (!expr)
13212
+ return null;
13213
+ if (expr.kind === "identifier")
13214
+ return expr.name;
13215
+ if (expr.kind === "member" && !expr.computed) {
13216
+ const object = identifierPathFromParsed(expr.object);
13217
+ return object === null ? null : `${object}.${expr.property}`;
13218
+ }
13219
+ return null;
13220
+ }
12484
13221
 
12485
13222
  // src/ir-to-client-js/compute-inlinability.ts
12486
13223
  function buildEnvFromCtx(ctx) {
@@ -12500,7 +13237,7 @@ function buildEnvFromCtx(ctx) {
12500
13237
  effects: ctx.effects,
12501
13238
  onMounts: ctx.onMounts,
12502
13239
  initStatements: ctx.initStatements,
12503
- imports: [],
13240
+ imports: ctx.imports,
12504
13241
  templateImports: [],
12505
13242
  namedExports: [],
12506
13243
  localFunctions: ctx.localFunctions,
@@ -12634,9 +13371,9 @@ function populateCsrInlinable(ctx, relocateEnv) {
12634
13371
  continue;
12635
13372
  const env = buildEnvWithConsts();
12636
13373
  const source = c.value.trim();
12637
- const { rewritten, freeIdentifiers } = csrSubstitute(source, env);
13374
+ const { rewritten, freeIdentifiers: freeIdentifiers2 } = csrSubstitute(source, env);
12638
13375
  let pendingDependency = false;
12639
- for (const id of freeIdentifiers) {
13376
+ for (const id of freeIdentifiers2) {
12640
13377
  if (id === c.name)
12641
13378
  continue;
12642
13379
  const dep = ctx.localConstants.find((o) => o.name === id);
@@ -12652,7 +13389,7 @@ function populateCsrInlinable(ctx, relocateEnv) {
12652
13389
  ctx.csrInlinable.set(c.name, null);
12653
13390
  } else {
12654
13391
  const bridgedRewritten = inlineResult.rewrittenValue;
12655
- const bridgedFreeIdentifiers = recomputeFreeIdentifiers(bridgedRewritten, freeIdentifiers);
13392
+ const bridgedFreeIdentifiers = recomputeFreeIdentifiers(bridgedRewritten, freeIdentifiers2);
12656
13393
  ctx.csrInlinable.set(c.name, { rewrittenValue: bridgedRewritten, freeIdentifiers: bridgedFreeIdentifiers });
12657
13394
  constSubs.set(c.name, {
12658
13395
  kind: "identifier",
@@ -13199,14 +13936,26 @@ function sortDeclarations(declarations, declNameSet, graph) {
13199
13936
  var ENV_SIGNAL_CLIENT_FACTORY = {
13200
13937
  search: "createSearchParams"
13201
13938
  };
13202
- function searchParamsLocalNames(metadata) {
13939
+ var ENV_SIGNAL_READERS = new Map([
13940
+ ["search", { key: "search", canonicalName: "searchParams", methods: new Set(["get"]) }]
13941
+ ]);
13942
+ function envSignalReaderFor(key) {
13943
+ if (key === undefined)
13944
+ return null;
13945
+ return ENV_SIGNAL_READERS.get(key) ?? null;
13946
+ }
13947
+ function envSignalLocalNames(metadata, key) {
13203
13948
  const names = new Set;
13204
13949
  for (const s of metadata.signals) {
13205
- if (s.envReader === "search")
13950
+ if (s.envReader !== undefined && (key === undefined || s.envReader === key)) {
13206
13951
  names.add(s.getter);
13952
+ }
13207
13953
  }
13208
13954
  return names;
13209
13955
  }
13956
+ function searchParamsLocalNames(metadata) {
13957
+ return envSignalLocalNames(metadata, "search");
13958
+ }
13210
13959
  function importsSearchParams(metadata) {
13211
13960
  return searchParamsLocalNames(metadata).size > 0;
13212
13961
  }
@@ -15249,6 +15998,15 @@ function emitTemplateCloneInline(template) {
15249
15998
  }
15250
15999
  return `const __tpl = document.createElement('template'); __tpl.innerHTML = \`${template}\`; return __tpl.content.firstElementChild.cloneNode(true)`;
15251
16000
  }
16001
+ function emitHoistedTemplateDecl(lines, indent, tplVar, skeletonTemplate) {
16002
+ const isSvg = templateRootIsSvg(skeletonTemplate);
16003
+ const html = isSvg ? `<svg>${skeletonTemplate}</svg>` : skeletonTemplate;
16004
+ lines.push(`${indent}const ${tplVar} = document.createElement('template')`);
16005
+ lines.push(`${indent}${tplVar}.innerHTML = \`${html}\``);
16006
+ }
16007
+ function hoistedCloneExpr(tplVar, skeletonTemplate) {
16008
+ return templateRootIsSvg(skeletonTemplate) ? `${tplVar}.content.firstElementChild.firstElementChild.cloneNode(true)` : `${tplVar}.content.firstElementChild.cloneNode(true)`;
16009
+ }
15252
16010
  function emitTemplateCloneLines(template, indent) {
15253
16011
  if (templateRootIsSvg(template)) {
15254
16012
  return [
@@ -15547,6 +16305,7 @@ function stringifyPlainLoop(lines, plan, topIndent = " ") {
15547
16305
  indexParam,
15548
16306
  mapPreambleWrapped,
15549
16307
  template,
16308
+ skeletonTemplate,
15550
16309
  reactiveEffects,
15551
16310
  childRefs,
15552
16311
  bodyIsMultiRoot,
@@ -15557,11 +16316,16 @@ function stringifyPlainLoop(lines, plan, topIndent = " ") {
15557
16316
  stringifyAnchoredLoop(lines, plan, topIndent, anchorKeyExpr);
15558
16317
  return;
15559
16318
  }
16319
+ const hoistedTpl = !bodyIsMultiRoot && skeletonTemplate ? skeletonTemplate : null;
16320
+ const tplVar = `__tpl_${markerId.replace(/[^A-Za-z0-9_$]/g, "_")}`;
16321
+ if (hoistedTpl) {
16322
+ emitHoistedTemplateDecl(lines, topIndent, tplVar, hoistedTpl);
16323
+ }
15560
16324
  const loopBfId = plan.profileLoopId ? `, ${JSON.stringify(plan.profileLoopId)}` : "";
15561
16325
  if (reactiveEffects === null && !bodyIsMultiRoot && childRefs.length === 0) {
15562
16326
  const unwrapInline = paramUnwrap ? `${paramUnwrap} ` : "";
15563
16327
  const preamble = mapPreambleWrapped ? `${mapPreambleWrapped}; ` : "";
15564
- const cloneExpr = emitTemplateCloneInline(template);
16328
+ const cloneExpr = hoistedTpl ? `return ${hoistedCloneExpr(tplVar, hoistedTpl)}` : emitTemplateCloneInline(template);
15565
16329
  lines.push(`${topIndent}mapArray(() => ${arrayExpr}, ${containerVar}, ${keyFn}, (${paramHead}, ${indexParam}, __existing) => { ${unwrapInline}${preamble}if (__existing) return __existing; ${cloneExpr} }, '${markerId}'${loopBfId})`);
15566
16330
  return;
15567
16331
  }
@@ -15571,12 +16335,16 @@ function stringifyPlainLoop(lines, plan, topIndent = " ") {
15571
16335
  lines.push(`${bodyIndent}${paramUnwrap}`);
15572
16336
  if (mapPreambleWrapped)
15573
16337
  lines.push(`${bodyIndent}${mapPreambleWrapped}`);
15574
- emitLoopItemElementSetup(lines, {
15575
- template,
15576
- bodyIsMultiRoot,
15577
- indent: bodyIndent,
15578
- singleRootLayout: "inline"
15579
- });
16338
+ if (hoistedTpl) {
16339
+ lines.push(`${bodyIndent}const __el = __existing ?? ${hoistedCloneExpr(tplVar, hoistedTpl)}`);
16340
+ } else {
16341
+ emitLoopItemElementSetup(lines, {
16342
+ template,
16343
+ bodyIsMultiRoot,
16344
+ indent: bodyIndent,
16345
+ singleRootLayout: "inline"
16346
+ });
16347
+ }
15580
16348
  if (reactiveEffects !== null) {
15581
16349
  stringifyReactiveEffects(lines, reactiveEffects, { indent: bodyIndent, elVar: "__el", bodyIsMultiRoot });
15582
16350
  }
@@ -16262,6 +17030,7 @@ function buildPlainLoopPlan(elem, profileComponentName) {
16262
17030
  indexParam: elem.index || "__idx",
16263
17031
  mapPreambleWrapped: elem.mapPreamble ? wrap(elem.mapPreamble) : "",
16264
17032
  template: elem.template,
17033
+ skeletonTemplate: elem.skeletonTemplate,
16265
17034
  reactiveEffects: hasReactive2 ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
16266
17035
  childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
16267
17036
  bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
@@ -16868,6 +17637,7 @@ function createContext(ir, scope, adapterCapabilities, profile) {
16868
17637
  initStatements: ir.metadata.initStatements ?? [],
16869
17638
  localFunctions: ir.metadata.localFunctions,
16870
17639
  localConstants: ir.metadata.localConstants,
17640
+ imports: ir.metadata.imports,
16871
17641
  propsParams: ir.metadata.propsParams,
16872
17642
  propsObjectName: ir.metadata.propsObjectName,
16873
17643
  restPropsName: ir.metadata.restPropsName,
@@ -16951,119 +17721,6 @@ ${externalImports.join(`
16951
17721
  return generatedCode.replace(IMPORT_PLACEHOLDER, allImports);
16952
17722
  }
16953
17723
 
16954
- // src/module-exports.ts
16955
- function generateModuleExports(ir, extraInlineExported = new Set, rewriteRelativeImport) {
16956
- const lines = [];
16957
- for (const constant of ir.metadata.localConstants) {
16958
- if (!constant.isExported)
16959
- continue;
16960
- const keyword = constant.declarationKind ?? "const";
16961
- if (!constant.value) {
16962
- lines.push(`export ${keyword} ${constant.name}`);
16963
- continue;
16964
- }
16965
- const value = constant.value.trim();
16966
- if (/^createContext\b/.test(value) || /^new WeakMap\b/.test(value))
16967
- continue;
16968
- lines.push(`export ${keyword} ${constant.name} = ${constant.value}`);
16969
- }
16970
- for (const func of ir.metadata.localFunctions) {
16971
- if (!func.isExported)
16972
- continue;
16973
- const params = func.typedParams !== undefined ? func.typedParams : func.params.map(formatParamWithType).join(", ");
16974
- const returnAnnotation = func.typedReturnType ? `: ${func.typedReturnType}` : "";
16975
- const body = func.typedBody ?? func.body;
16976
- const asyncKw = func.isAsync ? "async " : "";
16977
- lines.push(`export ${asyncKw}function ${func.name}(${params})${returnAnnotation} ${body}`);
16978
- }
16979
- const inlineExported = collectInlineExportedNames(ir);
16980
- for (const name of extraInlineExported)
16981
- inlineExported.add(name);
16982
- for (const block of ir.metadata.namedExports) {
16983
- const isReexportFrom = block.source !== null;
16984
- const survivingSpecs = block.specifiers.filter((spec) => {
16985
- if (isReexportFrom)
16986
- return true;
16987
- return !(inlineExported.has(spec.name) && spec.alias == null);
16988
- });
16989
- if (survivingSpecs.length === 0)
16990
- continue;
16991
- const specText = survivingSpecs.map((s) => {
16992
- const prefix = s.isTypeOnly ? "type " : "";
16993
- return s.alias ? `${prefix}${s.name} as ${s.alias}` : `${prefix}${s.name}`;
16994
- }).join(", ");
16995
- const typeKw = block.isTypeOnly ? "type " : "";
16996
- if (isReexportFrom) {
16997
- const source = rewriteRelativeImport && block.source.startsWith(".") ? rewriteRelativeImport(block.source) : block.source;
16998
- lines.push(`export ${typeKw}{ ${specText} } from '${source}'`);
16999
- } else {
17000
- lines.push(`export ${typeKw}{ ${specText} }`);
17001
- }
17002
- }
17003
- return lines.length > 0 ? lines.join(`
17004
- `) : null;
17005
- }
17006
- function collectInlineExportedNames(ir) {
17007
- const names = new Set;
17008
- for (const c of ir.metadata.localConstants) {
17009
- if (c.isExported)
17010
- names.add(c.name);
17011
- }
17012
- for (const f of ir.metadata.localFunctions) {
17013
- if (f.isExported)
17014
- names.add(f.name);
17015
- }
17016
- if (ir.metadata.isExported && ir.metadata.componentName) {
17017
- names.add(ir.metadata.componentName);
17018
- }
17019
- return names;
17020
- }
17021
- function formatParamWithType(p) {
17022
- const rest = p.isRest ? "..." : "";
17023
- const optional = p.optional ? "?" : "";
17024
- const typeAnnotation = p.type?.raw && p.type.raw !== "unknown" ? `: ${p.type.raw}` : "";
17025
- const defaultPart = p.defaultValue !== undefined ? ` = ${p.defaultValue}` : "";
17026
- return `${rest}${p.name}${optional}${typeAnnotation}${defaultPart}`;
17027
- }
17028
- function findReachableNames(primaryRefs, declarations) {
17029
- const allNames = new Set(declarations.map((d) => d.name));
17030
- const bodyMap = new Map(declarations.map((d) => [d.name, d.body]));
17031
- const reachable = new Set;
17032
- const queue = [];
17033
- for (const name of allNames) {
17034
- if (new RegExp(`\\b${name}\\b`).test(primaryRefs)) {
17035
- reachable.add(name);
17036
- queue.push(name);
17037
- }
17038
- }
17039
- while (queue.length > 0) {
17040
- const current = queue.shift();
17041
- const body = bodyMap.get(current) || "";
17042
- for (const name of allNames) {
17043
- if (!reachable.has(name) && new RegExp(`\\b${name}\\b`).test(body)) {
17044
- reachable.add(name);
17045
- queue.push(name);
17046
- }
17047
- }
17048
- }
17049
- return reachable;
17050
- }
17051
- function extractFunctionParams(value) {
17052
- const arrowMatch = value.match(/^(?:async\s*)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/);
17053
- if (arrowMatch) {
17054
- return arrowMatch[1].split(",").map((p) => p.trim().split(":")[0].split("=")[0].trim()).filter(Boolean).join(", ");
17055
- }
17056
- const singleMatch = value.match(/^(?:async\s*)?(\w+)\s*=>/);
17057
- if (singleMatch) {
17058
- return singleMatch[1];
17059
- }
17060
- const funcMatch = value.match(/^(?:async\s*)?function\s*\w*\s*\(([^)]*)\)/);
17061
- if (funcMatch) {
17062
- return funcMatch[1].split(",").map((p) => p.trim().split(":")[0].split("=")[0].trim()).filter(Boolean).join(", ");
17063
- }
17064
- return "";
17065
- }
17066
-
17067
17724
  // src/css-layer-prefixer.ts
17068
17725
  function prefixClass(cls, layerName) {
17069
17726
  if (!cls || cls.startsWith("layer-"))
@@ -17571,16 +18228,14 @@ function extractSsrDefaults(metadata) {
17571
18228
  propsLike.add(metadata.propsObjectName);
17572
18229
  for (const p of metadata.propsParams)
17573
18230
  propsLike.add(p.name);
17574
- if (metadata.propsObjectName === null) {
17575
- for (const p of metadata.propsParams) {
17576
- if (p.isRest)
17577
- continue;
17578
- if (p.defaultValue !== undefined) {
17579
- const value = tryStaticEval(p.defaultValue, { bindings: {}, propsLike });
17580
- out[p.name] = { propName: p.name, value: resultToJsonable(value) };
17581
- } else {
17582
- out[p.name] = { propName: p.name, value: null };
17583
- }
18231
+ for (const p of metadata.propsParams) {
18232
+ if (p.isRest)
18233
+ continue;
18234
+ if (metadata.propsObjectName === null && p.defaultValue !== undefined) {
18235
+ const value = tryStaticEval(p.defaultValue, { bindings: {}, propsLike });
18236
+ out[p.name] = { propName: p.name, value: resultToJsonable(value) };
18237
+ } else {
18238
+ out[p.name] = { propName: p.name, value: null };
17584
18239
  }
17585
18240
  }
17586
18241
  if (metadata.restPropsName) {
@@ -17897,47 +18552,501 @@ function evalNode(node, ctx) {
17897
18552
  return UNRESOLVED;
17898
18553
  }
17899
18554
 
17900
- // src/compiler.ts
17901
- function mergeTemplateImports(lines) {
17902
- const result = [];
17903
- const valueIdx = new Map;
17904
- const valueNames = new Map;
17905
- const typeIdx = new Map;
17906
- const typeNames = new Map;
17907
- const seenOther = new Set;
17908
- const fold = (src, rawNames, idx, names, render) => {
17909
- if (!idx.has(src)) {
17910
- idx.set(src, result.length);
17911
- names.set(src, new Set);
17912
- result.push("");
17913
- }
17914
- const set = names.get(src);
17915
- for (const n of rawNames.split(",").map((s) => s.trim()).filter(Boolean))
17916
- set.add(n);
17917
- result[idx.get(src)] = render(src, set);
17918
- };
17919
- for (const raw of lines) {
17920
- const line = raw.trim();
17921
- if (!line)
18555
+ // src/augment-inherited-props.ts
18556
+ import ts17 from "typescript";
18557
+ function collectContextConsumers(metadata) {
18558
+ const constants = metadata.localConstants ?? [];
18559
+ const contextDefaults = new Map;
18560
+ const contextDefaultKinds = new Map;
18561
+ for (const c of constants) {
18562
+ if (c.systemConstructKind !== "createContext" || c.value === undefined)
17922
18563
  continue;
17923
- const typeMatch = line.match(/^import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?$/);
17924
- const valueMatch = line.match(/^import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?$/);
17925
- if (valueMatch) {
17926
- fold(valueMatch[2], valueMatch[1], valueIdx, valueNames, (s, n) => `import { ${[...n].join(", ")} } from '${s}'`);
17927
- } else if (typeMatch) {
17928
- fold(typeMatch[2], typeMatch[1], typeIdx, typeNames, (s, n) => `import type { ${[...n].join(", ")} } from '${s}'`);
17929
- } else if (!seenOther.has(line)) {
17930
- seenOther.add(line);
17931
- result.push(line);
17932
- }
18564
+ contextDefaults.set(c.name, parseCreateContextDefault(c.value));
18565
+ if (isObjectLiteralCreateContextDefault(c.value))
18566
+ contextDefaultKinds.set(c.name, "object");
17933
18567
  }
17934
- return result.filter(Boolean).join(`
17935
- `);
17936
- }
17937
- function compileMultipleComponents(source, filePath, componentNames, options) {
17938
- const files = [];
17939
- const errors = [];
17940
- const adapter = options.adapter;
18568
+ if (contextDefaults.size === 0)
18569
+ return [];
18570
+ const consumers = [];
18571
+ for (const c of constants) {
18572
+ if (c.value === undefined)
18573
+ continue;
18574
+ const ctxName = parseUseContextArg(c.value);
18575
+ if (ctxName === null || !contextDefaults.has(ctxName))
18576
+ continue;
18577
+ consumers.push({
18578
+ localName: c.name,
18579
+ contextName: ctxName,
18580
+ defaultValue: contextDefaults.get(ctxName) ?? null,
18581
+ defaultKind: contextDefaultKinds.get(ctxName)
18582
+ });
18583
+ }
18584
+ return consumers;
18585
+ }
18586
+ function parseUseContextArg(source) {
18587
+ const expr = parseSingleExpression(source);
18588
+ if (!expr || !ts17.isCallExpression(expr))
18589
+ return null;
18590
+ if (!ts17.isIdentifier(expr.expression) || expr.expression.text !== "useContext")
18591
+ return null;
18592
+ if (expr.arguments.length !== 1)
18593
+ return null;
18594
+ const arg = expr.arguments[0];
18595
+ return ts17.isIdentifier(arg) ? arg.text : null;
18596
+ }
18597
+ function parseCreateContextDefault(source) {
18598
+ const expr = parseSingleExpression(source);
18599
+ if (!expr || !ts17.isCallExpression(expr))
18600
+ return null;
18601
+ if (expr.arguments.length === 0)
18602
+ return null;
18603
+ const arg = expr.arguments[0];
18604
+ if (ts17.isStringLiteral(arg) || ts17.isNoSubstitutionTemplateLiteral(arg))
18605
+ return arg.text;
18606
+ if (ts17.isNumericLiteral(arg))
18607
+ return Number(arg.text);
18608
+ if (arg.kind === ts17.SyntaxKind.TrueKeyword)
18609
+ return true;
18610
+ if (arg.kind === ts17.SyntaxKind.FalseKeyword)
18611
+ return false;
18612
+ return null;
18613
+ }
18614
+ function isObjectLiteralCreateContextDefault(source) {
18615
+ const expr = parseSingleExpression(source);
18616
+ if (!expr || !ts17.isCallExpression(expr))
18617
+ return false;
18618
+ if (expr.arguments.length === 0)
18619
+ return false;
18620
+ return ts17.isObjectLiteralExpression(expr.arguments[0]);
18621
+ }
18622
+ function parseSingleExpression(source) {
18623
+ const sf = ts17.createSourceFile("__ctx.ts", `(${source})`, ts17.ScriptTarget.Latest, false);
18624
+ const stmt = sf.statements[0];
18625
+ if (!stmt || !ts17.isExpressionStatement(stmt))
18626
+ return null;
18627
+ let e = stmt.expression;
18628
+ while (ts17.isParenthesizedExpression(e))
18629
+ e = e.expression;
18630
+ return e;
18631
+ }
18632
+ function augmentInheritedPropAccesses(ir) {
18633
+ const propsObj = ir.metadata.propsObjectName;
18634
+ if (!propsObj)
18635
+ return;
18636
+ const existing = new Set(ir.metadata.propsParams.map((p) => p.name));
18637
+ const bareRefProps = new Set;
18638
+ const booleanAttrProps = new Set;
18639
+ const accessed = new Set;
18640
+ const accessRe = new RegExp(`(?:^|[^\\w$.])${propsObj}\\.([A-Za-z_$][\\w$]*)`, "g");
18641
+ const scan = (s, also) => {
18642
+ if (!s)
18643
+ return;
18644
+ for (const m of s.matchAll(accessRe)) {
18645
+ accessed.add(m[1]);
18646
+ also?.add(m[1]);
18647
+ }
18648
+ };
18649
+ const coalesceLiteralTypes = new Map;
18650
+ const pinCoalesceLiterals = (s) => {
18651
+ if (!s || !s.includes(propsObj))
18652
+ return;
18653
+ const sf = ts17.createSourceFile("__aug.ts", `(${s})`, ts17.ScriptTarget.Latest, false);
18654
+ const visit3 = (n) => {
18655
+ if (ts17.isBinaryExpression(n) && (n.operatorToken.kind === ts17.SyntaxKind.QuestionQuestionToken || n.operatorToken.kind === ts17.SyntaxKind.BarBarToken)) {
18656
+ let left = n.left;
18657
+ while (ts17.isParenthesizedExpression(left))
18658
+ left = left.expression;
18659
+ if (ts17.isPropertyAccessExpression(left) && ts17.isIdentifier(left.expression) && left.expression.text === propsObj) {
18660
+ const name = left.name.text;
18661
+ let right = n.right;
18662
+ while (ts17.isParenthesizedExpression(right))
18663
+ right = right.expression;
18664
+ if (ts17.isPrefixUnaryExpression(right))
18665
+ right = right.operand;
18666
+ const kind = ts17.isNumericLiteral(right) ? "number" : right.kind === ts17.SyntaxKind.TrueKeyword || right.kind === ts17.SyntaxKind.FalseKeyword ? "boolean" : ts17.isStringLiteralLike(right) ? "string" : null;
18667
+ if (kind && !coalesceLiteralTypes.has(name))
18668
+ coalesceLiteralTypes.set(name, kind);
18669
+ }
18670
+ }
18671
+ ts17.forEachChild(n, visit3);
18672
+ };
18673
+ visit3(sf);
18674
+ };
18675
+ for (const memo of ir.metadata.memos) {
18676
+ scan(memo.computation);
18677
+ pinCoalesceLiterals(memo.computation);
18678
+ }
18679
+ for (const signal of ir.metadata.signals) {
18680
+ scan(signal.initialValue);
18681
+ pinCoalesceLiterals(signal.initialValue);
18682
+ }
18683
+ for (const stmt of ir.metadata.initStatements ?? [])
18684
+ scan(stmt.body);
18685
+ for (const eff of ir.metadata.effects ?? [])
18686
+ scan(eff.body);
18687
+ for (const c of ir.metadata.localConstants ?? []) {
18688
+ if (c.isModule)
18689
+ continue;
18690
+ scan(c.value);
18691
+ pinCoalesceLiterals(c.value);
18692
+ }
18693
+ const walk = (node) => {
18694
+ if (!node)
18695
+ return;
18696
+ const carrier = node;
18697
+ if (carrier.type === "expression") {
18698
+ scan(carrier.expr);
18699
+ pinCoalesceLiterals(carrier.expr);
18700
+ }
18701
+ scan(carrier.condition, bareRefProps);
18702
+ pinCoalesceLiterals(carrier.condition);
18703
+ scan(carrier.array, bareRefProps);
18704
+ const el = node;
18705
+ for (const prop of node.props ?? []) {
18706
+ const v = prop.value;
18707
+ if (v?.parts) {
18708
+ for (const part of v.parts) {
18709
+ if (part.type === "string")
18710
+ scan(part.value);
18711
+ else if (part.type === "ternary") {
18712
+ scan(part.condition);
18713
+ scan(part.whenTrue);
18714
+ scan(part.whenFalse);
18715
+ } else if (part.type === "lookup")
18716
+ scan(part.key);
18717
+ }
18718
+ }
18719
+ if (v?.kind === "expression" && typeof v.expr === "string") {
18720
+ scan(v.expr, bareRefProps);
18721
+ pinCoalesceLiterals(v.expr);
18722
+ }
18723
+ }
18724
+ for (const attr of el.attrs ?? []) {
18725
+ const v = attr.value;
18726
+ if (v?.parts) {
18727
+ for (const part of v.parts) {
18728
+ if (part.type === "string")
18729
+ scan(part.value);
18730
+ else if (part.type === "ternary") {
18731
+ scan(part.condition);
18732
+ scan(part.whenTrue);
18733
+ scan(part.whenFalse);
18734
+ } else if (part.type === "lookup")
18735
+ scan(part.key);
18736
+ }
18737
+ }
18738
+ if (v?.kind === "expression" && typeof v.expr === "string") {
18739
+ scan(v.expr);
18740
+ const expr = v.expr.trim();
18741
+ const prefix = `${propsObj}.`;
18742
+ if (isBooleanAttr(attr.name) || v.presenceOrUndefined) {
18743
+ const m = expr.match(new RegExp(`^${propsObj}\\.([A-Za-z_$][\\w$]*)`));
18744
+ if (m)
18745
+ booleanAttrProps.add(m[1]);
18746
+ } else if (expr.startsWith(prefix)) {
18747
+ const rest = expr.slice(prefix.length);
18748
+ if (/^[A-Za-z_$][\w$]*$/.test(rest))
18749
+ bareRefProps.add(rest);
18750
+ }
18751
+ }
18752
+ }
18753
+ for (const child of el.children ?? []) {
18754
+ const c = child;
18755
+ walk(c.element ?? child);
18756
+ }
18757
+ const branchy = node;
18758
+ walk(branchy.whenTrue);
18759
+ walk(branchy.whenFalse);
18760
+ walk(branchy.consequent);
18761
+ walk(branchy.alternate);
18762
+ };
18763
+ walk(ir.root);
18764
+ for (const name of accessed) {
18765
+ if (existing.has(name))
18766
+ continue;
18767
+ let raw;
18768
+ if (booleanAttrProps.has(name))
18769
+ raw = "boolean";
18770
+ else if (coalesceLiteralTypes.has(name))
18771
+ raw = coalesceLiteralTypes.get(name);
18772
+ else if (bareRefProps.has(name))
18773
+ raw = "unknown";
18774
+ else
18775
+ raw = "string";
18776
+ const type = raw === "unknown" ? { kind: "unknown", raw: "unknown" } : { kind: "primitive", raw, primitive: raw };
18777
+ ir.metadata.propsParams.push({ name, type, optional: true });
18778
+ existing.add(name);
18779
+ }
18780
+ }
18781
+ function parseStaticStringConst(source) {
18782
+ const sf = ts17.createSourceFile("__const.ts", `const __x = (${source});`, ts17.ScriptTarget.Latest, false);
18783
+ const stmt = sf.statements[0];
18784
+ if (!stmt || !ts17.isVariableStatement(stmt))
18785
+ return null;
18786
+ let init = stmt.declarationList.declarations[0]?.initializer;
18787
+ while (init && ts17.isParenthesizedExpression(init))
18788
+ init = init.expression;
18789
+ if (!init)
18790
+ return null;
18791
+ if (ts17.isStringLiteral(init) || ts17.isNoSubstitutionTemplateLiteral(init)) {
18792
+ return init.text;
18793
+ }
18794
+ return evalStringArrayJoin(source);
18795
+ }
18796
+ function evalTemplateOfStringConsts(source, resolved) {
18797
+ const sf = ts17.createSourceFile("__const.ts", `const __x = (${source});`, ts17.ScriptTarget.Latest, false);
18798
+ const stmt = sf.statements[0];
18799
+ if (!stmt || !ts17.isVariableStatement(stmt))
18800
+ return null;
18801
+ let init = stmt.declarationList.declarations[0]?.initializer;
18802
+ while (init && ts17.isParenthesizedExpression(init))
18803
+ init = init.expression;
18804
+ if (!init || !ts17.isTemplateExpression(init))
18805
+ return null;
18806
+ let out = init.head.text;
18807
+ for (const span of init.templateSpans) {
18808
+ if (!ts17.isIdentifier(span.expression))
18809
+ return null;
18810
+ const value = resolved.get(span.expression.text);
18811
+ if (value === undefined)
18812
+ return null;
18813
+ out += value + span.literal.text;
18814
+ }
18815
+ return out;
18816
+ }
18817
+ function collectModuleStringConsts(constants) {
18818
+ const map = new Map;
18819
+ const candidates = (constants ?? []).filter((c) => c.isModule && c.value !== undefined);
18820
+ let progressed = true;
18821
+ while (progressed) {
18822
+ progressed = false;
18823
+ for (const c of candidates) {
18824
+ if (map.has(c.name))
18825
+ continue;
18826
+ const literal = parseStaticStringConst(c.value) ?? evalTemplateOfStringConsts(c.value, map);
18827
+ if (literal !== null) {
18828
+ map.set(c.name, literal);
18829
+ progressed = true;
18830
+ }
18831
+ }
18832
+ }
18833
+ return map;
18834
+ }
18835
+ function lookupStaticRecordLiteral(objectName, key, constants) {
18836
+ const constInfo = (constants ?? []).find((c) => c.name === objectName && c.isModule);
18837
+ if (constInfo?.value === undefined)
18838
+ return null;
18839
+ const sf = ts17.createSourceFile("__rec.ts", `(${constInfo.value})`, ts17.ScriptTarget.Latest, true);
18840
+ if (sf.statements.length !== 1)
18841
+ return null;
18842
+ const stmt = sf.statements[0];
18843
+ if (!ts17.isExpressionStatement(stmt))
18844
+ return null;
18845
+ let parsed = stmt.expression;
18846
+ while (ts17.isParenthesizedExpression(parsed))
18847
+ parsed = parsed.expression;
18848
+ if (!ts17.isObjectLiteralExpression(parsed))
18849
+ return null;
18850
+ for (const prop of parsed.properties) {
18851
+ if (!ts17.isPropertyAssignment(prop))
18852
+ continue;
18853
+ const name = prop.name;
18854
+ const propKey = ts17.isIdentifier(name) || ts17.isStringLiteral(name) || ts17.isNoSubstitutionTemplateLiteral(name) ? name.text : null;
18855
+ if (propKey !== key)
18856
+ continue;
18857
+ let v = prop.initializer;
18858
+ while (ts17.isParenthesizedExpression(v))
18859
+ v = v.expression;
18860
+ if (ts17.isNumericLiteral(v))
18861
+ return { kind: "number", text: v.text };
18862
+ if (ts17.isStringLiteral(v) || ts17.isNoSubstitutionTemplateLiteral(v)) {
18863
+ return { kind: "string", text: v.text };
18864
+ }
18865
+ return null;
18866
+ }
18867
+ return null;
18868
+ }
18869
+ function evalStringArrayJoin(source) {
18870
+ const sf = ts17.createSourceFile("__join.ts", `const __x = (${source});`, ts17.ScriptTarget.Latest, false);
18871
+ const stmt = sf.statements[0];
18872
+ if (!stmt || !ts17.isVariableStatement(stmt))
18873
+ return null;
18874
+ let node = stmt.declarationList.declarations[0]?.initializer;
18875
+ while (node && ts17.isParenthesizedExpression(node))
18876
+ node = node.expression;
18877
+ if (!node || !ts17.isCallExpression(node))
18878
+ return null;
18879
+ const callee = node.expression;
18880
+ if (!ts17.isPropertyAccessExpression(callee))
18881
+ return null;
18882
+ if (callee.name.text !== "join")
18883
+ return null;
18884
+ let recv = callee.expression;
18885
+ while (ts17.isParenthesizedExpression(recv))
18886
+ recv = recv.expression;
18887
+ if (!ts17.isArrayLiteralExpression(recv))
18888
+ return null;
18889
+ const parts = [];
18890
+ for (const el of recv.elements) {
18891
+ if (ts17.isStringLiteral(el) || ts17.isNoSubstitutionTemplateLiteral(el)) {
18892
+ parts.push(el.text);
18893
+ } else {
18894
+ return null;
18895
+ }
18896
+ }
18897
+ let sep2 = ",";
18898
+ if (node.arguments.length >= 1) {
18899
+ const arg = node.arguments[0];
18900
+ if (ts17.isStringLiteral(arg) || ts17.isNoSubstitutionTemplateLiteral(arg))
18901
+ sep2 = arg.text;
18902
+ else
18903
+ return null;
18904
+ }
18905
+ return parts.join(sep2);
18906
+ }
18907
+ function parseRecordIndexAccess(val, localConstants, propsParams, resolveKey) {
18908
+ if (!ts17.isElementAccessExpression(val))
18909
+ return null;
18910
+ const obj = val.expression;
18911
+ const arg = val.argumentExpression;
18912
+ if (!ts17.isIdentifier(obj) || !ts17.isIdentifier(arg))
18913
+ return null;
18914
+ let indexPropName;
18915
+ let defaultKey;
18916
+ const resolved = resolveKey?.(arg.text);
18917
+ if (resolved) {
18918
+ indexPropName = resolved.propName;
18919
+ defaultKey = resolved.defaultLiteral;
18920
+ } else if (propsParams.some((p) => p.name === arg.text)) {
18921
+ indexPropName = arg.text;
18922
+ } else {
18923
+ return null;
18924
+ }
18925
+ const constInfo = localConstants.find((c) => c.name === obj.text && c.isModule);
18926
+ if (constInfo?.value === undefined)
18927
+ return null;
18928
+ const sf = ts17.createSourceFile("__rec.ts", `(${constInfo.value})`, ts17.ScriptTarget.Latest, true);
18929
+ if (sf.statements.length !== 1)
18930
+ return null;
18931
+ const stmt = sf.statements[0];
18932
+ if (!ts17.isExpressionStatement(stmt))
18933
+ return null;
18934
+ let parsed = stmt.expression;
18935
+ while (ts17.isParenthesizedExpression(parsed))
18936
+ parsed = parsed.expression;
18937
+ if (!ts17.isObjectLiteralExpression(parsed))
18938
+ return null;
18939
+ const entries = [];
18940
+ for (const prop of parsed.properties) {
18941
+ if (!ts17.isPropertyAssignment(prop))
18942
+ return null;
18943
+ let key;
18944
+ if (ts17.isIdentifier(prop.name)) {
18945
+ key = prop.name.text;
18946
+ } else if (ts17.isStringLiteral(prop.name) || ts17.isNoSubstitutionTemplateLiteral(prop.name)) {
18947
+ key = prop.name.text;
18948
+ } else {
18949
+ return null;
18950
+ }
18951
+ let v = prop.initializer;
18952
+ while (ts17.isParenthesizedExpression(v))
18953
+ v = v.expression;
18954
+ if (ts17.isNumericLiteral(v)) {
18955
+ entries.push({ key, value: { kind: "number", text: v.text } });
18956
+ } else if (ts17.isStringLiteral(v) || ts17.isNoSubstitutionTemplateLiteral(v)) {
18957
+ entries.push({ key, value: { kind: "string", text: v.text } });
18958
+ } else {
18959
+ return null;
18960
+ }
18961
+ }
18962
+ return { indexPropName, entries, defaultKey };
18963
+ }
18964
+
18965
+ // src/ssr-seed-plan.ts
18966
+ function classify2(name, origin, expr, parsed, available) {
18967
+ if (!isSupported(parsed).supported)
18968
+ return { kind: "opaque", name, origin };
18969
+ const frees = freeIdentifiers(parsed);
18970
+ if (frees === null)
18971
+ return { kind: "opaque", name, origin };
18972
+ for (const free of frees) {
18973
+ if (!available.has(free))
18974
+ return { kind: "opaque", name, origin };
18975
+ }
18976
+ return { kind: "derived", name, origin, expr, parsed, frees: [...frees] };
18977
+ }
18978
+ function computeSsrSeedPlan(metadata) {
18979
+ const baseScope = metadata.propsParams.map((p) => p.name);
18980
+ if (metadata.propsObjectName)
18981
+ baseScope.push(metadata.propsObjectName);
18982
+ for (const name of collectModuleStringConsts(metadata.localConstants).keys()) {
18983
+ baseScope.push(name);
18984
+ }
18985
+ const available = new Set(baseScope);
18986
+ const steps = [];
18987
+ for (const signal of metadata.signals) {
18988
+ if (signal.envReader) {
18989
+ const reader = envSignalReaderFor(signal.envReader);
18990
+ if (reader) {
18991
+ steps.push({ kind: "env-reader", name: signal.getter, reader });
18992
+ available.add(signal.getter);
18993
+ continue;
18994
+ }
18995
+ }
18996
+ const expr = signal.initialValue.trim();
18997
+ steps.push(expr === "" ? { kind: "opaque", name: signal.getter, origin: "signal" } : classify2(signal.getter, "signal", expr, parseExpression(expr), available));
18998
+ available.add(signal.getter);
18999
+ }
19000
+ for (const memo of metadata.memos) {
19001
+ const body = extractArrowBodyExpression(memo.computation);
19002
+ const expr = body?.trim() ?? "";
19003
+ steps.push(expr === "" ? { kind: "opaque", name: memo.name, origin: "memo" } : classify2(memo.name, "memo", expr, memo.parsed ?? parseExpression(expr), available));
19004
+ available.add(memo.name);
19005
+ }
19006
+ return { baseScope, steps };
19007
+ }
19008
+
19009
+ // src/compiler.ts
19010
+ function mergeTemplateImports(lines) {
19011
+ const result = [];
19012
+ const valueIdx = new Map;
19013
+ const valueNames = new Map;
19014
+ const typeIdx = new Map;
19015
+ const typeNames = new Map;
19016
+ const seenOther = new Set;
19017
+ const fold = (src, rawNames, idx, names, render) => {
19018
+ if (!idx.has(src)) {
19019
+ idx.set(src, result.length);
19020
+ names.set(src, new Set);
19021
+ result.push("");
19022
+ }
19023
+ const set = names.get(src);
19024
+ for (const n of rawNames.split(",").map((s) => s.trim()).filter(Boolean))
19025
+ set.add(n);
19026
+ result[idx.get(src)] = render(src, set);
19027
+ };
19028
+ for (const raw of lines) {
19029
+ const line = raw.trim();
19030
+ if (!line)
19031
+ continue;
19032
+ const typeMatch = line.match(/^import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?$/);
19033
+ const valueMatch = line.match(/^import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?$/);
19034
+ if (valueMatch) {
19035
+ fold(valueMatch[2], valueMatch[1], valueIdx, valueNames, (s, n) => `import { ${[...n].join(", ")} } from '${s}'`);
19036
+ } else if (typeMatch) {
19037
+ fold(typeMatch[2], typeMatch[1], typeIdx, typeNames, (s, n) => `import type { ${[...n].join(", ")} } from '${s}'`);
19038
+ } else if (!seenOther.has(line)) {
19039
+ seenOther.add(line);
19040
+ result.push(line);
19041
+ }
19042
+ }
19043
+ return result.filter(Boolean).join(`
19044
+ `);
19045
+ }
19046
+ function compileMultipleComponents(source, filePath, componentNames, options) {
19047
+ const files = [];
19048
+ const errors = [];
19049
+ const adapter = options.adapter;
17941
19050
  const entries = [];
17942
19051
  const program = options.program ?? (needsTypeBasedDetection(source) ? createProgramForFile(source, filePath)?.program : undefined);
17943
19052
  for (const componentName of componentNames) {
@@ -18036,7 +19145,8 @@ function compileMultipleComponents(source, filePath, componentNames, options) {
18036
19145
  files.push({
18037
19146
  path: dir + output.componentName + adapter.extension,
18038
19147
  content: output.rawTemplate,
18039
- type: "markedTemplate"
19148
+ type: "markedTemplate",
19149
+ componentName: output.componentName
18040
19150
  });
18041
19151
  const ir = entries.find((e) => e.componentIR.metadata.componentName === output.componentName);
18042
19152
  const ssrDefaults = ir ? extractSsrDefaults(ir.componentIR.metadata) : undefined;
@@ -18044,7 +19154,8 @@ function compileMultipleComponents(source, filePath, componentNames, options) {
18044
19154
  files.push({
18045
19155
  path: dir + output.componentName + ".ssr-defaults.json",
18046
19156
  content: JSON.stringify(ssrDefaults),
18047
- type: "ssrDefaults"
19157
+ type: "ssrDefaults",
19158
+ componentName: output.componentName
18048
19159
  });
18049
19160
  }
18050
19161
  }
@@ -18209,7 +19320,7 @@ function compileMultipleComponents(source, filePath, componentNames, options) {
18209
19320
  return { files, errors };
18210
19321
  }
18211
19322
  function buildMetadata(ctx) {
18212
- return {
19323
+ const metadata = {
18213
19324
  componentName: ctx.componentName || "Unknown",
18214
19325
  hasDefaultExport: ctx.hasDefaultExport,
18215
19326
  isExported: ctx.isExported,
@@ -18231,6 +19342,8 @@ function buildMetadata(ctx) {
18231
19342
  localFunctions: ctx.localFunctions,
18232
19343
  localConstants: ctx.localConstants
18233
19344
  };
19345
+ metadata.ssrSeedPlan = computeSsrSeedPlan(metadata);
19346
+ return metadata;
18234
19347
  }
18235
19348
  function compileJSX(source, filePath, options) {
18236
19349
  const files = [];
@@ -18338,7 +19451,8 @@ function compileJSX(source, filePath, options) {
18338
19451
  files.push({
18339
19452
  path: filePath.replace(/\.tsx?$/, adapter.extension),
18340
19453
  content,
18341
- type: "markedTemplate"
19454
+ type: "markedTemplate",
19455
+ componentName: componentIR.metadata.componentName
18342
19456
  });
18343
19457
  {
18344
19458
  const ssrDefaults = extractSsrDefaults(componentIR.metadata);
@@ -18346,7 +19460,8 @@ function compileJSX(source, filePath, options) {
18346
19460
  files.push({
18347
19461
  path: filePath.replace(/\.tsx?$/, ".ssr-defaults.json"),
18348
19462
  content: JSON.stringify(ssrDefaults),
18349
- type: "ssrDefaults"
19463
+ type: "ssrDefaults",
19464
+ componentName: componentIR.metadata.componentName
18350
19465
  });
18351
19466
  }
18352
19467
  }
@@ -18393,7 +19508,7 @@ function compileJSX(source, filePath, options) {
18393
19508
  return { files, errors };
18394
19509
  }
18395
19510
  // src/shared-program.ts
18396
- import ts17 from "typescript";
19511
+ import ts18 from "typescript";
18397
19512
  function commonParent(paths) {
18398
19513
  if (paths.length === 0)
18399
19514
  return process.cwd();
@@ -18414,10 +19529,10 @@ function commonParent(paths) {
18414
19529
  function createProgramForCorpus(files, options = {}) {
18415
19530
  const baseUrl = options.baseUrl ?? commonParent(files);
18416
19531
  const compilerOptions = {
18417
- target: ts17.ScriptTarget.Latest,
18418
- module: ts17.ModuleKind.ESNext,
18419
- moduleResolution: ts17.ModuleResolutionKind.Bundler,
18420
- jsx: ts17.JsxEmit.ReactJSX,
19532
+ target: ts18.ScriptTarget.Latest,
19533
+ module: ts18.ModuleKind.ESNext,
19534
+ moduleResolution: ts18.ModuleResolutionKind.Bundler,
19535
+ jsx: ts18.JsxEmit.ReactJSX,
18421
19536
  strict: true,
18422
19537
  skipLibCheck: true,
18423
19538
  noEmit: true,
@@ -18427,7 +19542,7 @@ function createProgramForCorpus(files, options = {}) {
18427
19542
  ...options.compilerOptions
18428
19543
  };
18429
19544
  const absolute = files.map((f) => path_default.resolve(f));
18430
- return ts17.createProgram(absolute, compilerOptions, undefined, options.oldProgram);
19545
+ return ts18.createProgram(absolute, compilerOptions, undefined, options.oldProgram);
18431
19546
  }
18432
19547
  // src/adapters/interface.ts
18433
19548
  class BaseAdapter {
@@ -18915,7 +20030,7 @@ function emitParsedExpr(expr, emitter) {
18915
20030
  return emitter.objectLiteral(expr.properties, expr.raw, emit);
18916
20031
  case "array-method":
18917
20032
  if (expr.method === "flat") {
18918
- return emitter.flatMethod(expr.object, expr.flatDepth, emit);
20033
+ return emitter.flatMethod(expr.object, expr.depthExpr ? { expr: expr.depthExpr } : expr.flatDepth, emit);
18919
20034
  }
18920
20035
  return emitter.arrayMethod(expr.method, expr.object, expr.args, emit);
18921
20036
  case "unsupported":
@@ -18984,42 +20099,9 @@ function isOmitBranch(node) {
18984
20099
  if (node.kind === "identifier")
18985
20100
  return node.name === "undefined";
18986
20101
  if (node.kind === "literal") {
18987
- return node.literalType === "null" || node.literalType === "string" && node.value === "";
18988
- }
18989
- return false;
18990
- }
18991
- // src/lowering-registry.ts
18992
- var plugins = [];
18993
- function registerLoweringPlugin(plugin) {
18994
- const existing = plugins.findIndex((p) => p.name === plugin.name);
18995
- if (existing >= 0)
18996
- plugins[existing] = plugin;
18997
- else
18998
- plugins.push(plugin);
18999
- }
19000
- function getLoweringPlugins() {
19001
- return [...plugins];
19002
- }
19003
- function prepareLoweringMatchers(metadata) {
19004
- const matchers = [];
19005
- for (const plugin of plugins) {
19006
- const matcher = plugin.prepare(metadata);
19007
- if (matcher)
19008
- matchers.push(matcher);
19009
- }
19010
- return matchers;
19011
- }
19012
- function matchLoweringCall(callee, args, metadata) {
19013
- for (const matcher of prepareLoweringMatchers(metadata)) {
19014
- const node = matcher(callee, args);
19015
- if (node)
19016
- return node;
19017
- }
19018
- return null;
19019
- }
19020
- function __resetLoweringPluginsForTest(next = []) {
19021
- plugins.length = 0;
19022
- plugins.push(...next);
20102
+ return node.literalType === "null" || node.literalType === "string" && node.value === "";
20103
+ }
20104
+ return false;
19023
20105
  }
19024
20106
  // src/builtin-lowering-plugins.ts
19025
20107
  var queryHrefPlugin = {
@@ -19095,7 +20177,7 @@ function emitAttrValue(value, emitter, name) {
19095
20177
  }
19096
20178
  }
19097
20179
  // src/combine-client-js.ts
19098
- import ts18 from "typescript";
20180
+ import ts19 from "typescript";
19099
20181
  var CHILD_PLACEHOLDER_RE = /import '\/\* @bf-child:(\w+) \*\/'/g;
19100
20182
  function combineParentChildClientJs(files) {
19101
20183
  const result = new Map;
@@ -19152,10 +20234,10 @@ function combineParentChildClientJs(files) {
19152
20234
  return result;
19153
20235
  }
19154
20236
  function parseAndMerge(content, importsBySource, otherImports, codeSections) {
19155
- const sourceFile = ts18.createSourceFile("combine.js", content, ts18.ScriptTarget.Latest, false, ts18.ScriptKind.JS);
20237
+ const sourceFile = ts19.createSourceFile("combine.js", content, ts19.ScriptTarget.Latest, false, ts19.ScriptKind.JS);
19156
20238
  const importSpans = [];
19157
20239
  for (const stmt of sourceFile.statements) {
19158
- if (!ts18.isImportDeclaration(stmt))
20240
+ if (!ts19.isImportDeclaration(stmt))
19159
20241
  continue;
19160
20242
  const start = stmt.getStart(sourceFile);
19161
20243
  const end = stmt.getEnd();
@@ -19165,8 +20247,8 @@ function parseAndMerge(content, importsBySource, otherImports, codeSections) {
19165
20247
  continue;
19166
20248
  const clause = stmt.importClause;
19167
20249
  const bindings = clause?.namedBindings;
19168
- const specifier = ts18.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : "";
19169
- if (clause && !clause.name && bindings && ts18.isNamedImports(bindings)) {
20250
+ const specifier = ts19.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : "";
20251
+ if (clause && !clause.name && bindings && ts19.isNamedImports(bindings)) {
19170
20252
  if (!importsBySource.has(specifier)) {
19171
20253
  importsBySource.set(specifier, new Set);
19172
20254
  }
@@ -19194,8 +20276,7 @@ function parseAndMerge(content, importsBySource, otherImports, codeSections) {
19194
20276
  }
19195
20277
  }
19196
20278
  // src/loop-destructure.ts
19197
- var SIMPLE_FIELD = /^\.[A-Za-z_$][\w$]*$/;
19198
- function isLowerableObjectRestDestructure(loop) {
20279
+ function isLowerableLoopDestructure(loop) {
19199
20280
  const bindings = loop.paramBindings;
19200
20281
  if (!bindings || bindings.length === 0)
19201
20282
  return false;
@@ -19207,18 +20288,20 @@ function isLowerableObjectRestDestructure(loop) {
19207
20288
  }
19208
20289
  for (const b of bindings) {
19209
20290
  if (b.rest) {
19210
- if (b.rest.kind !== "object")
20291
+ if (!b.segments)
19211
20292
  return false;
19212
- } else if (!SIMPLE_FIELD.test(b.path)) {
20293
+ } else if (!b.segments || b.segments.length === 0) {
19213
20294
  return false;
19214
20295
  }
19215
20296
  }
19216
- const restNames = bindings.filter((b) => b.rest).map((b) => b.name);
19217
- if (restNames.length === 0)
20297
+ const objectRestNames = bindings.filter((b) => b.rest?.kind === "object").map((b) => b.name);
20298
+ if (objectRestNames.length === 0)
19218
20299
  return true;
19219
- return !restNamesMisused(loop, restNames);
20300
+ return !restNamesMisused(loop, objectRestNames);
19220
20301
  }
20302
+ var isLowerableObjectRestDestructure = isLowerableLoopDestructure;
19221
20303
  function restNamesMisused(loop, names) {
20304
+ const nameSet = new Set(names);
19222
20305
  const valueUse = names.map((n) => new RegExp(`(?<![\\w.$])${escapeRe(n)}(?!\\s*\\??\\.)(?![\\w$])`));
19223
20306
  let misused = false;
19224
20307
  const check = (s) => {
@@ -19231,7 +20314,10 @@ function restNamesMisused(loop, names) {
19231
20314
  }
19232
20315
  }
19233
20316
  };
19234
- const attr = (v) => {
20317
+ const attr = (v, isIntrinsicElementAttrs) => {
20318
+ if (v.kind === "spread" && isIntrinsicElementAttrs && nameSet.has(v.expr.trim())) {
20319
+ return;
20320
+ }
19235
20321
  if (v.kind === "expression" || v.kind === "spread") {
19236
20322
  check(v.expr);
19237
20323
  check(v.templateExpr);
@@ -19291,16 +20377,16 @@ function restNamesMisused(loop, names) {
19291
20377
  visit3(node.alternate);
19292
20378
  break;
19293
20379
  case "element":
19294
- node.attrs.forEach((a) => attr(a.value));
20380
+ node.attrs.forEach((a) => attr(a.value, true));
19295
20381
  node.events.forEach((e) => check(e.handler));
19296
20382
  node.children.forEach(visit3);
19297
20383
  break;
19298
20384
  case "component":
19299
- node.props.forEach((p) => attr(p.value));
20385
+ node.props.forEach((p) => attr(p.value, false));
19300
20386
  node.children.forEach(visit3);
19301
20387
  break;
19302
20388
  case "provider":
19303
- attr(node.valueProp.value);
20389
+ attr(node.valueProp.value, false);
19304
20390
  node.children.forEach(visit3);
19305
20391
  break;
19306
20392
  case "fragment":
@@ -19325,7 +20411,7 @@ function escapeRe(s) {
19325
20411
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19326
20412
  }
19327
20413
  // src/debug.ts
19328
- import ts19 from "typescript";
20414
+ import ts20 from "typescript";
19329
20415
  function buildComponentGraph(source, filePath, componentName) {
19330
20416
  const ctx = analyzeComponent(source, filePath, componentName);
19331
20417
  if (!ctx.jsxReturn) {
@@ -20610,7 +21696,7 @@ function truncateExpr(expr, max = 40) {
20610
21696
  function exprReadsPropMember(expr, propsObjectName) {
20611
21697
  let sf;
20612
21698
  try {
20613
- sf = ts19.createSourceFile("__attr.tsx", `(${expr})`, ts19.ScriptTarget.Latest, true, ts19.ScriptKind.TSX);
21699
+ sf = ts20.createSourceFile("__attr.tsx", `(${expr})`, ts20.ScriptTarget.Latest, true, ts20.ScriptKind.TSX);
20614
21700
  } catch {
20615
21701
  return false;
20616
21702
  }
@@ -20618,11 +21704,11 @@ function exprReadsPropMember(expr, propsObjectName) {
20618
21704
  const visit3 = (n) => {
20619
21705
  if (found)
20620
21706
  return;
20621
- if (ts19.isPropertyAccessExpression(n) && ts19.isIdentifier(n.expression) && n.expression.text === propsObjectName && n.name.text !== "children") {
21707
+ if (ts20.isPropertyAccessExpression(n) && ts20.isIdentifier(n.expression) && n.expression.text === propsObjectName && n.name.text !== "children") {
20622
21708
  found = true;
20623
21709
  return;
20624
21710
  }
20625
- ts19.forEachChild(n, visit3);
21711
+ ts20.forEachChild(n, visit3);
20626
21712
  };
20627
21713
  visit3(sf);
20628
21714
  return found;
@@ -20692,7 +21778,7 @@ function findSourceFile2(meta) {
20692
21778
  return null;
20693
21779
  }
20694
21780
  // src/profiler.ts
20695
- import ts20 from "typescript";
21781
+ import ts21 from "typescript";
20696
21782
  var PROFILE_SCHEMA_VERSION = 1;
20697
21783
  var DEFAULT_FANOUT_THRESHOLD = 8;
20698
21784
  function buildStaticBudget(source, filePath, componentName, options = {}) {
@@ -20962,15 +22048,15 @@ function joinProfilerEvents(events, index) {
20962
22048
  return { joined, unattributed, diagnostics };
20963
22049
  }
20964
22050
  function findUninstrumentedEffects(source, filePath, instrumentedLines) {
20965
- const sf = ts20.createSourceFile(filePath, source, ts20.ScriptTarget.Latest, true, ts20.ScriptKind.TSX);
22051
+ const sf = ts21.createSourceFile(filePath, source, ts21.ScriptTarget.Latest, true, ts21.ScriptKind.TSX);
20966
22052
  const out = [];
20967
22053
  const visit3 = (node) => {
20968
- if (ts20.isCallExpression(node) && ts20.isIdentifier(node.expression) && node.expression.text === "createEffect") {
22054
+ if (ts21.isCallExpression(node) && ts21.isIdentifier(node.expression) && node.expression.text === "createEffect") {
20969
22055
  const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
20970
22056
  if (!instrumentedLines.has(line))
20971
22057
  out.push({ file: filePath, line });
20972
22058
  }
20973
- ts20.forEachChild(node, visit3);
22059
+ ts21.forEachChild(node, visit3);
20974
22060
  };
20975
22061
  visit3(sf);
20976
22062
  out.sort((a, b) => a.line - b.line);
@@ -21278,13 +22364,13 @@ function assessBatchSafety(args) {
21278
22364
  const signalGetters = new Set(args.graph.signals.map((s) => s.name));
21279
22365
  let sf;
21280
22366
  try {
21281
- sf = ts20.createSourceFile("__h.ts", `const __h = ${args.handler}`, ts20.ScriptTarget.Latest, true);
22367
+ sf = ts21.createSourceFile("__h.ts", `const __h = ${args.handler}`, ts21.ScriptTarget.Latest, true);
21282
22368
  } catch {
21283
22369
  return "unverified";
21284
22370
  }
21285
22371
  const calls = [];
21286
22372
  const visit3 = (node) => {
21287
- if (ts20.isCallExpression(node) && ts20.isIdentifier(node.expression)) {
22373
+ if (ts21.isCallExpression(node) && ts21.isIdentifier(node.expression)) {
21288
22374
  const name = node.expression.text;
21289
22375
  if (setters.has(name))
21290
22376
  calls.push({ pos: node.getStart(sf), kind: "write" });
@@ -21293,7 +22379,7 @@ function assessBatchSafety(args) {
21293
22379
  else if (!signalGetters.has(name) && !memoNames.has(name))
21294
22380
  calls.push({ pos: node.getStart(sf), kind: "risky" });
21295
22381
  }
21296
- ts20.forEachChild(node, visit3);
22382
+ ts21.forEachChild(node, visit3);
21297
22383
  };
21298
22384
  visit3(sf);
21299
22385
  calls.sort((a, b) => a.pos - b.pos);
@@ -21658,608 +22744,273 @@ function computeMetrics(graph, eventSummary, hydrated) {
21658
22744
  maxSignalFanOut: maxFanOut,
21659
22745
  hotSignal,
21660
22746
  maxMemoChainDepth,
21661
- totalSubscriptions,
21662
- batchCandidateCount
21663
- };
21664
- }
21665
- function computeMaxMemoChainDepth(graph) {
21666
- if (graph.memos.length === 0)
21667
- return 0;
21668
- const memoSet = new Set(graph.memos.map((m) => m.name));
21669
- const memoDeps = new Map;
21670
- for (const memo of graph.memos) {
21671
- memoDeps.set(memo.name, memo.deps.filter((d) => memoSet.has(d)));
21672
- }
21673
- const cache = new Map;
21674
- function depth(name, visited) {
21675
- if (cache.has(name))
21676
- return cache.get(name);
21677
- if (visited.has(name))
21678
- return 0;
21679
- const children = memoDeps.get(name) ?? [];
21680
- if (children.length === 0) {
21681
- cache.set(name, 1);
21682
- return 1;
21683
- }
21684
- visited.add(name);
21685
- const d = 1 + Math.max(...children.map((c) => depth(c, new Set(visited))));
21686
- cache.set(name, d);
21687
- return d;
21688
- }
21689
- let max = 0;
21690
- for (const memo of graph.memos) {
21691
- const d = depth(memo.name, new Set);
21692
- if (d > max)
21693
- max = d;
21694
- }
21695
- return max;
21696
- }
21697
- function computeFindings(metrics, graph, eventSummary) {
21698
- const findings = [];
21699
- for (const signal of graph.signals) {
21700
- if (signal.consumers.length > THRESHOLDS.highFanOut) {
21701
- findings.push({
21702
- kind: "high-fan-out",
21703
- severity: "warning",
21704
- signal: signal.name,
21705
- message: `${signal.name} has ${signal.consumers.length} consumers (fan-out > ${THRESHOLDS.highFanOut})`,
21706
- suggestion: `Split ${signal.name} into finer-grained signals, or add a createMemo to shield downstream consumers from unrelated updates`,
21707
- loc: { file: signal.loc.file, line: signal.loc.line }
21708
- });
21709
- }
21710
- }
21711
- if (metrics.maxMemoChainDepth > THRESHOLDS.deepMemoChain) {
21712
- findings.push({
21713
- kind: "deep-memo-chain",
21714
- severity: "warning",
21715
- depth: metrics.maxMemoChainDepth,
21716
- message: `Memo chain depth ${metrics.maxMemoChainDepth} (threshold: ${THRESHOLDS.deepMemoChain}) — a single signal update cascades through ${metrics.maxMemoChainDepth} memo levels`,
21717
- suggestion: "Flatten intermediate memos that do not cache expensive computations; deep chains increase propagation latency"
21718
- });
21719
- }
21720
- const batchSeen = new Set;
21721
- for (const event of eventSummary.events) {
21722
- const distinct = new Set;
21723
- const setterNames = [];
21724
- for (const sc of event.setterCalls) {
21725
- if (sc.signal) {
21726
- distinct.add(sc.signal);
21727
- setterNames.push(sc.setter);
21728
- }
21729
- }
21730
- if (distinct.size >= THRESHOLDS.batchMinSignals) {
21731
- const dedupeKey = `${event.eventName}|${event.loc.file ?? ""}|${event.loc.start.line}|${[...distinct].sort().join(",")}`;
21732
- if (batchSeen.has(dedupeKey))
21733
- continue;
21734
- batchSeen.add(dedupeKey);
21735
- findings.push({
21736
- kind: "batch-candidate",
21737
- severity: "info",
21738
- signals: [...distinct],
21739
- message: `${event.eventName} on <${event.elementContext}> sets ${distinct.size} signals (${[...distinct].join(", ")}) — triggers ${distinct.size} separate update cycles (static; verify setters are not in separate if/else branches)`,
21740
- suggestion: `If all listed setters fire unconditionally in the same handler path, wrap in batch(() => { ${setterNames.join("; ")}; }) to collapse ${distinct.size} cycles into 1`,
21741
- loc: event.loc.file ? { file: event.loc.file, line: event.loc.start.line } : undefined
21742
- });
21743
- }
21744
- }
21745
- if (metrics.dynamicBindings > 0 && metrics.fallbacks >= THRESHOLDS.fallbackHeavyMin && metrics.fallbacks / metrics.dynamicBindings > THRESHOLDS.fallbackHeavyRatio) {
21746
- findings.push({
21747
- kind: "fallback-heavy",
21748
- severity: "info",
21749
- message: `${metrics.fallbacks}/${metrics.dynamicBindings} bindings (${Math.round(metrics.fallbacks / metrics.dynamicBindings * 100)}%) are fallback-wrapped — reactivity not statically provable`,
21750
- suggestion: "Run `bf debug fallbacks` to see each expression and fix them so the compiler can prove reactivity without the fallback wrapper"
21751
- });
21752
- }
21753
- return findings;
21754
- }
21755
- function diffProfiles(before, after) {
21756
- const worseWhenHigher = new Set([
21757
- "fallbacks",
21758
- "maxSignalFanOut",
21759
- "maxMemoChainDepth",
21760
- "totalSubscriptions",
21761
- "batchCandidateCount"
21762
- ]);
21763
- const numericKeys = [
21764
- "signals",
21765
- "memos",
21766
- "effects",
21767
- "loops",
21768
- "eventHandlers",
21769
- "dynamicBindings",
21770
- "fallbacks",
21771
- "conditionals",
21772
- "maxSignalFanOut",
21773
- "maxMemoChainDepth",
21774
- "totalSubscriptions",
21775
- "batchCandidateCount"
21776
- ];
21777
- const regressions = [];
21778
- const improvements = [];
21779
- const neutral = [];
21780
- for (const key of numericKeys) {
21781
- const b = before[key];
21782
- const a = after[key];
21783
- if (a === b)
21784
- continue;
21785
- const entry = { metric: key, before: b, after: a, delta: a - b };
21786
- if (worseWhenHigher.has(key)) {
21787
- if (a > b)
21788
- regressions.push(entry);
21789
- else
21790
- improvements.push(entry);
21791
- } else {
21792
- neutral.push(entry);
21793
- }
21794
- }
21795
- return { componentName: after.componentName, before, after, regressions, improvements, neutral };
21796
- }
21797
- function formatSingleProfile(profile) {
21798
- const m = profile.metrics;
21799
- const lines = [];
21800
- lines.push(`${m.componentName} — reactive profile (static)`);
21801
- if (m.sourceFile)
21802
- lines.push(` source: ${m.sourceFile}`);
21803
- lines.push(` hydrated: ${m.hydrated ? "yes" : "no"}`);
21804
- lines.push("");
21805
- lines.push(" Counts:");
21806
- lines.push(` signals: ${m.signals}`);
21807
- lines.push(` memos: ${m.memos}`);
21808
- if (m.effects > 0)
21809
- lines.push(` effects: ${m.effects}`);
21810
- lines.push(` dynamic bindings: ${m.dynamicBindings}`);
21811
- if (m.fallbacks > 0)
21812
- lines.push(` fallbacks: ${m.fallbacks}`);
21813
- if (m.loops > 0)
21814
- lines.push(` loops: ${m.loops}`);
21815
- if (m.conditionals > 0)
21816
- lines.push(` conditionals: ${m.conditionals}`);
21817
- if (m.eventHandlers > 0)
21818
- lines.push(` event handlers: ${m.eventHandlers}`);
21819
- lines.push("");
21820
- lines.push(" Reactive budget (SR5):");
21821
- const fanOutSuffix = m.hotSignal ? ` (${m.hotSignal})` : "";
21822
- lines.push(` max signal fan-out: ${m.maxSignalFanOut}${fanOutSuffix}`);
21823
- lines.push(` max memo chain depth: ${m.maxMemoChainDepth}`);
21824
- lines.push(` total subscriptions: ${m.totalSubscriptions}`);
21825
- if (m.batchCandidateCount > 0) {
21826
- lines.push(` batch candidates: ${m.batchCandidateCount} handler(s) set ≥2 signals`);
21827
- }
21828
- if (profile.findings.length > 0) {
21829
- lines.push("");
21830
- lines.push(" Findings:");
21831
- for (const f of profile.findings) {
21832
- const icon = f.severity === "warning" ? "⚠" : "→";
21833
- lines.push(` ${icon} [${f.kind}] ${f.message}`);
21834
- lines.push(` fix: ${f.suggestion}`);
21835
- if (f.loc) {
21836
- const file = f.loc.file.split("/").pop() ?? f.loc.file;
21837
- lines.push(` at ${file}:${f.loc.line}`);
21838
- }
21839
- }
21840
- } else {
21841
- lines.push("");
21842
- lines.push(" No findings — component is within all thresholds.");
21843
- }
21844
- return lines.join(`
21845
- `);
21846
- }
21847
- function formatProfileTable(profiles) {
21848
- if (profiles.length === 0)
21849
- return "No components found.";
21850
- const sorted = [...profiles].sort((a, b) => b.metrics.totalSubscriptions - a.metrics.totalSubscriptions);
21851
- const lines = [];
21852
- lines.push("Component sig memo bind fall fanOut chain subs batch findings");
21853
- lines.push("─".repeat(90));
21854
- for (const p of sorted) {
21855
- const m = p.metrics;
21856
- const name = m.componentName.padEnd(23).slice(0, 23);
21857
- const findingStr = p.findings.length > 0 ? p.findings.map((f) => f.kind.replace(/-/g, "_")).join(",") : "—";
21858
- const row = [
21859
- name,
21860
- String(m.signals).padStart(3),
21861
- String(m.memos).padStart(5),
21862
- String(m.dynamicBindings).padStart(5),
21863
- String(m.fallbacks).padStart(5),
21864
- String(m.maxSignalFanOut).padStart(7),
21865
- String(m.maxMemoChainDepth).padStart(6),
21866
- String(m.totalSubscriptions).padStart(5),
21867
- String(m.batchCandidateCount).padStart(6),
21868
- ` ${findingStr}`
21869
- ].join(" ");
21870
- lines.push(row);
21871
- }
21872
- const allFindings = sorted.flatMap((p) => p.findings.map((f) => ({ component: p.metrics.componentName, finding: f })));
21873
- if (allFindings.length > 0) {
21874
- lines.push("");
21875
- lines.push("Findings:");
21876
- for (const { component, finding } of allFindings) {
21877
- const icon = finding.severity === "warning" ? "⚠" : "→";
21878
- lines.push(` ${icon} ${component}: ${finding.message}`);
21879
- lines.push(` fix: ${finding.suggestion}`);
21880
- if (finding.loc) {
21881
- const file = finding.loc.file.split("/").pop() ?? finding.loc.file;
21882
- lines.push(` at ${file}:${finding.loc.line}`);
21883
- }
21884
- }
21885
- } else {
21886
- lines.push("");
21887
- lines.push("No findings across all components.");
21888
- }
21889
- return lines.join(`
21890
- `);
21891
- }
21892
- function formatProfileDiff(diff) {
21893
- const lines = [];
21894
- lines.push(`${diff.componentName} — reactive profile diff (before → after)`);
21895
- lines.push("");
21896
- if (diff.regressions.length === 0 && diff.improvements.length === 0 && diff.neutral.length === 0) {
21897
- lines.push(" No changes in reactive metrics.");
21898
- return lines.join(`
21899
- `);
21900
- }
21901
- if (diff.regressions.length > 0) {
21902
- lines.push(" Regressions (reactive cost increased):");
21903
- for (const e of diff.regressions) {
21904
- lines.push(` ${e.metric}: ${e.before} → ${e.after} (+${e.delta})`);
21905
- }
22747
+ totalSubscriptions,
22748
+ batchCandidateCount
22749
+ };
22750
+ }
22751
+ function computeMaxMemoChainDepth(graph) {
22752
+ if (graph.memos.length === 0)
22753
+ return 0;
22754
+ const memoSet = new Set(graph.memos.map((m) => m.name));
22755
+ const memoDeps = new Map;
22756
+ for (const memo of graph.memos) {
22757
+ memoDeps.set(memo.name, memo.deps.filter((d) => memoSet.has(d)));
21906
22758
  }
21907
- if (diff.improvements.length > 0) {
21908
- lines.push(" Improvements (reactive cost decreased):");
21909
- for (const e of diff.improvements) {
21910
- lines.push(` ${e.metric}: ${e.before} → ${e.after} (${e.delta})`);
22759
+ const cache = new Map;
22760
+ function depth(name, visited) {
22761
+ if (cache.has(name))
22762
+ return cache.get(name);
22763
+ if (visited.has(name))
22764
+ return 0;
22765
+ const children = memoDeps.get(name) ?? [];
22766
+ if (children.length === 0) {
22767
+ cache.set(name, 1);
22768
+ return 1;
21911
22769
  }
22770
+ visited.add(name);
22771
+ const d = 1 + Math.max(...children.map((c) => depth(c, new Set(visited))));
22772
+ cache.set(name, d);
22773
+ return d;
21912
22774
  }
21913
- if (diff.neutral.length > 0) {
21914
- lines.push(" Structural changes (count changes, no clear direction):");
21915
- for (const e of diff.neutral) {
21916
- const sign = (e.delta ?? 0) > 0 ? "+" : "";
21917
- lines.push(` ${e.metric}: ${e.before} → ${e.after} (${sign}${e.delta})`);
21918
- }
22775
+ let max = 0;
22776
+ for (const memo of graph.memos) {
22777
+ const d = depth(memo.name, new Set);
22778
+ if (d > max)
22779
+ max = d;
21919
22780
  }
21920
- return lines.join(`
21921
- `);
21922
- }
21923
- function profileToJSON(profile) {
21924
- return {
21925
- metrics: profile.metrics,
21926
- findings: profile.findings
21927
- };
22781
+ return max;
21928
22782
  }
21929
- // src/augment-inherited-props.ts
21930
- import ts21 from "typescript";
21931
- function collectContextConsumers(metadata) {
21932
- const constants = metadata.localConstants ?? [];
21933
- const contextDefaults = new Map;
21934
- for (const c of constants) {
21935
- if (c.systemConstructKind !== "createContext" || c.value === undefined)
21936
- continue;
21937
- contextDefaults.set(c.name, parseCreateContextDefault(c.value));
22783
+ function computeFindings(metrics, graph, eventSummary) {
22784
+ const findings = [];
22785
+ for (const signal of graph.signals) {
22786
+ if (signal.consumers.length > THRESHOLDS.highFanOut) {
22787
+ findings.push({
22788
+ kind: "high-fan-out",
22789
+ severity: "warning",
22790
+ signal: signal.name,
22791
+ message: `${signal.name} has ${signal.consumers.length} consumers (fan-out > ${THRESHOLDS.highFanOut})`,
22792
+ suggestion: `Split ${signal.name} into finer-grained signals, or add a createMemo to shield downstream consumers from unrelated updates`,
22793
+ loc: { file: signal.loc.file, line: signal.loc.line }
22794
+ });
22795
+ }
21938
22796
  }
21939
- if (contextDefaults.size === 0)
21940
- return [];
21941
- const consumers = [];
21942
- for (const c of constants) {
21943
- if (c.value === undefined)
21944
- continue;
21945
- const ctxName = parseUseContextArg(c.value);
21946
- if (ctxName === null || !contextDefaults.has(ctxName))
21947
- continue;
21948
- consumers.push({
21949
- localName: c.name,
21950
- contextName: ctxName,
21951
- defaultValue: contextDefaults.get(ctxName) ?? null
22797
+ if (metrics.maxMemoChainDepth > THRESHOLDS.deepMemoChain) {
22798
+ findings.push({
22799
+ kind: "deep-memo-chain",
22800
+ severity: "warning",
22801
+ depth: metrics.maxMemoChainDepth,
22802
+ message: `Memo chain depth ${metrics.maxMemoChainDepth} (threshold: ${THRESHOLDS.deepMemoChain}) — a single signal update cascades through ${metrics.maxMemoChainDepth} memo levels`,
22803
+ suggestion: "Flatten intermediate memos that do not cache expensive computations; deep chains increase propagation latency"
21952
22804
  });
21953
22805
  }
21954
- return consumers;
21955
- }
21956
- function parseUseContextArg(source) {
21957
- const expr = parseSingleExpression(source);
21958
- if (!expr || !ts21.isCallExpression(expr))
21959
- return null;
21960
- if (!ts21.isIdentifier(expr.expression) || expr.expression.text !== "useContext")
21961
- return null;
21962
- if (expr.arguments.length !== 1)
21963
- return null;
21964
- const arg = expr.arguments[0];
21965
- return ts21.isIdentifier(arg) ? arg.text : null;
21966
- }
21967
- function parseCreateContextDefault(source) {
21968
- const expr = parseSingleExpression(source);
21969
- if (!expr || !ts21.isCallExpression(expr))
21970
- return null;
21971
- if (expr.arguments.length === 0)
21972
- return null;
21973
- const arg = expr.arguments[0];
21974
- if (ts21.isStringLiteral(arg) || ts21.isNoSubstitutionTemplateLiteral(arg))
21975
- return arg.text;
21976
- if (ts21.isNumericLiteral(arg))
21977
- return Number(arg.text);
21978
- if (arg.kind === ts21.SyntaxKind.TrueKeyword)
21979
- return true;
21980
- if (arg.kind === ts21.SyntaxKind.FalseKeyword)
21981
- return false;
21982
- return null;
21983
- }
21984
- function parseSingleExpression(source) {
21985
- const sf = ts21.createSourceFile("__ctx.ts", `(${source})`, ts21.ScriptTarget.Latest, false);
21986
- const stmt = sf.statements[0];
21987
- if (!stmt || !ts21.isExpressionStatement(stmt))
21988
- return null;
21989
- let e = stmt.expression;
21990
- while (ts21.isParenthesizedExpression(e))
21991
- e = e.expression;
21992
- return e;
21993
- }
21994
- function augmentInheritedPropAccesses(ir) {
21995
- const propsObj = ir.metadata.propsObjectName;
21996
- if (!propsObj)
21997
- return;
21998
- const existing = new Set(ir.metadata.propsParams.map((p) => p.name));
21999
- const bareRefProps = new Set;
22000
- const booleanAttrProps = new Set;
22001
- const accessed = new Set;
22002
- const accessRe = new RegExp(`(?:^|[^\\w$.])${propsObj}\\.([A-Za-z_$][\\w$]*)`, "g");
22003
- const scan = (s) => {
22004
- if (!s)
22005
- return;
22006
- for (const m of s.matchAll(accessRe))
22007
- accessed.add(m[1]);
22008
- };
22009
- for (const memo of ir.metadata.memos)
22010
- scan(memo.computation);
22011
- for (const signal of ir.metadata.signals)
22012
- scan(signal.initialValue);
22013
- for (const stmt of ir.metadata.initStatements ?? [])
22014
- scan(stmt.body);
22015
- for (const eff of ir.metadata.effects ?? [])
22016
- scan(eff.body);
22017
- for (const c of ir.metadata.localConstants ?? []) {
22018
- if (c.isModule)
22019
- continue;
22020
- scan(c.value);
22021
- }
22022
- const walk = (node) => {
22023
- if (!node)
22024
- return;
22025
- const el = node;
22026
- for (const attr of el.attrs ?? []) {
22027
- const v = attr.value;
22028
- if (v?.parts) {
22029
- for (const part of v.parts) {
22030
- if (part.type === "string")
22031
- scan(part.value);
22032
- else if (part.type === "ternary") {
22033
- scan(part.condition);
22034
- scan(part.whenTrue);
22035
- scan(part.whenFalse);
22036
- } else if (part.type === "lookup")
22037
- scan(part.key);
22038
- }
22039
- }
22040
- if (v?.kind === "expression" && typeof v.expr === "string") {
22041
- scan(v.expr);
22042
- const expr = v.expr.trim();
22043
- const prefix = `${propsObj}.`;
22044
- if (isBooleanAttr(attr.name) || v.presenceOrUndefined) {
22045
- const m = expr.match(new RegExp(`^${propsObj}\\.([A-Za-z_$][\\w$]*)`));
22046
- if (m)
22047
- booleanAttrProps.add(m[1]);
22048
- } else if (expr.startsWith(prefix)) {
22049
- const rest = expr.slice(prefix.length);
22050
- if (/^[A-Za-z_$][\w$]*$/.test(rest))
22051
- bareRefProps.add(rest);
22052
- }
22806
+ const batchSeen = new Set;
22807
+ for (const event of eventSummary.events) {
22808
+ const distinct = new Set;
22809
+ const setterNames = [];
22810
+ for (const sc of event.setterCalls) {
22811
+ if (sc.signal) {
22812
+ distinct.add(sc.signal);
22813
+ setterNames.push(sc.setter);
22053
22814
  }
22054
22815
  }
22055
- for (const child of el.children ?? []) {
22056
- const c = child;
22057
- walk(c.element ?? child);
22816
+ if (distinct.size >= THRESHOLDS.batchMinSignals) {
22817
+ const dedupeKey = `${event.eventName}|${event.loc.file ?? ""}|${event.loc.start.line}|${[...distinct].sort().join(",")}`;
22818
+ if (batchSeen.has(dedupeKey))
22819
+ continue;
22820
+ batchSeen.add(dedupeKey);
22821
+ findings.push({
22822
+ kind: "batch-candidate",
22823
+ severity: "info",
22824
+ signals: [...distinct],
22825
+ message: `${event.eventName} on <${event.elementContext}> sets ${distinct.size} signals (${[...distinct].join(", ")}) — triggers ${distinct.size} separate update cycles (static; verify setters are not in separate if/else branches)`,
22826
+ suggestion: `If all listed setters fire unconditionally in the same handler path, wrap in batch(() => { ${setterNames.join("; ")}; }) to collapse ${distinct.size} cycles into 1`,
22827
+ loc: event.loc.file ? { file: event.loc.file, line: event.loc.start.line } : undefined
22828
+ });
22058
22829
  }
22059
- const branchy = node;
22060
- walk(branchy.whenTrue);
22061
- walk(branchy.whenFalse);
22062
- walk(branchy.consequent);
22063
- walk(branchy.alternate);
22064
- };
22065
- walk(ir.root);
22066
- for (const name of accessed) {
22067
- if (existing.has(name))
22068
- continue;
22069
- let raw;
22070
- if (booleanAttrProps.has(name))
22071
- raw = "boolean";
22072
- else if (bareRefProps.has(name))
22073
- raw = "unknown";
22074
- else
22075
- raw = "string";
22076
- const type = raw === "boolean" ? { kind: "primitive", raw: "boolean", primitive: "boolean" } : raw === "string" ? { kind: "primitive", raw: "string", primitive: "string" } : { kind: "unknown", raw: "unknown" };
22077
- ir.metadata.propsParams.push({ name, type, optional: true });
22078
- existing.add(name);
22079
22830
  }
22831
+ if (metrics.dynamicBindings > 0 && metrics.fallbacks >= THRESHOLDS.fallbackHeavyMin && metrics.fallbacks / metrics.dynamicBindings > THRESHOLDS.fallbackHeavyRatio) {
22832
+ findings.push({
22833
+ kind: "fallback-heavy",
22834
+ severity: "info",
22835
+ message: `${metrics.fallbacks}/${metrics.dynamicBindings} bindings (${Math.round(metrics.fallbacks / metrics.dynamicBindings * 100)}%) are fallback-wrapped — reactivity not statically provable`,
22836
+ suggestion: "Run `bf debug fallbacks` to see each expression and fix them so the compiler can prove reactivity without the fallback wrapper"
22837
+ });
22838
+ }
22839
+ return findings;
22080
22840
  }
22081
- function parseStaticStringConst(source) {
22082
- const sf = ts21.createSourceFile("__const.ts", `const __x = (${source});`, ts21.ScriptTarget.Latest, false);
22083
- const stmt = sf.statements[0];
22084
- if (!stmt || !ts21.isVariableStatement(stmt))
22085
- return null;
22086
- let init = stmt.declarationList.declarations[0]?.initializer;
22087
- while (init && ts21.isParenthesizedExpression(init))
22088
- init = init.expression;
22089
- if (!init)
22090
- return null;
22091
- if (ts21.isStringLiteral(init) || ts21.isNoSubstitutionTemplateLiteral(init)) {
22092
- return init.text;
22841
+ function diffProfiles(before, after) {
22842
+ const worseWhenHigher = new Set([
22843
+ "fallbacks",
22844
+ "maxSignalFanOut",
22845
+ "maxMemoChainDepth",
22846
+ "totalSubscriptions",
22847
+ "batchCandidateCount"
22848
+ ]);
22849
+ const numericKeys = [
22850
+ "signals",
22851
+ "memos",
22852
+ "effects",
22853
+ "loops",
22854
+ "eventHandlers",
22855
+ "dynamicBindings",
22856
+ "fallbacks",
22857
+ "conditionals",
22858
+ "maxSignalFanOut",
22859
+ "maxMemoChainDepth",
22860
+ "totalSubscriptions",
22861
+ "batchCandidateCount"
22862
+ ];
22863
+ const regressions = [];
22864
+ const improvements = [];
22865
+ const neutral = [];
22866
+ for (const key of numericKeys) {
22867
+ const b = before[key];
22868
+ const a = after[key];
22869
+ if (a === b)
22870
+ continue;
22871
+ const entry = { metric: key, before: b, after: a, delta: a - b };
22872
+ if (worseWhenHigher.has(key)) {
22873
+ if (a > b)
22874
+ regressions.push(entry);
22875
+ else
22876
+ improvements.push(entry);
22877
+ } else {
22878
+ neutral.push(entry);
22879
+ }
22093
22880
  }
22094
- return evalStringArrayJoin(source);
22881
+ return { componentName: after.componentName, before, after, regressions, improvements, neutral };
22095
22882
  }
22096
- function evalTemplateOfStringConsts(source, resolved) {
22097
- const sf = ts21.createSourceFile("__const.ts", `const __x = (${source});`, ts21.ScriptTarget.Latest, false);
22098
- const stmt = sf.statements[0];
22099
- if (!stmt || !ts21.isVariableStatement(stmt))
22100
- return null;
22101
- let init = stmt.declarationList.declarations[0]?.initializer;
22102
- while (init && ts21.isParenthesizedExpression(init))
22103
- init = init.expression;
22104
- if (!init || !ts21.isTemplateExpression(init))
22105
- return null;
22106
- let out = init.head.text;
22107
- for (const span of init.templateSpans) {
22108
- if (!ts21.isIdentifier(span.expression))
22109
- return null;
22110
- const value = resolved.get(span.expression.text);
22111
- if (value === undefined)
22112
- return null;
22113
- out += value + span.literal.text;
22883
+ function formatSingleProfile(profile) {
22884
+ const m = profile.metrics;
22885
+ const lines = [];
22886
+ lines.push(`${m.componentName} reactive profile (static)`);
22887
+ if (m.sourceFile)
22888
+ lines.push(` source: ${m.sourceFile}`);
22889
+ lines.push(` hydrated: ${m.hydrated ? "yes" : "no"}`);
22890
+ lines.push("");
22891
+ lines.push(" Counts:");
22892
+ lines.push(` signals: ${m.signals}`);
22893
+ lines.push(` memos: ${m.memos}`);
22894
+ if (m.effects > 0)
22895
+ lines.push(` effects: ${m.effects}`);
22896
+ lines.push(` dynamic bindings: ${m.dynamicBindings}`);
22897
+ if (m.fallbacks > 0)
22898
+ lines.push(` fallbacks: ${m.fallbacks}`);
22899
+ if (m.loops > 0)
22900
+ lines.push(` loops: ${m.loops}`);
22901
+ if (m.conditionals > 0)
22902
+ lines.push(` conditionals: ${m.conditionals}`);
22903
+ if (m.eventHandlers > 0)
22904
+ lines.push(` event handlers: ${m.eventHandlers}`);
22905
+ lines.push("");
22906
+ lines.push(" Reactive budget (SR5):");
22907
+ const fanOutSuffix = m.hotSignal ? ` (${m.hotSignal})` : "";
22908
+ lines.push(` max signal fan-out: ${m.maxSignalFanOut}${fanOutSuffix}`);
22909
+ lines.push(` max memo chain depth: ${m.maxMemoChainDepth}`);
22910
+ lines.push(` total subscriptions: ${m.totalSubscriptions}`);
22911
+ if (m.batchCandidateCount > 0) {
22912
+ lines.push(` batch candidates: ${m.batchCandidateCount} handler(s) set ≥2 signals`);
22114
22913
  }
22115
- return out;
22116
- }
22117
- function collectModuleStringConsts(constants) {
22118
- const map = new Map;
22119
- const candidates = (constants ?? []).filter((c) => c.isModule && c.value !== undefined);
22120
- let progressed = true;
22121
- while (progressed) {
22122
- progressed = false;
22123
- for (const c of candidates) {
22124
- if (map.has(c.name))
22125
- continue;
22126
- const literal = parseStaticStringConst(c.value) ?? evalTemplateOfStringConsts(c.value, map);
22127
- if (literal !== null) {
22128
- map.set(c.name, literal);
22129
- progressed = true;
22914
+ if (profile.findings.length > 0) {
22915
+ lines.push("");
22916
+ lines.push(" Findings:");
22917
+ for (const f of profile.findings) {
22918
+ const icon = f.severity === "warning" ? "⚠" : "→";
22919
+ lines.push(` ${icon} [${f.kind}] ${f.message}`);
22920
+ lines.push(` fix: ${f.suggestion}`);
22921
+ if (f.loc) {
22922
+ const file = f.loc.file.split("/").pop() ?? f.loc.file;
22923
+ lines.push(` at ${file}:${f.loc.line}`);
22130
22924
  }
22131
22925
  }
22926
+ } else {
22927
+ lines.push("");
22928
+ lines.push(" No findings — component is within all thresholds.");
22132
22929
  }
22133
- return map;
22930
+ return lines.join(`
22931
+ `);
22134
22932
  }
22135
- function lookupStaticRecordLiteral(objectName, key, constants) {
22136
- const constInfo = (constants ?? []).find((c) => c.name === objectName && c.isModule);
22137
- if (constInfo?.value === undefined)
22138
- return null;
22139
- const sf = ts21.createSourceFile("__rec.ts", `(${constInfo.value})`, ts21.ScriptTarget.Latest, true);
22140
- if (sf.statements.length !== 1)
22141
- return null;
22142
- const stmt = sf.statements[0];
22143
- if (!ts21.isExpressionStatement(stmt))
22144
- return null;
22145
- let parsed = stmt.expression;
22146
- while (ts21.isParenthesizedExpression(parsed))
22147
- parsed = parsed.expression;
22148
- if (!ts21.isObjectLiteralExpression(parsed))
22149
- return null;
22150
- for (const prop of parsed.properties) {
22151
- if (!ts21.isPropertyAssignment(prop))
22152
- continue;
22153
- const name = prop.name;
22154
- const propKey = ts21.isIdentifier(name) || ts21.isStringLiteral(name) || ts21.isNoSubstitutionTemplateLiteral(name) ? name.text : null;
22155
- if (propKey !== key)
22156
- continue;
22157
- let v = prop.initializer;
22158
- while (ts21.isParenthesizedExpression(v))
22159
- v = v.expression;
22160
- if (ts21.isNumericLiteral(v))
22161
- return { kind: "number", text: v.text };
22162
- if (ts21.isStringLiteral(v) || ts21.isNoSubstitutionTemplateLiteral(v)) {
22163
- return { kind: "string", text: v.text };
22164
- }
22165
- return null;
22933
+ function formatProfileTable(profiles) {
22934
+ if (profiles.length === 0)
22935
+ return "No components found.";
22936
+ const sorted = [...profiles].sort((a, b) => b.metrics.totalSubscriptions - a.metrics.totalSubscriptions);
22937
+ const lines = [];
22938
+ lines.push("Component sig memo bind fall fanOut chain subs batch findings");
22939
+ lines.push("─".repeat(90));
22940
+ for (const p of sorted) {
22941
+ const m = p.metrics;
22942
+ const name = m.componentName.padEnd(23).slice(0, 23);
22943
+ const findingStr = p.findings.length > 0 ? p.findings.map((f) => f.kind.replace(/-/g, "_")).join(",") : "—";
22944
+ const row = [
22945
+ name,
22946
+ String(m.signals).padStart(3),
22947
+ String(m.memos).padStart(5),
22948
+ String(m.dynamicBindings).padStart(5),
22949
+ String(m.fallbacks).padStart(5),
22950
+ String(m.maxSignalFanOut).padStart(7),
22951
+ String(m.maxMemoChainDepth).padStart(6),
22952
+ String(m.totalSubscriptions).padStart(5),
22953
+ String(m.batchCandidateCount).padStart(6),
22954
+ ` ${findingStr}`
22955
+ ].join(" ");
22956
+ lines.push(row);
22166
22957
  }
22167
- return null;
22168
- }
22169
- function evalStringArrayJoin(source) {
22170
- const sf = ts21.createSourceFile("__join.ts", `const __x = (${source});`, ts21.ScriptTarget.Latest, false);
22171
- const stmt = sf.statements[0];
22172
- if (!stmt || !ts21.isVariableStatement(stmt))
22173
- return null;
22174
- let node = stmt.declarationList.declarations[0]?.initializer;
22175
- while (node && ts21.isParenthesizedExpression(node))
22176
- node = node.expression;
22177
- if (!node || !ts21.isCallExpression(node))
22178
- return null;
22179
- const callee = node.expression;
22180
- if (!ts21.isPropertyAccessExpression(callee))
22181
- return null;
22182
- if (callee.name.text !== "join")
22183
- return null;
22184
- let recv = callee.expression;
22185
- while (ts21.isParenthesizedExpression(recv))
22186
- recv = recv.expression;
22187
- if (!ts21.isArrayLiteralExpression(recv))
22188
- return null;
22189
- const parts = [];
22190
- for (const el of recv.elements) {
22191
- if (ts21.isStringLiteral(el) || ts21.isNoSubstitutionTemplateLiteral(el)) {
22192
- parts.push(el.text);
22193
- } else {
22194
- return null;
22958
+ const allFindings = sorted.flatMap((p) => p.findings.map((f) => ({ component: p.metrics.componentName, finding: f })));
22959
+ if (allFindings.length > 0) {
22960
+ lines.push("");
22961
+ lines.push("Findings:");
22962
+ for (const { component, finding } of allFindings) {
22963
+ const icon = finding.severity === "warning" ? "⚠" : "→";
22964
+ lines.push(` ${icon} ${component}: ${finding.message}`);
22965
+ lines.push(` fix: ${finding.suggestion}`);
22966
+ if (finding.loc) {
22967
+ const file = finding.loc.file.split("/").pop() ?? finding.loc.file;
22968
+ lines.push(` at ${file}:${finding.loc.line}`);
22969
+ }
22195
22970
  }
22971
+ } else {
22972
+ lines.push("");
22973
+ lines.push("No findings across all components.");
22196
22974
  }
22197
- let sep2 = ",";
22198
- if (node.arguments.length >= 1) {
22199
- const arg = node.arguments[0];
22200
- if (ts21.isStringLiteral(arg) || ts21.isNoSubstitutionTemplateLiteral(arg))
22201
- sep2 = arg.text;
22202
- else
22203
- return null;
22204
- }
22205
- return parts.join(sep2);
22975
+ return lines.join(`
22976
+ `);
22206
22977
  }
22207
- function parseRecordIndexAccess(val, localConstants, propsParams, resolveKey) {
22208
- if (!ts21.isElementAccessExpression(val))
22209
- return null;
22210
- const obj = val.expression;
22211
- const arg = val.argumentExpression;
22212
- if (!ts21.isIdentifier(obj) || !ts21.isIdentifier(arg))
22213
- return null;
22214
- let indexPropName;
22215
- let defaultKey;
22216
- const resolved = resolveKey?.(arg.text);
22217
- if (resolved) {
22218
- indexPropName = resolved.propName;
22219
- defaultKey = resolved.defaultLiteral;
22220
- } else if (propsParams.some((p) => p.name === arg.text)) {
22221
- indexPropName = arg.text;
22222
- } else {
22223
- return null;
22978
+ function formatProfileDiff(diff) {
22979
+ const lines = [];
22980
+ lines.push(`${diff.componentName} — reactive profile diff (before → after)`);
22981
+ lines.push("");
22982
+ if (diff.regressions.length === 0 && diff.improvements.length === 0 && diff.neutral.length === 0) {
22983
+ lines.push(" No changes in reactive metrics.");
22984
+ return lines.join(`
22985
+ `);
22224
22986
  }
22225
- const constInfo = localConstants.find((c) => c.name === obj.text && c.isModule);
22226
- if (constInfo?.value === undefined)
22227
- return null;
22228
- const sf = ts21.createSourceFile("__rec.ts", `(${constInfo.value})`, ts21.ScriptTarget.Latest, true);
22229
- if (sf.statements.length !== 1)
22230
- return null;
22231
- const stmt = sf.statements[0];
22232
- if (!ts21.isExpressionStatement(stmt))
22233
- return null;
22234
- let parsed = stmt.expression;
22235
- while (ts21.isParenthesizedExpression(parsed))
22236
- parsed = parsed.expression;
22237
- if (!ts21.isObjectLiteralExpression(parsed))
22238
- return null;
22239
- const entries = [];
22240
- for (const prop of parsed.properties) {
22241
- if (!ts21.isPropertyAssignment(prop))
22242
- return null;
22243
- let key;
22244
- if (ts21.isIdentifier(prop.name)) {
22245
- key = prop.name.text;
22246
- } else if (ts21.isStringLiteral(prop.name) || ts21.isNoSubstitutionTemplateLiteral(prop.name)) {
22247
- key = prop.name.text;
22248
- } else {
22249
- return null;
22987
+ if (diff.regressions.length > 0) {
22988
+ lines.push(" Regressions (reactive cost increased):");
22989
+ for (const e of diff.regressions) {
22990
+ lines.push(` ${e.metric}: ${e.before} → ${e.after} (+${e.delta})`);
22250
22991
  }
22251
- let v = prop.initializer;
22252
- while (ts21.isParenthesizedExpression(v))
22253
- v = v.expression;
22254
- if (ts21.isNumericLiteral(v)) {
22255
- entries.push({ key, value: { kind: "number", text: v.text } });
22256
- } else if (ts21.isStringLiteral(v) || ts21.isNoSubstitutionTemplateLiteral(v)) {
22257
- entries.push({ key, value: { kind: "string", text: v.text } });
22258
- } else {
22259
- return null;
22992
+ }
22993
+ if (diff.improvements.length > 0) {
22994
+ lines.push(" Improvements (reactive cost decreased):");
22995
+ for (const e of diff.improvements) {
22996
+ lines.push(` ${e.metric}: ${e.before} ${e.after} (${e.delta})`);
22260
22997
  }
22261
22998
  }
22262
- return { indexPropName, entries, defaultKey };
22999
+ if (diff.neutral.length > 0) {
23000
+ lines.push(" Structural changes (count changes, no clear direction):");
23001
+ for (const e of diff.neutral) {
23002
+ const sign = (e.delta ?? 0) > 0 ? "+" : "";
23003
+ lines.push(` ${e.metric}: ${e.before} → ${e.after} (${sign}${e.delta})`);
23004
+ }
23005
+ }
23006
+ return lines.join(`
23007
+ `);
23008
+ }
23009
+ function profileToJSON(profile) {
23010
+ return {
23011
+ metrics: profile.metrics,
23012
+ findings: profile.findings
23013
+ };
22263
23014
  }
22264
23015
 
22265
23016
  // src/index.ts
@@ -22292,6 +23043,7 @@ export {
22292
23043
  parseBlockBodyTolerant,
22293
23044
  parseBlockBody,
22294
23045
  needsTypeBasedDetection,
23046
+ materializeGetterCalls,
22295
23047
  matchSearchParamsMethodCall,
22296
23048
  matchQueryHrefCall,
22297
23049
  matchLoweringCall,
@@ -22301,8 +23053,10 @@ export {
22301
23053
  listComponentFunctions,
22302
23054
  jsxToIR,
22303
23055
  joinProfilerEvents,
23056
+ isValidHelperId,
22304
23057
  isSupported,
22305
23058
  isLowerableObjectRestDestructure,
23059
+ isLowerableLoopDestructure,
22306
23060
  isBooleanAttr,
22307
23061
  importsSearchParams,
22308
23062
  identifierPath,
@@ -22315,6 +23069,7 @@ export {
22315
23069
  generateClientJsWithSourceMap,
22316
23070
  generateClientJs,
22317
23071
  freeVarsInBody,
23072
+ freeIdentifiers,
22318
23073
  formatWhyUpdate,
22319
23074
  formatWastedReReruns,
22320
23075
  formatUpdatePath,
@@ -22343,6 +23098,8 @@ export {
22343
23098
  exprToString,
22344
23099
  evaluateProfileGates,
22345
23100
  evalStringArrayJoin,
23101
+ envSignalReaderFor,
23102
+ envSignalLocalNames,
22346
23103
  enableCompilerInstrumentation,
22347
23104
  emitParsedExpr,
22348
23105
  emitIRNode,
@@ -22355,6 +23112,7 @@ export {
22355
23112
  createProgramForCorpus,
22356
23113
  createError,
22357
23114
  containsHigherOrder,
23115
+ computeSsrSeedPlan,
22358
23116
  compileJSX,
22359
23117
  combineParentChildClientJs,
22360
23118
  collectModuleStringConsts,
@@ -22390,6 +23148,7 @@ export {
22390
23148
  PROFILE_SCHEMA_VERSION,
22391
23149
  JsxAdapter,
22392
23150
  ErrorCodes,
23151
+ ENV_SIGNAL_READERS,
22393
23152
  CALLBACK_METHODS,
22394
23153
  BaseAdapter,
22395
23154
  BUILTIN_LOWERING_PLUGINS,