@jxsuite/compiler 0.28.5 → 0.30.1

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.
@@ -17,7 +17,7 @@ import {
17
17
  rmSync,
18
18
  writeFileSync,
19
19
  } from "node:fs";
20
- import { dirname, join, resolve } from "node:path";
20
+ import { basename, dirname, join, resolve } from "node:path";
21
21
  import { isMappedArray, isRef } from "@jxsuite/schema/guards";
22
22
  import { loadProjectConfig } from "./site-loader.ts";
23
23
  import { discoverPages, expandDynamicRoutes, readPageDocument } from "./pages-discovery.ts";
@@ -54,6 +54,7 @@ import type {
54
54
  JxAttributeValue,
55
55
  JxElement,
56
56
  JxHeadEntry,
57
+ JxMappedArray,
57
58
  JxMutableNode,
58
59
  JxStateDefinition,
59
60
  JxStyle,
@@ -157,7 +158,11 @@ export async function buildSite(
157
158
  formats: formatRegistry,
158
159
  });
159
160
  for (const f of result.files) {
160
- const outName = f.path.includes("/") ? (f.path.split("/").pop() as string) : f.path;
161
+ // Reduce the emitted path (an absolute source path with .json swapped to .js) to a bare
162
+ // Filename for the dist/components/ output. basename handles both / and \ — a
163
+ // Forward-slash-only split left the full drive path on Windows, so resolve() then wrote
164
+ // The sidecar back into the source tree instead of dist.
165
+ const outName = basename(f.path);
161
166
  writeFileSync(resolve(componentOutDir, outName), f.content, "utf8");
162
167
  if (f.tagName) {
163
168
  compiledComponentTags.push(f.tagName);
@@ -188,8 +193,7 @@ export async function buildSite(
188
193
  }
189
194
 
190
195
  // ── 5b. Collect server entries from components (for site-wide bundling) ──
191
- /** @type {{ exportName: string; src: string }[]} */
192
- const siteServerEntries = [];
196
+ const siteServerEntries: { exportName: string; src: string }[] = [];
193
197
  if (projectConfig.build.adapter) {
194
198
  for (const [, doc] of componentDefs) {
195
199
  const entries = collectServerEntries(doc);
@@ -207,8 +211,7 @@ export async function buildSite(
207
211
 
208
212
  const cfImages = projectConfig.images.optimize && projectConfig.images.service === "cloudflare";
209
213
  const imageCache = projectConfig.images.optimize && !cfImages ? loadCache(projectRoot) : null;
210
- /** @type {import("./image-transform.ts").ImageMetaCache | null} */
211
- const imageMetaCache = cfImages ? new Map() : null;
214
+ const imageMetaCache: ImageMetaCache | null = cfImages ? new Map() : null;
212
215
  if (cfImages) {
213
216
  console.log(
214
217
  `images.service is "cloudflare" — srcsets use /cdn-cgi/image transform URLs. ` +
@@ -274,7 +277,7 @@ export async function buildSite(
274
277
  mode: "export",
275
278
  })) as string;
276
279
  if (content) {
277
- const sidecarPath = outPath.replace(/\.html$/, fmt.extensions[0]);
280
+ const sidecarPath = outPath.replace(/\.html$/, fmt.extensions[0]!);
278
281
  writeFileSync(sidecarPath, content, "utf8");
279
282
  fileCount += 1;
280
283
  }
@@ -326,7 +329,7 @@ export async function buildSite(
326
329
  const { adapter } = projectConfig.build;
327
330
  log("Generating site-wide server worker...");
328
331
 
329
- const deduped = new Map();
332
+ const deduped = new Map<string, { exportName: string; src: string }>();
330
333
  for (const entry of siteServerEntries) {
331
334
  if (!deduped.has(entry.exportName)) {
332
335
  deduped.set(entry.exportName, entry);
@@ -460,7 +463,7 @@ async function compilePage(
460
463
  // Determine the page title — resolve template strings against the scope
461
464
  let title = pageTitle ?? projectConfig.name ?? "Jx Site";
462
465
  if (typeof title === "string" && isTemplateString(title)) {
463
- title = evaluateStaticTemplate(title, scope) ?? (title as string);
466
+ title = (evaluateStaticTemplate(title, scope) as string | null) ?? (title as string);
464
467
  }
465
468
 
466
469
  // Resolve template strings in $head entries
@@ -595,14 +598,16 @@ function resolveHeadTemplates(headEntries: JxHeadEntry[], scope: Record<string,
595
598
  resolved.attributes = { ...resolved.attributes };
596
599
  for (const [k, v] of Object.entries(resolved.attributes)) {
597
600
  if (typeof v === "string" && isTemplateString(v)) {
598
- resolved.attributes[k] = evaluateStaticTemplate(v, scope) ?? (v as string | boolean);
601
+ resolved.attributes[k] =
602
+ (evaluateStaticTemplate(v, scope) as string | boolean | null) ??
603
+ (v as string | boolean);
599
604
  }
600
605
  }
601
606
  }
602
607
  if (typeof resolved.textContent === "string" && isTemplateString(resolved.textContent)) {
603
608
  resolved.textContent =
604
- evaluateStaticTemplate(resolved.textContent, scope) ??
605
- (resolved.textContent as string | undefined);
609
+ (evaluateStaticTemplate(resolved.textContent, scope) as string | null) ??
610
+ resolved.textContent;
606
611
  }
607
612
  return resolved;
608
613
  });
@@ -662,11 +667,11 @@ function expandMapTemplate(template: JxElement, scope: Record<string, unknown>):
662
667
  const node = {} as JxElement;
663
668
  for (const [k, v] of Object.entries(template)) {
664
669
  if (k === "children" && Array.isArray(v)) {
665
- node.children = v.map((child) => {
670
+ node.children = (v as (string | JxElement)[]).map((child) => {
666
671
  if (typeof child === "string") {
667
672
  return child;
668
673
  }
669
- return expandMapTemplate(/** @type {JxMutableNode} */ child, scope);
674
+ return expandMapTemplate(child, scope);
670
675
  });
671
676
  } else if (k === "style" && v && typeof v === "object") {
672
677
  const style: JxStyle = { ...(v as JxStyle) };
@@ -718,10 +723,26 @@ function evaluateMapTemplate(str: string, scope: Record<string, unknown>) {
718
723
  const index = (scope.$map as Record<string, unknown>)?.index;
719
724
  const singleExprMatch = str.match(/^\$\{(.+)\}$/s);
720
725
  if (singleExprMatch) {
721
- const fn = new Function("state", "$map", "item", "index", `return (${singleExprMatch[1]})`);
726
+ const fn = new Function(
727
+ "state",
728
+ "$map",
729
+ "item",
730
+ "index",
731
+ `return (${singleExprMatch[1]})`,
732
+ ) as (
733
+ state: Record<string, unknown>,
734
+ $map: unknown,
735
+ item: unknown,
736
+ index: unknown,
737
+ ) => unknown;
722
738
  return fn(scope, scope.$map, item, index);
723
739
  }
724
- const fn = new Function("state", "$map", "item", "index", `return \`${str}\``);
740
+ const fn = new Function("state", "$map", "item", "index", `return \`${str}\``) as (
741
+ state: Record<string, unknown>,
742
+ $map: unknown,
743
+ item: unknown,
744
+ index: unknown,
745
+ ) => unknown;
725
746
  return fn(scope, scope.$map, item, index);
726
747
  } catch {
727
748
  return null;
@@ -751,49 +772,38 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
751
772
  }
752
773
  if (typeof node.textContent === "string" && isTemplateString(node.textContent)) {
753
774
  node.textContent =
754
- evaluateStaticTemplate(node.textContent, scope) ?? (node.textContent as string | null);
775
+ (evaluateStaticTemplate(node.textContent, scope) as string | null) ??
776
+ (node.textContent as string | null);
755
777
  }
756
778
  if (node.style && typeof node.style === "object") {
757
779
  for (const [k, v] of Object.entries(node.style)) {
758
780
  if (typeof v === "string" && isTemplateString(v)) {
759
- node.style[k] = evaluateStaticTemplate(v, scope) ?? (v as string | number | JxStyle);
781
+ node.style[k] =
782
+ (evaluateStaticTemplate(v, scope) as string | number | JxStyle | undefined) ??
783
+ (v as string | number | JxStyle);
760
784
  }
761
785
  }
762
786
  }
763
787
  if (node.attributes && typeof node.attributes === "object") {
764
788
  for (const [k, v] of Object.entries(node.attributes)) {
765
789
  if (typeof v === "string" && isTemplateString(v)) {
766
- node.attributes[k] = evaluateStaticTemplate(v, scope) ?? v;
790
+ node.attributes[k] = (evaluateStaticTemplate(v, scope) as JxAttributeValue | null) ?? v;
767
791
  }
768
792
  }
769
793
  }
770
794
  if (node.$props && typeof node.$props === "object") {
771
795
  for (const [k, v] of Object.entries(node.$props)) {
772
796
  if (typeof v === "string" && isTemplateString(v)) {
773
- node.$props[k] = evaluateStaticTemplate(v, scope) ?? v;
797
+ node.$props[k] = (evaluateStaticTemplate(v, scope) as JsonValue | null) ?? v;
774
798
  }
775
799
  }
776
800
  }
777
801
  const rawChildren = node.children;
802
+ // Legacy whole-children repeater: expand the items into the node's static children.
778
803
  if (isMappedArray(rawChildren)) {
779
- const itemsSrc = rawChildren.items;
780
- let items = null;
781
- if (isRef(itemsSrc)) {
782
- items = resolveRefValue(itemsSrc.$ref, scope);
783
- } else if (Array.isArray(itemsSrc)) {
784
- items = itemsSrc;
785
- }
786
- const mapTemplate = rawChildren.map;
787
- if (Array.isArray(items) && mapTemplate) {
788
- node.children = items.map((item, index) => {
789
- const childScope = Object.create(scope);
790
- childScope.$map = { index, item };
791
- childScope["$map/item"] = item;
792
- childScope["$map/index"] = index;
793
- const expanded = expandMapTemplate(mapTemplate, childScope);
794
- resolveDocTemplates(expanded, childScope);
795
- return expanded;
796
- });
804
+ const expanded = expandMappedArrayStatic(rawChildren, scope);
805
+ if (expanded) {
806
+ node.children = expanded;
797
807
  return;
798
808
  }
799
809
  }
@@ -809,7 +819,7 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
809
819
  } else if (Array.isArray(node.children)) {
810
820
  let i = 0;
811
821
  while (i < node.children.length) {
812
- const child = node.children[i];
822
+ const child = node.children[i]!;
813
823
  if (typeof child === "string" && isTemplateString(child)) {
814
824
  const resolved = evaluateStaticTemplate(child, scope);
815
825
  if (Array.isArray(resolved)) {
@@ -822,12 +832,55 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
822
832
  continue;
823
833
  }
824
834
  }
835
+ // Array pseudo-element among siblings: expand its items in place.
836
+ if (isMappedArray(child)) {
837
+ const expanded = expandMappedArrayStatic(child, scope);
838
+ if (expanded) {
839
+ node.children.splice(i, 1, ...expanded);
840
+ i += expanded.length;
841
+ continue;
842
+ }
843
+ }
825
844
  resolveDocTemplates(child, scope);
826
845
  i += 1;
827
846
  }
828
847
  }
829
848
  }
830
849
 
850
+ /**
851
+ * Statically expand a mapped array to its resolved item nodes when `items` resolves to an array at
852
+ * build time, or null otherwise (leave the array node for client-side rendering).
853
+ *
854
+ * @param {JxMappedArray} arrayDef
855
+ * @param {Record<string, unknown>} scope
856
+ * @returns {(JxElement | string)[] | null}
857
+ */
858
+ function expandMappedArrayStatic(
859
+ arrayDef: JxMappedArray,
860
+ scope: Record<string, unknown>,
861
+ ): (JxElement | string)[] | null {
862
+ const itemsSrc = arrayDef.items;
863
+ let items: unknown = null;
864
+ if (isRef(itemsSrc)) {
865
+ items = resolveRefValue(itemsSrc.$ref, scope);
866
+ } else if (Array.isArray(itemsSrc)) {
867
+ items = itemsSrc;
868
+ }
869
+ const mapTemplate = arrayDef.map;
870
+ if (!Array.isArray(items) || !mapTemplate) {
871
+ return null;
872
+ }
873
+ return (items as unknown[]).map((item: unknown, index) => {
874
+ const childScope = Object.create(scope) as Record<string, unknown>;
875
+ childScope.$map = { index, item };
876
+ childScope["$map/item"] = item;
877
+ childScope["$map/index"] = index;
878
+ const expanded = expandMapTemplate(mapTemplate, childScope);
879
+ resolveDocTemplates(expanded, childScope);
880
+ return expanded;
881
+ });
882
+ }
883
+
831
884
  /** Collector for CSS rules extracted from component slot content during expansion. */
832
885
  interface SlotCssCollector {
833
886
  rules: string[];
@@ -853,7 +906,7 @@ function expandComponents(
853
906
  return;
854
907
  }
855
908
  if (Array.isArray(node)) {
856
- for (const n of node) {
909
+ for (const n of node as (JxElement | string)[]) {
857
910
  expandComponents(n, componentDefs, slotCss);
858
911
  }
859
912
  return;
@@ -1012,11 +1065,11 @@ function injectHead(html: string, headEntries: JxHeadEntry[], lang: string) {
1012
1065
  const existingMatch = html.match(headPattern);
1013
1066
  let preservedBlocks = "";
1014
1067
  if (existingMatch) {
1015
- const styles = existingMatch[1].match(/<style>[\s\S]*?<\/style>/gi);
1068
+ const styles = existingMatch[1]!.match(/<style>[\s\S]*?<\/style>/gi);
1016
1069
  if (styles) {
1017
1070
  preservedBlocks += `\n ${styles.join("\n ")}`;
1018
1071
  }
1019
- const scripts = existingMatch[1].match(/<script[\s\S]*?<\/script>/gi);
1072
+ const scripts = existingMatch[1]!.match(/<script[\s\S]*?<\/script>/gi);
1020
1073
  if (scripts) {
1021
1074
  preservedBlocks += `\n ${scripts.join("\n ")}`;
1022
1075
  }
@@ -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
  }