@jxsuite/compiler 0.28.0 → 0.28.3

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.
@@ -5,67 +5,100 @@
5
5
  * state and lit-html for rendering.
6
6
  */
7
7
 
8
- import { camelToKebab, RESERVED_KEYS } from "@jxsuite/runtime";
8
+ import { RESERVED_KEYS, camelToKebab } from "@jxsuite/runtime";
9
9
  import {
10
- escapeHtml,
11
- tagNameToClassName,
12
- isSchemaOnly,
13
10
  collectStyles,
14
11
  compileExpression,
12
+ escapeHtml,
15
13
  isMutating,
14
+ isSchemaOnly,
15
+ tagNameToClassName,
16
16
  } from "../shared.ts";
17
+ import {
18
+ isExpressionDef,
19
+ isFunctionDef,
20
+ isMappedArray,
21
+ isRef,
22
+ paramNames,
23
+ } from "@jxsuite/schema/guards";
24
+ import { parseJxDocument } from "@jxsuite/schema/parse";
17
25
  import type { ExpressionNode } from "../shared.ts";
18
- import type { JxStyle, JxMutableNode } from "@jxsuite/schema/types";
26
+ import type { FormatRegistry } from "@jxsuite/schema/format-registry";
27
+ import type {
28
+ JxDocument,
29
+ JxExpressionDef,
30
+ JxFunctionDef,
31
+ JxMappedArray,
32
+ JxMutableNode,
33
+ JxStyle,
34
+ } from "@jxsuite/schema/types";
35
+
36
+ /** Options accepted by compileElement. */
37
+ export interface CompileElementOptions {
38
+ /** Resolve an `$elements` ref path to its emitted JS module path. */
39
+ resolveElementPath?: (refPath: string, currentDir: string | null) => string;
40
+ $media?: Record<string, string>;
41
+ formats?: FormatRegistry;
42
+ basePath?: string | null;
43
+ [key: string]: unknown;
44
+ }
19
45
 
20
46
  /**
21
47
  * Compile a Jx custom element document to a JS module string.
22
48
  *
23
- * @param {string | any} sourcePath - Path to .json file or raw object
24
- * @param {Record<string, unknown>} [opts]
49
+ * @param {string | JxDocument} sourcePath - Path to .json file or raw object
50
+ * @param {CompileElementOptions} [opts]
25
51
  * @returns {Promise<{ files: { path: string; content: string; tagName: string }[] }>}
26
52
  */
27
- export async function compileElement(sourcePath: string | any, opts: Record<string, unknown> = {}) {
28
- const { resolveElementPath, $media: optsMedia, formats } = opts as JxMutableNode;
53
+ export async function compileElement(
54
+ sourcePath: string | JxDocument,
55
+ opts: CompileElementOptions = {},
56
+ ) {
57
+ const { resolveElementPath, $media: optsMedia, formats } = opts;
29
58
  /** @type {{ path: string; content: string; tagName: string }[]} */
30
59
  const files: { path: string; content: string; tagName: string }[] = [];
31
- const visited: Set<string> = new Set();
60
+ const visited = new Set<string>();
32
61
 
33
62
  /**
34
- * @param {string | any} srcPath
63
+ * @param {string | JxDocument} srcPath
35
64
  * @param {string | null} parentDir
36
65
  */
37
- async function processElement(srcPath: string | any, parentDir: string | null) {
38
- /** @type {JxMutableNode} */
39
- let doc;
40
- /** @type {string | null} */
41
- let filePath;
66
+ async function processElement(srcPath: string | JxDocument, parentDir: string | null) {
67
+ let doc: JxDocument;
68
+ let filePath: string | null;
42
69
  if (typeof srcPath === "string") {
43
70
  const { readFileSync } = await import("node:fs");
44
71
  const { resolve } = await import("node:path");
45
72
  filePath = parentDir ? resolve(parentDir, srcPath) : resolve(srcPath);
46
- if (visited.has(filePath)) return;
73
+ if (visited.has(filePath)) {
74
+ return;
75
+ }
47
76
  visited.add(filePath);
48
77
  if (filePath.endsWith(".json")) {
49
- doc = JSON.parse(readFileSync(filePath, "utf8"));
78
+ doc = parseJxDocument(readFileSync(filePath, "utf8"), filePath);
50
79
  } else {
51
80
  const { extname } = await import("node:path");
52
81
  const ext = extname(filePath).toLowerCase();
53
- const registry = formats as import("@jxsuite/schema/format-registry").FormatRegistry;
54
- const entry = registry?.byExtension?.(ext, "parse");
82
+ const entry = formats?.byExtension?.(ext, "parse");
55
83
  if (!entry) {
56
84
  const { unknownFormatError } = await import("../site/format-host.ts");
57
85
  throw unknownFormatError(filePath, ext);
58
86
  }
59
- doc = (await entry.call("parse", readFileSync(filePath, "utf8"))) as JxMutableNode;
87
+ // Format plugins contractually parse source text into a Jx document.
88
+ doc = (await entry.call("parse", readFileSync(filePath, "utf8"))) as JxDocument;
60
89
  }
61
90
  } else {
62
91
  doc = srcPath;
63
92
  filePath = null;
64
- if (visited.has(doc.tagName as string)) return;
65
- visited.add(doc.tagName as string);
93
+ if (doc.tagName && visited.has(doc.tagName)) {
94
+ return;
95
+ }
96
+ if (doc.tagName) {
97
+ visited.add(doc.tagName);
98
+ }
66
99
  }
67
100
 
68
- const tagName = doc.tagName;
101
+ const { tagName } = doc;
69
102
  if (!tagName || !tagName.includes("-")) {
70
103
  throw new Error(`compileElement: tagName "${tagName}" must contain a hyphen`);
71
104
  }
@@ -77,20 +110,19 @@ export async function compileElement(sourcePath: string | any, opts: Record<stri
77
110
  const elementImports: string[] = [];
78
111
  if (Array.isArray(doc.$elements)) {
79
112
  for (const elRef of doc.$elements) {
80
- const refPath = (elRef as JxMutableNode).$ref ?? elRef;
81
- if (typeof refPath !== "string") continue;
113
+ const refPath = isRef(elRef) ? elRef.$ref : elRef;
114
+ if (typeof refPath !== "string") {
115
+ continue;
116
+ }
82
117
 
83
118
  if (currentDir) {
84
119
  await processElement(refPath, currentDir);
85
120
  }
86
121
 
87
122
  /** @type {string} */
88
- let importPath;
89
- if (resolveElementPath) {
90
- importPath = resolveElementPath(refPath, currentDir);
91
- } else {
92
- importPath = refPath.replace(/\.json$/, ".js");
93
- }
123
+ const importPath = resolveElementPath
124
+ ? resolveElementPath(refPath, currentDir)
125
+ : refPath.replace(/\.json$/, ".js");
94
126
  elementImports.push(importPath);
95
127
  }
96
128
  }
@@ -101,10 +133,10 @@ export async function compileElement(sourcePath: string | any, opts: Record<stri
101
133
  }
102
134
  const jsContent = emitElementModule(doc, className, elementImports);
103
135
  const outputPath = filePath ? filePath.replace(/\.json$/, ".js") : `${tagName}.js`;
104
- files.push({ path: outputPath, content: jsContent, tagName });
136
+ files.push({ content: jsContent, path: outputPath, tagName });
105
137
  }
106
138
 
107
- await processElement(sourcePath, (opts.basePath ?? null) as string | null);
139
+ await processElement(sourcePath, opts.basePath ?? null);
108
140
  return { files };
109
141
  }
110
142
 
@@ -120,17 +152,25 @@ export async function compileElement(sourcePath: string | any, opts: Record<stri
120
152
  * }>}
121
153
  */
122
154
  export async function compileElementPage(
123
- sourcePath: string | any,
124
- opts: Record<string, unknown> = {},
155
+ sourcePath: string | JxDocument,
156
+ opts: {
157
+ title?: string;
158
+ reactivitySrc?: string;
159
+ litHtmlSrc?: string;
160
+ [key: string]: unknown;
161
+ } = {},
125
162
  ) {
126
163
  const {
127
164
  title = "Jx App",
128
165
  reactivitySrc = "https://esm.sh/@vue/reactivity@3.5.13",
129
166
  litHtmlSrc = "https://esm.sh/lit-html@3.3.0",
130
- } = opts as JxMutableNode;
167
+ } = opts;
131
168
 
132
169
  const result = await compileElement(sourcePath, opts);
133
- const root = result.files[result.files.length - 1];
170
+ const root = result.files.at(-1);
171
+ if (!root) {
172
+ throw new Error("compileElementPage: no element modules were produced");
173
+ }
134
174
 
135
175
  const { basename } = await import("node:path");
136
176
  const rootScript = basename(root.path);
@@ -156,7 +196,7 @@ export async function compileElementPage(
156
196
  </body>
157
197
  </html>`;
158
198
 
159
- return { html: htmlContent, files: result.files };
199
+ return { files: result.files, html: htmlContent };
160
200
  }
161
201
 
162
202
  // ─── Element code generation helpers ──────────────────────────────────────────
@@ -179,7 +219,7 @@ function extractInitialValue(def: JxMutableNode) {
179
219
  }
180
220
  // Pure schema-only type definitions — skip (no runtime value)
181
221
  if (isSchemaOnly(def)) {
182
- return undefined; // caller should skip this entry
222
+ return; // Caller should skip this entry
183
223
  }
184
224
  // $prototype entries (LocalStorage, SessionStorage, Request, etc.)
185
225
  if (def.$prototype === "LocalStorage" || def.$prototype === "SessionStorage") {
@@ -195,33 +235,32 @@ function extractInitialValue(def: JxMutableNode) {
195
235
  /**
196
236
  * Generate a complete ES module string for a custom element.
197
237
  *
198
- * @param {JxMutableNode | Record<string, any>} doc
238
+ * @param {JxDocument} doc
199
239
  * @param {string} className
200
240
  * @param {string[]} elementImports
201
241
  * @returns {string}
202
242
  */
203
- export function emitElementModule(
204
- doc: JxMutableNode | Record<string, any>,
205
- className: string,
206
- elementImports: string[],
207
- ) {
208
- const lines: string[] = [];
243
+ export function emitElementModule(doc: JxDocument, className: string, elementImports: string[]) {
244
+ const lines: string[] = ["// Generated by @jxsuite/compiler — do not edit manually"];
209
245
 
210
- lines.push("// Generated by @jxsuite/compiler — do not edit manually");
211
- if (doc.$id) lines.push(`// Source: ${doc.$id}`);
246
+ if (doc.$id) {
247
+ lines.push(`// Source: ${doc.$id}`);
248
+ }
212
249
 
213
250
  for (const imp of elementImports) {
214
251
  lines.push(`import '${imp}';`);
215
252
  }
216
253
 
217
254
  // Collect $src imports from state entries before emitting other imports
218
- const srcImportMap: Map<string, string[]> = new Map();
255
+ const srcImportMap = new Map<string, string[]>();
219
256
  const defs = doc.state ?? {};
220
257
  for (const [key, def] of Object.entries(defs)) {
221
258
  const d = def as JxMutableNode;
222
259
  if (d && typeof d === "object" && !Array.isArray(d) && d.$prototype === "Function" && d.$src) {
223
260
  const srcPath = d.$src;
224
- if (!srcImportMap.has(srcPath)) srcImportMap.set(srcPath, []);
261
+ if (!srcImportMap.has(srcPath)) {
262
+ srcImportMap.set(srcPath, []);
263
+ }
225
264
  (srcImportMap.get(srcPath) as string[]).push(key);
226
265
  }
227
266
  }
@@ -241,19 +280,19 @@ export function emitElementModule(
241
280
  lines.push(" super();");
242
281
 
243
282
  const stateEntries: [string, string][] = [];
244
- const computedEntries: [string, any][] = [];
245
- const functionEntries: [string, any][] = [];
283
+ const computedEntries: [string, JxExpressionDef | JxFunctionDef][] = [];
284
+ const functionEntries: [string, JxExpressionDef | JxFunctionDef][] = [];
246
285
 
247
286
  for (const [key, def] of Object.entries(defs)) {
248
287
  const d = def as JxMutableNode;
249
- if (d && typeof d === "object" && !Array.isArray(d) && "$expression" in d) {
250
- const node = (d as Record<string, unknown>).$expression as ExpressionNode;
288
+ if (isExpressionDef(d)) {
289
+ const node = d.$expression as ExpressionNode;
251
290
  if (isMutating(node.operator)) {
252
291
  functionEntries.push([key, d]);
253
292
  } else {
254
293
  computedEntries.push([key, d]);
255
294
  }
256
- } else if (d && typeof d === "object" && !Array.isArray(d) && d.$prototype === "Function") {
295
+ } else if (isFunctionDef(d)) {
257
296
  if (typeof d.body === "string" && d.body.includes("return")) {
258
297
  computedEntries.push([key, d]);
259
298
  } else {
@@ -278,46 +317,46 @@ export function emitElementModule(
278
317
  // Emit functions: this.state.fnName = (state) => { body } or imported $src function
279
318
  for (const [key, def] of functionEntries) {
280
319
  lines.push("");
281
- if ("$expression" in def) {
282
- const compiled = compileExpression(def.$expression, {
283
- statePrefix: "s",
320
+ if (isExpressionDef(def)) {
321
+ const compiled = compileExpression(def.$expression as ExpressionNode, {
284
322
  eventParam: "e",
323
+ statePrefix: "s",
285
324
  });
286
325
  lines.push(` this.state.${key} = (s, e) => { ${compiled}; };`);
287
- } else if (def.$src) {
288
- // $src function — wrap imported function so it receives state
289
- const args = def.parameters ?? def.arguments ?? ["state"];
290
- const paramList = args.join(", ");
291
- lines.push(` this.state.${key} = (${paramList}) => ${key}(${paramList});`);
292
326
  } else {
293
- const args = def.parameters ?? def.arguments ?? ["state"];
327
+ const args = def.parameters ? paramNames(def.parameters) : (def.arguments ?? ["state"]);
294
328
  const paramList = args.join(", ");
295
- lines.push(` this.state.${key} = (${paramList}) => {`);
296
- lines.push(` ${def.body}`);
297
- lines.push(" };");
329
+ if (def.$src) {
330
+ // $src function — wrap imported function so it receives state
331
+ lines.push(` this.state.${key} = (${paramList}) => ${key}(${paramList});`);
332
+ } else {
333
+ lines.push(` this.state.${key} = (${paramList}) => {`);
334
+ lines.push(` ${def.body ?? ""}`);
335
+ lines.push(" };");
336
+ }
298
337
  }
299
338
  }
300
339
 
301
340
  // Emit computed signals — $src or inline body
302
341
  for (const [key, def] of computedEntries) {
303
342
  lines.push("");
304
- if ("$expression" in def) {
305
- const compiled = compileExpression(def.$expression, {
306
- statePrefix: "this.state",
343
+ if (isExpressionDef(def)) {
344
+ const compiled = compileExpression(def.$expression as ExpressionNode, {
307
345
  eventParam: "e",
346
+ statePrefix: "this.state",
308
347
  });
309
348
  lines.push(` this.state.${key} = computed(() => ${compiled});`);
310
349
  } else if (def.$src) {
311
350
  lines.push(` this.state.${key} = computed(() => ${key}(this.state));`);
312
351
  } else {
313
352
  lines.push(` this.state.${key} = computed(() => {`);
314
- const body = def.body.replace(/state\./g, "this.state.");
353
+ const body = (def.body ?? "").replaceAll("state.", "this.state.");
315
354
  lines.push(` ${body}`);
316
355
  lines.push(" });");
317
356
  }
318
357
  }
319
358
 
320
- lines.push(" }"); // end constructor
359
+ lines.push(" }"); // End constructor
321
360
  lines.push("");
322
361
 
323
362
  // Collect CSS rules from children tree (assigns .jx-N classes to defs)
@@ -338,7 +377,7 @@ export function emitElementModule(
338
377
  lines.push(" }");
339
378
  lines.push("");
340
379
 
341
- // connectedCallback
380
+ // ConnectedCallback
342
381
  lines.push(" connectedCallback() {");
343
382
  // Read $props from data-jx-props attribute (set by compiler for pre-rendered instances)
344
383
  lines.push(" const _pa = this.getAttribute('data-jx-props');");
@@ -367,9 +406,12 @@ export function emitElementModule(
367
406
  prop.startsWith("&") ||
368
407
  prop.startsWith("[") ||
369
408
  prop.startsWith("@")
370
- )
409
+ ) {
371
410
  continue;
372
- if (value === null || typeof value === "object") continue;
411
+ }
412
+ if (value === null || typeof value === "object") {
413
+ continue;
414
+ }
373
415
  const cssProp = camelToKebab(prop);
374
416
  if (typeof value === "string" && value.includes("${")) {
375
417
  dynamicStyles.push([cssProp, value]);
@@ -378,9 +420,9 @@ export function emitElementModule(
378
420
  if (dynamicStyles.length > 0) {
379
421
  lines.push(" effect(() => {");
380
422
  for (const [cssProp, value] of dynamicStyles) {
381
- const expr = value.replace(
423
+ const expr = value.replaceAll(
382
424
  /\$\{([^}]+)\}/g,
383
- (_: string, e: string) => "${" + e.replace(/state\./g, "this.state.") + "}",
425
+ (_: string, e: string) => `\${${e.replaceAll("state.", "this.state.")}}`,
384
426
  );
385
427
  lines.push(` this.style['${cssProp}'] = \`${expr}\`;`);
386
428
  }
@@ -410,7 +452,7 @@ export function emitElementModule(
410
452
  lines.push(" }");
411
453
  lines.push("");
412
454
 
413
- // disconnectedCallback
455
+ // DisconnectedCallback
414
456
  lines.push(" disconnectedCallback() {");
415
457
  lines.push(" if (this.#dispose) { this.#dispose(); this.#dispose = null; }");
416
458
  lines.push(" }");
@@ -426,23 +468,27 @@ export function emitElementModule(
426
468
  /**
427
469
  * Convert Jx children to lit-html template content.
428
470
  *
429
- * @param {(JxMutableNode | string)[] | JxMutableNode | undefined} children
430
- * @param {JxStyle | Record<string, any> | null | undefined} parentStyle
471
+ * @param {JxMutableNode["children"]} children
472
+ * @param {JxStyle | null | undefined} parentStyle
431
473
  * @param {string} indent
432
474
  * @returns {string}
433
475
  */
434
476
  function emitLitChildren(
435
- children: (JxMutableNode | string)[] | JxMutableNode | undefined,
436
- parentStyle: JxStyle | Record<string, any> | null | undefined,
477
+ children: JxMutableNode["children"],
478
+ parentStyle: JxStyle | null | undefined,
437
479
  indent: string,
438
480
  ) {
439
- if (!children) return "";
481
+ if (!children) {
482
+ return "";
483
+ }
440
484
 
441
- if ((children as JxMutableNode).$prototype === "Array") {
442
- return emitMappedArray(/** @type {JxMutableNode} */ children, indent);
485
+ if (isMappedArray(children)) {
486
+ return emitMappedArray(children, indent);
443
487
  }
444
488
 
445
- if (!Array.isArray(children)) return "";
489
+ if (!Array.isArray(children)) {
490
+ return "";
491
+ }
446
492
 
447
493
  return children.map((child: JxMutableNode | string) => emitLitNode(child, indent)).join("\n");
448
494
  }
@@ -455,13 +501,17 @@ function emitLitChildren(
455
501
  function emitLitNode(def: JxMutableNode | string, indent: string) {
456
502
  // String children are text nodes
457
503
  if (typeof def === "string") {
458
- if (def.includes("${")) return `${indent}${toLitTextContent(def)}`;
504
+ if (def.includes("${")) {
505
+ return `${indent}${toLitTextContent(def)}`;
506
+ }
459
507
  return `${indent}${escapeHtml(def)}`;
460
508
  }
461
509
  if (typeof def === "number" || typeof def === "boolean") {
462
510
  return `${indent}${escapeHtml(String(def))}`;
463
511
  }
464
- if (!def || typeof def !== "object") return "";
512
+ if (!def || typeof def !== "object") {
513
+ return "";
514
+ }
465
515
 
466
516
  const tag = def.tagName ?? "div";
467
517
 
@@ -469,7 +519,9 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
469
519
 
470
520
  if (def.attributes) {
471
521
  for (const [key, val] of Object.entries(def.attributes)) {
472
- if (typeof val === "string" && val.includes("${")) {
522
+ if (val && typeof val === "object" && isRef(val)) {
523
+ parts.push(`${key}="\${${refToExpr(val.$ref)}}"`);
524
+ } else if (typeof val === "string" && val.includes("${")) {
473
525
  parts.push(`${key}="${toLitExpr(val)}"`);
474
526
  } else {
475
527
  parts.push(`${key}="${val}"`);
@@ -477,8 +529,12 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
477
529
  }
478
530
  }
479
531
 
480
- if (def.id) parts.push(`id="${def.id}"`);
481
- if (def.className) parts.push(`class="${def.className}"`);
532
+ if (def.id) {
533
+ parts.push(`id="${def.id}"`);
534
+ }
535
+ if (def.className) {
536
+ parts.push(`class="${def.className}"`);
537
+ }
482
538
 
483
539
  for (const [key, val] of Object.entries(def)) {
484
540
  if (
@@ -493,8 +549,9 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
493
549
  key === "textContent" ||
494
550
  key === "innerHTML" ||
495
551
  key === "attributes"
496
- )
552
+ ) {
497
553
  continue;
554
+ }
498
555
 
499
556
  if (val && typeof val === "object" && (val as JxMutableNode).$ref) {
500
557
  parts.push(`.${key}="\${${refToExpr((val as JxMutableNode).$ref as string)}}"`);
@@ -514,7 +571,9 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
514
571
  }
515
572
 
516
573
  for (const [key, val] of Object.entries(def)) {
517
- if (!key.startsWith("on") || key === "observedAttributes") continue;
574
+ if (!key.startsWith("on") || key === "observedAttributes") {
575
+ continue;
576
+ }
518
577
  const eventName = key.slice(2).toLowerCase();
519
578
  if (val && typeof val === "object" && (val as JxMutableNode).$ref) {
520
579
  parts.push(
@@ -524,22 +583,22 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
524
583
  const compiled = compileExpression(
525
584
  (val as Record<string, unknown>).$expression as ExpressionNode,
526
585
  {
527
- statePrefix: "s",
528
586
  eventParam: "e",
587
+ statePrefix: "s",
529
588
  },
530
589
  );
531
590
  parts.push(`@${eventName}="\${(e) => { ${compiled}; }}"`);
532
- } else if (val && typeof val === "object" && (val as JxMutableNode).$prototype === "Function") {
533
- parts.push(
534
- `@${eventName}="\${(e) => { ${inlineHandlerBody(/** @type {JxMutableNode} */ val)} }}"`,
535
- );
591
+ } else if (isFunctionDef(val)) {
592
+ parts.push(`@${eventName}="\${(e) => { ${inlineHandlerBody(val)} }}"`);
536
593
  }
537
594
  }
538
595
 
539
596
  const styleStr = emitStyleString(def.style);
540
- if (styleStr) parts.push(`style="${styleStr}"`);
597
+ if (styleStr) {
598
+ parts.push(`style="${styleStr}"`);
599
+ }
541
600
 
542
- const attrsStr = parts.length > 0 ? "\n" + indent + " " + parts.join("\n" + indent + " ") : "";
601
+ const attrsStr = parts.length > 0 ? `\n${indent} ${parts.join(`\n${indent} `)}` : "";
543
602
 
544
603
  const selfClosing = new Set(["input", "br", "hr", "img", "meta", "link"]);
545
604
  if (selfClosing.has(tag)) {
@@ -547,35 +606,50 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
547
606
  }
548
607
 
549
608
  let inner = "";
550
- if (def.textContent !== undefined) {
609
+ if (def.$switch) {
610
+ const switchRef = isRef(def.$switch) ? def.$switch.$ref : (def.$switch as string);
611
+ const switchExpr = refToExpr(switchRef);
612
+ const cases = (def.cases ?? {}) as Record<string, JxMutableNode>;
613
+ const caseEntries: string[] = [];
614
+ for (const [key, caseDef] of Object.entries(cases)) {
615
+ if (!caseDef || isRef(caseDef)) {
616
+ continue;
617
+ }
618
+ const renderedCase = emitLitNode(caseDef, `${indent} `);
619
+ caseEntries.push(` ${JSON.stringify(key)}: html\`\n${renderedCase}\n \``);
620
+ }
621
+ inner = `\${{\n${caseEntries.join(",\n")}\n}[${switchExpr}]}`;
622
+ } else if (def.textContent !== undefined) {
551
623
  inner = toLitTextContent(def.textContent);
552
624
  } else if (def.innerHTML !== undefined) {
553
625
  inner = def.innerHTML;
554
626
  } else if (def.children) {
555
- inner = "\n" + emitLitChildren(def.children, def.style, indent + " ") + "\n" + indent;
627
+ inner = `\n${emitLitChildren(def.children, def.style, `${indent} `)}\n${indent}`;
556
628
  }
557
629
 
558
630
  return `${indent}<${tag}${attrsStr}\n${indent}>${inner}</${tag}>`;
559
631
  }
560
632
 
561
633
  /**
562
- * @param {JxMutableNode} arrayDef
634
+ * @param {JxMappedArray} arrayDef
563
635
  * @param {string} indent
564
636
  * @returns {string}
565
637
  */
566
- function emitMappedArray(arrayDef: JxMutableNode, indent: string) {
567
- const itemsExpr = arrayDef.items?.$ref ? refToExpr(arrayDef.items.$ref) : "ITEMS";
638
+ function emitMappedArray(arrayDef: JxMappedArray, indent: string) {
639
+ const itemsExpr = isRef(arrayDef.items) ? refToExpr(arrayDef.items.$ref) : "ITEMS";
568
640
  const mapDef = arrayDef.map;
569
641
 
570
- if (!mapDef) return "";
642
+ if (!mapDef) {
643
+ return "";
644
+ }
571
645
 
572
646
  const tag = mapDef.tagName ?? "div";
573
647
  const parts: string[] = [];
574
648
 
575
649
  if (mapDef.$props) {
576
650
  for (const [key, val] of Object.entries(mapDef.$props)) {
577
- if (val && typeof val === "object" && (val as JxMutableNode).$ref) {
578
- parts.push(`.${key}="\${${mapRefToExpr((val as JxMutableNode).$ref as string)}}"`);
651
+ if (isRef(val)) {
652
+ parts.push(`.${key}="\${${mapRefToExpr(val.$ref)}}"`);
579
653
  } else {
580
654
  parts.push(`.${key}="\${${JSON.stringify(val)}}"`);
581
655
  }
@@ -583,36 +657,33 @@ function emitMappedArray(arrayDef: JxMutableNode, indent: string) {
583
657
  }
584
658
 
585
659
  const styleStr = emitStyleString(mapDef.style);
586
- if (styleStr) parts.push(`style="${styleStr}"`);
660
+ if (styleStr) {
661
+ parts.push(`style="${styleStr}"`);
662
+ }
587
663
 
588
664
  for (const [key, val] of Object.entries(mapDef)) {
589
- if (!key.startsWith("on")) continue;
665
+ if (!key.startsWith("on")) {
666
+ continue;
667
+ }
590
668
  const eventName = key.slice(2).toLowerCase();
591
- if (val && typeof val === "object" && (val as JxMutableNode).$ref) {
592
- parts.push(
593
- `@${eventName}="\${(e) => ${refToExpr((val as JxMutableNode).$ref as string)}(s, e)}"`,
594
- );
595
- } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ val) {
596
- const compiled = compileExpression(
597
- (val as Record<string, unknown>).$expression as ExpressionNode,
598
- {
599
- statePrefix: "s",
600
- eventParam: "e",
601
- },
602
- );
669
+ if (isRef(val)) {
670
+ parts.push(`@${eventName}="\${(e) => ${refToExpr(val.$ref)}(s, e)}"`);
671
+ } else if (isExpressionDef(val)) {
672
+ const compiled = compileExpression(val.$expression as ExpressionNode, {
673
+ eventParam: "e",
674
+ statePrefix: "s",
675
+ });
603
676
  parts.push(`@${eventName}="\${(e) => { ${compiled}; }}"`);
604
677
  }
605
678
  }
606
679
 
607
- const attrsStr =
608
- parts.length > 0 ? "\n" + indent + " " + parts.join("\n" + indent + " ") : "";
680
+ const attrsStr = parts.length > 0 ? `\n${indent} ${parts.join(`\n${indent} `)}` : "";
609
681
 
610
682
  let inner = "";
611
683
  if (mapDef.textContent !== undefined) {
612
684
  inner = toLitTextContent(mapDef.textContent);
613
685
  } else if (mapDef.children) {
614
- inner =
615
- "\n" + emitLitChildren(mapDef.children, null, indent + " ") + "\n" + indent + " ";
686
+ inner = `\n${emitLitChildren(mapDef.children, null, `${indent} `)}\n${indent} `;
616
687
  }
617
688
 
618
689
  return `${indent}\${${itemsExpr}.map((item, index) => html\`\n${indent} <${tag}${attrsStr}\n${indent} >${inner}</${tag}>\n${indent}\`)}`;
@@ -627,13 +698,13 @@ function emitMappedArray(arrayDef: JxMutableNode, indent: string) {
627
698
  function refToExpr(ref: string) {
628
699
  if (ref.startsWith("#/state/")) {
629
700
  const path = ref.slice("#/state/".length);
630
- return "s." + path.replace(/\//g, ".");
701
+ return `s.${path.replaceAll("/", ".")}`;
631
702
  }
632
703
  if (ref.startsWith("$map/")) {
633
704
  const path = ref.slice("$map/".length);
634
- return path.replace(/\//g, ".");
705
+ return path.replaceAll("/", ".");
635
706
  }
636
- return "s." + ref;
707
+ return `s.${ref}`;
637
708
  }
638
709
 
639
710
  /**
@@ -642,7 +713,7 @@ function refToExpr(ref: string) {
642
713
  */
643
714
  function mapRefToExpr(ref: string) {
644
715
  if (ref.startsWith("$map/")) {
645
- return ref.slice("$map/".length).replace(/\//g, ".");
716
+ return ref.slice("$map/".length).replaceAll("/", ".");
646
717
  }
647
718
  return refToExpr(ref);
648
719
  }
@@ -652,7 +723,7 @@ function mapRefToExpr(ref: string) {
652
723
  * @returns {string}
653
724
  */
654
725
  function toLitExpr(str: string) {
655
- return str.replace(/state\./g, "s.");
726
+ return str.replaceAll("state.", "s.");
656
727
  }
657
728
 
658
729
  /**
@@ -678,20 +749,22 @@ function toLitTextContent(value: unknown) {
678
749
  }
679
750
 
680
751
  /**
681
- * @param {JxMutableNode} def
752
+ * @param {import("@jxsuite/schema/types").JxFunctionDef} def
682
753
  * @returns {string}
683
754
  */
684
- function inlineHandlerBody(def: JxMutableNode) {
755
+ function inlineHandlerBody(def: JxFunctionDef) {
685
756
  const body = def.body ?? "";
686
- return body.replace(/(?<!this\.)state\./g, "s.").replace(/(?<!this\.)state(?!\.)/g, "s");
757
+ return body.replaceAll(/(?<!this\.)state\./g, "s.").replaceAll(/(?<!this\.)state(?!\.)/g, "s");
687
758
  }
688
759
 
689
760
  /**
690
- * @param {JxStyle | Record<string, any> | null | undefined} styleDef
761
+ * @param {JxStyle | null | undefined} styleDef
691
762
  * @returns {string}
692
763
  */
693
- function emitStyleString(styleDef: JxStyle | Record<string, any> | null | undefined) {
694
- if (!styleDef || typeof styleDef !== "object") return "";
764
+ function emitStyleString(styleDef: JxStyle | null | undefined) {
765
+ if (!styleDef || typeof styleDef !== "object") {
766
+ return "";
767
+ }
695
768
 
696
769
  const parts: string[] = [];
697
770
  for (const [prop, value] of Object.entries(styleDef)) {
@@ -701,10 +774,13 @@ function emitStyleString(styleDef: JxStyle | Record<string, any> | null | undefi
701
774
  prop.startsWith("&") ||
702
775
  prop.startsWith("[") ||
703
776
  prop.startsWith("@")
704
- )
777
+ ) {
705
778
  continue;
779
+ }
706
780
 
707
- if (value === null || typeof value === "object") continue;
781
+ if (value === null || typeof value === "object") {
782
+ continue;
783
+ }
708
784
 
709
785
  if (typeof value === "string" && value.includes("${")) {
710
786
  const cssProp = camelToKebab(prop);
@@ -718,15 +794,23 @@ function emitStyleString(styleDef: JxStyle | Record<string, any> | null | undefi
718
794
  /**
719
795
  * Check if a children tree contains a `<slot>` element.
720
796
  *
721
- * @param {(JxMutableNode | string)[] | undefined} children
797
+ * @param {JxMutableNode["children"]} children
722
798
  * @returns {boolean}
723
799
  */
724
- function treeHasSlot(children: (JxMutableNode | string)[] | undefined) {
725
- if (!Array.isArray(children)) return false;
800
+ function treeHasSlot(children: JxMutableNode["children"]): boolean {
801
+ if (!Array.isArray(children)) {
802
+ return false;
803
+ }
726
804
  for (const child of children) {
727
- if (!child || typeof child !== "object") continue;
728
- if (child.tagName === "slot") return true;
729
- if (treeHasSlot(child.children)) return true;
805
+ if (!child || typeof child !== "object") {
806
+ continue;
807
+ }
808
+ if (child.tagName === "slot") {
809
+ return true;
810
+ }
811
+ if (treeHasSlot(child.children)) {
812
+ return true;
813
+ }
730
814
  }
731
815
  return false;
732
816
  }