@jxsuite/compiler 0.19.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,11 +512,8 @@ 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") {
491
- function emitProjectRules(
492
- /** @type {string} */ selector,
493
- /** @type {Record<string, any>} */ obj,
494
- ) {
495
- const props = toCSSText(/** @type {any} */ (obj));
515
+ function emitProjectRules(/** @type {string} */ selector, /** @type {JxMutableNode} */ obj) {
516
+ const props = toCSSText(obj);
496
517
  if (props) rules.push(`${selector} { ${props} }`);
497
518
  for (const [key, val] of Object.entries(obj)) {
498
519
  if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
@@ -500,9 +521,9 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
500
521
  const query = key.startsWith("@--")
501
522
  ? (mediaQueries[key.slice(1)] ?? key.slice(1))
502
523
  : key.slice(1);
503
- const mProps = toCSSText(/** @type {any} */ (val));
524
+ const mProps = toCSSText(val);
504
525
  if (mProps) rules.push(`@media ${query} { ${selector} { ${mProps} } }`);
505
- for (const [sel, sub] of Object.entries(/** @type {Record<string, any>} */ (val))) {
526
+ for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ (val))) {
506
527
  if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
507
528
  if (sel.startsWith("@")) continue;
508
529
  const resolved = sel.startsWith("&")
@@ -510,7 +531,7 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
510
531
  : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
511
532
  ? `${selector}${sel}`
512
533
  : `${selector} ${sel}`;
513
- const subProps = toCSSText(/** @type {any} */ (sub));
534
+ const subProps = toCSSText(sub);
514
535
  if (subProps) rules.push(`@media ${query} { ${resolved} { ${subProps} } }`);
515
536
  }
516
537
  continue;
@@ -520,13 +541,13 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
520
541
  : key.startsWith(":") || key.startsWith(".") || key.startsWith("[")
521
542
  ? `${selector}${key}`
522
543
  : `${selector} ${key}`;
523
- emitProjectRules(resolved, val);
544
+ emitProjectRules(resolved, /** @type {Record<string, unknown>} */ (val));
524
545
  }
525
546
  }
526
547
 
527
548
  for (const [key, val] of Object.entries(projectStyle)) {
528
549
  if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
529
- emitProjectRules(key, /** @type {any} */ (val));
550
+ emitProjectRules(key, /** @type {Record<string, unknown>} */ (val));
530
551
  } else if (
531
552
  val !== null &&
532
553
  typeof val === "object" &&
@@ -540,14 +561,14 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
540
561
  const query = key.startsWith("@--")
541
562
  ? (mediaQueries[key.slice(1)] ?? key.slice(1))
542
563
  : key.slice(1);
543
- rules.push(`@media ${query} { body { ${toCSSText(/** @type {any} */ (val))} } }`);
564
+ rules.push(`@media ${query} { body { ${toCSSText(/** @type {object} */ (val))} } }`);
544
565
  }
545
566
  }
546
567
  // Collect CSS custom properties into :root {}
547
- /** @type {Record<string, any>} */
568
+ /** @type {Record<string, unknown>} */
548
569
  const rootProps = {};
549
570
  // Collect direct CSS properties into body {}
550
- /** @type {Record<string, any>} */
571
+ /** @type {Record<string, unknown>} */
551
572
  const bodyProps = {};
552
573
  for (const [key, val] of Object.entries(projectStyle)) {
553
574
  if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[") || key.startsWith("@"))
@@ -559,11 +580,11 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
559
580
  bodyProps[key] = val;
560
581
  }
561
582
  }
562
- const rootCSS = toCSSText(/** @type {any} */ (rootProps));
583
+ const rootCSS = toCSSText(rootProps);
563
584
  if (rootCSS) {
564
585
  rules.push(`:root { ${rootCSS} }`);
565
586
  }
566
- const bodyCSS = toCSSText(/** @type {any} */ (bodyProps));
587
+ const bodyCSS = toCSSText(bodyProps);
567
588
  if (bodyCSS) {
568
589
  rules.push(`body { ${bodyCSS} }`);
569
590
  }
@@ -579,12 +600,12 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
579
600
  * Recursively emit CSS rules for a nested element selector.
580
601
  *
581
602
  * @param {string} selector
582
- * @param {Record<string, any>} obj
603
+ * @param {Record<string, unknown>} obj
583
604
  * @param {string[]} rules
584
- * @param {Record<string, any>} mediaQueries
605
+ * @param {Record<string, string>} mediaQueries
585
606
  */
586
607
  function emitNestedElement(selector, obj, rules, mediaQueries) {
587
- const props = toCSSText(/** @type {any} */ (obj));
608
+ const props = toCSSText(obj);
588
609
  if (props) rules.push(`${selector} { ${props} }`);
589
610
  for (const [key, val] of Object.entries(obj)) {
590
611
  if (val === null || typeof val !== "object" || Array.isArray(val)) continue;
@@ -592,9 +613,9 @@ function emitNestedElement(selector, obj, rules, mediaQueries) {
592
613
  const query = key.startsWith("@--")
593
614
  ? (mediaQueries[key.slice(1)] ?? key.slice(1))
594
615
  : key.slice(1);
595
- const mProps = toCSSText(/** @type {any} */ (val));
616
+ const mProps = toCSSText(val);
596
617
  if (mProps) rules.push(`@media ${query} { ${selector} { ${mProps} } }`);
597
- for (const [sel, sub] of Object.entries(/** @type {Record<string, any>} */ (val))) {
618
+ for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ (val))) {
598
619
  if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
599
620
  if (sel.startsWith("@")) continue;
600
621
  const resolved = sel.startsWith("&")
@@ -602,7 +623,7 @@ function emitNestedElement(selector, obj, rules, mediaQueries) {
602
623
  : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
603
624
  ? `${selector}${sel}`
604
625
  : `${selector} ${sel}`;
605
- const subProps = toCSSText(/** @type {any} */ (sub));
626
+ const subProps = toCSSText(sub);
606
627
  if (subProps) rules.push(`@media ${query} { ${resolved} { ${subProps} } }`);
607
628
  }
608
629
  continue;
@@ -612,14 +633,14 @@ function emitNestedElement(selector, obj, rules, mediaQueries) {
612
633
  : key.startsWith(":") || key.startsWith(".") || key.startsWith("[")
613
634
  ? `${selector}${key}`
614
635
  : `${selector} ${key}`;
615
- emitNestedElement(resolved, val, rules, mediaQueries);
636
+ emitNestedElement(resolved, /** @type {Record<string, unknown>} */ (val), rules, mediaQueries);
616
637
  }
617
638
  }
618
639
 
619
640
  /**
620
- * @param {any} def
641
+ * @param {JxElement | JxMutableNode | string} def
621
642
  * @param {string[]} rules
622
- * @param {Record<string, any>} mediaQueries
643
+ * @param {Record<string, string>} mediaQueries
623
644
  * @param {string} [_parentSel]
624
645
  * @param {{ n: number }} [counter]
625
646
  */
@@ -670,8 +691,10 @@ export function collectStyles(
670
691
  const query = prop.startsWith("@--")
671
692
  ? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
672
693
  : prop.slice(1);
673
- rules.push(`@media ${query} { ${selector} { ${toCSSText(/** @type {any} */ (val))} } }`);
674
- for (const [sel, nestedRules] of Object.entries(/** @type {Record<string, any>} */ (val))) {
694
+ rules.push(`@media ${query} { ${selector} { ${toCSSText(val)} } }`);
695
+ for (const [sel, nestedRules] of Object.entries(
696
+ /** @type {Record<string, unknown>} */ (val),
697
+ )) {
675
698
  if (nestedRules === null || typeof nestedRules !== "object" || Array.isArray(nestedRules))
676
699
  continue;
677
700
  if (sel.startsWith("@")) continue;
@@ -680,9 +703,7 @@ export function collectStyles(
680
703
  : sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
681
704
  ? `${selector}${sel}`
682
705
  : `${selector} ${sel}`;
683
- rules.push(
684
- `@media ${query} { ${resolved} { ${toCSSText(/** @type {any} */ (nestedRules))} } }`,
685
- );
706
+ rules.push(`@media ${query} { ${resolved} { ${toCSSText(nestedRules)} } }`);
686
707
  }
687
708
  } else {
688
709
  const resolved = prop.startsWith("&")
@@ -690,13 +711,18 @@ export function collectStyles(
690
711
  : prop.startsWith(":") || prop.startsWith(".") || prop.startsWith("[")
691
712
  ? `${selector}${prop}`
692
713
  : `${selector} ${prop}`;
693
- emitNestedElement(resolved, val, rules, mediaQueries);
714
+ emitNestedElement(
715
+ resolved,
716
+ /** @type {Record<string, unknown>} */ (val),
717
+ rules,
718
+ mediaQueries,
719
+ );
694
720
  }
695
721
  }
696
722
  }
697
723
 
698
724
  if (Array.isArray(def.children)) {
699
- def.children.forEach((/** @type {any} */ c) => {
725
+ def.children.forEach((/** @type {JxElement | JxMutableNode | string} */ c) => {
700
726
  collectStyles(c, rules, mediaQueries, selector, counter, prefix);
701
727
  });
702
728
  }
@@ -747,7 +773,7 @@ export function tagNameToClassName(tagName) {
747
773
  /**
748
774
  * Recursively collect unique $src values from $prototype: "Function" entries.
749
775
  *
750
- * @param {any} doc
776
+ * @param {JxElement} doc
751
777
  * @returns {string[]}
752
778
  */
753
779
  export function collectSrcImports(doc) {
@@ -758,7 +784,7 @@ export function collectSrcImports(doc) {
758
784
  }
759
785
 
760
786
  /**
761
- * @param {any} def
787
+ * @param {JxElement | JxMutableNode | string} def
762
788
  * @param {Set<string>} srcs
763
789
  */
764
790
  function _walkSrc(def, srcs) {
@@ -768,40 +794,42 @@ function _walkSrc(def, srcs) {
768
794
  if (
769
795
  d &&
770
796
  typeof d === "object" &&
771
- /** @type {any} */ (d).$prototype === "Function" &&
772
- /** @type {any} */ (d).$src
797
+ /** @type {JxMutableNode} */ (d).$prototype === "Function" &&
798
+ /** @type {JxMutableNode} */ (d).$src
773
799
  ) {
774
- srcs.add(/** @type {any} */ (d).$src);
800
+ srcs.add(/** @type {string} */ (/** @type {JxMutableNode} */ (d).$src));
775
801
  }
776
802
  }
777
803
  }
778
804
  if (Array.isArray(def.children)) {
779
- def.children.forEach((/** @type {any} */ c) => _walkSrc(c, srcs));
805
+ def.children.forEach((/** @type {JxElement | JxMutableNode | string} */ c) =>
806
+ _walkSrc(c, srcs),
807
+ );
780
808
  }
781
809
  }
782
810
 
783
811
  /**
784
812
  * Recursively collect all `timing: "server"` entries from the document tree.
785
813
  *
786
- * @param {any} doc
787
- * @returns {any[]}
814
+ * @param {JxElement} doc
815
+ * @returns {{ key: string; exportName: string; src: string }[]}
788
816
  */
789
817
  export function collectServerEntries(doc) {
790
- /** @type {Map<string, any>} */
818
+ /** @type {Map<string, { key: string; exportName: string; src: string }>} */
791
819
  const entries = new Map();
792
820
  _walkServerEntries(doc, entries);
793
821
  return [...entries.values()];
794
822
  }
795
823
 
796
824
  /**
797
- * @param {any} def
798
- * @param {Map<string, any>} entries
825
+ * @param {JxElement | JxMutableNode | string} def
826
+ * @param {Map<string, { key: string; exportName: string; src: string }>} entries
799
827
  */
800
828
  function _walkServerEntries(def, entries) {
801
829
  if (!def || typeof def !== "object") return;
802
830
  if (def.state) {
803
831
  for (const [key, d] of Object.entries(def.state)) {
804
- const entry = /** @type {any} */ (d);
832
+ const entry = /** @type {JxMutableNode} */ (d);
805
833
  if (
806
834
  entry &&
807
835
  typeof entry === "object" &&
@@ -815,7 +843,9 @@ function _walkServerEntries(def, entries) {
815
843
  }
816
844
  }
817
845
  if (Array.isArray(def.children)) {
818
- def.children.forEach((/** @type {any} */ c) => _walkServerEntries(c, entries));
846
+ def.children.forEach((/** @type {JxElement | JxMutableNode | string} */ c) =>
847
+ _walkServerEntries(c, entries),
848
+ );
819
849
  }
820
850
  }
821
851
 
@@ -827,8 +857,8 @@ const SELF_CLOSING = new Set(["input", "br", "hr", "img", "meta", "link", "area"
827
857
  /**
828
858
  * Recursively render a Jx node tree to static HTML for pre-rendering.
829
859
  *
830
- * @param {any} node
831
- * @param {any} scope
860
+ * @param {JxElement | JxMutableNode | string} node
861
+ * @param {Record<string, unknown>} scope
832
862
  * @param {string | null} [slotContent] - HTML to substitute for `<slot>` elements
833
863
  * @returns {string}
834
864
  */
@@ -842,7 +872,11 @@ export function renderStaticNode(node, scope, slotContent = null) {
842
872
  }
843
873
  if (typeof node === "number" || typeof node === "boolean") return escapeHtml(String(node));
844
874
  if (Array.isArray(node))
845
- 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");
846
880
  if (!node || typeof node !== "object") return "";
847
881
 
848
882
  // Skip mapped arrays — can't pre-render dynamic lists
@@ -863,10 +897,12 @@ export function renderStaticNode(node, scope, slotContent = null) {
863
897
  inner = val != null ? escapeHtml(String(val)) : "";
864
898
  } else if (node.innerHTML) {
865
899
  const val = resolveStaticValue(node.innerHTML, scope);
866
- inner = val != null ? val : node.innerHTML;
900
+ inner = val != null ? String(val) : /** @type {string} */ (node.innerHTML);
867
901
  } else if (Array.isArray(node.children)) {
868
902
  inner = node.children
869
- .map((/** @type {any} */ c) => renderStaticNode(c, scope, slotContent))
903
+ .map((/** @type {JxElement | JxMutableNode | string} */ c) =>
904
+ renderStaticNode(c, scope, slotContent),
905
+ )
870
906
  .join("\n");
871
907
  }
872
908
 
@@ -876,39 +912,41 @@ export function renderStaticNode(node, scope, slotContent = null) {
876
912
  /**
877
913
  * Pre-render a component definition to static HTML for its inner content.
878
914
  *
879
- * @param {any} doc - Component JSON definition
880
- * @param {Record<string, any> | null} [propsOverride] - Instance-specific prop values to merge into
881
- * state
915
+ * @param {JxElement} doc - Component JSON definition
916
+ * @param {Record<string, unknown> | null} [propsOverride] - Instance-specific prop values to merge
917
+ * into state
882
918
  * @param {string | null} [slotContent] - HTML to substitute for `<slot>` elements
883
919
  * @returns {string} The pre-rendered innerHTML
884
920
  */
885
921
  export function preRenderComponentHtml(doc, propsOverride = null, slotContent = null) {
922
+ /** @type {Record<string, JxStateDefinition>} */
886
923
  let stateDefs = doc.state ?? {};
887
924
  if (propsOverride) {
888
925
  stateDefs = { ...stateDefs };
889
926
  for (const [key, value] of Object.entries(propsOverride)) {
890
927
  if (key in stateDefs) {
891
928
  const existing = stateDefs[key];
892
- // If the existing definition is an expanded signal with "default", override the default
893
929
  if (
894
930
  existing &&
895
931
  typeof existing === "object" &&
896
932
  !Array.isArray(existing) &&
897
933
  "default" in existing
898
934
  ) {
899
- stateDefs[key] = { ...existing, default: value };
935
+ stateDefs[key] = { .../** @type {JxStateObject} */ (existing), default: value };
900
936
  } else {
901
- stateDefs[key] = value;
937
+ stateDefs[key] = /** @type {JxStateDefinition} */ (value);
902
938
  }
903
939
  } else {
904
- stateDefs[key] = value;
940
+ stateDefs[key] = /** @type {JxStateDefinition} */ (value);
905
941
  }
906
942
  }
907
943
  }
908
944
  const scope = buildInitialScope(stateDefs, null);
909
945
  if (!Array.isArray(doc.children)) return "";
910
946
  return doc.children
911
- .map((/** @type {any} */ c) => renderStaticNode(c, scope, slotContent))
947
+ .map((/** @type {JxElement | JxMutableNode | string} */ c) =>
948
+ renderStaticNode(c, scope, slotContent),
949
+ )
912
950
  .join("\n");
913
951
  }
914
952
 
@@ -918,7 +956,7 @@ export function preRenderComponentHtml(doc, propsOverride = null, slotContent =
918
956
  * Returns true when: no event handlers, no $prototype entries (Functions, Request, Storage), no
919
957
  * $ref values. Conservative — returns false when uncertain.
920
958
  *
921
- * @param {any} doc - Component JSON definition
959
+ * @param {JxElement} doc - Component JSON definition
922
960
  * @returns {boolean}
923
961
  */
924
962
  export function isComponentFullyStatic(doc) {
@@ -926,7 +964,7 @@ export function isComponentFullyStatic(doc) {
926
964
  }
927
965
 
928
966
  /**
929
- * @param {any} node
967
+ * @param {JxElement | string | (JxElement | string)[]} node
930
968
  * @returns {boolean}
931
969
  */
932
970
  function _isStaticNode(node) {
@@ -942,7 +980,7 @@ function _isStaticNode(node) {
942
980
  if (node.state) {
943
981
  for (const def of Object.values(node.state)) {
944
982
  if (!def || typeof def !== "object") continue;
945
- const d = /** @type {any} */ (def);
983
+ const d = /** @type {JxMutableNode} */ (def);
946
984
  if (d.$prototype) return false;
947
985
  if (d.$ref) return false;
948
986
  }
@@ -969,9 +1007,9 @@ function _isStaticNode(node) {
969
1007
  * styles using .jx-N selectors via collectStyles.
970
1008
  *
971
1009
  * @param {string} tagName - The custom element tag name (used as CSS selector)
972
- * @param {any} styleDef - The component's style object
973
- * @param {any} [doc] - The full component document (for walking children)
974
- * @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
975
1013
  * @returns {string} CSS text, or empty string if no styles
976
1014
  */
977
1015
  export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries = {}) {
@@ -1003,7 +1041,7 @@ export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries =
1003
1041
  const query = prop.startsWith("@--")
1004
1042
  ? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
1005
1043
  : prop.slice(1);
1006
- rules.push(`@media ${query} { ${tagName} { ${toCSSText(/** @type {any} */ (val))} } }`);
1044
+ rules.push(`@media ${query} { ${tagName} { ${toCSSText(/** @type {object} */ (val))} } }`);
1007
1045
  } else if (
1008
1046
  prop.startsWith(":") ||
1009
1047
  prop.startsWith(".") ||
@@ -1011,7 +1049,7 @@ export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries =
1011
1049
  prop.startsWith("[")
1012
1050
  ) {
1013
1051
  const resolved = prop.startsWith("&") ? prop.replace("&", tagName) : `${tagName}${prop}`;
1014
- rules.push(`${resolved} { ${toCSSText(/** @type {any} */ (val))} }`);
1052
+ rules.push(`${resolved} { ${toCSSText(/** @type {object} */ (val))} }`);
1015
1053
  }
1016
1054
  }
1017
1055
  }