@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.
@@ -11,41 +11,75 @@
11
11
  * state + computed signals), bind (DOM getters), on (event handlers), hydrate()
12
12
  */
13
13
 
14
- import { camelToKebab, RESERVED_KEYS } from "@jxsuite/runtime";
14
+ import { RESERVED_KEYS, camelToKebab } from "@jxsuite/runtime";
15
15
  import {
16
- isSchemaOnly,
17
- isTemplateString,
18
- isRefObject,
19
- resolveStaticValue,
20
- createCompileContext,
16
+ DEFAULT_LIT_HTML_SRC,
17
+ DEFAULT_REACTIVITY_SRC,
21
18
  buildAttrs,
19
+ compileExpression,
22
20
  compileStyles,
21
+ createCompileContext,
23
22
  escapeHtml,
24
- DEFAULT_REACTIVITY_SRC,
25
- DEFAULT_LIT_HTML_SRC,
26
- compileExpression,
27
23
  isMutating,
24
+ isRefObject,
25
+ isSchemaOnly,
26
+ isTemplateString,
27
+ resolveStaticValue,
28
28
  } from "../shared.ts";
29
- import type { ExpressionNode } from "../shared.ts";
30
- import type { JxStyle, JxMutableNode } from "@jxsuite/schema/types";
29
+ import {
30
+ isExpandedSignal,
31
+ isExpressionDef,
32
+ isFunctionDef,
33
+ isMappedArray,
34
+ isPrototypeDef,
35
+ paramNames,
36
+ } from "@jxsuite/schema/guards";
37
+ import type { CompileContext, ExpressionNode } from "../shared.ts";
38
+ import type {
39
+ JsonObject,
40
+ JsonValue,
41
+ JxDocument,
42
+ JxMutableNode,
43
+ JxPrototypeDef,
44
+ JxStyle,
45
+ } from "@jxsuite/schema/types";
46
+
47
+ /** A compiled event handler entry: parameter names + function body. */
48
+ interface HandlerDef {
49
+ args?: string[] | undefined;
50
+ body?: string | undefined;
51
+ }
31
52
 
32
53
  /**
33
54
  * Compile a Jx document to pre-rendered HTML + reactive JS module.
34
55
  *
35
- * @param {JxMutableNode | Record<string, any>} raw
36
- * @param {Record<string, unknown>} opts
56
+ * @param {JxDocument} raw
57
+ * @param {{
58
+ * title?: string;
59
+ * reactivitySrc?: string;
60
+ * litHtmlSrc?: string;
61
+ * modulePath?: string;
62
+ * projectStyle?: JxStyle | null;
63
+ * }} opts
37
64
  * @returns {{ html: string; files: { path: string; content: string }[] }}
38
65
  */
39
66
  export function compileClient(
40
- raw: JxMutableNode | Record<string, any>,
41
- opts: Record<string, unknown>,
67
+ raw: JxDocument,
68
+ opts: {
69
+ title?: string;
70
+ reactivitySrc?: string;
71
+ litHtmlSrc?: string;
72
+ modulePath?: string;
73
+ projectStyle?: JxStyle | null;
74
+ [key: string]: unknown;
75
+ },
42
76
  ) {
43
77
  const {
44
- title,
78
+ title = "Jx App",
45
79
  reactivitySrc = DEFAULT_REACTIVITY_SRC,
46
80
  litHtmlSrc = DEFAULT_LIT_HTML_SRC,
47
81
  modulePath = "app.js",
48
- } = opts as JxMutableNode;
82
+ } = opts;
49
83
 
50
84
  const context = createCompileContext(raw, null, raw.state ?? {}, raw.$media ?? {});
51
85
  const styleBlock = compileStyles(
@@ -55,19 +89,19 @@ export function compileClient(
55
89
  );
56
90
 
57
91
  // Collectors for bindings and handlers
58
- const counter = { t: 0, s: 0, h: 0, m: 0, sw: 0, l: 0, needsLit: false };
59
- const bindings: Map<string, string> = new Map(); // key → expression string
60
- const handlers: Map<string, any> = new Map(); // key → { body, args }
92
+ const counter = { h: 0, l: 0, m: 0, needsLit: false, s: 0, sw: 0, t: 0 };
93
+ const bindings = new Map<string, string>(); // Key → expression string
94
+ const handlers = new Map<string, HandlerDef>(); // Key → { body, args }
61
95
 
62
96
  // Classify state entries into reactive state, computed, bind, on, and init blocks
63
- const stateEntries: [string, any][] = []; // [key, initValue] → reactive({...})
97
+ const stateEntries: [string, JsonValue | undefined][] = []; // [key, initValue] → reactive({...})
64
98
  const computedEntries: [string, string][] = []; // [key, bodyExpr] → state.key = computed(...)
65
99
  const bindEntries: [string, string][] = []; // [key, bodyExpr] → bind = {...}
66
- const onEntries: [string, any][] = []; // [key, { args, body }] → on = {...}
67
- const initBlocks: string[] = []; // lines emitted after state for prototype init
100
+ const onEntries: [string, HandlerDef][] = []; // [key, { args, body }] → on = {...}
101
+ const initBlocks: string[] = []; // Lines emitted after state for prototype init
68
102
 
69
103
  // Map $src path → Set of function names to import
70
- const srcImportMap: Map<string, Set<string>> = new Map();
104
+ const srcImportMap = new Map<string, Set<string>>();
71
105
 
72
106
  const defs = raw.state ?? {};
73
107
  for (const [key, def] of Object.entries(defs)) {
@@ -75,88 +109,88 @@ export function compileClient(
75
109
  // Naked primitive or array → reactive state
76
110
  if (typeof def === "string" && isTemplateString(def)) {
77
111
  // Template string → computed on state so other computeds can ref it
78
- computedEntries.push([key, "() => `" + def + "`"]);
112
+ computedEntries.push([key, `() => \`${def}\``]);
79
113
  } else {
80
114
  stateEntries.push([key, def]);
81
115
  }
82
116
  continue;
83
117
  }
84
118
 
85
- const d = def as JxMutableNode;
86
-
87
119
  // $expression: Shape 5
88
- if ("$expression" in d) {
89
- const node = (d as Record<string, unknown>).$expression as ExpressionNode;
120
+ if (isExpressionDef(def)) {
121
+ const node = def.$expression as ExpressionNode;
122
+ const compiled = compileExpression(node, {
123
+ eventParam: "e",
124
+ statePrefix: "state",
125
+ });
90
126
  if (isMutating(node.operator)) {
91
- const compiled = compileExpression(node, {
92
- statePrefix: "state",
93
- eventParam: "e",
94
- });
95
127
  onEntries.push([key, { args: ["state", "e"], body: compiled }]);
96
128
  } else {
97
- const compiled = compileExpression(node, {
98
- statePrefix: "state",
99
- eventParam: "e",
100
- });
101
129
  computedEntries.push([key, `() => ${compiled}`]);
102
130
  }
103
131
  continue;
104
132
  }
105
133
 
106
134
  // $prototype: "Function"
107
- if (d.$prototype === "Function") {
108
- const args = d.parameters ?? d.arguments;
109
- if (d.$src) {
110
- if (!srcImportMap.has(d.$src)) srcImportMap.set(d.$src, new Set());
111
- (srcImportMap.get(d.$src) as Set<string>).add(key);
135
+ if (isFunctionDef(def)) {
136
+ const args = def.parameters ? paramNames(def.parameters) : def.arguments;
137
+ if (def.$src) {
138
+ if (!srcImportMap.has(def.$src)) {
139
+ srcImportMap.set(def.$src, new Set());
140
+ }
141
+ (srcImportMap.get(def.$src) as Set<string>).add(key);
112
142
 
113
143
  // $src functions always produce computed entries (they return values)
114
- computedEntries.push([key, "() => { return " + key + "(state); }"]);
115
- } else if (d.body && d.body.includes("return")) {
144
+ computedEntries.push([key, `() => { return ${key}(state); }`]);
145
+ } else if (def.body && def.body.includes("return")) {
116
146
  // Body contains return → computed
117
- computedEntries.push([key, "() => { " + d.body + " }"]);
147
+ computedEntries.push([key, `() => { ${def.body} }`]);
118
148
  } else {
119
149
  // No return → event handler
120
- onEntries.push([key, { args: args ?? ["state"], body: d.body }]);
150
+ onEntries.push([key, { args: args ?? ["state"], body: def.body }]);
121
151
  }
122
152
  continue;
123
153
  }
124
154
 
125
155
  // Pure schema-only type def → skip
126
- if (isSchemaOnly(d)) continue;
127
-
128
- // Expanded signal with default
129
- if ("default" in d && !d.$prototype) {
130
- stateEntries.push([key, d.default]);
156
+ if (isSchemaOnly(def)) {
131
157
  continue;
132
158
  }
133
159
 
134
- // $prototype: "LocalStorage" / "SessionStorage"
135
- if (d.$prototype === "LocalStorage" || d.$prototype === "SessionStorage") {
136
- const storeName = d.$prototype === "LocalStorage" ? "localStorage" : "sessionStorage";
137
- const storageKey = d.key ?? key;
138
- const defaultVal = d.default ?? null;
139
- stateEntries.push([key, null]);
140
- initBlocks.push(emitStorageInit(key, storeName, storageKey, defaultVal));
160
+ // Expanded signal with default
161
+ if (isExpandedSignal(def)) {
162
+ stateEntries.push([key, def.default]);
141
163
  continue;
142
164
  }
143
165
 
144
- // $prototype: "Request"
145
- if (d.$prototype === "Request") {
146
- stateEntries.push([key, null]);
147
- initBlocks.push(emitRequestInit(key, d));
148
- continue;
149
- }
166
+ if (isPrototypeDef(def)) {
167
+ // $prototype: "LocalStorage" / "SessionStorage"
168
+ if (def.$prototype === "LocalStorage" || def.$prototype === "SessionStorage") {
169
+ const storeName = def.$prototype === "LocalStorage" ? "localStorage" : "sessionStorage";
170
+ const storageKey = def.key ?? key;
171
+ const defaultVal = def.default ?? null;
172
+ stateEntries.push([key, null]);
173
+ initBlocks.push(emitStorageInit(key, storeName, storageKey, defaultVal));
174
+ continue;
175
+ }
150
176
 
151
- // $prototype: "Cookie"
152
- if (d.$prototype === "Cookie") {
153
- stateEntries.push([key, null]);
154
- initBlocks.push(emitCookieInit(key, d.name ?? key, d.default ?? null));
155
- continue;
177
+ // $prototype: "Request"
178
+ if (def.$prototype === "Request") {
179
+ stateEntries.push([key, null]);
180
+ initBlocks.push(emitRequestInit(key, def));
181
+ continue;
182
+ }
183
+
184
+ // $prototype: "Cookie"
185
+ if (def.$prototype === "Cookie") {
186
+ stateEntries.push([key, null]);
187
+ initBlocks.push(emitCookieInit(key, def.name ?? key, def.default ?? null));
188
+ continue;
189
+ }
156
190
  }
157
191
 
158
- // Plain object → reactive state
159
- stateEntries.push([key, d]);
192
+ // Plain object → reactive state (parsed JSON, so structurally a JsonObject)
193
+ stateEntries.push([key, def as JsonObject]);
160
194
  }
161
195
 
162
196
  // Build HTML tree with data-bind markers
@@ -213,7 +247,7 @@ ${importmapEntries.join(",\n")}
213
247
  </body>
214
248
  </html>`;
215
249
 
216
- return { html, files: [{ path: modulePath, content: moduleContent }] };
250
+ return { files: [{ content: moduleContent, path: modulePath }], html };
217
251
  }
218
252
 
219
253
  // ─── HTML tree walker ─────────────────────────────────────────────────────────
@@ -221,9 +255,9 @@ ${importmapEntries.join(",\n")}
221
255
  /**
222
256
  * @param {JxMutableNode} def
223
257
  * @param {JxMutableNode} raw
224
- * @param {any} context
258
+ * @param {CompileContext} context
225
259
  * @param {Map<string, string>} bindings
226
- * @param {Map<string, any>} handlers
260
+ * @param {Map<string, HandlerDef>} handlers
227
261
  * @param {{
228
262
  * t: number;
229
263
  * s: number;
@@ -238,9 +272,9 @@ ${importmapEntries.join(",\n")}
238
272
  function buildClientNode(
239
273
  def: JxMutableNode,
240
274
  raw: JxMutableNode,
241
- context: any,
275
+ context: CompileContext,
242
276
  bindings: Map<string, string>,
243
- handlers: Map<string, any>,
277
+ handlers: Map<string, HandlerDef>,
244
278
  counter: {
245
279
  t: number;
246
280
  s: number;
@@ -258,7 +292,9 @@ function buildClientNode(
258
292
  if (typeof def === "number" || typeof def === "boolean") {
259
293
  return escapeHtml(String(def));
260
294
  }
261
- if (!def || typeof def !== "object") return "";
295
+ if (!def || typeof def !== "object") {
296
+ return "";
297
+ }
262
298
 
263
299
  const nextContext = createCompileContext(
264
300
  raw,
@@ -271,7 +307,7 @@ function buildClientNode(
271
307
  const bindAttrs = [];
272
308
  let needsBind = false;
273
309
 
274
- // textContent bindings
310
+ // TextContent bindings
275
311
  if (def.textContent !== undefined) {
276
312
  const tc = raw?.textContent ?? (def.textContent as unknown as JxMutableNode);
277
313
  if (isRefObject(tc)) {
@@ -280,40 +316,41 @@ function buildClientNode(
280
316
  addRefBinding(bindings, key, (tc as JxMutableNode).$ref as string);
281
317
  needsBind = true;
282
318
  } else if (isTemplateString(tc)) {
283
- const key = `_t${counter.t++}`;
319
+ const key = `_t${counter.t}`;
320
+ counter.t += 1;
284
321
  bindAttrs.push(`:text-content="${key}"`);
285
- bindings.set(key, "() => `" + tc + "`");
322
+ bindings.set(key, `() => \`${tc}\``);
286
323
  needsBind = true;
287
324
  }
288
325
  }
289
326
 
290
327
  // Event handlers (onclick, oninput, etc.)
291
328
  for (const [prop, val] of Object.entries(def)) {
292
- if (!prop.startsWith("on") || prop === "observedAttributes") continue;
329
+ if (!prop.startsWith("on") || prop === "observedAttributes") {
330
+ continue;
331
+ }
293
332
  const eventName = prop.slice(2).toLowerCase();
294
333
  if (isRefObject(val)) {
295
- const key = refToBindingKey((val as JxMutableNode).$ref as string);
334
+ const key = refToBindingKey(val.$ref);
296
335
  bindAttrs.push(`@${eventName}="${key}"`);
297
336
  needsBind = true;
298
- } else if (val && typeof val === "object" && (val as JxMutableNode).$prototype === "Function") {
299
- const v = val as JxMutableNode;
300
- const key = `_h${counter.h++}`;
337
+ } else if (isFunctionDef(val)) {
338
+ const key = `_h${counter.h}`;
339
+ counter.h += 1;
301
340
  bindAttrs.push(`@${eventName}="${key}"`);
302
341
  handlers.set(key, {
303
- args: v.parameters ?? v.arguments ?? ["state", "event"],
304
- body: v.body,
342
+ args: val.parameters ? paramNames(val.parameters) : (val.arguments ?? ["state", "event"]),
343
+ body: val.body,
305
344
  });
306
345
  needsBind = true;
307
- } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ val) {
308
- const key = `_h${counter.h++}`;
346
+ } else if (isExpressionDef(val)) {
347
+ const key = `_h${counter.h}`;
348
+ counter.h += 1;
309
349
  bindAttrs.push(`@${eventName}="${key}"`);
310
- const compiled = compileExpression(
311
- (val as Record<string, unknown>).$expression as ExpressionNode,
312
- {
313
- statePrefix: "state",
314
- eventParam: "e",
315
- },
316
- );
350
+ const compiled = compileExpression(val.$expression as ExpressionNode, {
351
+ eventParam: "e",
352
+ statePrefix: "state",
353
+ });
317
354
  handlers.set(key, { args: ["state", "e"], body: compiled });
318
355
  needsBind = true;
319
356
  }
@@ -328,13 +365,17 @@ function buildClientNode(
328
365
  prop.startsWith("&") ||
329
366
  prop.startsWith("[") ||
330
367
  prop.startsWith("@")
331
- )
368
+ ) {
369
+ continue;
370
+ }
371
+ if (val === null || typeof val === "object") {
332
372
  continue;
333
- if (val === null || typeof val === "object") continue;
373
+ }
334
374
  if (isTemplateString(val)) {
335
- const key = `_s${counter.s++}`;
375
+ const key = `_s${counter.s}`;
376
+ counter.s += 1;
336
377
  bindAttrs.push(`:style.${camelToKebab(prop)}="${key}"`);
337
- bindings.set(key, "() => `" + val + "`");
378
+ bindings.set(key, `() => \`${val}\``);
338
379
  needsBind = true;
339
380
  }
340
381
  }
@@ -349,9 +390,10 @@ function buildClientNode(
349
390
  addRefBinding(bindings, key, (val as JxMutableNode).$ref as string);
350
391
  needsBind = true;
351
392
  } else if (isTemplateString(val)) {
352
- const key = `_t${counter.t++}`;
393
+ const key = `_t${counter.t}`;
394
+ counter.t += 1;
353
395
  bindAttrs.push(`:attr.${attr}="${key}"`);
354
- bindings.set(key, "() => `" + val + "`");
396
+ bindings.set(key, `() => \`${val}\``);
355
397
  needsBind = true;
356
398
  }
357
399
  }
@@ -371,17 +413,19 @@ function buildClientNode(
371
413
  prop === "textContent" ||
372
414
  prop === "innerHTML" ||
373
415
  prop === "attributes"
374
- )
416
+ ) {
375
417
  continue;
418
+ }
376
419
  if (isRefObject(val)) {
377
420
  const key = refToBindingKey((val as JxMutableNode).$ref as string);
378
421
  bindAttrs.push(`:${camelToKebab(prop)}="${key}"`);
379
422
  addRefBinding(bindings, key, (val as JxMutableNode).$ref as string);
380
423
  needsBind = true;
381
424
  } else if (isTemplateString(val)) {
382
- const key = `_t${counter.t++}`;
425
+ const key = `_t${counter.t}`;
426
+ counter.t += 1;
383
427
  bindAttrs.push(`:${camelToKebab(prop)}="${key}"`);
384
- bindings.set(key, "() => `" + val + "`");
428
+ bindings.set(key, `() => \`${val}\``);
385
429
  needsBind = true;
386
430
  }
387
431
  }
@@ -389,7 +433,7 @@ function buildClientNode(
389
433
  // Build static attrs
390
434
  const staticAttrs = buildAttrs(def, nextContext.scope);
391
435
  const dataBindAttr = needsBind ? " data-bind" : "";
392
- const bindAttrStr = bindAttrs.length > 0 ? " " + bindAttrs.join(" ") : "";
436
+ const bindAttrStr = bindAttrs.length > 0 ? ` ${bindAttrs.join(" ")}` : "";
393
437
 
394
438
  // Inner content
395
439
  let inner = "";
@@ -401,25 +445,21 @@ function buildClientNode(
401
445
  const value = resolveStaticValue(source.textContent, nextContext.scope);
402
446
  inner = value == null ? "" : escapeHtml(String(value));
403
447
  } else if (source.innerHTML) {
404
- // resolveStaticValue may return null if innerHTML contains `${` from rendered content
448
+ // ResolveStaticValue may return null if innerHTML contains `${` from rendered content
405
449
  // (e.g., code examples) that isn't an actual template expression. Fall back to raw value.
406
450
  inner = (resolveStaticValue(source.innerHTML, nextContext.scope) as string) ?? source.innerHTML;
407
- } else if (
408
- source.children &&
409
- typeof source.children === "object" &&
410
- !Array.isArray(source.children) &&
411
- (source.children as JxMutableNode).$prototype === "Array"
412
- ) {
451
+ } else if (isMappedArray(source.children)) {
413
452
  // ─── Mapped array → lit-html render binding ───
414
453
  counter.needsLit = true;
415
- const listKey = `_list${counter.l++}`;
416
- const arrayDef = source.children as JxMutableNode;
454
+ const listKey = `_list${counter.l}`;
455
+ counter.l += 1;
456
+ const arrayDef = source.children;
417
457
 
418
458
  // Resolve items source expression
419
459
  let itemsExpr;
420
460
  if (isRefObject(arrayDef.items)) {
421
461
  const path = refToBindingKey(arrayDef.items.$ref);
422
- itemsExpr = "state." + path;
462
+ itemsExpr = `state.${path}`;
423
463
  } else {
424
464
  itemsExpr = JSON.stringify(arrayDef.items);
425
465
  }
@@ -428,27 +468,27 @@ function buildClientNode(
428
468
  const litTemplate = emitLitMapTemplate(arrayDef.map);
429
469
  bindings.set(
430
470
  listKey,
431
- "() => (" + itemsExpr + " ?? []).map((item, index) => html`" + litTemplate + "`)",
471
+ `() => (${itemsExpr} ?? []).map((item, index) => html\`${litTemplate}\`)`,
432
472
  );
433
473
 
434
474
  bindAttrs.push(`:render="${listKey}"`);
435
475
  needsBind = true;
436
476
  // Re-derive the data-bind/attr strings since we added to bindAttrs
437
477
  const dataBindAttr2 = " data-bind";
438
- const bindAttrStr2 = " " + bindAttrs.join(" ");
478
+ const bindAttrStr2 = ` ${bindAttrs.join(" ")}`;
439
479
  const selfClosing = new Set(["input", "br", "hr", "img", "meta", "link"]);
440
480
  if (selfClosing.has(tag)) {
441
481
  return `<${tag}${staticAttrs}${dataBindAttr2}${bindAttrStr2}>`;
442
482
  }
443
483
  return `<${tag}${staticAttrs}${dataBindAttr2}${bindAttrStr2}></${tag}>`;
444
484
  } else if (Array.isArray(source.children)) {
445
- const rawChildren = raw?.children;
485
+ const rawChildren = Array.isArray(raw?.children) ? raw.children : undefined;
446
486
  inner = source.children
447
487
  .map((c, i) => {
448
488
  const childRaw = rawChildren?.[i] ?? c;
449
489
  return buildClientNode(
450
- c as unknown as JxMutableNode,
451
- childRaw as unknown as JxMutableNode,
490
+ c as JxMutableNode,
491
+ childRaw as JxMutableNode,
452
492
  nextContext,
453
493
  bindings,
454
494
  handlers,
@@ -476,26 +516,31 @@ function buildClientNode(
476
516
  * @param {JxMutableNode} def
477
517
  * @returns {string}
478
518
  */
479
- function emitLitMapTemplate(def: JxMutableNode) {
480
- if (!def) return "";
519
+ function emitLitMapTemplate(def: JxMutableNode | undefined) {
520
+ if (!def) {
521
+ return "";
522
+ }
481
523
  const tag = def.tagName ?? "div";
482
524
  let attrs = "";
483
525
 
484
- if (def.id) attrs += ' id="' + def.id + '"';
485
- if (def.className) attrs += ' class="' + mapRefsToLit(def.className) + '"';
526
+ if (def.id) {
527
+ attrs += ` id="${def.id}"`;
528
+ }
529
+ if (def.className) {
530
+ attrs += ` class="${mapRefsToLit(def.className)}"`;
531
+ }
486
532
 
487
- // attributes object
533
+ // Attributes object
488
534
  if (def.attributes && typeof def.attributes === "object") {
489
535
  for (const [k, v] of Object.entries(def.attributes)) {
490
- if (typeof v === "string" && isTemplateString(v)) {
491
- attrs += " " + k + '="' + mapRefsToLit(v) + '"';
492
- } else {
493
- attrs += " " + k + '="' + escapeHtml(String(v)) + '"';
494
- }
536
+ attrs +=
537
+ typeof v === "string" && isTemplateString(v)
538
+ ? ` ${k}="${mapRefsToLit(v)}"`
539
+ : ` ${k}="${escapeHtml(String(v))}"`;
495
540
  }
496
541
  }
497
542
 
498
- // style → inline CSS
543
+ // Style → inline CSS
499
544
  if (def.style && typeof def.style === "object") {
500
545
  const parts: string[] = [];
501
546
  for (const [k, v] of Object.entries(def.style)) {
@@ -505,46 +550,51 @@ function emitLitMapTemplate(def: JxMutableNode) {
505
550
  k.startsWith("&") ||
506
551
  k.startsWith("[") ||
507
552
  k.startsWith("@")
508
- )
553
+ ) {
554
+ continue;
555
+ }
556
+ if (v === null || typeof v === "object") {
509
557
  continue;
510
- if (v === null || typeof v === "object") continue;
558
+ }
511
559
  const cssProp = camelToKebab(k);
512
560
  if (isTemplateString(String(v))) {
513
- parts.push(cssProp + ": " + mapRefsToLit(String(v)));
561
+ parts.push(`${cssProp}: ${mapRefsToLit(String(v))}`);
514
562
  } else {
515
- parts.push(cssProp + ": " + v);
563
+ parts.push(`${cssProp}: ${v}`);
516
564
  }
517
565
  }
518
566
  if (parts.length > 0) {
519
- attrs += ' style="' + parts.join("; ") + '"';
567
+ attrs += ` style="${parts.join("; ")}"`;
520
568
  }
521
569
  }
522
570
 
523
571
  // Event handlers in map template
524
572
  for (const [prop, val] of Object.entries(def)) {
525
- if (!prop.startsWith("on") || prop === "observedAttributes") continue;
573
+ if (!prop.startsWith("on") || prop === "observedAttributes") {
574
+ continue;
575
+ }
526
576
  const eventName = prop.slice(2).toLowerCase();
527
577
  if (isRefObject(val)) {
528
578
  const key = refToBindingKey((val as JxMutableNode).$ref as string);
529
- attrs += " @" + eventName + "=${(e) => { state.$map = { item, index }; on." + key + "(e); }}";
579
+ attrs += ` @${eventName}=\${(e) => { state.$map = { item, index }; on.${key}(e); }}`;
530
580
  } else if (val && typeof val === "object" && (val as JxMutableNode).$prototype === "Function") {
531
581
  const body = mapRefsToLit((val as JxMutableNode).body as string);
532
- attrs += " @" + eventName + "=${(e) => { " + body + " }}";
582
+ attrs += ` @${eventName}=\${(e) => { ${body} }}`;
533
583
  } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ val) {
534
584
  const compiled = compileExpression(
535
585
  (val as Record<string, unknown>).$expression as ExpressionNode,
536
586
  {
537
- statePrefix: "state",
538
587
  eventParam: "e",
588
+ statePrefix: "state",
539
589
  },
540
590
  );
541
- attrs += " @" + eventName + "=${(e) => { state.$map = { item, index }; " + compiled + "; }}";
591
+ attrs += ` @${eventName}=\${(e) => { state.$map = { item, index }; ${compiled}; }}`;
542
592
  }
543
593
  }
544
594
 
545
595
  // Non-reserved properties that render as attributes
546
596
  if (def.contentEditable) {
547
- attrs += ' contenteditable="' + def.contentEditable + '"';
597
+ attrs += ` contenteditable="${def.contentEditable}"`;
548
598
  }
549
599
 
550
600
  // Inner content
@@ -555,22 +605,23 @@ function emitLitMapTemplate(def: JxMutableNode) {
555
605
  inner = mapRefsToLit(tc);
556
606
  } else if (isRefObject(def.textContent)) {
557
607
  const path = refToBindingKey((def.textContent as unknown as JxMutableNode).$ref as string);
558
- inner = "${state." + path + "}";
608
+ inner = `\${state.${path}}`;
559
609
  } else {
560
610
  inner = escapeHtml(tc);
561
611
  }
562
612
  } else if (def.innerHTML) {
563
613
  inner = mapRefsToLit(String(def.innerHTML));
564
614
  } else if (Array.isArray(def.children)) {
565
- inner =
566
- "\n " +
567
- def.children.map((c) => emitLitMapTemplate(c as JxMutableNode)).join("\n ") +
568
- "\n ";
615
+ inner = `\n ${def.children
616
+ .map((c) => emitLitMapTemplate(c as JxMutableNode))
617
+ .join("\n ")}\n `;
569
618
  }
570
619
 
571
620
  const voidTags = new Set(["input", "br", "hr", "img", "meta", "link"]);
572
- if (voidTags.has(tag)) return "<" + tag + attrs + ">";
573
- return "<" + tag + attrs + ">" + inner + "</" + tag + ">";
621
+ if (voidTags.has(tag)) {
622
+ return `<${tag}${attrs}>`;
623
+ }
624
+ return `<${tag}${attrs}>${inner}</${tag}>`;
574
625
  }
575
626
 
576
627
  /**
@@ -580,7 +631,7 @@ function emitLitMapTemplate(def: JxMutableNode) {
580
631
  * @returns {string}
581
632
  */
582
633
  function mapRefsToLit(str: string) {
583
- return str.replace(/\$map\./g, "");
634
+ return str.replaceAll("$map.", "");
584
635
  }
585
636
 
586
637
  // ─── JS module generation ─────────────────────────────────────────────────────
@@ -605,10 +656,10 @@ function mapRefsToLit(str: string) {
605
656
  * @returns {string}
606
657
  */
607
658
  function emitClientModule(
608
- stateEntries: [string, any][],
659
+ stateEntries: [string, JsonValue | undefined][],
609
660
  computedEntries: [string, string][],
610
661
  bindEntries: [string, string][],
611
- onEntries: [string, any][],
662
+ onEntries: [string, HandlerDef][],
612
663
  initBlocks: string[],
613
664
  srcImportMap: Map<string, Set<string>>,
614
665
  counter: {
@@ -623,32 +674,34 @@ function emitClientModule(
623
674
  _reactivitySrc: string,
624
675
  ) {
625
676
  const lines: string[] = [];
626
- const needsLit = counter.needsLit;
677
+ const { needsLit } = counter;
627
678
  const needsComputed = computedEntries.length > 0;
628
679
 
629
680
  lines.push("// Generated by @jxsuite/compiler — do not edit manually");
630
681
 
631
682
  // Reactivity imports
632
683
  const reactivityImports = ["reactive", "effect"];
633
- if (needsComputed) reactivityImports.push("computed");
634
- lines.push("import { " + reactivityImports.join(", ") + " } from '@vue/reactivity';");
684
+ if (needsComputed) {
685
+ reactivityImports.push("computed");
686
+ }
687
+ lines.push(`import { ${reactivityImports.join(", ")} } from '@vue/reactivity';`);
635
688
 
636
- // lit-html imports (only when arrays are present)
689
+ // Lit-html imports (only when arrays are present)
637
690
  if (needsLit) {
638
691
  lines.push("import { html, render } from 'lit-html';");
639
692
  }
640
693
 
641
694
  // $src imports
642
695
  for (const [src, names] of srcImportMap) {
643
- lines.push("import { " + [...names].join(", ") + " } from '" + src + "';");
696
+ lines.push(`import { ${[...names].join(", ")} } from '${src}';`);
644
697
  }
645
698
 
646
699
  lines.push("");
647
700
 
648
- // state — reactive state
701
+ // State — reactive state
649
702
  lines.push("const state = reactive({");
650
703
  for (const [key, val] of stateEntries) {
651
- lines.push(" " + key + ": " + JSON.stringify(val) + ",");
704
+ lines.push(` ${key}: ${JSON.stringify(val)},`);
652
705
  }
653
706
  lines.push("});");
654
707
  lines.push("");
@@ -664,16 +717,16 @@ function emitClientModule(
664
717
  // Computed signals on state
665
718
  if (computedEntries.length > 0) {
666
719
  for (const [key, expr] of computedEntries) {
667
- lines.push("state." + key + " = computed(" + expr + ");");
720
+ lines.push(`state.${key} = computed(${expr});`);
668
721
  }
669
722
  lines.push("");
670
723
  }
671
724
 
672
- // bind — DOM getters
725
+ // Bind — DOM getters
673
726
  if (bindEntries.length > 0) {
674
727
  lines.push("const bind = {");
675
728
  for (const [key, expr] of bindEntries) {
676
- lines.push(" " + key + ": " + expr + ",");
729
+ lines.push(` ${key}: ${expr},`);
677
730
  }
678
731
  lines.push("};");
679
732
  } else {
@@ -681,22 +734,16 @@ function emitClientModule(
681
734
  }
682
735
  lines.push("");
683
736
 
684
- // on — event handlers
737
+ // On — event handlers
685
738
  if (onEntries.length > 0) {
686
739
  lines.push("const on = {");
687
740
  for (const [key, def] of onEntries) {
688
741
  const argNames = def.args ?? ["state"];
689
742
  const callArgs = argNames.map((a: string) => (a === "state" ? "state" : "e")).join(", ");
690
743
  lines.push(
691
- " " +
692
- key +
693
- ": (e) => { const fn = (" +
694
- argNames.join(", ") +
695
- ") => { " +
696
- def.body +
697
- " }; fn(" +
698
- callArgs +
699
- "); },",
744
+ ` ${key}: (e) => { const fn = (${argNames.join(", ")}) => { ${def.body} }; fn(${
745
+ callArgs
746
+ }); },`,
700
747
  );
701
748
  }
702
749
  lines.push("};");
@@ -746,42 +793,47 @@ function emitClientModule(
746
793
  * @param {JxMutableNode} def
747
794
  * @returns {string}
748
795
  */
749
- function emitRequestInit(key: string, def: JxMutableNode) {
750
- const url = def.url;
796
+ function emitRequestInit(key: string, def: JxPrototypeDef) {
797
+ const { url } = def;
751
798
  const method = def.method ?? "GET";
752
799
  const isTemplateUrl = isTemplateString(url);
753
800
 
754
801
  if (def.manual) {
755
- return "// " + key + ": manual Request — fetch triggered by user action";
802
+ return `// ${key}: manual Request — fetch triggered by user action`;
756
803
  }
757
804
 
758
- const lines: string[] = [];
759
- lines.push("// " + key + ": auto-fetch from " + (isTemplateUrl ? "(dynamic URL)" : url));
760
- lines.push("effect(() => {");
805
+ const lines: string[] = [
806
+ `// ${key}: auto-fetch from ${isTemplateUrl ? "(dynamic URL)" : url}`,
807
+ "effect(() => {",
808
+ ];
761
809
 
762
810
  if (isTemplateUrl) {
763
- lines.push(" const url = `" + url + "`;");
811
+ lines.push(` const url = \`${url}\`;`);
764
812
  lines.push(' if (!url || url === "undefined" || url.includes("undefined")) return;');
765
813
  } else {
766
- lines.push(" const url = " + JSON.stringify(url) + ";");
814
+ lines.push(` const url = ${JSON.stringify(url)};`);
767
815
  }
768
816
 
769
817
  const fetchOpts: string[] = [];
770
- if (method !== "GET") fetchOpts.push("method: " + JSON.stringify(method));
771
- if (def.headers) fetchOpts.push("headers: " + JSON.stringify(def.headers));
818
+ if (method !== "GET") {
819
+ fetchOpts.push(`method: ${JSON.stringify(method)}`);
820
+ }
821
+ if (def.headers) {
822
+ fetchOpts.push(`headers: ${JSON.stringify(def.headers)}`);
823
+ }
772
824
  if (def.body) {
773
825
  const bodyStr =
774
826
  typeof def.body === "object"
775
827
  ? JSON.stringify(JSON.stringify(def.body))
776
828
  : JSON.stringify(def.body);
777
- fetchOpts.push("body: " + bodyStr);
829
+ fetchOpts.push(`body: ${bodyStr}`);
778
830
  }
779
831
 
780
- const optsStr = fetchOpts.length > 0 ? ", { " + fetchOpts.join(", ") + " }" : "";
781
- lines.push(" fetch(url" + optsStr + ")");
832
+ const optsStr = fetchOpts.length > 0 ? `, { ${fetchOpts.join(", ")} }` : "";
833
+ lines.push(` fetch(url${optsStr})`);
782
834
  lines.push(" .then(r => r.ok ? r.json() : Promise.reject(r.statusText))");
783
- lines.push(" .then(d => { state." + key + " = d; })");
784
- lines.push(" .catch(e => { state." + key + " = { error: String(e) }; });");
835
+ lines.push(` .then(d => { state.${key} = d; })`);
836
+ lines.push(` .catch(e => { state.${key} = { error: String(e) }; });`);
785
837
  lines.push("});");
786
838
 
787
839
  return lines.join("\n");
@@ -795,25 +847,20 @@ function emitRequestInit(key: string, def: JxMutableNode) {
795
847
  * @returns {string}
796
848
  */
797
849
  function emitStorageInit(key: string, storeName: string, storageKey: string, defaultVal: unknown) {
798
- const lines: string[] = [];
799
- lines.push("// " + key + ": " + storeName + ' (key: "' + storageKey + '")');
800
- lines.push("try {");
801
- lines.push(" const _s = " + storeName + ".getItem(" + JSON.stringify(storageKey) + ");");
802
- lines.push(
803
- " state." + key + " = _s !== null ? JSON.parse(_s) : " + JSON.stringify(defaultVal) + ";",
804
- );
805
- lines.push("} catch { state." + key + " = " + JSON.stringify(defaultVal) + "; }");
806
- lines.push("effect(() => {");
807
- lines.push(" const v = state." + key + ";");
808
- lines.push(" try {");
809
- lines.push(
810
- " if (v === null) " + storeName + ".removeItem(" + JSON.stringify(storageKey) + ");",
811
- );
812
- lines.push(
813
- " else " + storeName + ".setItem(" + JSON.stringify(storageKey) + ", JSON.stringify(v));",
814
- );
815
- lines.push(" } catch {}");
816
- lines.push("});");
850
+ const lines: string[] = [
851
+ `// ${key}: ${storeName} (key: "${storageKey}")`,
852
+ "try {",
853
+ ` const _s = ${storeName}.getItem(${JSON.stringify(storageKey)});`,
854
+ ` state.${key} = _s !== null ? JSON.parse(_s) : ${JSON.stringify(defaultVal)};`,
855
+ `} catch { state.${key} = ${JSON.stringify(defaultVal)}; }`,
856
+ "effect(() => {",
857
+ ` const v = state.${key};`,
858
+ " try {",
859
+ ` if (v === null) ${storeName}.removeItem(${JSON.stringify(storageKey)});`,
860
+ ` else ${storeName}.setItem(${JSON.stringify(storageKey)}, JSON.stringify(v));`,
861
+ " } catch {}",
862
+ "});",
863
+ ];
817
864
  return lines.join("\n");
818
865
  }
819
866
 
@@ -824,21 +871,16 @@ function emitStorageInit(key: string, storeName: string, storageKey: string, def
824
871
  * @returns {string}
825
872
  */
826
873
  function emitCookieInit(key: string, cookieName: string, defaultVal: unknown) {
827
- const lines: string[] = [];
828
- lines.push("// " + key + ': Cookie (name: "' + cookieName + '")');
829
- lines.push("{");
830
- lines.push(
831
- ' const _m = document.cookie.match(new RegExp("(?:^|; )' + cookieName + '=([^;]*)"));',
832
- );
833
- lines.push(
834
- " try { state." +
835
- key +
836
- " = _m ? JSON.parse(decodeURIComponent(_m[1])) : " +
837
- JSON.stringify(defaultVal) +
838
- "; }",
839
- );
840
- lines.push(" catch { state." + key + " = _m ? _m[1] : " + JSON.stringify(defaultVal) + "; }");
841
- lines.push("}");
874
+ const lines: string[] = [
875
+ `// ${key}: Cookie (name: "${cookieName}")`,
876
+ "{",
877
+ ` const _m = document.cookie.match(new RegExp("(?:^|; )${cookieName}=([^;]*)"));`,
878
+ ` try { state.${key} = _m ? JSON.parse(decodeURIComponent(_m[1])) : ${JSON.stringify(
879
+ defaultVal,
880
+ )}; }`,
881
+ ` catch { state.${key} = _m ? _m[1] : ${JSON.stringify(defaultVal)}; }`,
882
+ "}",
883
+ ];
842
884
  return lines.join("\n");
843
885
  }
844
886
 
@@ -850,9 +892,9 @@ function emitCookieInit(key: string, cookieName: string, defaultVal: unknown) {
850
892
  */
851
893
  function refToBindingKey(ref: string) {
852
894
  if (ref.startsWith("#/state/")) {
853
- return ref.slice("#/state/".length).replace(/\//g, "_");
895
+ return ref.slice("#/state/".length).replaceAll("/", "_");
854
896
  }
855
- return ref.replace(/\//g, "_");
897
+ return ref.replaceAll("/", "_");
856
898
  }
857
899
 
858
900
  /**
@@ -861,12 +903,14 @@ function refToBindingKey(ref: string) {
861
903
  * @param {string} ref
862
904
  */
863
905
  function addRefBinding(bindings: Map<string, string>, key: string, ref: string) {
864
- if (bindings.has(key)) return;
906
+ if (bindings.has(key)) {
907
+ return;
908
+ }
865
909
  if (ref.startsWith("#/state/")) {
866
910
  const path = ref.slice("#/state/".length);
867
911
  const parts = path.split("/");
868
- bindings.set(key, "() => state." + parts.join("."));
912
+ bindings.set(key, `() => state.${parts.join(".")}`);
869
913
  } else {
870
- bindings.set(key, "() => state." + ref);
914
+ bindings.set(key, `() => state.${ref}`);
871
915
  }
872
916
  }