@barefootjs/jsx 0.26.4 → 0.27.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 (73) hide show
  1. package/dist/compiler.d.ts.map +1 -1
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +321 -104
  5. package/dist/ir-to-client-js/client-only-elision.d.ts +103 -0
  6. package/dist/ir-to-client-js/client-only-elision.d.ts.map +1 -0
  7. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  8. package/dist/ir-to-client-js/control-flow/shared.d.ts +1 -1
  9. package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts +54 -0
  10. package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts.map +1 -0
  11. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/control-flow/stringify/insert.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +17 -20
  15. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +63 -4
  17. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow.d.ts +1 -1
  19. package/dist/ir-to-client-js/element-refs.d.ts +8 -3
  20. package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/emit-reactive.d.ts +8 -1
  22. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/html-template.d.ts +24 -0
  24. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/imports.d.ts +2 -2
  26. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  28. package/dist/ir-to-client-js/types.d.ts +9 -1
  29. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  30. package/dist/ir-to-client-js/utils.d.ts +1 -1
  31. package/dist/types.d.ts +25 -2
  32. package/dist/types.d.ts.map +1 -1
  33. package/package.json +2 -2
  34. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +398 -671
  35. package/src/__tests__/child-components-in-map.test.ts +4 -4
  36. package/src/__tests__/client-js-generation.test.ts +19 -11
  37. package/src/__tests__/composite-branch-loop.test.ts +2 -2
  38. package/src/__tests__/cross-file-client-signal.test.ts +5 -1
  39. package/src/__tests__/early-return-scope-var-ref.test.ts +10 -11
  40. package/src/__tests__/inline-jsx-callback.test.ts +6 -4
  41. package/src/__tests__/ir-jsx-props.test.ts +6 -5
  42. package/src/__tests__/loop-fallback-wrap.test.ts +3 -4
  43. package/src/__tests__/module-level-signal.audit.test.ts +7 -2
  44. package/src/__tests__/nested-loop-conditional.test.ts +9 -7
  45. package/src/__tests__/nested-loop-reactive-attrs.test.ts +2 -2
  46. package/src/__tests__/preamble-region-patch.test.ts +18 -12
  47. package/src/__tests__/profile-cond-binding-ids.test.ts +3 -2
  48. package/src/__tests__/profile-loop-binding-ids.test.ts +4 -3
  49. package/src/__tests__/profile-nested-binding-ids.test.ts +11 -6
  50. package/src/__tests__/reactive-attrs-in-map.test.ts +6 -4
  51. package/src/__tests__/template-closure.test.ts +1 -1
  52. package/src/compiler.ts +11 -0
  53. package/src/index.ts +1 -0
  54. package/src/ir-to-client-js/client-only-elision.ts +273 -0
  55. package/src/ir-to-client-js/collect-elements.ts +8 -4
  56. package/src/ir-to-client-js/control-flow/shared.ts +1 -1
  57. package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +12 -4
  58. package/src/ir-to-client-js/control-flow/stringify/claim-plan.ts +68 -0
  59. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +18 -9
  60. package/src/ir-to-client-js/control-flow/stringify/insert.ts +17 -11
  61. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +35 -16
  62. package/src/ir-to-client-js/control-flow/stringify/loop.ts +48 -46
  63. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +241 -31
  64. package/src/ir-to-client-js/control-flow.ts +1 -1
  65. package/src/ir-to-client-js/element-refs.ts +9 -11
  66. package/src/ir-to-client-js/emit-reactive.ts +50 -14
  67. package/src/ir-to-client-js/html-template.ts +29 -9
  68. package/src/ir-to-client-js/imports.ts +7 -4
  69. package/src/ir-to-client-js/reactivity.ts +3 -2
  70. package/src/ir-to-client-js/types.ts +10 -2
  71. package/src/ir-to-client-js/utils.ts +1 -1
  72. package/src/jsx-to-ir.ts +8 -7
  73. package/src/types.ts +25 -2
package/dist/index.js CHANGED
@@ -3505,6 +3505,10 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
3505
3505
  case "expression": {
3506
3506
  if (node.expr === "null" || node.expr === "undefined")
3507
3507
  return "";
3508
+ if (node.markerless) {
3509
+ const bare = wrapInterpolation(wrapExpr(node.expr));
3510
+ return `\${${bare}}`;
3511
+ }
3508
3512
  const inner = wrapInterpolation(wrapExpr(node.expr));
3509
3513
  const valueExpr = node.joinArrayChild ? `Array.isArray(${inner}) ? ${inner}.join('') : (${inner} ?? '')` : inner;
3510
3514
  if (node.slotId) {
@@ -4380,7 +4384,7 @@ function generateCsrTemplateWithOpts(node, opts) {
4380
4384
  if (node.expr === "null" || node.expr === "undefined")
4381
4385
  return "";
4382
4386
  if (node.clientOnly && node.slotId) {
4383
- return `<!--bf-client:${node.slotId}--><!--/-->`;
4387
+ return `<!--bf:${node.slotId}--><!--/-->`;
4384
4388
  }
4385
4389
  {
4386
4390
  const transformed = transformExpr(node.expr, node.templateExpr);
@@ -14402,7 +14406,11 @@ function collectElements(node, ctx, siblingOffsets, insideConditional = false) {
14402
14406
  },
14403
14407
  expression: ({ node: ex, scope: inCond }) => {
14404
14408
  if (ex.clientOnly && ex.slotId) {
14405
- ctx.clientOnlyElements.push({ slotId: ex.slotId, expression: ex.expr });
14409
+ ctx.clientOnlyElements.push({
14410
+ slotId: ex.slotId,
14411
+ expression: ex.expr,
14412
+ elidedPath: ex.markerless ? ex.elidedPath : undefined
14413
+ });
14406
14414
  return;
14407
14415
  }
14408
14416
  if (!ex.slotId || inCond)
@@ -15279,13 +15287,11 @@ var RUNTIME_IMPORT_CANDIDATES = [
15279
15287
  "onMount",
15280
15288
  "hydrate",
15281
15289
  "insert",
15282
- "reconcileElements",
15283
15290
  "getLoopChildren",
15284
15291
  "getLoopNodes",
15285
15292
  "mapArray",
15286
15293
  "mapArrayAnchored",
15287
15294
  "patchLeaf",
15288
- "patchSlotRange",
15289
15295
  "createDisposableEffect",
15290
15296
  "createComponent",
15291
15297
  "renderChild",
@@ -15293,7 +15299,6 @@ var RUNTIME_IMPORT_CANDIDATES = [
15293
15299
  "registerTemplate",
15294
15300
  "initChild",
15295
15301
  "upsertChild",
15296
- "updateClientMarker",
15297
15302
  "createPortal",
15298
15303
  "provideContext",
15299
15304
  "createContext",
@@ -15305,6 +15310,7 @@ var RUNTIME_IMPORT_CANDIDATES = [
15305
15310
  "styleToCss",
15306
15311
  "escapeAttr",
15307
15312
  "escapeText",
15313
+ "escapeTextOrNode",
15308
15314
  "qsa",
15309
15315
  "qsaItem",
15310
15316
  "qsaChildScope",
@@ -15313,7 +15319,8 @@ var RUNTIME_IMPORT_CANDIDATES = [
15313
15319
  "__slot",
15314
15320
  "__bfSlot",
15315
15321
  "__bfText",
15316
- "tAfter",
15322
+ "claimSlots",
15323
+ "lazySlots",
15317
15324
  "beginTurn",
15318
15325
  "endTurn",
15319
15326
  "date",
@@ -18484,6 +18491,22 @@ function buildArmBody(branch, options) {
18484
18491
 
18485
18492
  // src/ir-to-client-js/emit-reactive.ts
18486
18493
  import ts14 from "typescript";
18494
+
18495
+ // src/ir-to-client-js/control-flow/stringify/claim-plan.ts
18496
+ function slotSpecLiteral(slot) {
18497
+ const pathSrc = slot.pathExpr ?? `[${slot.path.join(", ")}]`;
18498
+ const markerlessSrc = slot.markerless ? ", markerless: true" : "";
18499
+ return `{ id: '${slot.id}', kind: '${slot.kind}', path: ${pathSrc}${markerlessSrc} }`;
18500
+ }
18501
+ function claimPlanLiteral(slots) {
18502
+ return `[${slots.map(slotSpecLiteral).join(", ")}]`;
18503
+ }
18504
+ function claimWriterVarName(slots, sanitize) {
18505
+ const first = slots[0]?.id ?? "0";
18506
+ return `__bfw_${sanitize(first)}`;
18507
+ }
18508
+
18509
+ // src/ir-to-client-js/emit-reactive.ts
18487
18510
  function bindingIdArg(ctx, slotId) {
18488
18511
  if (!ctx.profile || !slotId)
18489
18512
  return "";
@@ -18667,17 +18690,18 @@ function emitDynamicTextUpdates(lines, ctx) {
18667
18690
  const conditionalElems = elems.filter((e) => e.insideConditional);
18668
18691
  const normalElems = elems.filter((e) => !e.insideConditional);
18669
18692
  if (normalElems.length > 0 || conditionalElems.length > 0) {
18670
- for (const elem of normalElems) {
18671
- const v = varSlotId(elem.slotId);
18672
- lines.push(` let __anchor_${v} = _${v}`);
18673
- }
18674
18693
  const __textSlot = (normalElems[0] ?? conditionalElems[0])?.slotId;
18694
+ let writer = "";
18695
+ if (normalElems.length > 0) {
18696
+ const slots = normalElems.map((elem) => ({ id: elem.slotId, kind: "markup", path: [] }));
18697
+ writer = claimWriterVarName(slots, varSlotId);
18698
+ lines.push(` const ${writer} = lazySlots(__scope, ${claimPlanLiteral(slots)})`);
18699
+ }
18675
18700
  lines.push(` createEffect(() => {`);
18676
18701
  if (normalElems.length > 0) {
18677
18702
  lines.push(` const __val = ${expr}`);
18678
18703
  for (const elem of normalElems) {
18679
- const v = varSlotId(elem.slotId);
18680
- lines.push(` __anchor_${v} = __bfText(__anchor_${v}, __val)`);
18704
+ lines.push(` ${writer}('${elem.slotId}', escapeTextOrNode(__val))`);
18681
18705
  }
18682
18706
  for (const elem of conditionalElems) {
18683
18707
  const v = varSlotId(elem.slotId);
@@ -18700,10 +18724,13 @@ function emitDynamicTextUpdates(lines, ctx) {
18700
18724
  }
18701
18725
  function emitClientOnlyExpressions(lines, ctx) {
18702
18726
  for (const elem of ctx.clientOnlyElements) {
18727
+ const slots = elem.elidedPath ? [{ id: elem.slotId, kind: "text", path: elem.elidedPath, markerless: true }] : [{ id: elem.slotId, kind: "text", path: [] }];
18728
+ const writer = claimWriterVarName(slots, varSlotId);
18703
18729
  lines.push(` // @client: ${elem.slotId}`);
18730
+ lines.push(` { const ${writer} = lazySlots(__scope, ${claimPlanLiteral(slots)})`);
18704
18731
  lines.push(` createEffect(() => {`);
18705
- lines.push(` updateClientMarker(__scope, '${elem.slotId}', ${elem.expression})`);
18706
- lines.push(` }${bindingIdArg(ctx, elem.slotId)})`);
18732
+ lines.push(` ${writer}('${elem.slotId}', ${elem.expression})`);
18733
+ lines.push(` }${bindingIdArg(ctx, elem.slotId)}) }`);
18707
18734
  lines.push("");
18708
18735
  }
18709
18736
  }
@@ -18861,13 +18888,18 @@ function stringifyBranchInnerLoops(lines, plan, indent, pc) {
18861
18888
  if (inner.legacyComponents.length > 0 || inner.legacyEvents.length > 0) {
18862
18889
  emitComponentAndEventSetup(lines, `${indent} `, `__bel${uid}`, [...inner.legacyComponents], [...inner.legacyEvents], inner.outerLoopParam, inner.outerLoopParamBindings);
18863
18890
  }
18864
- for (const text of inner.reactiveTexts) {
18891
+ const conditionalTexts = inner.reactiveTexts.filter((t) => t.insideConditional);
18892
+ const plainTexts = inner.reactiveTexts.filter((t) => !t.insideConditional);
18893
+ for (const text of conditionalTexts) {
18865
18894
  const bf = profileBindingId(pc, text.slotId);
18866
- if (text.insideConditional) {
18867
- lines.push(`${indent} createEffect(() => { const [__rt] = $t(__bel${uid}, '${text.slotId}'); if (__rt) __rt.textContent = String(${text.wrappedExpression}) }${bf})`);
18868
- } else {
18869
- lines.push(`${indent} { const [__rt] = $t(__bel${uid}, '${text.slotId}')`);
18870
- lines.push(`${indent} if (__rt) createEffect(() => { __rt.textContent = String(${text.wrappedExpression}) }${bf}) }`);
18895
+ lines.push(`${indent} createEffect(() => { claimSlots(__bel${uid}, [{ id: '${text.slotId}', kind: 'text', path: [] }]).write('${text.slotId}', String(${text.wrappedExpression})) }${bf})`);
18896
+ }
18897
+ if (plainTexts.length > 0) {
18898
+ const slots = plainTexts.map((t) => ({ id: t.slotId, kind: "text", path: [] }));
18899
+ const writer = claimWriterVarName(slots, varSlotId);
18900
+ lines.push(`${indent} const ${writer} = lazySlots(__bel${uid}, ${claimPlanLiteral(slots)})`);
18901
+ for (const text of plainTexts) {
18902
+ lines.push(`${indent} createEffect(() => { ${writer}('${text.slotId}', String(${text.wrappedExpression})) }${profileBindingId(pc, text.slotId)})`);
18871
18903
  }
18872
18904
  }
18873
18905
  if (inner.nestedConditionals.length > 0) {
@@ -18910,24 +18942,41 @@ function stringifyLoopChildArm(lines, arm, armIndent, pc) {
18910
18942
  stringifyLoopChildConditional(lines, cond, `${armIndent} `, pc);
18911
18943
  lines.push(`${armIndent}}))`);
18912
18944
  }
18913
- for (const text of arm.texts) {
18914
- const varName = `__rt_${varSlotId(text.slotId)}`;
18915
- lines.push(`${armIndent}let ${varName} = $t(__branchScope, '${text.slotId}')[0]`);
18916
- lines.push(`${armIndent}__disposers.push(createDisposableEffect(() => { ${varName} = __bfText(${varName}, ${text.wrappedExpression}) }${profileBindingId(pc, text.slotId)}))`);
18945
+ if (arm.texts.length > 0) {
18946
+ const slots = arm.texts.map((t) => ({ id: t.slotId, kind: "markup", path: [] }));
18947
+ const writer = claimWriterVarName(slots, varSlotId);
18948
+ lines.push(`${armIndent}const ${writer} = lazySlots(__branchScope, ${claimPlanLiteral(slots)})`);
18949
+ for (const text of arm.texts) {
18950
+ lines.push(`${armIndent}__disposers.push(createDisposableEffect(() => { ${writer}('${text.slotId}', escapeTextOrNode(${text.wrappedExpression})) }${profileBindingId(pc, text.slotId)}))`);
18951
+ }
18917
18952
  }
18918
18953
  lines.push(`${armIndent}return () => __disposers.forEach(d => d())`);
18919
18954
  }
18920
18955
 
18921
18956
  // src/ir-to-client-js/control-flow/stringify/reactive-effects.ts
18922
18957
  function stringifyReactiveEffects(lines, plan, opts) {
18923
- const { indent, elVar, bodyIsMultiRoot, elementIndexBySlot, textIndexBySlot } = opts;
18958
+ const { indent, elVar, bodyIsMultiRoot, elementIndexBySlot, textClaimPathExprs, preambleRegions = [], mapPreambleWrapped } = opts;
18924
18959
  const lookup = bodyIsMultiRoot ? "qsaItem" : "qsa";
18925
- const pc = plan.profileComponentName;
18960
+ const pc = plan?.profileComponentName;
18926
18961
  const bindingBfId = (slotId) => profileBindingId(pc, slotId);
18927
- for (const slot of plan.attrSlots) {
18962
+ const attrSlots = plan?.attrSlots ?? [];
18963
+ const outerTexts = plan?.outerTexts ?? [];
18964
+ const conditionals = plan?.conditionals ?? [];
18965
+ if (pc) {
18966
+ emitAttrSlotsGranular(lines, indent, elVar, lookup, attrSlots, elementIndexBySlot, bindingBfId);
18967
+ emitOuterTexts(lines, indent, elVar, outerTexts, bindingBfId, textClaimPathExprs);
18968
+ emitPreambleRegionsEffect(lines, indent, elVar, preambleRegions, mapPreambleWrapped);
18969
+ } else {
18970
+ emitConsolidatedRowEffect(lines, indent, elVar, lookup, attrSlots, outerTexts, elementIndexBySlot, textClaimPathExprs, preambleRegions, mapPreambleWrapped);
18971
+ }
18972
+ for (const cond of conditionals) {
18973
+ emitOuterConditional(lines, indent, elVar, cond, pc);
18974
+ }
18975
+ }
18976
+ function emitAttrSlotsGranular(lines, indent, elVar, lookup, attrSlots, elementIndexBySlot, bindingBfId) {
18977
+ for (const slot of attrSlots) {
18928
18978
  const varName = `__ra_${varSlotId(slot.slotId)}`;
18929
- const pIdx = elementIndexBySlot?.get(slot.slotId);
18930
- const lookupExpr = pIdx !== undefined ? `__p ? __p[${pIdx}] : ${lookup}(${elVar}, '[bf="${slot.slotId}"]')` : `${lookup}(${elVar}, '[bf="${slot.slotId}"]')`;
18979
+ const lookupExpr = attrLookupExpr(slot.slotId, varName, elVar, lookup, elementIndexBySlot);
18931
18980
  lines.push(`${indent}{ const ${varName} = ${lookupExpr}`);
18932
18981
  lines.push(`${indent}if (${varName}) {`);
18933
18982
  for (const attr of slot.attrs) {
@@ -18939,21 +18988,83 @@ function stringifyReactiveEffects(lines, plan, opts) {
18939
18988
  }
18940
18989
  lines.push(`${indent}} }`);
18941
18990
  }
18942
- for (const text of plan.outerTexts) {
18943
- emitOuterText(lines, indent, elVar, text, bindingBfId(text.slotId), textIndexBySlot?.get(text.slotId));
18991
+ }
18992
+ function emitPreambleRegionsEffect(lines, indent, elVar, preambleRegions, mapPreambleWrapped) {
18993
+ if (preambleRegions.length === 0)
18994
+ return;
18995
+ const slots = preambleRegions.map((r) => ({ id: r.slotId, kind: "markup", path: [] }));
18996
+ const writer = claimWriterVarName(slots, varSlotId);
18997
+ lines.push(`${indent}const ${writer} = lazySlots(${elVar}, ${claimPlanLiteral(slots)})`);
18998
+ lines.push(`${indent}createEffect(() => {`);
18999
+ if (mapPreambleWrapped)
19000
+ lines.push(`${indent} ${mapPreambleWrapped}`);
19001
+ for (const region of preambleRegions) {
19002
+ lines.push(`${indent} ${writer}('${region.slotId}', ${region.valueExpr})`);
18944
19003
  }
18945
- for (const cond of plan.conditionals) {
18946
- emitOuterConditional(lines, indent, elVar, cond, pc);
19004
+ lines.push(`${indent}})`);
19005
+ }
19006
+ function attrLookupExpr(slotId, varName, elVar, lookup, elementIndexBySlot) {
19007
+ const pIdx = elementIndexBySlot?.get(slotId);
19008
+ return pIdx !== undefined ? `__p ? __p[${pIdx}] : ${lookup}(${elVar}, '[bf="${slotId}"]')` : `${lookup}(${elVar}, '[bf="${slotId}"]')`;
19009
+ }
19010
+ function emitConsolidatedRowEffect(lines, indent, elVar, lookup, attrSlots, outerTexts, elementIndexBySlot, textClaimPathExprs, preambleRegions, mapPreambleWrapped) {
19011
+ if (attrSlots.length === 0 && outerTexts.length === 0 && preambleRegions.length === 0)
19012
+ return;
19013
+ for (const slot of attrSlots) {
19014
+ const varName = `__ra_${varSlotId(slot.slotId)}`;
19015
+ lines.push(`${indent}const ${varName} = ${attrLookupExpr(slot.slotId, varName, elVar, lookup, elementIndexBySlot)}`);
19016
+ }
19017
+ const claimSlots = [
19018
+ ...outerTexts.map((t) => ({ id: t.slotId, kind: "text", path: [], pathExpr: textClaimPathExprs?.get(t.slotId) })),
19019
+ ...preambleRegions.map((r) => ({ id: r.slotId, kind: "markup", path: [] }))
19020
+ ];
19021
+ const writer = claimSlots.length > 0 ? claimWriterVarName(claimSlots, varSlotId) : null;
19022
+ if (writer) {
19023
+ lines.push(`${indent}const ${writer} = lazySlots(${elVar}, ${claimPlanLiteral(claimSlots)})`);
18947
19024
  }
19025
+ if (attrSlots.length === 0 && preambleRegions.length === 0 && outerTexts.length === 1) {
19026
+ const text = outerTexts[0];
19027
+ lines.push(`${indent}createEffect(() => { ${writer}('${text.slotId}', String(${text.wrappedExpression})) })`);
19028
+ return;
19029
+ }
19030
+ lines.push(`${indent}createEffect(() => {`);
19031
+ for (const slot of attrSlots) {
19032
+ const varName = `__ra_${varSlotId(slot.slotId)}`;
19033
+ lines.push(`${indent} if (${varName}) {`);
19034
+ for (const attr of slot.attrs) {
19035
+ lines.push(`${indent} {`);
19036
+ for (const stmt of emitAttrUpdate(varName, attr.attrName, attr.wrappedExpression, attr.meta)) {
19037
+ lines.push(`${indent} ${stmt}`);
19038
+ }
19039
+ lines.push(`${indent} }`);
19040
+ }
19041
+ lines.push(`${indent} }`);
19042
+ }
19043
+ if (preambleRegions.length > 0 && mapPreambleWrapped) {
19044
+ lines.push(`${indent} ${mapPreambleWrapped}`);
19045
+ }
19046
+ for (const text of outerTexts) {
19047
+ lines.push(`${indent} ${writer}('${text.slotId}', String(${text.wrappedExpression}))`);
19048
+ }
19049
+ for (const region of preambleRegions) {
19050
+ lines.push(`${indent} ${writer}('${region.slotId}', ${region.valueExpr})`);
19051
+ }
19052
+ lines.push(`${indent}})`);
18948
19053
  }
18949
- function emitOuterText(lines, indent, elVar, text, bfId = "", pIdx) {
18950
- const varName = `__rt_${varSlotId(text.slotId)}`;
18951
- if (pIdx !== undefined) {
18952
- lines.push(`${indent}{ const ${varName} = __p ? tAfter(__p[${pIdx}]) : $t(${elVar}, '${text.slotId}')[0]`);
18953
- } else {
18954
- lines.push(`${indent}{ const [${varName}] = $t(${elVar}, '${text.slotId}')`);
19054
+ function emitOuterTexts(lines, indent, elVar, texts, bindingBfId, textClaimPathExprs) {
19055
+ if (texts.length === 0)
19056
+ return;
19057
+ const slots = texts.map((t) => ({
19058
+ id: t.slotId,
19059
+ kind: "text",
19060
+ path: [],
19061
+ pathExpr: textClaimPathExprs?.get(t.slotId)
19062
+ }));
19063
+ const writer = claimWriterVarName(slots, varSlotId);
19064
+ lines.push(`${indent}const ${writer} = lazySlots(${elVar}, ${claimPlanLiteral(slots)})`);
19065
+ for (const text of texts) {
19066
+ lines.push(`${indent}createEffect(() => { ${writer}('${text.slotId}', String(${text.wrappedExpression})) }${bindingBfId(text.slotId)})`);
18955
19067
  }
18956
- lines.push(`${indent}if (${varName}) createEffect(() => { ${varName}.textContent = String(${text.wrappedExpression}) }${bfId}) }`);
18957
19068
  }
18958
19069
  function emitOuterConditional(lines, indent, elVar, cond, pc) {
18959
19070
  const armIndent = `${indent} `;
@@ -19074,22 +19185,6 @@ function emitLoopChildRefs(lines, refs, opts) {
19074
19185
  lines.push(`${indent}if (${varName}) ${emitRefCall(ref.callback, varName)} }`);
19075
19186
  }
19076
19187
  }
19077
- function emitPreambleRegionEffects(lines, regions, mapPreambleWrapped, opts) {
19078
- if (regions.length === 0)
19079
- return;
19080
- const { indent, elVar } = opts;
19081
- for (const region of regions) {
19082
- const v = varSlotId(region.slotId);
19083
- lines.push(`${indent}{ let __last_${v}`);
19084
- lines.push(`${indent}createEffect(() => {`);
19085
- if (mapPreambleWrapped)
19086
- lines.push(`${indent} ${mapPreambleWrapped}`);
19087
- lines.push(`${indent} const __html_${v} = ${region.valueExpr}`);
19088
- lines.push(`${indent} if (__last_${v} === undefined) { __last_${v} = __html_${v}; return }`);
19089
- lines.push(`${indent} if (__html_${v} !== __last_${v}) { __last_${v} = __html_${v}; patchSlotRange(${elVar}, '${region.slotId}', __html_${v}) }`);
19090
- lines.push(`${indent}}) }`);
19091
- }
19092
- }
19093
19188
  function stringifyLoop(lines, plan) {
19094
19189
  switch (plan.kind) {
19095
19190
  case "static":
@@ -19188,26 +19283,34 @@ function stringifyPlainLoop(lines, plan, topIndent = " ") {
19188
19283
  ...reactiveEffects?.attrSlots.map((s) => s.slotId) ?? [],
19189
19284
  ...childRefs.map((r) => r.childSlotId)
19190
19285
  ];
19191
- const textSlotIds = reactiveEffects?.outerTexts.map((t) => t.slotId) ?? [];
19192
- if (elementSlotIds.length > 0 || textSlotIds.length > 0) {
19193
- const built = buildSkeletonPathPlan(plan.skeletonPaths, "__el", { elementSlotIds, textSlotIds });
19286
+ if (elementSlotIds.length > 0) {
19287
+ const built = buildSkeletonPathPlan(plan.skeletonPaths, "__el", { elementSlotIds, textSlotIds: [] });
19194
19288
  if (built.arrayElems.length > 0) {
19195
19289
  pathPlan = built;
19196
19290
  lines.push(`${bodyIndent}const __p = __existing ? null : [${built.arrayElems.join(", ")}]`);
19197
19291
  }
19198
19292
  }
19199
19293
  }
19200
- if (reactiveEffects !== null) {
19294
+ const textClaimPathExprs = new Map;
19295
+ if (hoistedTpl && plan.skeletonPaths) {
19296
+ for (const text of reactiveEffects?.outerTexts ?? []) {
19297
+ const path = plan.skeletonPaths.textMarkerPaths.get(text.slotId);
19298
+ if (path)
19299
+ textClaimPathExprs.set(text.slotId, `__existing ? [] : [${path.join(", ")}]`);
19300
+ }
19301
+ }
19302
+ if (reactiveEffects !== null || preambleRegions.length > 0) {
19201
19303
  stringifyReactiveEffects(lines, reactiveEffects, {
19202
19304
  indent: bodyIndent,
19203
19305
  elVar: "__el",
19204
19306
  bodyIsMultiRoot,
19205
19307
  elementIndexBySlot: pathPlan?.elementIndexBySlot,
19206
- textIndexBySlot: pathPlan?.textIndexBySlot
19308
+ textClaimPathExprs,
19309
+ preambleRegions,
19310
+ mapPreambleWrapped
19207
19311
  });
19208
19312
  }
19209
19313
  emitLoopChildRefs(lines, childRefs, { indent: bodyIndent, elVar: "__el", bodyIsMultiRoot, elementIndexBySlot: pathPlan?.elementIndexBySlot });
19210
- emitPreambleRegionEffects(lines, preambleRegions, mapPreambleWrapped, { indent: bodyIndent, elVar: "__el" });
19211
19314
  lines.push(`${bodyIndent}return __el`);
19212
19315
  lines.push(`${topIndent}}, '${markerId}'${loopBfId})`);
19213
19316
  }
@@ -19316,10 +19419,13 @@ function stringifyStaticLoop(lines, plan) {
19316
19419
  }
19317
19420
  lines.push(` }`);
19318
19421
  }
19319
- for (const text of texts) {
19320
- const vn = `__rt_${varSlotId(text.slotId)}`;
19321
- lines.push(` { const [${vn}] = $t(__iterEl, '${text.slotId}')`);
19322
- lines.push(` if (${vn}) createEffect(() => { ${vn}.textContent = String(${text.expression}) }${profileBindingId(pc, text.slotId)}) }`);
19422
+ if (texts.length > 0) {
19423
+ const slots = texts.map((t) => ({ id: t.slotId, kind: "text", path: [] }));
19424
+ const writer = claimWriterVarName(slots, varSlotId);
19425
+ lines.push(` const ${writer} = lazySlots(__iterEl, ${claimPlanLiteral(slots)})`);
19426
+ for (const text of texts) {
19427
+ lines.push(` createEffect(() => { ${writer}('${text.slotId}', String(${text.expression})) }${profileBindingId(pc, text.slotId)})`);
19428
+ }
19323
19429
  }
19324
19430
  emitLoopChildRefs(lines, childRefs, { indent: " ", elVar: "__iterEl", bodyIsMultiRoot: false });
19325
19431
  lines.push(` }`);
@@ -19374,13 +19480,18 @@ function emitReactive(lines, inner, indent, pc) {
19374
19480
  if (inner.childLevels.length > 0) {
19375
19481
  stringifyInnerLoops(lines, inner.childLevels, `${indent} `, pc);
19376
19482
  }
19377
- for (const text of emit.reactiveTexts) {
19483
+ const conditionalTexts = emit.reactiveTexts.filter((t) => t.insideConditional);
19484
+ const plainTexts = emit.reactiveTexts.filter((t) => !t.insideConditional);
19485
+ for (const text of conditionalTexts) {
19378
19486
  const bf = profileBindingId(pc, text.slotId);
19379
- if (text.insideConditional) {
19380
- lines.push(`${indent} createEffect(() => { const [__rt] = $t(__innerEl${uid}, '${text.slotId}'); if (__rt) __rt.textContent = String(${text.wrappedExpression}) }${bf})`);
19381
- } else {
19382
- lines.push(`${indent} { const [__rt] = $t(__innerEl${uid}, '${text.slotId}')`);
19383
- lines.push(`${indent} if (__rt) createEffect(() => { __rt.textContent = String(${text.wrappedExpression}) }${bf}) }`);
19487
+ lines.push(`${indent} createEffect(() => { claimSlots(__innerEl${uid}, [{ id: '${text.slotId}', kind: 'text', path: [] }]).write('${text.slotId}', String(${text.wrappedExpression})) }${bf})`);
19488
+ }
19489
+ if (plainTexts.length > 0) {
19490
+ const slots = plainTexts.map((t) => ({ id: t.slotId, kind: "text", path: [] }));
19491
+ const writer = claimWriterVarName(slots, varSlotId);
19492
+ lines.push(`${indent} const ${writer} = lazySlots(__innerEl${uid}, ${claimPlanLiteral(slots)})`);
19493
+ for (const text of plainTexts) {
19494
+ lines.push(`${indent} createEffect(() => { ${writer}('${text.slotId}', String(${text.wrappedExpression})) }${profileBindingId(pc, text.slotId)})`);
19384
19495
  }
19385
19496
  }
19386
19497
  for (const attr of emit.reactiveAttrs) {
@@ -19751,11 +19862,16 @@ function emitPlain(lines, plan) {
19751
19862
  indent: " ",
19752
19863
  singleRootLayout: "inline"
19753
19864
  });
19754
- if (reactiveEffects !== null) {
19755
- stringifyReactiveEffects(lines, reactiveEffects, { indent: " ", elVar: "__el", bodyIsMultiRoot });
19865
+ if (reactiveEffects !== null || preambleRegions.length > 0) {
19866
+ stringifyReactiveEffects(lines, reactiveEffects, {
19867
+ indent: " ",
19868
+ elVar: "__el",
19869
+ bodyIsMultiRoot,
19870
+ preambleRegions,
19871
+ mapPreambleWrapped
19872
+ });
19756
19873
  }
19757
19874
  emitLoopChildRefs(lines, childRefs, { indent: " ", elVar: "__el", bodyIsMultiRoot });
19758
- emitPreambleRegionEffects(lines, preambleRegions, mapPreambleWrapped, { indent: " ", elVar: "__el" });
19759
19875
  lines.push(` return __el`);
19760
19876
  lines.push(` }, '${markerId}'${loopBfId})`);
19761
19877
  }
@@ -19841,13 +19957,13 @@ function emitArmBody(lines, body, mode, indent, profileComponentName) {
19841
19957
  }
19842
19958
  lines.push(`${indent}} }`);
19843
19959
  }
19844
- for (const te of body.textEffects) {
19845
- const v = varSlotId(te.slotId);
19846
- lines.push(`${indent}let __anchor_${v} = $t(__branchScope, '${te.slotId}')[0]`);
19847
- lines.push(`${indent}__disposers.push(createDisposableEffect(() => {`);
19848
- lines.push(`${indent} const __val = ${te.expression}`);
19849
- lines.push(`${indent} __anchor_${v} = __bfText(__anchor_${v}, __val)`);
19850
- lines.push(`${indent}}${bindingBfId(te.slotId)}))`);
19960
+ if (body.textEffects.length > 0) {
19961
+ const slots = body.textEffects.map((te) => ({ id: te.slotId, kind: "markup", path: [] }));
19962
+ const writer = claimWriterVarName(slots, varSlotId);
19963
+ lines.push(`${indent}const ${writer} = lazySlots(__branchScope, ${claimPlanLiteral(slots)})`);
19964
+ for (const te of body.textEffects) {
19965
+ lines.push(`${indent}__disposers.push(createDisposableEffect(() => { ${writer}('${te.slotId}', escapeTextOrNode(${te.expression})) }${bindingBfId(te.slotId)}))`);
19966
+ }
19851
19967
  }
19852
19968
  if (body.loops.length > 0) {
19853
19969
  stringifyBranchLoops(lines, body.loops);
@@ -20079,7 +20195,6 @@ function emitLoopEventDelegation(lines, elem, kind, profileComponentName) {
20079
20195
  // src/ir-to-client-js/element-refs.ts
20080
20196
  function generateElementRefs(ctx) {
20081
20197
  const regularSlots = new Set;
20082
- const textSlots = new Set;
20083
20198
  const componentSlots = new Set;
20084
20199
  const conditionalSlotIds = collectConditionalSlotIds(ctx);
20085
20200
  for (const elem of ctx.interactiveElements) {
@@ -20087,11 +20202,6 @@ function generateElementRefs(ctx) {
20087
20202
  regularSlots.add(elem.slotId);
20088
20203
  }
20089
20204
  }
20090
- for (const elem of ctx.dynamicElements) {
20091
- if (!elem.insideConditional) {
20092
- textSlots.add(elem.slotId);
20093
- }
20094
- }
20095
20205
  for (const elem of ctx.conditionalElements) {
20096
20206
  regularSlots.add(elem.slotId);
20097
20207
  }
@@ -20120,11 +20230,10 @@ function generateElementRefs(ctx) {
20120
20230
  for (const slotId of componentSlots) {
20121
20231
  regularSlots.delete(slotId);
20122
20232
  }
20123
- if (regularSlots.size === 0 && textSlots.size === 0 && componentSlots.size === 0)
20233
+ if (regularSlots.size === 0 && componentSlots.size === 0)
20124
20234
  return "";
20125
20235
  const refLines = [];
20126
20236
  emitSlotRefs(refLines, [...regularSlots], "$");
20127
- emitSlotRefs(refLines, [...textSlots], "$t");
20128
20237
  emitSlotRefs(refLines, [...componentSlots], "$c");
20129
20238
  return refLines.join(`
20130
20239
  `);
@@ -20680,6 +20789,111 @@ ${externalImports.join(`
20680
20789
  return generatedCode.replace(IMPORT_PLACEHOLDER, allImports);
20681
20790
  }
20682
20791
 
20792
+ // src/ir-to-client-js/client-only-elision.ts
20793
+ function decideClientOnlyElision(root) {
20794
+ walkNode(root, [], new Set, { bailed: false });
20795
+ }
20796
+ function walkNode(node, path, forceCloseAncestors, state) {
20797
+ if (state.bailed)
20798
+ return;
20799
+ if (node.type === "element") {
20800
+ if (!elementIsPathSafe(node.tag, flattenSkeletonChildren(node.children))) {
20801
+ state.bailed = true;
20802
+ return;
20803
+ }
20804
+ const groupIdx = skeletonForceCloseGroup(node.tag);
20805
+ if (groupIdx >= 0 && forceCloseAncestors.has(groupIdx)) {
20806
+ state.bailed = true;
20807
+ return;
20808
+ }
20809
+ const nextAncestors = groupIdx >= 0 ? new Set([...forceCloseAncestors, groupIdx]) : forceCloseAncestors;
20810
+ walkChildren(flattenSkeletonChildren(node.children), path, nextAncestors, state);
20811
+ } else if (node.type === "fragment") {
20812
+ walkChildren(flattenSkeletonChildren(node.children), path, forceCloseAncestors, state);
20813
+ }
20814
+ }
20815
+ function walkChildren(children, parentPath, forceCloseAncestors, state) {
20816
+ let frozen = false;
20817
+ let idx = 0;
20818
+ let pendingText = false;
20819
+ for (let i = 0;i < children.length; i++) {
20820
+ if (state.bailed)
20821
+ return;
20822
+ const child = children[i];
20823
+ switch (child.type) {
20824
+ case "text": {
20825
+ if (child.value === "")
20826
+ continue;
20827
+ if (!pendingText)
20828
+ idx += 1;
20829
+ pendingText = true;
20830
+ continue;
20831
+ }
20832
+ case "expression": {
20833
+ if (child.expr === "null" || child.expr === "undefined")
20834
+ continue;
20835
+ if (child.clientOnly && child.slotId) {
20836
+ const adjacent = isTextLike(children[i - 1]) || isTextLike(children[i + 1]);
20837
+ if (!frozen && !adjacent) {
20838
+ markElided(child, [...parentPath, idx]);
20839
+ frozen = true;
20840
+ } else {
20841
+ frozen = true;
20842
+ }
20843
+ idx += 1;
20844
+ pendingText = false;
20845
+ continue;
20846
+ }
20847
+ frozen = true;
20848
+ idx += 1;
20849
+ pendingText = false;
20850
+ continue;
20851
+ }
20852
+ case "element": {
20853
+ if (!elementIsPathSafe(child.tag, flattenSkeletonChildren(child.children))) {
20854
+ state.bailed = true;
20855
+ return;
20856
+ }
20857
+ if (!frozen) {
20858
+ walkNode(child, [...parentPath, idx], forceCloseAncestors, state);
20859
+ }
20860
+ idx += 1;
20861
+ pendingText = false;
20862
+ continue;
20863
+ }
20864
+ case "fragment":
20865
+ continue;
20866
+ default:
20867
+ frozen = true;
20868
+ idx += 1;
20869
+ pendingText = false;
20870
+ continue;
20871
+ }
20872
+ }
20873
+ }
20874
+ function isTextLike(node) {
20875
+ if (!node)
20876
+ return false;
20877
+ if (node.type === "text")
20878
+ return node.value !== "";
20879
+ if (node.type === "expression")
20880
+ return node.expr !== "null" && node.expr !== "undefined";
20881
+ return false;
20882
+ }
20883
+ function markElided(expr, path) {
20884
+ expr.markerless = true;
20885
+ expr.elidedPath = path;
20886
+ }
20887
+ function elementIsPathSafe(tag, flatChildren) {
20888
+ if (SKELETON_PATH_HAZARD_TAGS.has(tag))
20889
+ return false;
20890
+ if (VOID_ELEMENTS.has(tag) && flatChildren.length > 0)
20891
+ return false;
20892
+ if (tag === "tr" && hasForeignTableRowContent(flatChildren))
20893
+ return false;
20894
+ return true;
20895
+ }
20896
+
20683
20897
  // src/css-layer-prefixer.ts
20684
20898
  function prefixClass(cls, layerName) {
20685
20899
  if (!cls || cls.startsWith("layer-"))
@@ -21972,7 +22186,7 @@ function checkRichTypeMethodCalls(root, metadata, errors) {
21972
22186
  return;
21973
22187
  const matchers = prepareLoweringMatchers(metadata);
21974
22188
  const seen = new Set;
21975
- walkNode(root, metadata, EMPTY_BINDINGS2, matchers, errors, seen);
22189
+ walkNode2(root, metadata, EMPTY_BINDINGS2, matchers, errors, seen);
21976
22190
  }
21977
22191
  function isLoweringClaimed(matchers, callee, args) {
21978
22192
  return matchers.some((m) => m(callee, args) !== null);
@@ -22109,7 +22323,7 @@ function walkAttrValue(value, clientOnly, loc, meta, bindings, matchers, errors,
22109
22323
  walkTemplateParts(value.parts, loc, meta, bindings, matchers, errors, seen);
22110
22324
  }
22111
22325
  }
22112
- function walkNode(node, meta, bindings, matchers, errors, seen) {
22326
+ function walkNode2(node, meta, bindings, matchers, errors, seen) {
22113
22327
  if (node.type === "expression") {
22114
22328
  if (!node.clientOnly && node.parsed)
22115
22329
  checkExpr(node.parsed, node.loc, meta, bindings, matchers, errors, seen);
@@ -22135,12 +22349,12 @@ function walkNode(node, meta, bindings, matchers, errors, seen) {
22135
22349
  case "fragment":
22136
22350
  case "provider":
22137
22351
  for (const child of node.children)
22138
- walkNode(child, meta, bindings, matchers, errors, seen);
22352
+ walkNode2(child, meta, bindings, matchers, errors, seen);
22139
22353
  break;
22140
22354
  case "async":
22141
- walkNode(node.fallback, meta, bindings, matchers, errors, seen);
22355
+ walkNode2(node.fallback, meta, bindings, matchers, errors, seen);
22142
22356
  for (const child of node.children)
22143
- walkNode(child, meta, bindings, matchers, errors, seen);
22357
+ walkNode2(child, meta, bindings, matchers, errors, seen);
22144
22358
  break;
22145
22359
  case "loop": {
22146
22360
  if (node.clientOnly)
@@ -22153,35 +22367,35 @@ function walkNode(node, meta, bindings, matchers, errors, seen) {
22153
22367
  if (node.index)
22154
22368
  loopBindings.set(node.index, null);
22155
22369
  for (const child of node.children)
22156
- walkNode(child, meta, loopBindings, matchers, errors, seen);
22370
+ walkNode2(child, meta, loopBindings, matchers, errors, seen);
22157
22371
  if (node.childComponent) {
22158
22372
  for (const child of node.childComponent.children)
22159
- walkNode(child, meta, loopBindings, matchers, errors, seen);
22373
+ walkNode2(child, meta, loopBindings, matchers, errors, seen);
22160
22374
  }
22161
22375
  for (const nested of node.nestedComponents ?? []) {
22162
22376
  for (const child of nested.children)
22163
- walkNode(child, meta, loopBindings, matchers, errors, seen);
22377
+ walkNode2(child, meta, loopBindings, matchers, errors, seen);
22164
22378
  }
22165
22379
  for (const seg of node.flatMapCallback?.segments ?? []) {
22166
22380
  if (seg.kind === "jsx")
22167
- walkNode(seg.ir, meta, loopBindings, matchers, errors, seen);
22381
+ walkNode2(seg.ir, meta, loopBindings, matchers, errors, seen);
22168
22382
  }
22169
22383
  for (const seg of node.preamble?.segments ?? []) {
22170
22384
  if (seg.kind === "jsx")
22171
- walkNode(seg.ir, meta, loopBindings, matchers, errors, seen);
22385
+ walkNode2(seg.ir, meta, loopBindings, matchers, errors, seen);
22172
22386
  }
22173
22387
  break;
22174
22388
  }
22175
22389
  case "conditional":
22176
22390
  if (node.clientOnly)
22177
22391
  break;
22178
- walkNode(node.whenTrue, meta, bindings, matchers, errors, seen);
22179
- walkNode(node.whenFalse, meta, bindings, matchers, errors, seen);
22392
+ walkNode2(node.whenTrue, meta, bindings, matchers, errors, seen);
22393
+ walkNode2(node.whenFalse, meta, bindings, matchers, errors, seen);
22180
22394
  break;
22181
22395
  case "if-statement":
22182
- walkNode(node.consequent, meta, bindings, matchers, errors, seen);
22396
+ walkNode2(node.consequent, meta, bindings, matchers, errors, seen);
22183
22397
  if (node.alternate)
22184
- walkNode(node.alternate, meta, bindings, matchers, errors, seen);
22398
+ walkNode2(node.alternate, meta, bindings, matchers, errors, seen);
22185
22399
  break;
22186
22400
  }
22187
22401
  }
@@ -22247,6 +22461,7 @@ function compileMultipleComponents(source, filePath, componentNames, options) {
22247
22461
  };
22248
22462
  componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR);
22249
22463
  checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors);
22464
+ decideClientOnlyElision(componentIR.root);
22250
22465
  if (options.cssLayerPrefix) {
22251
22466
  applyCssLayerPrefix(componentIR, options.cssLayerPrefix);
22252
22467
  }
@@ -22586,6 +22801,7 @@ function compileJSX(source, filePath, options) {
22586
22801
  };
22587
22802
  componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR);
22588
22803
  checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors);
22804
+ decideClientOnlyElision(componentIR.root);
22589
22805
  if (ctx.importedClientSignalNames.size > 0) {
22590
22806
  const sources = new Set;
22591
22807
  for (const imp of ctx.imports) {
@@ -26653,6 +26869,7 @@ export {
26653
26869
  diffStaticBudget,
26654
26870
  diffProfiles,
26655
26871
  describeFallback,
26872
+ decideClientOnlyElision,
26656
26873
  dangerousInnerHtmlMetacharViolation,
26657
26874
  dangerousInnerHtmlDiagnostic,
26658
26875
  createProgramForFile,