@jxsuite/compiler 0.28.5 → 0.29.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.
@@ -128,16 +128,16 @@ export function compileClassJson(classDef: JxClassDef, _opts: Record<string, unk
128
128
  if (m.role === "accessor") {
129
129
  // Getter
130
130
  if (m.getter) {
131
- lines.push(` ${staticPrefix}get ${prefix}${name}() {`);
132
- lines.push(` ${m.getter.body}`);
133
- lines.push(" }");
131
+ lines.push(` ${staticPrefix}get ${prefix}${name}() {`, ` ${m.getter.body}`, " }");
134
132
  }
135
133
  // Setter
136
134
  if (m.setter) {
137
135
  const setterParams = resolveParams(m.setter.parameters ?? []);
138
- lines.push(` ${staticPrefix}set ${prefix}${name}(${setterParams}) {`);
139
- lines.push(` ${m.setter.body}`);
140
- lines.push(" }");
136
+ lines.push(
137
+ ` ${staticPrefix}set ${prefix}${name}(${setterParams}) {`,
138
+ ` ${m.setter.body}`,
139
+ " }",
140
+ );
141
141
  }
142
142
  } else {
143
143
  lines.push(` ${staticPrefix}${asyncPrefix}${prefix}${name}(${params}) {`);
@@ -148,11 +148,7 @@ export function compileClassJson(classDef: JxClassDef, _opts: Record<string, unk
148
148
  }
149
149
  }
150
150
 
151
- lines.push("}");
152
- lines.push("");
153
- lines.push(`export { ${className} };`);
154
- lines.push(`export default ${className};`);
155
- lines.push("");
151
+ lines.push("}", "", `export { ${className} };`, `export default ${className};`, "");
156
152
 
157
153
  return lines.join("\n");
158
154
  }
@@ -39,6 +39,7 @@ import type {
39
39
  JsonObject,
40
40
  JsonValue,
41
41
  JxDocument,
42
+ JxMappedArray,
42
43
  JxMutableNode,
43
44
  JxPrototypeDef,
44
45
  JxStyle,
@@ -481,6 +482,22 @@ function buildClientNode(
481
482
  return `<${tag}${staticAttrs}${dataBindAttr2}${bindAttrStr2}>`;
482
483
  }
483
484
  return `<${tag}${staticAttrs}${dataBindAttr2}${bindAttrStr2}></${tag}>`;
485
+ } else if (Array.isArray(source.children) && source.children.some((c) => isMappedArray(c))) {
486
+ // ─── Children mix static nodes with array pseudo-elements ───
487
+ // Render the whole children region via one lit binding on this element (wrapper-less, one
488
+ // Lit part per element so multiple arrays among siblings don't collide), with each array
489
+ // Member expanded inline as a `.map()` hole.
490
+ counter.needsLit = true;
491
+ const listKey = `_children${counter.l}`;
492
+ counter.l += 1;
493
+ const childrenTpl = source.children
494
+ .map((c) => emitChildLit(c as JxMutableNode))
495
+ .join("\n ");
496
+ bindings.set(listKey, `() => html\`\n ${childrenTpl}\n \``);
497
+ bindAttrs.push(`:render="${listKey}"`);
498
+ needsBind = true;
499
+ const bindAttrStr2 = ` ${bindAttrs.join(" ")}`;
500
+ return `<${tag}${staticAttrs} data-bind${bindAttrStr2}></${tag}>`;
484
501
  } else if (Array.isArray(source.children)) {
485
502
  const rawChildren = Array.isArray(raw?.children) ? raw.children : undefined;
486
503
  inner = source.children
@@ -611,10 +628,11 @@ function emitLitMapTemplate(def: JxMutableNode | undefined) {
611
628
  }
612
629
  } else if (def.innerHTML) {
613
630
  inner = mapRefsToLit(String(def.innerHTML));
631
+ } else if (isMappedArray(def.children)) {
632
+ // Legacy whole-children repeater nested inside a map template.
633
+ inner = `\n ${emitArrayHole(def.children)}\n `;
614
634
  } else if (Array.isArray(def.children)) {
615
- inner = `\n ${def.children
616
- .map((c) => emitLitMapTemplate(c as JxMutableNode))
617
- .join("\n ")}\n `;
635
+ inner = `\n ${def.children.map((c) => emitChildLit(c)).join("\n ")}\n `;
618
636
  }
619
637
 
620
638
  const voidTags = new Set(["input", "br", "hr", "img", "meta", "link"]);
@@ -624,6 +642,42 @@ function emitLitMapTemplate(def: JxMutableNode | undefined) {
624
642
  return `<${tag}${attrs}>${inner}</${tag}>`;
625
643
  }
626
644
 
645
+ /**
646
+ * Emit one child of a lit template: a string text node, an array pseudo-element (expanded inline
647
+ * via a `.map()` hole), or a nested element.
648
+ *
649
+ * @param {JxMutableNode | string} c
650
+ * @returns {string}
651
+ */
652
+ function emitChildLit(c: JxMutableNode | string): string {
653
+ if (typeof c === "string") {
654
+ return isTemplateString(c) ? mapRefsToLit(c) : escapeHtml(c);
655
+ }
656
+ if (typeof c === "number" || typeof c === "boolean") {
657
+ return escapeHtml(String(c));
658
+ }
659
+ if (isMappedArray(c)) {
660
+ return emitArrayHole(c);
661
+ }
662
+ return emitLitMapTemplate(c);
663
+ }
664
+
665
+ /**
666
+ * Emit a lit-html `${(items ?? []).map(...)}` hole for a mapped array, expanding its template
667
+ * inline among sibling nodes (wrapper-less).
668
+ *
669
+ * @param {JxMappedArray} arrayDef
670
+ * @returns {string}
671
+ */
672
+ function emitArrayHole(arrayDef: JxMappedArray): string {
673
+ const { items } = arrayDef;
674
+ const itemsExpr = isRefObject(items)
675
+ ? `state.${refToBindingKey((items as { $ref: string }).$ref)}`
676
+ : JSON.stringify(items ?? []);
677
+ const tpl = emitLitMapTemplate(arrayDef.map);
678
+ return `\${(${itemsExpr} ?? []).map((item, index) => html\`${tpl}\`)}`;
679
+ }
680
+
627
681
  /**
628
682
  * Replace $map references: $map.item → item, $map.index → index
629
683
  *
@@ -696,15 +750,12 @@ function emitClientModule(
696
750
  lines.push(`import { ${[...names].join(", ")} } from '${src}';`);
697
751
  }
698
752
 
699
- lines.push("");
700
-
701
753
  // State — reactive state
702
- lines.push("const state = reactive({");
754
+ lines.push("", "const state = reactive({");
703
755
  for (const [key, val] of stateEntries) {
704
756
  lines.push(` ${key}: ${JSON.stringify(val)},`);
705
757
  }
706
- lines.push("});");
707
- lines.push("");
758
+ lines.push("});", "");
708
759
 
709
760
  // Prototype init blocks (Request fetch, LocalStorage read, etc.)
710
761
  if (initBlocks.length > 0) {
@@ -750,38 +801,43 @@ function emitClientModule(
750
801
  } else {
751
802
  lines.push("const on = {};");
752
803
  }
753
- lines.push("");
754
-
755
804
  // Hydration function
756
- lines.push("function hydrate(root) {");
757
- lines.push(" root.querySelectorAll('[data-bind]').forEach(el => {");
758
- lines.push(" [...el.attributes].forEach(a => {");
759
- lines.push(" if (a.name.startsWith(':')) {");
760
- lines.push(" const parts = a.name.slice(1).split('.');");
761
- lines.push(" const key = a.value;");
805
+ lines.push(
806
+ "",
807
+ "function hydrate(root) {",
808
+ " root.querySelectorAll('[data-bind]').forEach(el => {",
809
+ " [...el.attributes].forEach(a => {",
810
+ " if (a.name.startsWith(':')) {",
811
+ " const parts = a.name.slice(1).split('.');",
812
+ " const key = a.value;",
813
+ );
762
814
  if (needsLit) {
763
- lines.push(" if (parts[0] === 'render') {");
764
- lines.push(" effect(() => { render(bind[key](), el); });");
765
- lines.push(" } else if (parts[0] === 'style' && parts.length > 1) {");
815
+ lines.push(
816
+ " if (parts[0] === 'render') {",
817
+ " effect(() => { render(bind[key](), el); });",
818
+ " } else if (parts[0] === 'style' && parts.length > 1) {",
819
+ );
766
820
  } else {
767
821
  lines.push(" if (parts[0] === 'style' && parts.length > 1) {");
768
822
  }
769
- lines.push(" effect(() => { el.style[parts[1]] = bind[key](); });");
770
- lines.push(" } else if (parts[0] === 'attr' && parts.length > 1) {");
771
- lines.push(" effect(() => { el.setAttribute(parts[1], bind[key]()); });");
772
- lines.push(" } else {");
773
- lines.push(" const prop = parts[0].replace(/-([a-z])/g, (_, c) => c.toUpperCase());");
774
- lines.push(" effect(() => { el[prop] = bind[key](); });");
775
- lines.push(" }");
776
- lines.push(" } else if (a.name.startsWith('@')) {");
777
- lines.push(" el.addEventListener(a.name.slice(1), on[a.value]);");
778
- lines.push(" }");
779
- lines.push(" });");
780
- lines.push(" });");
781
- lines.push("}");
782
- lines.push("");
783
- lines.push("hydrate(document);");
784
- lines.push("");
823
+ lines.push(
824
+ " effect(() => { el.style[parts[1]] = bind[key](); });",
825
+ " } else if (parts[0] === 'attr' && parts.length > 1) {",
826
+ " effect(() => { el.setAttribute(parts[1], bind[key]()); });",
827
+ " } else {",
828
+ " const prop = parts[0].replace(/-([a-z])/g, (_, c) => c.toUpperCase());",
829
+ " effect(() => { el[prop] = bind[key](); });",
830
+ " }",
831
+ " } else if (a.name.startsWith('@')) {",
832
+ " el.addEventListener(a.name.slice(1), on[a.value]);",
833
+ " }",
834
+ " });",
835
+ " });",
836
+ "}",
837
+ "",
838
+ "hydrate(document);",
839
+ "",
840
+ );
785
841
 
786
842
  return lines.join("\n");
787
843
  }
@@ -808,8 +864,10 @@ function emitRequestInit(key: string, def: JxPrototypeDef) {
808
864
  ];
809
865
 
810
866
  if (isTemplateUrl) {
811
- lines.push(` const url = \`${url}\`;`);
812
- lines.push(' if (!url || url === "undefined" || url.includes("undefined")) return;');
867
+ lines.push(
868
+ ` const url = \`${url}\`;`,
869
+ ' if (!url || url === "undefined" || url.includes("undefined")) return;',
870
+ );
813
871
  } else {
814
872
  lines.push(` const url = ${JSON.stringify(url)};`);
815
873
  }
@@ -830,11 +888,13 @@ function emitRequestInit(key: string, def: JxPrototypeDef) {
830
888
  }
831
889
 
832
890
  const optsStr = fetchOpts.length > 0 ? `, { ${fetchOpts.join(", ")} }` : "";
833
- lines.push(` fetch(url${optsStr})`);
834
- lines.push(" .then(r => r.ok ? r.json() : Promise.reject(r.statusText))");
835
- lines.push(` .then(d => { state.${key} = d; })`);
836
- lines.push(` .catch(e => { state.${key} = { error: String(e) }; });`);
837
- lines.push("});");
891
+ lines.push(
892
+ ` fetch(url${optsStr})`,
893
+ " .then(r => r.ok ? r.json() : Promise.reject(r.statusText))",
894
+ ` .then(d => { state.${key} = d; })`,
895
+ ` .catch(e => { state.${key} = { error: String(e) }; });`,
896
+ "});",
897
+ );
838
898
 
839
899
  return lines.join("\n");
840
900
  }
@@ -268,16 +268,17 @@ export function emitElementModule(doc: JxDocument, className: string, elementImp
268
268
  lines.push(`import { ${names.join(", ")} } from '${srcPath}';`);
269
269
  }
270
270
 
271
- lines.push(`import { reactive, computed, effect } from '@vue/reactivity';`);
272
- lines.push(`import { render, html } from 'lit-html';`);
273
- lines.push("");
274
- lines.push(`class ${className} extends HTMLElement {`);
275
- lines.push(" #dispose = null;");
276
- lines.push("");
277
-
278
- // Constructor: build reactive state
279
- lines.push(" constructor() {");
280
- lines.push(" super();");
271
+ lines.push(
272
+ `import { reactive, computed, effect } from '@vue/reactivity';`,
273
+ `import { render, html } from 'lit-html';`,
274
+ "",
275
+ `class ${className} extends HTMLElement {`,
276
+ " #dispose = null;",
277
+ "",
278
+ // Constructor: build reactive state
279
+ " constructor() {",
280
+ " super();",
281
+ );
281
282
 
282
283
  const stateEntries: [string, string][] = [];
283
284
  const computedEntries: [string, JxExpressionDef | JxFunctionDef][] = [];
@@ -330,9 +331,11 @@ export function emitElementModule(doc: JxDocument, className: string, elementImp
330
331
  // $src function — wrap imported function so it receives state
331
332
  lines.push(` this.state.${key} = (${paramList}) => ${key}(${paramList});`);
332
333
  } else {
333
- lines.push(` this.state.${key} = (${paramList}) => {`);
334
- lines.push(` ${def.body ?? ""}`);
335
- lines.push(" };");
334
+ lines.push(
335
+ ` this.state.${key} = (${paramList}) => {`,
336
+ ` ${def.body ?? ""}`,
337
+ " };",
338
+ );
336
339
  }
337
340
  }
338
341
  }
@@ -351,13 +354,11 @@ export function emitElementModule(doc: JxDocument, className: string, elementImp
351
354
  } else {
352
355
  lines.push(` this.state.${key} = computed(() => {`);
353
356
  const body = (def.body ?? "").replaceAll("state.", "this.state.");
354
- lines.push(` ${body}`);
355
- lines.push(" });");
357
+ lines.push(` ${body}`, " });");
356
358
  }
357
359
  }
358
360
 
359
- lines.push(" }"); // End constructor
360
- lines.push("");
361
+ lines.push(" }", ""); // End constructor
361
362
 
362
363
  // Collect CSS rules from children tree (assigns .jx-N classes to defs)
363
364
  const cssRules: string[] = [];
@@ -368,35 +369,36 @@ export function emitElementModule(doc: JxDocument, className: string, elementImp
368
369
  }
369
370
  }
370
371
 
371
- // Template method
372
- lines.push(" template() {");
373
- lines.push(" const s = this.state;");
374
- lines.push(" return html`");
375
- lines.push(emitLitChildren(doc.children, doc.style, " "));
376
- lines.push(" `;");
377
- lines.push(" }");
378
- lines.push("");
379
-
380
- // ConnectedCallback
381
- lines.push(" connectedCallback() {");
382
- // Read $props from data-jx-props attribute (set by compiler for pre-rendered instances)
383
- lines.push(" const _pa = this.getAttribute('data-jx-props');");
384
- lines.push(" if (_pa) {");
385
- lines.push(" try {");
386
- lines.push(" const _p = JSON.parse(_pa);");
387
- lines.push(" for (const [k, v] of Object.entries(_p)) {");
388
- lines.push(" if (k in this.state) this.state[k] = v;");
389
- lines.push(" }");
390
- lines.push(" } catch {}");
391
- lines.push(" this.removeAttribute('data-jx-props');");
392
- lines.push(" }");
393
- // Merge JS properties set before connection (by parent runtime).
394
- // Only check own properties to avoid inherited DOM properties like `title`.
395
- lines.push(" for (const key of Object.keys(this.state)) {");
396
- lines.push(" if (this.hasOwnProperty(key) && this[key] !== undefined) {");
397
- lines.push(" this.state[key] = this[key];");
398
- lines.push(" }");
399
- lines.push(" }");
372
+ lines.push(
373
+ // Template method
374
+ " template() {",
375
+ " const s = this.state;",
376
+ " return html`",
377
+ emitLitChildren(doc.children, doc.style, " "),
378
+ " `;",
379
+ " }",
380
+ "",
381
+ // ConnectedCallback
382
+ " connectedCallback() {",
383
+ // Read $props from data-jx-props attribute (set by compiler for pre-rendered instances)
384
+ " const _pa = this.getAttribute('data-jx-props');",
385
+ " if (_pa) {",
386
+ " try {",
387
+ " const _p = JSON.parse(_pa);",
388
+ " for (const [k, v] of Object.entries(_p)) {",
389
+ " if (k in this.state) this.state[k] = v;",
390
+ " }",
391
+ " } catch {}",
392
+ " this.removeAttribute('data-jx-props');",
393
+ " }",
394
+ // Merge JS properties set before connection (by parent runtime).
395
+ // Only check own properties to avoid inherited DOM properties like `title`.
396
+ " for (const key of Object.keys(this.state)) {",
397
+ " if (this.hasOwnProperty(key) && this[key] !== undefined) {",
398
+ " this.state[key] = this[key];",
399
+ " }",
400
+ " }",
401
+ );
400
402
  if (doc.style && typeof doc.style === "object") {
401
403
  const dynamicStyles: [string, string][] = [];
402
404
  for (const [prop, value] of Object.entries(doc.style)) {
@@ -436,31 +438,35 @@ export function emitElementModule(doc: JxDocument, className: string, elementImp
436
438
  " const _slotted = Array.from(this.childNodes).filter(n => n.nodeType === 1 || (n.nodeType === 3 && n.textContent.trim()));",
437
439
  );
438
440
  }
439
- lines.push(" if (this.hasAttribute('data-jx-prerendered')) {");
440
- lines.push(" this.removeAttribute('data-jx-prerendered');");
441
- lines.push(" }");
442
- lines.push(" this.innerHTML = '';");
443
- lines.push(" this.#dispose = effect(() => render(this.template(), this));");
441
+ lines.push(
442
+ " if (this.hasAttribute('data-jx-prerendered')) {",
443
+ " this.removeAttribute('data-jx-prerendered');",
444
+ " }",
445
+ " this.innerHTML = '';",
446
+ " this.#dispose = effect(() => render(this.template(), this));",
447
+ );
444
448
  if (hasSlot) {
445
449
  // Replace <slot> placeholder with saved slotted content
446
- lines.push(" const _slot = this.querySelector('slot');");
447
- lines.push(" if (_slot && _slotted.length > 0) {");
448
- lines.push(" for (const n of _slotted) _slot.before(n);");
449
- lines.push(" _slot.remove();");
450
- lines.push(" }");
450
+ lines.push(
451
+ " const _slot = this.querySelector('slot');",
452
+ " if (_slot && _slotted.length > 0) {",
453
+ " for (const n of _slotted) _slot.before(n);",
454
+ " _slot.remove();",
455
+ " }",
456
+ );
451
457
  }
452
- lines.push(" }");
453
- lines.push("");
454
-
455
- // DisconnectedCallback
456
- lines.push(" disconnectedCallback() {");
457
- lines.push(" if (this.#dispose) { this.#dispose(); this.#dispose = null; }");
458
- lines.push(" }");
459
-
460
- lines.push("}");
461
- lines.push("");
462
- lines.push(`customElements.define('${doc.tagName}', ${className});`);
463
- lines.push("");
458
+ lines.push(
459
+ " }",
460
+ "",
461
+ // DisconnectedCallback
462
+ " disconnectedCallback() {",
463
+ " if (this.#dispose) { this.#dispose(); this.#dispose = null; }",
464
+ " }",
465
+ "}",
466
+ "",
467
+ `customElements.define('${doc.tagName}', ${className});`,
468
+ "",
469
+ );
464
470
 
465
471
  return lines.join("\n");
466
472
  }
@@ -475,13 +481,14 @@ export function emitElementModule(doc: JxDocument, className: string, elementImp
475
481
  */
476
482
  function emitLitChildren(
477
483
  children: JxMutableNode["children"],
478
- parentStyle: JxStyle | null | undefined,
484
+ _parentStyle: JxStyle | null | undefined,
479
485
  indent: string,
480
486
  ) {
481
487
  if (!children) {
482
488
  return "";
483
489
  }
484
490
 
491
+ // Legacy whole-children repeater: the array IS the children slot.
485
492
  if (isMappedArray(children)) {
486
493
  return emitMappedArray(children, indent);
487
494
  }
@@ -490,7 +497,12 @@ function emitLitChildren(
490
497
  return "";
491
498
  }
492
499
 
493
- return children.map((child: JxMutableNode | string) => emitLitNode(child, indent)).join("\n");
500
+ // Mixed children: elements/text plus array pseudo-elements expanded inline among siblings.
501
+ return children
502
+ .map((child: JxMutableNode | string) =>
503
+ isMappedArray(child) ? emitMappedArray(child, indent) : emitLitNode(child, indent),
504
+ )
505
+ .join("\n");
494
506
  }
495
507
 
496
508
  /**