@sprlab/wccompiler 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -82,6 +82,26 @@ effect(() => {
82
82
  })
83
83
  ```
84
84
 
85
+ Effects support cleanup — return a function to run before re-execution:
86
+
87
+ ```js
88
+ effect(() => {
89
+ const id = setInterval(() => tick.set(tick() + 1), 1000)
90
+ return () => clearInterval(id) // called before re-run
91
+ })
92
+ ```
93
+
94
+ ### Watch
95
+
96
+ ```js
97
+ watch('count', (newVal, oldVal) => {
98
+ console.log(`Changed from ${oldVal} to ${newVal}`)
99
+ if (newVal > 10) api.save(newVal)
100
+ })
101
+ ```
102
+
103
+ `watch` observes a specific signal/prop/computed and provides both old and new values. The callback does not run on initial mount — only on subsequent changes.
104
+
85
105
  ### Constants
86
106
 
87
107
  ```js
@@ -238,11 +258,18 @@ onMount(() => {
238
258
  console.log('Component connected to DOM')
239
259
  })
240
260
 
261
+ onMount(async () => {
262
+ const data = await fetch('/api/items').then(r => r.json())
263
+ items.set(data)
264
+ })
265
+
241
266
  onDestroy(() => {
242
267
  console.log('Component removed from DOM')
243
268
  })
244
269
  ```
245
270
 
271
+ Async callbacks are wrapped in an IIFE — `connectedCallback` itself stays synchronous.
272
+
246
273
  ## CSS Scoping
247
274
 
248
275
  Styles are automatically scoped to the component using tag-name prefixing:
package/lib/codegen.js CHANGED
@@ -95,6 +95,14 @@ export function transformExpr(expr, signalNames, computedNames, propsObjectName
95
95
  });
96
96
  }
97
97
 
98
+ // Transform bare prop names → this._s_x() (for template expressions like :style="{ color: myProp }")
99
+ for (const propName of propNames) {
100
+ if (propsObjectName && propName === propsObjectName) continue;
101
+ if (emitsObjectName && propName === emitsObjectName) continue;
102
+ const bareRe = new RegExp(`\\b(${propName})\\b(?!\\.set\\()(?!\\()`, 'g');
103
+ result = result.replace(bareRe, `this._s_${propName}()`);
104
+ }
105
+
98
106
  // Transform computed names first (to avoid partial matches with signals)
99
107
  for (const name of computedNames) {
100
108
  // Skip propsObjectName and emitsObjectName
@@ -289,6 +297,103 @@ export function isStaticForExpr(expr, itemVar, indexVar, propsSet, rootVarNames,
289
297
  return true;
290
298
  }
291
299
 
300
+ /**
301
+ * Generate per-item setup code for bindings, events, show, attr, model, and slots.
302
+ * Used by both keyed and non-keyed each effects.
303
+ *
304
+ * @param {string[]} lines — Output lines array
305
+ * @param {object} forBlock — ForBlock with bindings, events, etc.
306
+ * @param {string} itemVar
307
+ * @param {string|null} indexVar
308
+ * @param {Set<string>} propNames
309
+ * @param {Set<string>} signalNamesSet
310
+ * @param {Set<string>} computedNamesSet
311
+ */
312
+ function generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) {
313
+ const indent = ' ';
314
+
315
+ // Bindings
316
+ for (const b of forBlock.bindings) {
317
+ const nodeRef = pathExpr(b.path, 'node');
318
+ if (isStaticForBinding(b.name, itemVar, indexVar)) {
319
+ lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
320
+ } else {
321
+ const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
322
+ lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
323
+ }
324
+ }
325
+
326
+ // Events
327
+ for (const e of forBlock.events) {
328
+ const nodeRef = pathExpr(e.path, 'node');
329
+ lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
330
+ }
331
+
332
+ // Show
333
+ for (const sb of forBlock.showBindings) {
334
+ const nodeRef = pathExpr(sb.path, 'node');
335
+ if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
336
+ lines.push(`${indent} ${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
337
+ } else {
338
+ const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
339
+ lines.push(`${indent} __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
340
+ }
341
+ }
342
+
343
+ // Attr bindings
344
+ for (const ab of forBlock.attrBindings) {
345
+ const nodeRef = pathExpr(ab.path, 'node');
346
+ if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
347
+ lines.push(`${indent} const __val_${ab.varName} = ${ab.expression};`);
348
+ lines.push(`${indent} if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
349
+ } else {
350
+ const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
351
+ lines.push(`${indent} __effect(() => {`);
352
+ lines.push(`${indent} const __val = ${expr};`);
353
+ lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
354
+ lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
355
+ lines.push(`${indent} });`);
356
+ }
357
+ }
358
+
359
+ // Model bindings
360
+ for (const mb of (forBlock.modelBindings || [])) {
361
+ const nodeRef = pathExpr(mb.path, 'node');
362
+ lines.push(`${indent} __effect(() => {`);
363
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
364
+ lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
365
+ } else if (mb.prop === 'checked') {
366
+ lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
367
+ } else {
368
+ lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
369
+ }
370
+ lines.push(`${indent} });`);
371
+ if (mb.prop === 'checked' && mb.radioValue === null) {
372
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
373
+ } else if (mb.coerce) {
374
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
375
+ } else {
376
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
377
+ }
378
+ }
379
+
380
+ // Scoped slots
381
+ for (const s of (forBlock.slots || [])) {
382
+ if (s.slotProps.length > 0) {
383
+ const slotNodeRef = pathExpr(s.path, 'node');
384
+ const propsEntries = s.slotProps.map(sp => `'${sp.prop}': ${sp.source}`).join(', ');
385
+ lines.push(`${indent} { const __slotEl = ${slotNodeRef};`);
386
+ lines.push(`${indent} const __sp = { ${propsEntries} };`);
387
+ lines.push(`${indent} let __h = __slotEl.innerHTML;`);
388
+ lines.push(`${indent} for (const [k, v] of Object.entries(__sp)) {`);
389
+ lines.push(`${indent} __h = __h.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
390
+ lines.push(`${indent} }`);
391
+ lines.push(`${indent} __slotEl.innerHTML = __h;`);
392
+ lines.push(`${indent} }`);
393
+ }
394
+ }
395
+ }
396
+
292
397
  /**
293
398
  * Generate a fully self-contained JS component from a ParseResult.
294
399
  *
@@ -320,8 +425,11 @@ export function generateComponent(parseResult) {
320
425
  attrBindings = [],
321
426
  slots = [],
322
427
  constantVars = [],
428
+ watchers = [],
323
429
  refs = [],
324
430
  refBindings = [],
431
+ childComponents = [],
432
+ childImports = [],
325
433
  } = parseResult;
326
434
 
327
435
  const signalNames = signals.map(s => s.name);
@@ -336,6 +444,14 @@ export function generateComponent(parseResult) {
336
444
  lines.push(reactiveRuntime.trim());
337
445
  lines.push('');
338
446
 
447
+ // ── 1b. Child component imports ──
448
+ for (const ci of childImports) {
449
+ lines.push(`import '${ci.importPath}';`);
450
+ }
451
+ if (childImports.length > 0) {
452
+ lines.push('');
453
+ }
454
+
339
455
  // ── 2. CSS injection (scoped CSS into document.head, always) ──
340
456
  if (style) {
341
457
  const scoped = scopeCSS(style, tagName);
@@ -410,6 +526,11 @@ export function generateComponent(parseResult) {
410
526
  lines.push(` this.${s.varName} = ${pathExpr(s.path, '__root')};`);
411
527
  }
412
528
 
529
+ // Assign DOM refs for child component instances
530
+ for (const cc of childComponents) {
531
+ lines.push(` this.${cc.varName} = ${pathExpr(cc.path, '__root')};`);
532
+ }
533
+
413
534
  // Assign DOM refs for attr bindings (reuse ref when same path)
414
535
  const attrPathMap = new Map();
415
536
  for (const ab of attrBindings) {
@@ -443,6 +564,11 @@ export function generateComponent(parseResult) {
443
564
  lines.push(` this._c_${c.name} = __computed(() => ${body});`);
444
565
  }
445
566
 
567
+ // Watcher prev-value initialization
568
+ for (const w of watchers) {
569
+ lines.push(` this.__prev_${w.target} = undefined;`);
570
+ }
571
+
446
572
  // ── if: template creation, anchor reference, state init ──
447
573
  for (const ifBlock of ifBlocks) {
448
574
  const vn = ifBlock.varName;
@@ -542,6 +668,27 @@ export function generateComponent(parseResult) {
542
668
  }
543
669
  }
544
670
 
671
+ // Child component reactive prop bindings
672
+ for (const cc of childComponents) {
673
+ for (const pb of cc.propBindings) {
674
+ let ref;
675
+ if (pb.type === 'prop') {
676
+ ref = `this._s_${pb.expr}()`;
677
+ } else if (pb.type === 'computed') {
678
+ ref = `this._c_${pb.expr}()`;
679
+ } else if (pb.type === 'signal') {
680
+ ref = `this._${pb.expr}()`;
681
+ } else if (pb.type === 'constant') {
682
+ ref = `this._const_${pb.expr}`;
683
+ } else {
684
+ ref = `this._${pb.expr}()`;
685
+ }
686
+ lines.push(' __effect(() => {');
687
+ lines.push(` this.${cc.varName}.setAttribute('${pb.attr}', ${ref} ?? '');`);
688
+ lines.push(' });');
689
+ }
690
+ }
691
+
545
692
  // User effects
546
693
  for (const eff of effects) {
547
694
  const body = transformMethodBody(eff.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
@@ -554,6 +701,31 @@ export function generateComponent(parseResult) {
554
701
  lines.push(' });');
555
702
  }
556
703
 
704
+ // Watcher effects
705
+ for (const w of watchers) {
706
+ // Determine the signal reference for the watch target
707
+ let watchRef;
708
+ if (propNames.has(w.target)) {
709
+ watchRef = `this._s_${w.target}()`;
710
+ } else if (computedNames.includes(w.target)) {
711
+ watchRef = `this._c_${w.target}()`;
712
+ } else {
713
+ watchRef = `this._${w.target}()`;
714
+ }
715
+ const body = transformMethodBody(w.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
716
+ lines.push(' __effect(() => {');
717
+ lines.push(` const ${w.newParam} = ${watchRef};`);
718
+ lines.push(` if (this.__prev_${w.target} !== undefined) {`);
719
+ lines.push(` const ${w.oldParam} = this.__prev_${w.target};`);
720
+ const bodyLines = body.split('\n');
721
+ for (const line of bodyLines) {
722
+ lines.push(` ${line}`);
723
+ }
724
+ lines.push(' }');
725
+ lines.push(` this.__prev_${w.target} = ${w.newParam};`);
726
+ lines.push(' });');
727
+ }
728
+
557
729
  // Event listeners
558
730
  for (const e of events) {
559
731
  lines.push(` this.${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
@@ -695,7 +867,7 @@ export function generateComponent(parseResult) {
695
867
  // ── each effects ──
696
868
  for (const forBlock of forBlocks) {
697
869
  const vn = forBlock.varName;
698
- const { itemVar, indexVar, source } = forBlock;
870
+ const { itemVar, indexVar, source, keyExpr } = forBlock;
699
871
 
700
872
  const signalNamesSet = new Set(signalNames);
701
873
  const computedNamesSet = new Set(computedNames);
@@ -706,116 +878,79 @@ export function generateComponent(parseResult) {
706
878
  lines.push(' __effect(() => {');
707
879
  lines.push(` const __source = ${sourceExpr};`);
708
880
  lines.push('');
709
- lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
710
- lines.push(` this.${vn}_nodes = [];`);
711
- lines.push('');
712
881
  lines.push(" const __iter = typeof __source === 'number'");
713
882
  lines.push(' ? Array.from({ length: __source }, (_, i) => i + 1)');
714
883
  lines.push(' : (__source || []);');
715
884
  lines.push('');
716
- lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
717
- lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
718
- lines.push(' const node = clone.firstChild;');
719
-
720
- // Setup bindings per item
721
- for (const b of forBlock.bindings) {
722
- const nodeRef = pathExpr(b.path, 'node');
723
- if (isStaticForBinding(b.name, itemVar, indexVar)) {
724
- // Static binding: reference only item/index, assign once
725
- lines.push(` ${nodeRef}.textContent = ${b.name} ?? '';`);
726
- } else {
727
- // Reactive binding: references component variables, wrap in effect
728
- const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
729
- lines.push(` __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
730
- }
731
- }
732
-
733
- // Setup events per item
734
- for (const e of forBlock.events) {
735
- const nodeRef = pathExpr(e.path, 'node');
736
- lines.push(` ${nodeRef}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
737
- }
738
885
 
739
- // Setup show per item
740
- for (const sb of forBlock.showBindings) {
741
- const nodeRef = pathExpr(sb.path, 'node');
742
- if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
743
- const expr = sb.expression;
744
- lines.push(` ${nodeRef}.style.display = (${expr}) ? '' : 'none';`);
745
- } else {
746
- const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
747
- lines.push(` __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
748
- }
749
- }
886
+ if (keyExpr) {
887
+ // ── Keyed reconciliation ──
888
+ lines.push(` const __oldMap = this.${vn}_keyMap || new Map();`);
889
+ lines.push(' const __newMap = new Map();');
890
+ lines.push(' const __newNodes = [];');
891
+ lines.push('');
892
+ lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
893
+ lines.push(` const __key = ${keyExpr};`);
894
+ lines.push(' if (__oldMap.has(__key)) {');
895
+ lines.push(' const node = __oldMap.get(__key);');
896
+ lines.push(' __newMap.set(__key, node);');
897
+ lines.push(' __newNodes.push(node);');
898
+ lines.push(' __oldMap.delete(__key);');
899
+ lines.push(' } else {');
900
+ lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
901
+ lines.push(' const node = clone.firstChild;');
902
+
903
+ // Setup bindings/events/show/attr/model/slots for NEW nodes only
904
+ // (reused nodes keep their existing bindings)
905
+ generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
906
+
907
+ lines.push(' __newMap.set(__key, node);');
908
+ lines.push(' __newNodes.push(node);');
909
+ lines.push(' }');
910
+ lines.push(' });');
911
+ lines.push('');
912
+ lines.push(' // Remove nodes no longer in the list');
913
+ lines.push(' for (const n of __oldMap.values()) n.remove();');
914
+ lines.push('');
915
+ lines.push(' // Reorder: insert all nodes in correct order before anchor');
916
+ lines.push(` for (const n of __newNodes) this.${vn}_anchor.parentNode.insertBefore(n, this.${vn}_anchor);`);
917
+ lines.push('');
918
+ lines.push(` this.${vn}_nodes = __newNodes;`);
919
+ lines.push(` this.${vn}_keyMap = __newMap;`);
920
+ lines.push(' });');
921
+ } else {
922
+ // ── Non-keyed: destroy all and recreate (original behavior) ──
923
+ lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
924
+ lines.push(` this.${vn}_nodes = [];`);
925
+ lines.push('');
926
+ lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
927
+ lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
928
+ lines.push(' const node = clone.firstChild;');
750
929
 
751
- // Setup attr bindings per item
752
- for (const ab of forBlock.attrBindings) {
753
- const nodeRef = pathExpr(ab.path, 'node');
754
- if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
755
- const expr = ab.expression;
756
- lines.push(` const __val_${ab.varName} = ${expr};`);
757
- lines.push(` if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
758
- } else {
759
- const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
760
- lines.push(` __effect(() => {`);
761
- lines.push(` const __val = ${expr};`);
762
- lines.push(` if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
763
- lines.push(` else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
764
- lines.push(` });`);
765
- }
766
- }
930
+ generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
767
931
 
768
- // Setup model bindings per item
769
- for (const mb of (forBlock.modelBindings || [])) {
770
- const nodeRef = pathExpr(mb.path, 'node');
771
- // Effect (signal → DOM) — always reactive since it references component variables
772
- lines.push(` __effect(() => {`);
773
- if (mb.prop === 'checked' && mb.radioValue !== null) {
774
- lines.push(` ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
775
- } else if (mb.prop === 'checked') {
776
- lines.push(` ${nodeRef}.checked = !!this._${mb.signal}();`);
777
- } else {
778
- lines.push(` ${nodeRef}.value = this._${mb.signal}() ?? '';`);
779
- }
780
- lines.push(` });`);
781
- // Listener (DOM → signal)
782
- if (mb.prop === 'checked' && mb.radioValue === null) {
783
- lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
784
- } else if (mb.coerce) {
785
- lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
786
- } else {
787
- lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
788
- }
789
- }
790
-
791
- // Setup scoped slot resolution per item
792
- for (const s of (forBlock.slots || [])) {
793
- if (s.slotProps.length > 0) {
794
- const slotNodeRef = pathExpr(s.path, 'node');
795
- const propsEntries = s.slotProps.map(sp => `'${sp.prop}': ${sp.source}`).join(', ');
796
- lines.push(` { const __slotEl = ${slotNodeRef};`);
797
- lines.push(` const __sp = { ${propsEntries} };`);
798
- lines.push(` let __h = __slotEl.innerHTML;`);
799
- lines.push(` for (const [k, v] of Object.entries(__sp)) {`);
800
- lines.push(` __h = __h.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
801
- lines.push(` }`);
802
- lines.push(` __slotEl.innerHTML = __h;`);
803
- lines.push(` }`);
804
- }
932
+ lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
933
+ lines.push(` this.${vn}_nodes.push(node);`);
934
+ lines.push(' });');
935
+ lines.push(' });');
805
936
  }
806
-
807
- lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
808
- lines.push(` this.${vn}_nodes.push(node);`);
809
- lines.push(' });');
810
- lines.push(' });');
811
937
  }
812
938
 
813
939
  // Lifecycle: onMount hooks (at the very end of connectedCallback)
814
940
  for (const hook of onMountHooks) {
815
941
  const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
816
- const bodyLines = body.split('\n');
817
- for (const line of bodyLines) {
818
- lines.push(` ${line}`);
942
+ if (hook.async) {
943
+ lines.push(' (async () => {');
944
+ const bodyLines = body.split('\n');
945
+ for (const line of bodyLines) {
946
+ lines.push(` ${line}`);
947
+ }
948
+ lines.push(' })();');
949
+ } else {
950
+ const bodyLines = body.split('\n');
951
+ for (const line of bodyLines) {
952
+ lines.push(` ${line}`);
953
+ }
819
954
  }
820
955
  }
821
956
 
@@ -827,9 +962,18 @@ export function generateComponent(parseResult) {
827
962
  lines.push(' disconnectedCallback() {');
828
963
  for (const hook of onDestroyHooks) {
829
964
  const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
830
- const bodyLines = body.split('\n');
831
- for (const line of bodyLines) {
832
- lines.push(` ${line}`);
965
+ if (hook.async) {
966
+ lines.push(' (async () => {');
967
+ const bodyLines = body.split('\n');
968
+ for (const line of bodyLines) {
969
+ lines.push(` ${line}`);
970
+ }
971
+ lines.push(' })();');
972
+ } else {
973
+ const bodyLines = body.split('\n');
974
+ for (const line of bodyLines) {
975
+ lines.push(` ${line}`);
976
+ }
833
977
  }
834
978
  }
835
979
  lines.push(' }');