@jxsuite/compiler 1.3.0 → 1.5.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.
@@ -16,21 +16,26 @@ import {
16
16
  DEFAULT_LIT_HTML_SRC,
17
17
  DEFAULT_REACTIVITY_SRC,
18
18
  buildAttrs,
19
+ childSeparator,
19
20
  colorSchemePrePaintScript,
20
21
  compileExpression,
21
22
  compileStatements,
22
23
  compileStyles,
23
24
  createCompileContext,
24
25
  emitFormulaFn,
26
+ emitRequestFetch,
25
27
  escapeHtml,
26
28
  isMutating,
27
29
  isRefObject,
28
30
  isSchemaOnly,
29
31
  isTemplateString,
32
+ PREFORMATTED_TAGS,
30
33
  pureSchemeOf,
31
34
  resolveStaticValue,
35
+ srcImportBinding,
32
36
  } from "../shared.ts";
33
37
  import {
38
+ bodyReturnsValue,
34
39
  hasStructuredBody,
35
40
  isExpandedSignal,
36
41
  isExpressionDef,
@@ -47,7 +52,6 @@ import type {
47
52
  JxDocument,
48
53
  JxMappedArray,
49
54
  JxMutableNode,
50
- JxPrototypeDef,
51
55
  JxStyle,
52
56
  } from "@jxsuite/schema/types";
53
57
 
@@ -161,11 +165,11 @@ export function compileClient(
161
165
  if (!srcImportMap.has(def.$src)) {
162
166
  srcImportMap.set(def.$src, new Set());
163
167
  }
164
- (srcImportMap.get(def.$src) as Set<string>).add(key);
168
+ (srcImportMap.get(def.$src) as Set<string>).add(srcImportBinding(key, def));
165
169
 
166
170
  // $src functions always produce computed entries (they return values)
167
171
  computedEntries.push([key, `() => { return ${key}(state); }`]);
168
- } else if (typeof def.body === "string" && def.body.includes("return")) {
172
+ } else if (typeof def.body === "string" && bodyReturnsValue(def.body)) {
169
173
  // Body contains return → computed
170
174
  computedEntries.push([key, `() => { ${def.body} }`]);
171
175
  } else {
@@ -200,7 +204,7 @@ export function compileClient(
200
204
  // $prototype: "Request"
201
205
  if (def.$prototype === "Request") {
202
206
  stateEntries.push([key, null]);
203
- initBlocks.push(emitRequestInit(key, def));
207
+ initBlocks.push(emitRequestFetch(key, def));
204
208
  continue;
205
209
  }
206
210
 
@@ -335,6 +339,8 @@ function buildClientNode(
335
339
  );
336
340
 
337
341
  const tag = def.tagName ?? "div";
342
+ // `white-space` inherits, so once inside a <pre> the whole subtree stays whitespace-significant.
343
+ nextContext.preformatted = context.preformatted === true || PREFORMATTED_TAGS.has(tag);
338
344
  const bindAttrs = [];
339
345
  let needsBind = false;
340
346
 
@@ -530,9 +536,14 @@ function buildClientNode(
530
536
  const listKey = `_children${counter.l}`;
531
537
  counter.l += 1;
532
538
  const childrenTpl = source.children
533
- .map((c) => emitChildLit(c as JxMutableNode))
534
- .join("\n ");
535
- bindings.set(listKey, `() => html\`\n ${childrenTpl}\n \``);
539
+ .map((c) => emitChildLit(c as JxMutableNode, nextContext.preformatted))
540
+ .join(childSeparator(nextContext.preformatted, " "));
541
+ bindings.set(
542
+ listKey,
543
+ nextContext.preformatted
544
+ ? `() => html\`${childrenTpl}\``
545
+ : `() => html\`\n ${childrenTpl}\n \``,
546
+ );
536
547
  bindAttrs.push(`:render="${listKey}"`);
537
548
  needsBind = true;
538
549
  const bindAttrStr2 = ` ${bindAttrs.join(" ")}`;
@@ -551,7 +562,7 @@ function buildClientNode(
551
562
  counter,
552
563
  );
553
564
  })
554
- .join("\n ");
565
+ .join(childSeparator(nextContext.preformatted));
555
566
  }
556
567
 
557
568
  // Self-closing tags
@@ -570,13 +581,16 @@ function buildClientNode(
570
581
  * index.
571
582
  *
572
583
  * @param {JxMutableNode} def
584
+ * @param {boolean} [preformatted]
573
585
  * @returns {string}
574
586
  */
575
- function emitLitMapTemplate(def: JxMutableNode | undefined) {
587
+ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false) {
576
588
  if (!def) {
577
589
  return "";
578
590
  }
579
591
  const tag = def.tagName ?? "div";
592
+ // `white-space` inherits, so once inside a <pre> the whole subtree stays whitespace-significant.
593
+ const inPre = preformatted || PREFORMATTED_TAGS.has(tag);
580
594
  let attrs = "";
581
595
 
582
596
  if (def.id) {
@@ -586,6 +600,20 @@ function emitLitMapTemplate(def: JxMutableNode | undefined) {
586
600
  attrs += ` class="${mapRefsToLit(def.className)}"`;
587
601
  }
588
602
 
603
+ // $props → lit property bindings. The element target emits these; the client target emitted
604
+ // Nothing, so a component in a list silently received none of its per-item props.
605
+ if (def.$props && typeof def.$props === "object") {
606
+ for (const [k, v] of Object.entries(def.$props)) {
607
+ if (isRefObject(v)) {
608
+ attrs += ` .${k}="\${${mapRefToClientExpr((v as { $ref: string }).$ref)}}"`;
609
+ } else if (typeof v === "string" && isTemplateString(v)) {
610
+ attrs += ` .${k}="${mapRefsToLit(v)}"`;
611
+ } else {
612
+ attrs += ` .${k}="\${${JSON.stringify(v)}}"`;
613
+ }
614
+ }
615
+ }
616
+
589
617
  // Attributes object
590
618
  if (def.attributes && typeof def.attributes === "object") {
591
619
  for (const [k, v] of Object.entries(def.attributes)) {
@@ -674,7 +702,8 @@ function emitLitMapTemplate(def: JxMutableNode | undefined) {
674
702
  // Legacy whole-children repeater nested inside a map template.
675
703
  inner = `\n ${emitArrayHole(def.children)}\n `;
676
704
  } else if (Array.isArray(def.children)) {
677
- inner = `\n ${def.children.map((c) => emitChildLit(c)).join("\n ")}\n `;
705
+ const rendered = def.children.map((c) => emitChildLit(c, inPre));
706
+ inner = inPre ? rendered.join("") : `\n ${rendered.join("\n ")}\n `;
678
707
  }
679
708
 
680
709
  const voidTags = new Set(["input", "br", "hr", "img", "meta", "link"]);
@@ -689,9 +718,10 @@ function emitLitMapTemplate(def: JxMutableNode | undefined) {
689
718
  * via a `.map()` hole), or a nested element.
690
719
  *
691
720
  * @param {JxMutableNode | string} c
721
+ * @param {boolean} [preformatted]
692
722
  * @returns {string}
693
723
  */
694
- function emitChildLit(c: JxMutableNode | string): string {
724
+ function emitChildLit(c: JxMutableNode | string, preformatted = false): string {
695
725
  if (typeof c === "string") {
696
726
  return isTemplateString(c) ? mapRefsToLit(c) : escapeHtml(c);
697
727
  }
@@ -701,7 +731,7 @@ function emitChildLit(c: JxMutableNode | string): string {
701
731
  if (isMappedArray(c)) {
702
732
  return emitArrayHole(c);
703
733
  }
704
- return emitLitMapTemplate(c);
734
+ return emitLitMapTemplate(c, preformatted);
705
735
  }
706
736
 
707
737
  /**
@@ -888,61 +918,8 @@ function emitClientModule(
888
918
  }
889
919
 
890
920
  // ─── Prototype init emitters ─────────────────────────────────────────────────
891
-
892
- /**
893
- * @param {string} key
894
- * @param {JxMutableNode} def
895
- * @returns {string}
896
- */
897
- function emitRequestInit(key: string, def: JxPrototypeDef) {
898
- const { url } = def;
899
- const method = def.method ?? "GET";
900
- const isTemplateUrl = isTemplateString(url);
901
-
902
- if (def.manual) {
903
- return `// ${key}: manual Request — fetch triggered by user action`;
904
- }
905
-
906
- const lines: string[] = [
907
- `// ${key}: auto-fetch from ${isTemplateUrl ? "(dynamic URL)" : url}`,
908
- "effect(() => {",
909
- ];
910
-
911
- if (isTemplateUrl) {
912
- lines.push(
913
- ` const url = \`${url}\`;`,
914
- ' if (!url || url === "undefined" || url.includes("undefined")) return;',
915
- );
916
- } else {
917
- lines.push(` const url = ${JSON.stringify(url)};`);
918
- }
919
-
920
- const fetchOpts: string[] = [];
921
- if (method !== "GET") {
922
- fetchOpts.push(`method: ${JSON.stringify(method)}`);
923
- }
924
- if (def.headers) {
925
- fetchOpts.push(`headers: ${JSON.stringify(def.headers)}`);
926
- }
927
- if (def.body) {
928
- const bodyStr =
929
- typeof def.body === "object"
930
- ? JSON.stringify(JSON.stringify(def.body))
931
- : JSON.stringify(def.body);
932
- fetchOpts.push(`body: ${bodyStr}`);
933
- }
934
-
935
- const optsStr = fetchOpts.length > 0 ? `, { ${fetchOpts.join(", ")} }` : "";
936
- lines.push(
937
- ` fetch(url${optsStr})`,
938
- " .then(r => r.ok ? r.json() : Promise.reject(r.statusText))",
939
- ` .then(d => { state.${key} = d; })`,
940
- ` .catch(e => { state.${key} = { error: String(e) }; });`,
941
- "});",
942
- );
943
-
944
- return lines.join("\n");
945
- }
921
+ // Request auto-fetch lives in shared.ts (`emitRequestFetch`) so the element target emits the same
922
+ // Effect against `this.state`.
946
923
 
947
924
  /**
948
925
  * @param {string} key
@@ -995,6 +972,25 @@ function emitCookieInit(key: string, cookieName: string, defaultVal: unknown) {
995
972
  * @param {string} ref
996
973
  * @returns {string}
997
974
  */
975
+ /**
976
+ * Lower a `$ref` to the JS expression that reads it inside a map callback — the mirror of the
977
+ * element target's `refToExpr`/`mapRefToExpr`. Distinct from `refToBindingKey` below, which
978
+ * flattens `/` to `_` to name a binding and would produce nonsense (`state.$map_item`) as an
979
+ * expression.
980
+ *
981
+ * @param {string} ref
982
+ * @returns {string}
983
+ */
984
+ function mapRefToClientExpr(ref: string) {
985
+ if (ref.startsWith("$map/")) {
986
+ return ref.slice("$map/".length).replaceAll("/", ".");
987
+ }
988
+ if (ref.startsWith("#/state/")) {
989
+ return `state.${ref.slice("#/state/".length).replaceAll("/", ".")}`;
990
+ }
991
+ return `state.${ref.replaceAll("/", ".")}`;
992
+ }
993
+
998
994
  function refToBindingKey(ref: string) {
999
995
  if (ref.startsWith("#/state/")) {
1000
996
  return ref.slice("#/state/".length).replaceAll("/", "_");