@jxsuite/compiler 5.1.2 → 5.1.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.
Files changed (40) hide show
  1. package/dist/cli.js +220 -351
  2. package/dist/compiler.js +126 -257
  3. package/package.json +4 -4
  4. package/src/cli.ts +12 -14
  5. package/src/compiler.ts +45 -23
  6. package/src/shadow.ts +9 -8
  7. package/src/shared.ts +350 -296
  8. package/src/site/asset-mounts.ts +22 -12
  9. package/src/site/bundler.ts +38 -12
  10. package/src/site/client-runtime.ts +31 -17
  11. package/src/site/csp.ts +20 -11
  12. package/src/site/db-push.ts +38 -24
  13. package/src/site/dev-command.ts +3 -3
  14. package/src/site/format-host.ts +56 -45
  15. package/src/site/headers-emitter.ts +7 -4
  16. package/src/site/i18n.ts +20 -19
  17. package/src/site/image-cache.ts +16 -3
  18. package/src/site/image-optimizer.ts +19 -5
  19. package/src/site/image-transform.ts +56 -60
  20. package/src/site/img-loading.ts +11 -5
  21. package/src/site/layout-resolver.ts +7 -6
  22. package/src/site/link-relations.ts +10 -6
  23. package/src/site/locale-negotiation.ts +1 -1
  24. package/src/site/pages-discovery.ts +65 -47
  25. package/src/site/preview-server.ts +22 -19
  26. package/src/site/project-sections.ts +12 -3
  27. package/src/site/prototype-resolver.ts +94 -42
  28. package/src/site/schema-command.ts +26 -13
  29. package/src/site/service-worker.ts +2 -2
  30. package/src/site/site-build.ts +379 -262
  31. package/src/site/site-loader.ts +6 -3
  32. package/src/site/static-host.ts +3 -3
  33. package/src/site/validate-command.ts +64 -34
  34. package/src/site/well-known.ts +57 -12
  35. package/src/targets/compile-class.ts +17 -5
  36. package/src/targets/compile-client.ts +141 -106
  37. package/src/targets/compile-element.ts +157 -140
  38. package/src/targets/compile-server.ts +11 -5
  39. package/src/targets/compile-static.ts +57 -63
  40. package/src/types.ts +5 -6
@@ -40,17 +40,21 @@ import {
40
40
  import {
41
41
  bodyReturnsValue,
42
42
  hasStructuredBody,
43
+ isBoolean,
43
44
  isExpandedSignal,
44
45
  isExpressionDef,
45
46
  isFunctionDef,
46
47
  isMappedArray,
47
48
  isNamedFormulaDef,
49
+ isNonNullObject,
50
+ isNumber,
48
51
  isPrototypeDef,
52
+ isString,
49
53
  isTagExpression,
50
54
  paramNames,
51
55
  tagNameCandidates,
52
56
  } from "@jxsuite/schema/guards";
53
- import type { CompileContext, ExpressionNode } from "../shared.ts";
57
+ import type { CompileContext } from "../shared.ts";
54
58
  import {
55
59
  escapeToken,
56
60
  objectKey,
@@ -60,13 +64,21 @@ import {
60
64
  } from "@jxsuite/runtime/pointer";
61
65
  import type {
62
66
  JsonObject,
67
+ JsonPrimitive,
63
68
  JsonValue,
64
69
  JxDocument,
70
+ JxExpressionNode,
65
71
  JxMappedArray,
66
72
  JxMutableNode,
67
73
  JxStyle,
68
74
  } from "@jxsuite/schema/types";
69
75
 
76
+ /**
77
+ * A repeater as `isMappedArray` proves it: only `$prototype` is checked, so a parsed document can
78
+ * reach the emitter without `items`, which then renders as an empty list.
79
+ */
80
+ type RepeaterSlot = Omit<JxMappedArray, "items"> & { items?: JxMappedArray["items"] };
81
+
70
82
  /** A compiled event handler entry: parameter names + function body. */
71
83
  interface HandlerDef {
72
84
  args?: string[] | undefined;
@@ -95,7 +107,7 @@ export function compileClient(
95
107
  modulePath?: string;
96
108
  projectStyle?: JxStyle | null;
97
109
  prePaintScheme?: boolean;
98
- [key: string]: unknown;
110
+ rewriteSrc?: (specifier: string) => string;
99
111
  },
100
112
  ) {
101
113
  const {
@@ -106,11 +118,7 @@ export function compileClient(
106
118
  } = opts;
107
119
 
108
120
  const context = createCompileContext(raw, null, raw.state ?? {}, raw.$media ?? {});
109
- const styleBlock = compileStyles(
110
- raw,
111
- raw.$media ?? {},
112
- (opts.projectStyle ?? null) as JxStyle | null,
113
- );
121
+ const styleBlock = compileStyles(raw, raw.$media ?? {}, opts.projectStyle ?? null);
114
122
 
115
123
  // Collectors for bindings and handlers
116
124
  const counter = { h: 0, l: 0, m: 0, needsLit: false, s: 0, sw: 0, t: 0 };
@@ -129,9 +137,9 @@ export function compileClient(
129
137
 
130
138
  const defs = raw.state ?? {};
131
139
  for (const [key, def] of Object.entries(defs)) {
132
- if (def === null || typeof def !== "object" || Array.isArray(def)) {
140
+ if (!isNonNullObject(def) || Array.isArray(def)) {
133
141
  // Naked primitive or array → reactive state
134
- if (typeof def === "string" && isTemplateString(def)) {
142
+ if (isTemplateString(def)) {
135
143
  // Template string → computed on state so other computeds can ref it
136
144
  computedEntries.push([key, `() => \`${def}\``]);
137
145
  } else {
@@ -142,7 +150,7 @@ export function compileClient(
142
150
 
143
151
  // $expression: Shape 5
144
152
  if (isExpressionDef(def)) {
145
- const node = def.$expression as ExpressionNode;
153
+ const node = def.$expression;
146
154
  const compiled = compileExpression(node, {
147
155
  eventParam: "e",
148
156
  statePrefix: "state",
@@ -162,10 +170,11 @@ export function compileClient(
162
170
  // $prototype: "Function"
163
171
  if (isFunctionDef(def)) {
164
172
  const args = def.parameters ? paramNames(def.parameters) : def.arguments;
165
- if (hasStructuredBody(def)) {
173
+ const { body } = def;
174
+ if (Array.isArray(body)) {
166
175
  // Structured body (spec §20) → an event handler compiled from statements.
167
176
  // With parameters, a positional callable like a named formula.
168
- const compiled = compileStatements(def.body, { eventParam: "e", statePrefix: "state" });
177
+ const compiled = compileStatements(body, { eventParam: "e", statePrefix: "state" });
169
178
  if (def.parameters && def.parameters.length > 0) {
170
179
  initBlocks.push(
171
180
  `state[${JSON.stringify(key)}] = ${emitFormulaFn(def, `(() => {\n${compiled}\n})()`)};`,
@@ -174,19 +183,21 @@ export function compileClient(
174
183
  onEntries.push([key, { args: ["state", "e"], body: compiled }]);
175
184
  }
176
185
  } else if (def.$src) {
177
- if (!srcImportMap.has(def.$src)) {
178
- srcImportMap.set(def.$src, new Set());
186
+ let names = srcImportMap.get(def.$src);
187
+ if (!names) {
188
+ names = new Set();
189
+ srcImportMap.set(def.$src, names);
179
190
  }
180
- (srcImportMap.get(def.$src) as Set<string>).add(srcImportBinding(key, def));
191
+ names.add(srcImportBinding(key, def));
181
192
 
182
193
  // $src functions always produce computed entries (they return values)
183
194
  computedEntries.push([key, `() => { return ${key}(state); }`]);
184
- } else if (typeof def.body === "string" && bodyReturnsValue(def.body)) {
195
+ } else if (isString(body) && bodyReturnsValue(body)) {
185
196
  // Body contains return → computed
186
- computedEntries.push([key, `() => { ${def.body} }`]);
197
+ computedEntries.push([key, `() => { ${body} }`]);
187
198
  } else {
188
199
  // No return → event handler
189
- onEntries.push([key, { args: args ?? ["state"], body: def.body as string | undefined }]);
200
+ onEntries.push([key, { args: args ?? ["state"], body }]);
190
201
  }
191
202
  continue;
192
203
  }
@@ -228,12 +239,17 @@ export function compileClient(
228
239
  }
229
240
  }
230
241
 
231
- // Plain object → reactive state (parsed JSON, so structurally a JsonObject)
242
+ /*
243
+ * Plain object → reactive state.
244
+ * SAFETY: `raw.state` is parsed JSON, so this entry is structurally a JsonObject; the checks
245
+ * above already took the primitives and arrays and every function, expression, schema-only,
246
+ * expanded-signal and storage/request/cookie shape, leaving the naked object literal.
247
+ */
232
248
  stateEntries.push([key, def as JsonObject]);
233
249
  }
234
250
 
235
251
  // Build HTML tree with data-bind markers
236
- const bodyContent = buildClientNode(raw, raw, context, bindings, handlers, counter);
252
+ const bodyContent = buildClientNode(raw, context, bindings, handlers, counter);
237
253
 
238
254
  // Merge inline-discovered bindings/handlers
239
255
  for (const [key, expr] of bindings) {
@@ -257,7 +273,7 @@ export function compileClient(
257
273
  srcImportMap,
258
274
  counter,
259
275
  reactivitySrc,
260
- opts.rewriteSrc as ((specifier: string) => string) | undefined,
276
+ opts.rewriteSrc,
261
277
  );
262
278
 
263
279
  // Build importmap entries — always include lit-html since compiled custom elements need it
@@ -300,8 +316,7 @@ ${importmapEntries.join(",\n")}
300
316
  // ─── HTML tree walker ─────────────────────────────────────────────────────────
301
317
 
302
318
  /**
303
- * @param {JxMutableNode} def
304
- * @param {JxMutableNode} raw
319
+ * @param {JxMutableNode | JsonPrimitive} def - A node or child slot as the document holds it
305
320
  * @param {CompileContext} context
306
321
  * @param {Map<string, string>} bindings
307
322
  * @param {Map<string, HandlerDef>} handlers
@@ -317,8 +332,7 @@ ${importmapEntries.join(",\n")}
317
332
  * @returns {string}
318
333
  */
319
334
  function buildClientNode(
320
- def: JxMutableNode,
321
- raw: JxMutableNode,
335
+ def: JxMutableNode | JsonPrimitive,
322
336
  context: CompileContext,
323
337
  bindings: Map<string, string>,
324
338
  handlers: Map<string, HandlerDef>,
@@ -333,21 +347,21 @@ function buildClientNode(
333
347
  },
334
348
  ) {
335
349
  // String children are text nodes
336
- if (typeof def === "string") {
350
+ if (isString(def)) {
337
351
  return escapeHtml(def);
338
352
  }
339
- if (typeof def === "number" || typeof def === "boolean") {
353
+ if (isNumber(def) || isBoolean(def)) {
340
354
  return escapeHtml(String(def));
341
355
  }
342
- if (!def || typeof def !== "object") {
356
+ if (!isNonNullObject(def)) {
343
357
  return "";
344
358
  }
345
359
 
346
360
  const nextContext = createCompileContext(
347
- raw,
361
+ def,
348
362
  context.scope,
349
- raw?.state ?? context.scopeDefs,
350
- raw?.$media ?? context.media,
363
+ def.state ?? context.scopeDefs,
364
+ def.$media ?? context.media,
351
365
  );
352
366
 
353
367
  /* REFUSED, LOUDLY. A chosen tag has no implementation in this target: the element's identity is
@@ -371,11 +385,11 @@ function buildClientNode(
371
385
 
372
386
  // TextContent bindings
373
387
  if (def.textContent !== undefined) {
374
- const tc = raw?.textContent ?? (def.textContent as unknown as JxMutableNode);
388
+ const tc = def.textContent;
375
389
  if (isRefObject(tc)) {
376
- const key = refToBindingKey((tc as JxMutableNode).$ref as string);
390
+ const key = refToBindingKey(tc.$ref);
377
391
  bindAttrs.push(`:text-content="${key}"`);
378
- addRefBinding(bindings, key, (tc as JxMutableNode).$ref as string);
392
+ addRefBinding(bindings, key, tc.$ref);
379
393
  needsBind = true;
380
394
  } else if (isTemplateString(tc)) {
381
395
  const key = `_t${counter.t}`;
@@ -396,29 +410,32 @@ function buildClientNode(
396
410
  const key = refToBindingKey(val.$ref);
397
411
  bindAttrs.push(`@${eventName}="${key}"`);
398
412
  needsBind = true;
399
- } else if (hasStructuredBody(val)) {
400
- const key = `_h${counter.h}`;
401
- counter.h += 1;
402
- bindAttrs.push(`@${eventName}="${key}"`);
403
- handlers.set(key, {
404
- args: ["state", "e"],
405
- body: compileStatements(val.body, { eventParam: "e", statePrefix: "state" }),
406
- });
407
- needsBind = true;
408
413
  } else if (isFunctionDef(val)) {
409
414
  const key = `_h${counter.h}`;
410
415
  counter.h += 1;
411
416
  bindAttrs.push(`@${eventName}="${key}"`);
412
- handlers.set(key, {
413
- args: val.parameters ? paramNames(val.parameters) : (val.arguments ?? ["state", "event"]),
414
- body: val.body as string | undefined,
415
- });
417
+ const { body } = val;
418
+ // A structured body (spec §20) compiles from statements; source text is used as written.
419
+ handlers.set(
420
+ key,
421
+ Array.isArray(body)
422
+ ? {
423
+ args: ["state", "e"],
424
+ body: compileStatements(body, { eventParam: "e", statePrefix: "state" }),
425
+ }
426
+ : {
427
+ args: val.parameters
428
+ ? paramNames(val.parameters)
429
+ : (val.arguments ?? ["state", "event"]),
430
+ body,
431
+ },
432
+ );
416
433
  needsBind = true;
417
434
  } else if (isExpressionDef(val)) {
418
435
  const key = `_h${counter.h}`;
419
436
  counter.h += 1;
420
437
  bindAttrs.push(`@${eventName}="${key}"`);
421
- const compiled = compileExpression(val.$expression as ExpressionNode, {
438
+ const compiled = compileExpression(val.$expression, {
422
439
  eventParam: "e",
423
440
  statePrefix: "state",
424
441
  });
@@ -428,7 +445,7 @@ function buildClientNode(
428
445
  }
429
446
 
430
447
  // Dynamic style properties
431
- if (def.style && typeof def.style === "object") {
448
+ if (isNonNullObject(def.style)) {
432
449
  for (const [prop, val] of Object.entries(def.style)) {
433
450
  if (
434
451
  prop.startsWith(":") ||
@@ -439,7 +456,7 @@ function buildClientNode(
439
456
  ) {
440
457
  continue;
441
458
  }
442
- if (val === null || typeof val === "object") {
459
+ if (isNonDeclarationValue(val)) {
443
460
  continue;
444
461
  }
445
462
  if (isTemplateString(val)) {
@@ -453,12 +470,12 @@ function buildClientNode(
453
470
  }
454
471
 
455
472
  // Dynamic attributes
456
- if (def.attributes && typeof def.attributes === "object") {
473
+ if (isNonNullObject(def.attributes)) {
457
474
  for (const [attr, val] of Object.entries(def.attributes)) {
458
475
  if (isRefObject(val)) {
459
- const key = refToBindingKey((val as JxMutableNode).$ref as string);
476
+ const key = refToBindingKey(val.$ref);
460
477
  bindAttrs.push(`:attr.${attr}="${key}"`);
461
- addRefBinding(bindings, key, (val as JxMutableNode).$ref as string);
478
+ addRefBinding(bindings, key, val.$ref);
462
479
  needsBind = true;
463
480
  } else if (isTemplateString(val)) {
464
481
  const key = `_t${counter.t}`;
@@ -488,9 +505,9 @@ function buildClientNode(
488
505
  continue;
489
506
  }
490
507
  if (isRefObject(val)) {
491
- const key = refToBindingKey((val as JxMutableNode).$ref as string);
508
+ const key = refToBindingKey(val.$ref);
492
509
  bindAttrs.push(`:${camelToKebab(prop)}="${key}"`);
493
- addRefBinding(bindings, key, (val as JxMutableNode).$ref as string);
510
+ addRefBinding(bindings, key, val.$ref);
494
511
  needsBind = true;
495
512
  } else if (isTemplateString(val)) {
496
513
  const key = `_t${counter.t}`;
@@ -508,7 +525,7 @@ function buildClientNode(
508
525
 
509
526
  // Inner content
510
527
  let inner = "";
511
- const source = raw ?? def;
528
+ const source = def;
512
529
  if (source.$switch) {
513
530
  // ─── $switch → lit-html render binding ───
514
531
  // Without this branch the node fell through to the generic element path, which emitted a
@@ -521,10 +538,11 @@ function buildClientNode(
521
538
  const swKey = `_sw${counter.sw}`;
522
539
  counter.sw += 1;
523
540
  const discriminant = isRefObject(source.$switch)
524
- ? `state.${refToBindingKey((source.$switch as { $ref: string }).$ref)}`
541
+ ? `state.${refToBindingKey(source.$switch.$ref)}`
525
542
  : `\`${String(source.$switch)}\``;
526
- const cases = (source.cases ?? {}) as Record<string, JxMutableNode>;
527
- const caseEntries = Object.entries(cases)
543
+ const cases = source.cases ?? {};
544
+ // Parsed JSON, so a case slot can hold `null` or a bare value the declared type leaves out.
545
+ const caseEntries = Object.entries<JxMutableNode | JsonPrimitive>(cases)
528
546
  // An external `$ref` case cannot be fetched at compile time, exactly as in the static target.
529
547
  .filter(([, caseDef]) => caseDef && !isRefObject(caseDef))
530
548
  .map(
@@ -545,7 +563,8 @@ function buildClientNode(
545
563
  } else if (source.innerHTML) {
546
564
  // ResolveStaticValue may return null if innerHTML contains `${` from rendered content
547
565
  // (e.g., code examples) that isn't an actual template expression. Fall back to raw value.
548
- inner = (resolveStaticValue(source.innerHTML, nextContext.scope) as string) ?? source.innerHTML;
566
+ const resolved = resolveStaticValue(source.innerHTML, nextContext.scope);
567
+ inner = resolved == null ? source.innerHTML : String(resolved);
549
568
  } else if (isMappedArray(source.children)) {
550
569
  // ─── Mapped array → lit-html render binding ───
551
570
  counter.needsLit = true;
@@ -588,7 +607,7 @@ function buildClientNode(
588
607
  const listKey = `_children${counter.l}`;
589
608
  counter.l += 1;
590
609
  const childrenTpl = source.children
591
- .map((c) => emitChildLit(c as JxMutableNode, nextContext.preformatted))
610
+ .map((c) => emitChildLit(c, nextContext.preformatted))
592
611
  .join(childSeparator(nextContext.preformatted, " "));
593
612
  bindings.set(
594
613
  listKey,
@@ -601,19 +620,8 @@ function buildClientNode(
601
620
  const bindAttrStr2 = ` ${bindAttrs.join(" ")}`;
602
621
  return `<${tag}${staticAttrs} data-bind${bindAttrStr2}></${tag}>`;
603
622
  } else if (Array.isArray(source.children)) {
604
- const rawChildren = Array.isArray(raw?.children) ? raw.children : undefined;
605
623
  inner = source.children
606
- .map((c, i) => {
607
- const childRaw = rawChildren?.[i] ?? c;
608
- return buildClientNode(
609
- c as JxMutableNode,
610
- childRaw as JxMutableNode,
611
- nextContext,
612
- bindings,
613
- handlers,
614
- counter,
615
- );
616
- })
624
+ .map((c) => buildClientNode(c, nextContext, bindings, handlers, counter))
617
625
  .join(childSeparator(nextContext.preformatted));
618
626
  }
619
627
 
@@ -632,11 +640,12 @@ function buildClientNode(
632
640
  * Compile a map definition to a lit-html template string. Converts $map.item → item, $map.index →
633
641
  * index.
634
642
  *
635
- * @param {JxMutableNode} def
643
+ * @param {JxMutableNode | null | undefined} def - Absent, or a JSON `null` child slot, renders
644
+ * nothing
636
645
  * @param {boolean} [preformatted]
637
646
  * @returns {string}
638
647
  */
639
- function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false) {
648
+ function emitLitMapTemplate(def: JxMutableNode | null | undefined, preformatted = false) {
640
649
  if (!def) {
641
650
  return "";
642
651
  }
@@ -663,11 +672,11 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
663
672
 
664
673
  // $props → lit property bindings. The element target emits these; the client target emitted
665
674
  // Nothing, so a component in a list silently received none of its per-item props.
666
- if (def.$props && typeof def.$props === "object") {
675
+ if (isNonNullObject(def.$props)) {
667
676
  for (const [k, v] of Object.entries(def.$props)) {
668
677
  if (isRefObject(v)) {
669
- attrs += ` .${k}="\${${mapRefToClientExpr((v as { $ref: string }).$ref)}}"`;
670
- } else if (typeof v === "string" && isTemplateString(v)) {
678
+ attrs += ` .${k}="\${${mapRefToClientExpr(v.$ref)}}"`;
679
+ } else if (isTemplateString(v)) {
671
680
  attrs += ` .${k}="${mapRefsToLit(v)}"`;
672
681
  } else {
673
682
  attrs += ` .${k}="\${${JSON.stringify(v)}}"`;
@@ -676,9 +685,9 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
676
685
  }
677
686
 
678
687
  // Attributes object
679
- if (def.attributes && typeof def.attributes === "object") {
688
+ if (isNonNullObject(def.attributes)) {
680
689
  for (const [k, v] of Object.entries(def.attributes)) {
681
- if (typeof v === "string" && isTemplateString(v)) {
690
+ if (isTemplateString(v)) {
682
691
  attrs += ` ${k}="${mapRefsToLit(v)}"`;
683
692
  continue;
684
693
  }
@@ -688,7 +697,7 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
688
697
  as it hydrates (spec.md §8.3). Stringifying here emitted `open="false"`, which HTML reads
689
698
  by presence and therefore as an OPEN element: the author wrote "closed" and the page
690
699
  rendered open. */
691
- if (typeof v === "boolean") {
700
+ if (isBoolean(v)) {
692
701
  const text = booleanAttrValue(k, v);
693
702
  if (text !== null) {
694
703
  attrs += text === "" ? ` ${k}` : ` ${k}="${escapeHtml(text)}"`;
@@ -700,7 +709,7 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
700
709
  }
701
710
 
702
711
  // Style → inline CSS
703
- if (def.style && typeof def.style === "object") {
712
+ if (isNonNullObject(def.style)) {
704
713
  const parts: string[] = [];
705
714
  for (const [k, v] of Object.entries(def.style)) {
706
715
  if (
@@ -712,7 +721,7 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
712
721
  ) {
713
722
  continue;
714
723
  }
715
- if (v === null || typeof v === "object") {
724
+ if (isNonDeclarationValue(v)) {
716
725
  continue;
717
726
  }
718
727
  const cssProp = camelToKebab(k);
@@ -734,22 +743,30 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
734
743
  }
735
744
  const eventName = prop.slice(2).toLowerCase();
736
745
  if (isRefObject(val)) {
737
- const key = refToBindingKey((val as JxMutableNode).$ref as string);
746
+ const key = refToBindingKey(val.$ref);
738
747
  attrs += ` @${eventName}=\${(e) => { state.$map = { item, index }; on.${key}(e); }}`;
739
748
  } else if (hasStructuredBody(val)) {
740
749
  const compiled = compileStatements(val.body, { eventParam: "e", statePrefix: "state" });
741
750
  attrs += ` @${eventName}=\${(e) => { state.$map = { item, index }; ${compiled} }}`;
742
- } else if (val && typeof val === "object" && (val as JxMutableNode).$prototype === "Function") {
743
- const body = mapRefsToLit((val as JxMutableNode).body as string);
751
+ } else if (isFunctionDef(val)) {
752
+ /*
753
+ * SAFETY: hasStructuredBody above took every statement-array body, so `body` is source text
754
+ * or absent. An absent one (a `$src` function) reaches replaceAll and throws, as it always
755
+ * has: a repeater handler is emitted inline and needs inline source.
756
+ */
757
+ const body = mapRefsToLit(val.body as string);
744
758
  attrs += ` @${eventName}=\${(e) => { ${body} }}`;
745
- } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ val) {
746
- const compiled = compileExpression(
747
- (val as Record<string, unknown>).$expression as ExpressionNode,
748
- {
749
- eventParam: "e",
750
- statePrefix: "state",
751
- },
752
- );
759
+ } else if (isNonNullObject(val) && "$expression" in val) {
760
+ /*
761
+ * SAFETY: an `on*` member carrying `$expression` is a Shape 5 expression entry, whose
762
+ * `$expression` the document schema defines as an expression node. Presence is all this
763
+ * branch has ever tested; a malformed node is not filtered here but fails inside
764
+ * compileExpression, as it did before.
765
+ */
766
+ const compiled = compileExpression(val.$expression as JxExpressionNode, {
767
+ eventParam: "e",
768
+ statePrefix: "state",
769
+ });
753
770
  attrs += ` @${eventName}=\${(e) => { state.$map = { item, index }; ${compiled}; }}`;
754
771
  }
755
772
  }
@@ -766,7 +783,7 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
766
783
  if (isTemplateString(tc)) {
767
784
  inner = mapRefsToLit(tc);
768
785
  } else if (isRefObject(def.textContent)) {
769
- const path = refToBindingKey((def.textContent as unknown as JxMutableNode).$ref as string);
786
+ const path = refToBindingKey(def.textContent.$ref);
770
787
  inner = `\${state.${path}}`;
771
788
  } else {
772
789
  inner = escapeHtml(tc);
@@ -792,15 +809,15 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
792
809
  * Emit one child of a lit template: a string text node, an array pseudo-element (expanded inline
793
810
  * via a `.map()` hole), or a nested element.
794
811
  *
795
- * @param {JxMutableNode | string} c
812
+ * @param {JxMutableNode | JsonPrimitive} c - A child slot as the document holds it
796
813
  * @param {boolean} [preformatted]
797
814
  * @returns {string}
798
815
  */
799
- function emitChildLit(c: JxMutableNode | string, preformatted = false): string {
800
- if (typeof c === "string") {
816
+ function emitChildLit(c: JxMutableNode | JsonPrimitive, preformatted = false): string {
817
+ if (isString(c)) {
801
818
  return isTemplateString(c) ? mapRefsToLit(c) : escapeHtml(c);
802
819
  }
803
- if (typeof c === "number" || typeof c === "boolean") {
820
+ if (isNumber(c) || isBoolean(c)) {
804
821
  return escapeHtml(String(c));
805
822
  }
806
823
  if (isMappedArray(c)) {
@@ -813,13 +830,13 @@ function emitChildLit(c: JxMutableNode | string, preformatted = false): string {
813
830
  * Emit a lit-html `${(items ?? []).map(...)}` hole for a mapped array, expanding its template
814
831
  * inline among sibling nodes (wrapper-less).
815
832
  *
816
- * @param {JxMappedArray} arrayDef
833
+ * @param {RepeaterSlot} arrayDef
817
834
  * @returns {string}
818
835
  */
819
- function emitArrayHole(arrayDef: JxMappedArray): string {
836
+ function emitArrayHole(arrayDef: RepeaterSlot): string {
820
837
  const { items } = arrayDef;
821
838
  const itemsExpr = isRefObject(items)
822
- ? `state.${refToBindingKey((items as { $ref: string }).$ref)}`
839
+ ? `state.${refToBindingKey(items.$ref)}`
823
840
  : JSON.stringify(items ?? []);
824
841
  const tpl = emitLitMapTemplate(arrayDef.map);
825
842
  return `\${(${itemsExpr} ?? []).map((item, index) => html\`${tpl}\`)}`;
@@ -1013,10 +1030,15 @@ function emitClientModule(
1013
1030
  * @param {string} key
1014
1031
  * @param {string} storeName
1015
1032
  * @param {string} storageKey
1016
- * @param {unknown} defaultVal
1033
+ * @param {JsonValue} defaultVal
1017
1034
  * @returns {string}
1018
1035
  */
1019
- function emitStorageInit(key: string, storeName: string, storageKey: string, defaultVal: unknown) {
1036
+ function emitStorageInit(
1037
+ key: string,
1038
+ storeName: string,
1039
+ storageKey: string,
1040
+ defaultVal: JsonValue,
1041
+ ) {
1020
1042
  const lines: string[] = [
1021
1043
  `// ${key}: ${storeName} (key: "${storageKey}")`,
1022
1044
  "try {",
@@ -1037,10 +1059,10 @@ function emitStorageInit(key: string, storeName: string, storageKey: string, def
1037
1059
  /**
1038
1060
  * @param {string} key
1039
1061
  * @param {string} cookieName
1040
- * @param {unknown} defaultVal
1062
+ * @param {JsonValue} defaultVal
1041
1063
  * @returns {string}
1042
1064
  */
1043
- function emitCookieInit(key: string, cookieName: string, defaultVal: unknown) {
1065
+ function emitCookieInit(key: string, cookieName: string, defaultVal: JsonValue) {
1044
1066
  const lines: string[] = [
1045
1067
  `// ${key}: Cookie (name: "${cookieName}")`,
1046
1068
  "{",
@@ -1056,6 +1078,19 @@ function emitCookieInit(key: string, cookieName: string, defaultVal: unknown) {
1056
1078
 
1057
1079
  // ─── Helpers ──────────────────────────────────────────────────────────────────
1058
1080
 
1081
+ /**
1082
+ * Whether a style entry is something other than one declaration value: a nested block, a block
1083
+ * list, a `$ref`, or a JSON `null`. None of those is emitted as an inline declaration.
1084
+ *
1085
+ * @param {JxStyle[string] | null} value
1086
+ * @returns {boolean}
1087
+ */
1088
+ function isNonDeclarationValue(
1089
+ value: JxStyle[string] | null,
1090
+ ): value is Exclude<JxStyle[string], string | number | undefined> | null {
1091
+ return value === null || isNonNullObject(value);
1092
+ }
1093
+
1059
1094
  /**
1060
1095
  * @param {string} ref
1061
1096
  * @returns {string}