@jxsuite/compiler 5.1.2 → 6.0.1

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