@jxsuite/compiler 0.18.0 → 0.20.0

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.
package/src/shared.js CHANGED
@@ -42,7 +42,7 @@ export const SCHEMA_KEYWORDS = new Set([
42
42
  /**
43
43
  * Returns true if a $src path points to a .class.json schema-defined class.
44
44
  *
45
- * @param {any} src
45
+ * @param {unknown} src
46
46
  * @returns {boolean}
47
47
  */
48
48
  export function isClassJsonSrc(src) {
@@ -52,7 +52,7 @@ export function isClassJsonSrc(src) {
52
52
  /**
53
53
  * Returns true if an object contains only schema keywords (no `default`, no `$prototype`).
54
54
  *
55
- * @param {any} obj
55
+ * @param {Record<string, unknown>} obj
56
56
  * @returns {boolean}
57
57
  */
58
58
  export function isSchemaOnly(obj) {
@@ -65,7 +65,7 @@ export function isSchemaOnly(obj) {
65
65
  /**
66
66
  * Returns true if a string contains a ${} template expression.
67
67
  *
68
- * @param {any} val
68
+ * @param {unknown} val
69
69
  * @returns {boolean}
70
70
  */
71
71
  export function isTemplateString(val) {
@@ -75,7 +75,7 @@ export function isTemplateString(val) {
75
75
  /**
76
76
  * Determine whether a node (or any of its descendants) requires client-side JavaScript.
77
77
  *
78
- * @param {any} def
78
+ * @param {JxElement | JxMutableNode | string} def
79
79
  * @returns {boolean}
80
80
  */
81
81
  export function isDynamic(def) {
@@ -90,22 +90,26 @@ export function isDynamic(def) {
90
90
  d &&
91
91
  typeof d === "object" &&
92
92
  !Array.isArray(d) &&
93
- /** @type {any} */ (d).timing === "compiler"
93
+ /** @type {JxPrototypeDef} */ (d).timing === "compiler"
94
94
  )
95
95
  continue;
96
96
  if (typeof d !== "object" || d === null || Array.isArray(d)) return true;
97
- if (/** @type {any} */ (d).$prototype) return true;
98
- if ("default" in /** @type {any} */ (d)) return true;
97
+ if (/** @type {JxPrototypeDef} */ (d).$prototype) return true;
98
+ if ("default" in /** @type {object} */ (d)) return true;
99
99
  if (isSchemaOnly(d)) continue;
100
100
  return true;
101
101
  }
102
102
  }
103
103
 
104
104
  if (def.$switch) return true;
105
- if (def.children?.$prototype === "Array") return true;
105
+ if (
106
+ !Array.isArray(def.children) &&
107
+ /** @type {JxMappedArray | undefined} */ (def.children)?.$prototype === "Array"
108
+ )
109
+ return true;
106
110
 
107
111
  if (Array.isArray(def.children)) {
108
- if (def.children.some(/** @param {any} c */ (c) => isDynamic(c))) return true;
112
+ if (def.children.some((c) => isDynamic(c))) return true;
109
113
  }
110
114
 
111
115
  for (const [key, val] of Object.entries(def)) {
@@ -113,7 +117,7 @@ export function isDynamic(def) {
113
117
  if (
114
118
  val !== null &&
115
119
  typeof val === "object" &&
116
- typeof (/** @type {any} */ (val).$ref) === "string"
120
+ typeof (/** @type {JxMutableNode} */ (val).$ref) === "string"
117
121
  )
118
122
  return true;
119
123
  if (isTemplateString(val)) return true;
@@ -137,21 +141,25 @@ export function isDynamic(def) {
137
141
  /**
138
142
  * Shallow variant of isDynamic — checks only this node's own properties, not its children.
139
143
  *
140
- * @param {any} def
144
+ * @param {JxElement | JxMutableNode | string} def
141
145
  * @returns {boolean}
142
146
  */
143
147
  export function isNodeDynamic(def) {
144
148
  if (!def || typeof def !== "object") return false;
145
149
 
146
150
  if (def.$switch) return true;
147
- if (def.children?.$prototype === "Array") return true;
151
+ if (
152
+ !Array.isArray(def.children) &&
153
+ /** @type {JxMappedArray | undefined} */ (def.children)?.$prototype === "Array"
154
+ )
155
+ return true;
148
156
 
149
157
  for (const [key, val] of Object.entries(def)) {
150
158
  if (RESERVED_KEYS.has(key)) continue;
151
159
  if (
152
160
  val !== null &&
153
161
  typeof val === "object" &&
154
- typeof (/** @type {any} */ (val).$ref) === "string"
162
+ typeof (/** @type {JxMutableNode} */ (val).$ref) === "string"
155
163
  )
156
164
  return true;
157
165
  if (isTemplateString(val)) return true;
@@ -175,25 +183,28 @@ export function isNodeDynamic(def) {
175
183
  /**
176
184
  * Returns true if any node in the tree will need dynamic handling.
177
185
  *
178
- * @param {any} def
186
+ * @param {JxElement | JxMutableNode | string} def
179
187
  * @returns {boolean}
180
188
  */
181
189
  export function hasAnyIsland(def) {
182
190
  if (!def || typeof def !== "object") return false;
183
191
  if (isDynamic(def)) return true;
184
- if (Array.isArray(def.children))
185
- return def.children.some(/** @param {any} c */ (c) => hasAnyIsland(c));
192
+ if (Array.isArray(def.children)) return def.children.some((c) => hasAnyIsland(c));
186
193
  return false;
187
194
  }
188
195
 
189
196
  // ─── Scope / value resolution ─────────────────────────────────────────────────
190
197
 
191
198
  /**
192
- * @param {any} raw
193
- * @param {any} [parentScope]
194
- * @param {Record<string, any>} [scopeDefs]
195
- * @param {Record<string, any>} [media]
196
- * @returns {{ scope: any; scopeDefs: Record<string, any>; media: Record<string, any> }}
199
+ * @param {JxElement | JxMutableNode | null} raw
200
+ * @param {Record<string, unknown> | null} [parentScope]
201
+ * @param {Record<string, unknown>} [scopeDefs]
202
+ * @param {Record<string, string>} [media]
203
+ * @returns {{
204
+ * scope: Record<string, unknown>;
205
+ * scopeDefs: Record<string, unknown>;
206
+ * media: Record<string, string>;
207
+ * }}
197
208
  */
198
209
  export function createCompileContext(raw, parentScope = null, scopeDefs = {}, media = {}) {
199
210
  const scope = raw?.state
@@ -203,9 +214,9 @@ export function createCompileContext(raw, parentScope = null, scopeDefs = {}, me
203
214
  }
204
215
 
205
216
  /**
206
- * @param {Record<string, any>} [defs]
207
- * @param {any} [parentScope]
208
- * @returns {any}
217
+ * @param {Record<string, JxStateDefinition>} [defs]
218
+ * @param {Record<string, unknown> | null} [parentScope]
219
+ * @returns {Record<string, unknown>}
209
220
  */
210
221
  export function buildInitialScope(defs = {}, parentScope = null) {
211
222
  const scope = Object.create(parentScope ?? null);
@@ -215,12 +226,13 @@ export function buildInitialScope(defs = {}, parentScope = null) {
215
226
  setOwnScopeValue(scope, key, cloneValue(def));
216
227
  continue;
217
228
  }
218
- if ("default" in def) {
219
- setOwnScopeValue(scope, key, cloneValue(def.default));
229
+ const d = /** @type {JxStateObject & JxPrototypeDef} */ (def);
230
+ if ("default" in d) {
231
+ setOwnScopeValue(scope, key, cloneValue(d.default));
220
232
  continue;
221
233
  }
222
- if (!def.$prototype && !isSchemaOnly(def)) {
223
- setOwnScopeValue(scope, key, cloneValue(def));
234
+ if (!d.$prototype && !isSchemaOnly(d)) {
235
+ setOwnScopeValue(scope, key, cloneValue(d));
224
236
  }
225
237
  }
226
238
 
@@ -230,21 +242,22 @@ export function buildInitialScope(defs = {}, parentScope = null) {
230
242
  continue;
231
243
  }
232
244
  if (!def || typeof def !== "object") continue;
233
- if (def.$prototype === "Function") {
234
- if (def.body) {
235
- const fn = new Function("state", ...(def.parameters ?? def.arguments ?? []), def.body);
236
- if (def.body.includes("return")) {
245
+ const d = /** @type {JxStateObject & JxPrototypeDef} */ (def);
246
+ if (d.$prototype === "Function") {
247
+ if (d.body) {
248
+ const fn = new Function("state", ...(d.parameters ?? d.arguments ?? []), d.body);
249
+ if (d.body.includes("return")) {
237
250
  defineLazyScopeValue(scope, key, () => fn(scope));
238
251
  } else {
239
252
  setOwnScopeValue(scope, key, fn);
240
253
  }
241
- } else if (!def.body?.includes("return")) {
254
+ } else if (!d.body?.includes("return")) {
242
255
  setOwnScopeValue(scope, key, () => {});
243
256
  }
244
257
  continue;
245
258
  }
246
- if (def.$prototype === "LocalStorage" || def.$prototype === "SessionStorage") {
247
- setOwnScopeValue(scope, key, cloneValue(def.default ?? null));
259
+ if (d.$prototype === "LocalStorage" || d.$prototype === "SessionStorage") {
260
+ setOwnScopeValue(scope, key, cloneValue(d.default ?? null));
248
261
  }
249
262
  }
250
263
 
@@ -252,9 +265,9 @@ export function buildInitialScope(defs = {}, parentScope = null) {
252
265
  }
253
266
 
254
267
  /**
255
- * @param {any} scope
268
+ * @param {Record<string, unknown>} scope
256
269
  * @param {string} key
257
- * @param {any} value
270
+ * @param {unknown} value
258
271
  */
259
272
  export function setOwnScopeValue(scope, key, value) {
260
273
  Object.defineProperty(scope, key, {
@@ -266,9 +279,9 @@ export function setOwnScopeValue(scope, key, value) {
266
279
  }
267
280
 
268
281
  /**
269
- * @param {any} scope
282
+ * @param {Record<string, unknown>} scope
270
283
  * @param {string} key
271
- * @param {() => any} getter
284
+ * @param {() => unknown} getter
272
285
  */
273
286
  export function defineLazyScopeValue(scope, key, getter) {
274
287
  Object.defineProperty(scope, key, {
@@ -279,35 +292,40 @@ export function defineLazyScopeValue(scope, key, getter) {
279
292
  }
280
293
 
281
294
  /**
282
- * @param {any} value
283
- * @param {any} scope
284
- * @returns {any}
295
+ * @param {unknown} value
296
+ * @param {Record<string, unknown>} scope
297
+ * @returns {unknown}
285
298
  */
286
299
  export function resolveStaticValue(value, scope) {
287
- if (isRefObject(value)) return resolveRefValue(value.$ref, scope);
288
- if (isTemplateString(value)) return evaluateStaticTemplate(value, scope);
300
+ if (isRefObject(value)) return resolveRefValue(/** @type {JxMutableNode} */ (value).$ref, scope);
301
+ if (isTemplateString(value)) return evaluateStaticTemplate(/** @type {string} */ (value), scope);
289
302
  return value;
290
303
  }
291
304
 
292
305
  /**
293
- * @param {any} value
306
+ * @param {unknown} value
294
307
  * @returns {boolean}
295
308
  */
296
309
  export function isRefObject(value) {
297
- return value !== null && typeof value === "object" && typeof value.$ref === "string";
310
+ return (
311
+ value !== null &&
312
+ typeof value === "object" &&
313
+ typeof (/** @type {JxMutableNode} */ (value).$ref) === "string"
314
+ );
298
315
  }
299
316
 
300
317
  /**
301
- * @param {any} refValue
302
- * @param {any} scope
303
- * @returns {any}
318
+ * @param {unknown} refValue
319
+ * @param {Record<string, unknown>} scope
320
+ * @returns {unknown}
304
321
  */
305
322
  export function resolveRefValue(refValue, scope) {
306
323
  if (typeof refValue !== "string") return refValue;
307
324
  if (refValue.startsWith("$map/")) {
308
325
  const parts = refValue.split("/");
309
326
  const key = parts[1];
310
- const base = scope.$map?.[key] ?? scope["$map/" + key];
327
+ const base = /** @type {Record<string, unknown> | undefined} */ (scope.$map)?.[key] ??
328
+ scope["$map/" + key];
311
329
  return parts.length > 2 ? getPathValue(base, parts.slice(2).join("/")) : base;
312
330
  }
313
331
  if (refValue.startsWith("#/state/")) {
@@ -321,13 +339,11 @@ export function resolveRefValue(refValue, scope) {
321
339
 
322
340
  /**
323
341
  * @param {string} str
324
- * @param {any} scope
325
- * @returns {any}
342
+ * @param {Record<string, unknown>} scope
343
+ * @returns {unknown}
326
344
  */
327
345
  export function evaluateStaticTemplate(str, scope) {
328
346
  try {
329
- // When the entire string is a single ${...} expression, evaluate it directly
330
- // to preserve the return type (boolean, number, etc.) instead of stringifying.
331
347
  const singleExprMatch = str.match(/^\$\{(.+)\}$/s);
332
348
  if (singleExprMatch) {
333
349
  const fn = new Function("state", "$map", `return (${singleExprMatch[1]})`);
@@ -341,23 +357,26 @@ export function evaluateStaticTemplate(str, scope) {
341
357
  }
342
358
 
343
359
  /**
344
- * @param {any} base
360
+ * @param {unknown} base
345
361
  * @param {string} path
346
- * @returns {any}
362
+ * @returns {unknown}
347
363
  */
348
364
  export function getPathValue(base, path) {
349
365
  if (!path) return base;
350
- return path
351
- .split("/")
352
- .reduce(
353
- (/** @type {any} */ acc, /** @type {string} */ key) => (acc == null ? undefined : acc[key]),
354
- base,
355
- );
366
+ return path.split("/").reduce(
367
+ /**
368
+ * @param {Record<string, unknown>} acc
369
+ * @param {string} key
370
+ * @returns {Record<string, unknown>}
371
+ */
372
+ (acc, key) => (/** @type {Record<string, unknown>} */ (acc == null ? undefined : acc[key])),
373
+ /** @type {Record<string, unknown>} */ (base),
374
+ );
356
375
  }
357
376
 
358
377
  /**
359
- * @param {any} value
360
- * @returns {any}
378
+ * @param {unknown} value
379
+ * @returns {unknown}
361
380
  */
362
381
  export function cloneValue(value) {
363
382
  if (value === null || typeof value !== "object") return value;
@@ -369,9 +388,8 @@ export function cloneValue(value) {
369
388
  /**
370
389
  * Build an HTML attribute string from a static element definition.
371
390
  *
372
- * @param {any} def
373
- * @param {any} scope
374
- * @returns {string}
391
+ * @param {JxElement | JxMutableNode} def
392
+ * @param {Record<string, unknown>} scope @returns {string}
375
393
  */
376
394
  export function buildAttrs(def, scope) {
377
395
  let out = "";
@@ -384,14 +402,14 @@ export function buildAttrs(def, scope) {
384
402
  const lang = resolveStaticValue(def.lang, scope);
385
403
  const dir = resolveStaticValue(def.dir, scope);
386
404
 
387
- if (id) out += ` id="${escapeHtml(id)}"`;
388
- if (className) out += ` class="${escapeHtml(className)}"`;
405
+ if (id) out += ` id="${escapeHtml(String(id))}"`;
406
+ if (className) out += ` class="${escapeHtml(String(className))}"`;
389
407
  if (hidden) out += " hidden";
390
408
  if (tabIndex !== undefined && tabIndex !== null)
391
409
  out += ` tabindex="${escapeHtml(String(tabIndex))}"`;
392
- if (title) out += ` title="${escapeHtml(title)}"`;
393
- if (lang) out += ` lang="${escapeHtml(lang)}"`;
394
- if (dir) out += ` dir="${escapeHtml(dir)}"`;
410
+ if (title) out += ` title="${escapeHtml(String(title))}"`;
411
+ if (lang) out += ` lang="${escapeHtml(String(lang))}"`;
412
+ if (dir) out += ` dir="${escapeHtml(String(dir))}"`;
395
413
 
396
414
  if (def.style && scope) {
397
415
  const inline = Object.entries(def.style)
@@ -445,9 +463,13 @@ export function buildAttrs(def, scope) {
445
463
  /**
446
464
  * Build the inner HTML (textContent or children) for a node.
447
465
  *
448
- * @param {any} def
449
- * @param {any} raw
450
- * @param {{ scope: any; scopeDefs: Record<string, any>; media: Record<string, any> }} context
466
+ * @param {JxElement} def
467
+ * @param {JxElement | null} raw
468
+ * @param {{
469
+ * scope: Record<string, unknown>;
470
+ * scopeDefs: Record<string, unknown>;
471
+ * media: Record<string, string>;
472
+ * }} context
451
473
  * @param {(def: any, raw: any, context: any) => string} childCompiler
452
474
  * @returns {string}
453
475
  */
@@ -458,12 +480,13 @@ export function buildInner(def, raw, context, childCompiler) {
458
480
  const value = resolveStaticValue(source.textContent, context.scope);
459
481
  return value == null ? "" : escapeHtml(String(value));
460
482
  }
461
- if (source.innerHTML) return resolveStaticValue(source.innerHTML, context.scope) ?? "";
483
+ if (source.innerHTML)
484
+ return /** @type {string} */ (resolveStaticValue(source.innerHTML, context.scope)) ?? "";
462
485
  if (Array.isArray(source.children)) {
463
486
  const rawChildren = raw?.children;
464
487
  return source.children
465
- .map((/** @type {any} */ c, /** @type {number} */ i) => {
466
- const childRaw = rawChildren?.[i] ?? c;
488
+ .map((/** @type {JxElement | JxMutableNode | string} */ c, /** @type {number} */ i) => {
489
+ const childRaw = /** @type {(JxElement | string)[] | undefined} */ (rawChildren)?.[i] ?? c;
467
490
  return childCompiler(c, childRaw, context);
468
491
  })
469
492
  .join("\n ");
@@ -476,8 +499,9 @@ export function buildInner(def, raw, context, childCompiler) {
476
499
  /**
477
500
  * Walk the entire document tree and collect all static nested CSS rules.
478
501
  *
479
- * @param {any} doc
480
- * @param {Record<string, any>} [mediaQueries]
502
+ * @param {JxElement | JxMutableNode} doc
503
+ * @param {Record<string, string>} [mediaQueries]
504
+ * @param {JxStyle | null} [projectStyle]
481
505
  * @returns {string}
482
506
  */
483
507
  export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
@@ -488,10 +512,42 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
488
512
  // everything else on body. Project-level style is implicitly :root, so a
489
513
  // flat object like { "--bg": "#000", "margin": "0" } is the expected format.
490
514
  if (projectStyle && typeof projectStyle === "object") {
515
+ function emitProjectRules(/** @type {string} */ selector, /** @type {JxMutableNode} */ obj) {
516
+ const props = toCSSText(obj);
517
+ if (props) rules.push(`${selector} { ${props} }`);
518
+ for (const [key, val] of Object.entries(obj)) {
519
+ if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
520
+ if (key.startsWith("@")) {
521
+ const query = key.startsWith("@--")
522
+ ? (mediaQueries[key.slice(1)] ?? key.slice(1))
523
+ : key.slice(1);
524
+ const mProps = toCSSText(val);
525
+ if (mProps) rules.push(`@media ${query} { ${selector} { ${mProps} } }`);
526
+ for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ (val))) {
527
+ if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
528
+ if (sel.startsWith("@")) continue;
529
+ const resolved = sel.startsWith("&")
530
+ ? sel.replace("&", selector)
531
+ : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
532
+ ? `${selector}${sel}`
533
+ : `${selector} ${sel}`;
534
+ const subProps = toCSSText(sub);
535
+ if (subProps) rules.push(`@media ${query} { ${resolved} { ${subProps} } }`);
536
+ }
537
+ continue;
538
+ }
539
+ const resolved = key.startsWith("&")
540
+ ? key.replace("&", selector)
541
+ : key.startsWith(":") || key.startsWith(".") || key.startsWith("[")
542
+ ? `${selector}${key}`
543
+ : `${selector} ${key}`;
544
+ emitProjectRules(resolved, /** @type {Record<string, unknown>} */ (val));
545
+ }
546
+ }
547
+
491
548
  for (const [key, val] of Object.entries(projectStyle)) {
492
549
  if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
493
- // Standalone selector (e.g. `.dark`, `:focus-visible`)
494
- rules.push(`${key} { ${toCSSText(/** @type {any} */ (val))} }`);
550
+ emitProjectRules(key, /** @type {Record<string, unknown>} */ (val));
495
551
  } else if (
496
552
  val !== null &&
497
553
  typeof val === "object" &&
@@ -499,21 +555,20 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
499
555
  !key.startsWith("@") &&
500
556
  !key.startsWith("--")
501
557
  ) {
502
- // Element selector with object value (e.g. `html`, `*`)
503
- rules.push(`${key} { ${toCSSText(/** @type {any} */ (val))} }`);
558
+ emitProjectRules(key, val);
504
559
  } else if (key.startsWith("@")) {
505
- // @media block
560
+ // @media block at top level
506
561
  const query = key.startsWith("@--")
507
562
  ? (mediaQueries[key.slice(1)] ?? key.slice(1))
508
563
  : key.slice(1);
509
- rules.push(`@media ${query} { body { ${toCSSText(/** @type {any} */ (val))} } }`);
564
+ rules.push(`@media ${query} { body { ${toCSSText(/** @type {object} */ (val))} } }`);
510
565
  }
511
566
  }
512
567
  // Collect CSS custom properties into :root {}
513
- /** @type {Record<string, any>} */
568
+ /** @type {Record<string, unknown>} */
514
569
  const rootProps = {};
515
570
  // Collect direct CSS properties into body {}
516
- /** @type {Record<string, any>} */
571
+ /** @type {Record<string, unknown>} */
517
572
  const bodyProps = {};
518
573
  for (const [key, val] of Object.entries(projectStyle)) {
519
574
  if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[") || key.startsWith("@"))
@@ -525,11 +580,11 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
525
580
  bodyProps[key] = val;
526
581
  }
527
582
  }
528
- const rootCSS = toCSSText(/** @type {any} */ (rootProps));
583
+ const rootCSS = toCSSText(rootProps);
529
584
  if (rootCSS) {
530
585
  rules.push(`:root { ${rootCSS} }`);
531
586
  }
532
- const bodyCSS = toCSSText(/** @type {any} */ (bodyProps));
587
+ const bodyCSS = toCSSText(bodyProps);
533
588
  if (bodyCSS) {
534
589
  rules.push(`body { ${bodyCSS} }`);
535
590
  }
@@ -542,9 +597,50 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
542
597
  }
543
598
 
544
599
  /**
545
- * @param {any} def
600
+ * Recursively emit CSS rules for a nested element selector.
601
+ *
602
+ * @param {string} selector
603
+ * @param {Record<string, unknown>} obj
604
+ * @param {string[]} rules
605
+ * @param {Record<string, string>} mediaQueries
606
+ */
607
+ function emitNestedElement(selector, obj, rules, mediaQueries) {
608
+ const props = toCSSText(obj);
609
+ if (props) rules.push(`${selector} { ${props} }`);
610
+ for (const [key, val] of Object.entries(obj)) {
611
+ if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
612
+ if (key.startsWith("@")) {
613
+ const query = key.startsWith("@--")
614
+ ? (mediaQueries[key.slice(1)] ?? key.slice(1))
615
+ : key.slice(1);
616
+ const mProps = toCSSText(val);
617
+ if (mProps) rules.push(`@media ${query} { ${selector} { ${mProps} } }`);
618
+ for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ (val))) {
619
+ if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
620
+ if (sel.startsWith("@")) continue;
621
+ const resolved = sel.startsWith("&")
622
+ ? sel.replace("&", selector)
623
+ : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
624
+ ? `${selector}${sel}`
625
+ : `${selector} ${sel}`;
626
+ const subProps = toCSSText(sub);
627
+ if (subProps) rules.push(`@media ${query} { ${resolved} { ${subProps} } }`);
628
+ }
629
+ continue;
630
+ }
631
+ const resolved = key.startsWith("&")
632
+ ? key.replace("&", selector)
633
+ : key.startsWith(":") || key.startsWith(".") || key.startsWith("[")
634
+ ? `${selector}${key}`
635
+ : `${selector} ${key}`;
636
+ emitNestedElement(resolved, /** @type {Record<string, unknown>} */ (val), rules, mediaQueries);
637
+ }
638
+ }
639
+
640
+ /**
641
+ * @param {JxElement | JxMutableNode | string} def
546
642
  * @param {string[]} rules
547
- * @param {Record<string, any>} mediaQueries
643
+ * @param {Record<string, string>} mediaQueries
548
644
  * @param {string} [_parentSel]
549
645
  * @param {{ n: number }} [counter]
550
646
  */
@@ -590,40 +686,43 @@ export function collectStyles(
590
686
  }
591
687
 
592
688
  for (const [prop, val] of Object.entries(def.style)) {
689
+ if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
593
690
  if (prop.startsWith("@")) {
594
691
  const query = prop.startsWith("@--")
595
692
  ? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
596
693
  : prop.slice(1);
597
- rules.push(`@media ${query} { ${selector} { ${toCSSText(/** @type {any} */ (val))} } }`);
598
- for (const [sel, nestedRules] of Object.entries(/** @type {Record<string, any>} */ (val))) {
599
- if (
600
- !(
601
- sel.startsWith(":") ||
602
- sel.startsWith(".") ||
603
- sel.startsWith("&") ||
604
- sel.startsWith("[")
605
- )
606
- )
694
+ rules.push(`@media ${query} { ${selector} { ${toCSSText(val)} } }`);
695
+ for (const [sel, nestedRules] of Object.entries(
696
+ /** @type {Record<string, unknown>} */ (val),
697
+ )) {
698
+ if (nestedRules === null || typeof nestedRules !== "object" || Array.isArray(nestedRules))
607
699
  continue;
608
- const resolved = sel.startsWith("&") ? sel.replace("&", selector) : `${selector}${sel}`;
609
- rules.push(
610
- `@media ${query} { ${resolved} { ${toCSSText(/** @type {any} */ (nestedRules))} } }`,
611
- );
700
+ if (sel.startsWith("@")) continue;
701
+ const resolved = sel.startsWith("&")
702
+ ? sel.replace("&", selector)
703
+ : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
704
+ ? `${selector}${sel}`
705
+ : `${selector} ${sel}`;
706
+ rules.push(`@media ${query} { ${resolved} { ${toCSSText(nestedRules)} } }`);
612
707
  }
613
- } else if (
614
- prop.startsWith(":") ||
615
- prop.startsWith(".") ||
616
- prop.startsWith("&") ||
617
- prop.startsWith("[")
618
- ) {
619
- const resolved = prop.startsWith("&") ? prop.replace("&", selector) : `${selector}${prop}`;
620
- rules.push(`${resolved} { ${toCSSText(/** @type {any} */ (val))} }`);
708
+ } else {
709
+ const resolved = prop.startsWith("&")
710
+ ? prop.replace("&", selector)
711
+ : prop.startsWith(":") || prop.startsWith(".") || prop.startsWith("[")
712
+ ? `${selector}${prop}`
713
+ : `${selector} ${prop}`;
714
+ emitNestedElement(
715
+ resolved,
716
+ /** @type {Record<string, unknown>} */ (val),
717
+ rules,
718
+ mediaQueries,
719
+ );
621
720
  }
622
721
  }
623
722
  }
624
723
 
625
724
  if (Array.isArray(def.children)) {
626
- def.children.forEach((/** @type {any} */ c) => {
725
+ def.children.forEach((/** @type {JxElement | JxMutableNode | string} */ c) => {
627
726
  collectStyles(c, rules, mediaQueries, selector, counter, prefix);
628
727
  });
629
728
  }
@@ -674,7 +773,7 @@ export function tagNameToClassName(tagName) {
674
773
  /**
675
774
  * Recursively collect unique $src values from $prototype: "Function" entries.
676
775
  *
677
- * @param {any} doc
776
+ * @param {JxElement} doc
678
777
  * @returns {string[]}
679
778
  */
680
779
  export function collectSrcImports(doc) {
@@ -685,7 +784,7 @@ export function collectSrcImports(doc) {
685
784
  }
686
785
 
687
786
  /**
688
- * @param {any} def
787
+ * @param {JxElement | JxMutableNode | string} def
689
788
  * @param {Set<string>} srcs
690
789
  */
691
790
  function _walkSrc(def, srcs) {
@@ -695,40 +794,42 @@ function _walkSrc(def, srcs) {
695
794
  if (
696
795
  d &&
697
796
  typeof d === "object" &&
698
- /** @type {any} */ (d).$prototype === "Function" &&
699
- /** @type {any} */ (d).$src
797
+ /** @type {JxMutableNode} */ (d).$prototype === "Function" &&
798
+ /** @type {JxMutableNode} */ (d).$src
700
799
  ) {
701
- srcs.add(/** @type {any} */ (d).$src);
800
+ srcs.add(/** @type {string} */ (/** @type {JxMutableNode} */ (d).$src));
702
801
  }
703
802
  }
704
803
  }
705
804
  if (Array.isArray(def.children)) {
706
- def.children.forEach((/** @type {any} */ c) => _walkSrc(c, srcs));
805
+ def.children.forEach((/** @type {JxElement | JxMutableNode | string} */ c) =>
806
+ _walkSrc(c, srcs),
807
+ );
707
808
  }
708
809
  }
709
810
 
710
811
  /**
711
812
  * Recursively collect all `timing: "server"` entries from the document tree.
712
813
  *
713
- * @param {any} doc
714
- * @returns {any[]}
814
+ * @param {JxElement} doc
815
+ * @returns {{ key: string; exportName: string; src: string }[]}
715
816
  */
716
817
  export function collectServerEntries(doc) {
717
- /** @type {Map<string, any>} */
818
+ /** @type {Map<string, { key: string; exportName: string; src: string }>} */
718
819
  const entries = new Map();
719
820
  _walkServerEntries(doc, entries);
720
821
  return [...entries.values()];
721
822
  }
722
823
 
723
824
  /**
724
- * @param {any} def
725
- * @param {Map<string, any>} entries
825
+ * @param {JxElement | JxMutableNode | string} def
826
+ * @param {Map<string, { key: string; exportName: string; src: string }>} entries
726
827
  */
727
828
  function _walkServerEntries(def, entries) {
728
829
  if (!def || typeof def !== "object") return;
729
830
  if (def.state) {
730
831
  for (const [key, d] of Object.entries(def.state)) {
731
- const entry = /** @type {any} */ (d);
832
+ const entry = /** @type {JxMutableNode} */ (d);
732
833
  if (
733
834
  entry &&
734
835
  typeof entry === "object" &&
@@ -742,7 +843,9 @@ function _walkServerEntries(def, entries) {
742
843
  }
743
844
  }
744
845
  if (Array.isArray(def.children)) {
745
- def.children.forEach((/** @type {any} */ c) => _walkServerEntries(c, entries));
846
+ def.children.forEach((/** @type {JxElement | JxMutableNode | string} */ c) =>
847
+ _walkServerEntries(c, entries),
848
+ );
746
849
  }
747
850
  }
748
851
 
@@ -754,8 +857,8 @@ const SELF_CLOSING = new Set(["input", "br", "hr", "img", "meta", "link", "area"
754
857
  /**
755
858
  * Recursively render a Jx node tree to static HTML for pre-rendering.
756
859
  *
757
- * @param {any} node
758
- * @param {any} scope
860
+ * @param {JxElement | JxMutableNode | string} node
861
+ * @param {Record<string, unknown>} scope
759
862
  * @param {string | null} [slotContent] - HTML to substitute for `<slot>` elements
760
863
  * @returns {string}
761
864
  */
@@ -769,7 +872,11 @@ export function renderStaticNode(node, scope, slotContent = null) {
769
872
  }
770
873
  if (typeof node === "number" || typeof node === "boolean") return escapeHtml(String(node));
771
874
  if (Array.isArray(node))
772
- return node.map((/** @type {any} */ c) => renderStaticNode(c, scope, slotContent)).join("\n");
875
+ return node
876
+ .map((/** @type {JxElement | JxMutableNode | string} */ c) =>
877
+ renderStaticNode(c, scope, slotContent),
878
+ )
879
+ .join("\n");
773
880
  if (!node || typeof node !== "object") return "";
774
881
 
775
882
  // Skip mapped arrays — can't pre-render dynamic lists
@@ -790,10 +897,12 @@ export function renderStaticNode(node, scope, slotContent = null) {
790
897
  inner = val != null ? escapeHtml(String(val)) : "";
791
898
  } else if (node.innerHTML) {
792
899
  const val = resolveStaticValue(node.innerHTML, scope);
793
- inner = val != null ? val : node.innerHTML;
900
+ inner = val != null ? String(val) : /** @type {string} */ (node.innerHTML);
794
901
  } else if (Array.isArray(node.children)) {
795
902
  inner = node.children
796
- .map((/** @type {any} */ c) => renderStaticNode(c, scope, slotContent))
903
+ .map((/** @type {JxElement | JxMutableNode | string} */ c) =>
904
+ renderStaticNode(c, scope, slotContent),
905
+ )
797
906
  .join("\n");
798
907
  }
799
908
 
@@ -803,39 +912,41 @@ export function renderStaticNode(node, scope, slotContent = null) {
803
912
  /**
804
913
  * Pre-render a component definition to static HTML for its inner content.
805
914
  *
806
- * @param {any} doc - Component JSON definition
807
- * @param {Record<string, any> | null} [propsOverride] - Instance-specific prop values to merge into
808
- * state
915
+ * @param {JxElement} doc - Component JSON definition
916
+ * @param {Record<string, unknown> | null} [propsOverride] - Instance-specific prop values to merge
917
+ * into state
809
918
  * @param {string | null} [slotContent] - HTML to substitute for `<slot>` elements
810
919
  * @returns {string} The pre-rendered innerHTML
811
920
  */
812
921
  export function preRenderComponentHtml(doc, propsOverride = null, slotContent = null) {
922
+ /** @type {Record<string, JxStateDefinition>} */
813
923
  let stateDefs = doc.state ?? {};
814
924
  if (propsOverride) {
815
925
  stateDefs = { ...stateDefs };
816
926
  for (const [key, value] of Object.entries(propsOverride)) {
817
927
  if (key in stateDefs) {
818
928
  const existing = stateDefs[key];
819
- // If the existing definition is an expanded signal with "default", override the default
820
929
  if (
821
930
  existing &&
822
931
  typeof existing === "object" &&
823
932
  !Array.isArray(existing) &&
824
933
  "default" in existing
825
934
  ) {
826
- stateDefs[key] = { ...existing, default: value };
935
+ stateDefs[key] = { .../** @type {JxStateObject} */ (existing), default: value };
827
936
  } else {
828
- stateDefs[key] = value;
937
+ stateDefs[key] = /** @type {JxStateDefinition} */ (value);
829
938
  }
830
939
  } else {
831
- stateDefs[key] = value;
940
+ stateDefs[key] = /** @type {JxStateDefinition} */ (value);
832
941
  }
833
942
  }
834
943
  }
835
944
  const scope = buildInitialScope(stateDefs, null);
836
945
  if (!Array.isArray(doc.children)) return "";
837
946
  return doc.children
838
- .map((/** @type {any} */ c) => renderStaticNode(c, scope, slotContent))
947
+ .map((/** @type {JxElement | JxMutableNode | string} */ c) =>
948
+ renderStaticNode(c, scope, slotContent),
949
+ )
839
950
  .join("\n");
840
951
  }
841
952
 
@@ -845,7 +956,7 @@ export function preRenderComponentHtml(doc, propsOverride = null, slotContent =
845
956
  * Returns true when: no event handlers, no $prototype entries (Functions, Request, Storage), no
846
957
  * $ref values. Conservative — returns false when uncertain.
847
958
  *
848
- * @param {any} doc - Component JSON definition
959
+ * @param {JxElement} doc - Component JSON definition
849
960
  * @returns {boolean}
850
961
  */
851
962
  export function isComponentFullyStatic(doc) {
@@ -853,7 +964,7 @@ export function isComponentFullyStatic(doc) {
853
964
  }
854
965
 
855
966
  /**
856
- * @param {any} node
967
+ * @param {JxElement | string | (JxElement | string)[]} node
857
968
  * @returns {boolean}
858
969
  */
859
970
  function _isStaticNode(node) {
@@ -869,7 +980,7 @@ function _isStaticNode(node) {
869
980
  if (node.state) {
870
981
  for (const def of Object.values(node.state)) {
871
982
  if (!def || typeof def !== "object") continue;
872
- const d = /** @type {any} */ (def);
983
+ const d = /** @type {JxMutableNode} */ (def);
873
984
  if (d.$prototype) return false;
874
985
  if (d.$ref) return false;
875
986
  }
@@ -896,9 +1007,9 @@ function _isStaticNode(node) {
896
1007
  * styles using .jx-N selectors via collectStyles.
897
1008
  *
898
1009
  * @param {string} tagName - The custom element tag name (used as CSS selector)
899
- * @param {any} styleDef - The component's style object
900
- * @param {any} [doc] - The full component document (for walking children)
901
- * @param {Record<string, any>} [mediaQueries] - Project media query definitions
1010
+ * @param {JxStyle | null} styleDef - The component's style object
1011
+ * @param {JxElement | null} [doc] - The full component document (for walking children)
1012
+ * @param {Record<string, string>} [mediaQueries] - Project media query definitions
902
1013
  * @returns {string} CSS text, or empty string if no styles
903
1014
  */
904
1015
  export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries = {}) {
@@ -930,7 +1041,7 @@ export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries =
930
1041
  const query = prop.startsWith("@--")
931
1042
  ? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
932
1043
  : prop.slice(1);
933
- rules.push(`@media ${query} { ${tagName} { ${toCSSText(/** @type {any} */ (val))} } }`);
1044
+ rules.push(`@media ${query} { ${tagName} { ${toCSSText(/** @type {object} */ (val))} } }`);
934
1045
  } else if (
935
1046
  prop.startsWith(":") ||
936
1047
  prop.startsWith(".") ||
@@ -938,7 +1049,7 @@ export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries =
938
1049
  prop.startsWith("[")
939
1050
  ) {
940
1051
  const resolved = prop.startsWith("&") ? prop.replace("&", tagName) : `${tagName}${prop}`;
941
- rules.push(`${resolved} { ${toCSSText(/** @type {any} */ (val))} }`);
1052
+ rules.push(`${resolved} { ${toCSSText(/** @type {object} */ (val))} }`);
942
1053
  }
943
1054
  }
944
1055
  }