@jxsuite/runtime 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/runtime.js CHANGED
@@ -25,7 +25,7 @@ import { reactive, ref, computed, effect, isRef, onEffectCleanup } from "@vue/re
25
25
  *
26
26
  * @param {string | Record<string, any>} source - Path to .json file, URL, or raw document object
27
27
  * @param {HTMLElement} [target] Default is `document.body`
28
- * @param {any} [options]
28
+ * @param {JxRenderOptions} [options]
29
29
  * @returns {Promise<Record<string, any>>} Resolves with the live component scope (state reactive
30
30
  * proxy)
31
31
  */
@@ -232,7 +232,7 @@ export { hasSchemaKeywords };
232
232
  *
233
233
  * @param {string} str
234
234
  * @param {Record<string, any>} state
235
- * @returns {any}
235
+ * @returns {string}
236
236
  */
237
237
  function evaluateTemplate(str, state) {
238
238
  const fn = new Function("state", "$map", `return \`${str}\``);
@@ -254,7 +254,7 @@ const _moduleCache = new Map();
254
254
  * @param {Record<string, any>} state - Reactive scope proxy
255
255
  * @param {string} key - Def key name
256
256
  * @param {string} [base] - Base URL for resolving $src imports
257
- * @returns {Promise<any>}
257
+ * @returns {Promise<unknown>}
258
258
  */
259
259
  async function resolveFunction(def, state, key, base) {
260
260
  if (def.body && def.$src) {
@@ -281,15 +281,15 @@ async function resolveFunction(def, state, key, base) {
281
281
  if (_moduleCache.has(src)) {
282
282
  mod = _moduleCache.get(src);
283
283
  } else {
284
- try {
285
- mod = await import(src);
286
- } catch {
287
- if (base) {
288
- const resolvedSrc = new URL(src, base).href;
284
+ if (base) {
285
+ const resolvedSrc = new URL(src, base).href;
286
+ try {
289
287
  mod = await import(resolvedSrc);
290
- } else {
291
- throw new Error(`Jx: failed to import '$src' "${src}" for "${key}"`);
288
+ } catch {
289
+ mod = await import(src);
292
290
  }
291
+ } else {
292
+ mod = await import(src);
293
293
  }
294
294
  _moduleCache.set(src, mod);
295
295
  }
@@ -299,8 +299,18 @@ async function resolveFunction(def, state, key, base) {
299
299
  }
300
300
  }
301
301
 
302
- // Detect computed: body contains a return statement
303
- if (def.body && /\breturn\b/.test(def.body)) {
302
+ // Detect computed: body contains a return statement, or $src function introspection.
303
+ // Functions with parameters (event handlers, callbacks) are never computed.
304
+ const hasParams = (def.parameters ?? def.arguments ?? []).length > 0;
305
+ let isComputed = false;
306
+ if (!hasParams) {
307
+ if (def.body) {
308
+ isComputed = /\breturn\b/.test(def.body);
309
+ } else if (fn) {
310
+ isComputed = fn.length <= 1 && /\breturn\b/.test(fn.toString());
311
+ }
312
+ }
313
+ if (isComputed) {
304
314
  return computed(() => fn(state));
305
315
  }
306
316
 
@@ -343,6 +353,8 @@ export const RESERVED_KEYS = new Set([
343
353
  "$ref",
344
354
  "$props",
345
355
  "$elements",
356
+ "$title",
357
+ "$description",
346
358
  "$switch",
347
359
  "$prototype",
348
360
  "$src",
@@ -373,7 +385,7 @@ export const RESERVED_KEYS = new Set([
373
385
  *
374
386
  * @param {Record<string, any>} def
375
387
  * @param {Record<string, any>} state - Reactive scope proxy (or child scope via Object.create)
376
- * @param {any} [options]
388
+ * @param {JxRenderOptions} [options]
377
389
  * @returns {HTMLElement | Text}
378
390
  */
379
391
  export function renderNode(def, state, options) {
@@ -436,7 +448,7 @@ export function renderNode(def, state, options) {
436
448
  /**
437
449
  * Check if a value is a template string (contains ${}).
438
450
  *
439
- * @param {any} val
451
+ * @param {unknown} val
440
452
  * @returns {boolean}
441
453
  */
442
454
  function isTemplateString(val) {
@@ -480,19 +492,22 @@ function applyProperties(el, def, state) {
480
492
  }
481
493
 
482
494
  /**
483
- * @param {any} el
495
+ * @param {HTMLElement} el
484
496
  * @param {string} key
485
- * @param {any} val
497
+ * @param {unknown} val
486
498
  * @param {Record<string, any>} state
487
499
  */
488
500
  function bindProperty(el, key, val, state) {
501
+ const target = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (el));
489
502
  if (isRefObj(val)) {
490
503
  if (key === "id") {
491
- el[key] = resolveRef(val.$ref, state);
504
+ target[key] = /** @type {string} */ (
505
+ resolveRef(/** @type {{ $ref: string }} */ (val).$ref, state)
506
+ );
492
507
  return;
493
508
  }
494
509
  effect(() => {
495
- el[key] = resolveRef(val.$ref, state);
510
+ target[key] = resolveRef(/** @type {{ $ref: string }} */ (val).$ref, state);
496
511
  });
497
512
  return;
498
513
  }
@@ -500,12 +515,12 @@ function bindProperty(el, key, val, state) {
500
515
  // Universal ${} reactivity — template strings in element properties
501
516
  if (isTemplateString(val)) {
502
517
  effect(() => {
503
- el[key] = evaluateTemplate(val, state);
518
+ target[key] = evaluateTemplate(/** @type {string} */ (val), state);
504
519
  });
505
520
  return;
506
521
  }
507
522
 
508
- el[key] = val;
523
+ target[key] = val;
509
524
  }
510
525
 
511
526
  /**
@@ -527,6 +542,7 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
527
542
  for (const [prop, val] of Object.entries(styleDef)) {
528
543
  if (prop.startsWith("@")) media[prop] = val;
529
544
  else if (isNestedSelector(prop)) nested[prop] = val;
545
+ else if (val !== null && typeof val === "object" && !Array.isArray(val)) nested[prop] = val;
530
546
  else if (prop.startsWith("--")) {
531
547
  if (isTemplateString(val))
532
548
  effect(() => {
@@ -535,9 +551,10 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
535
551
  else el.style.setProperty(prop, val);
536
552
  } else if (isTemplateString(val))
537
553
  effect(() => {
538
- /** @type {any} */ (el.style)[prop] = evaluateTemplate(val, state);
554
+ /** @type {Record<string, string>} */ (/** @type {unknown} */ (el.style))[prop] =
555
+ evaluateTemplate(val, state);
539
556
  });
540
- else /** @type {any} */ (el.style)[prop] = val;
557
+ else /** @type {Record<string, string>} */ (/** @type {unknown} */ (el.style))[prop] = val;
541
558
  }
542
559
 
543
560
  const hasNested = Object.keys(nested).length > 0;
@@ -549,13 +566,32 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
549
566
 
550
567
  let css = "";
551
568
 
569
+ function emitNested(/** @type {string} */ scope, /** @type {Record<string, any>} */ rules) {
570
+ const props = toCSSText(rules);
571
+ if (props) css += `${scope} { ${props} }\n`;
572
+ for (const [sel, sub] of Object.entries(rules)) {
573
+ if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
574
+ if (sel.startsWith("@")) continue;
575
+ const resolved = sel.startsWith("&")
576
+ ? sel.replace("&", scope)
577
+ : sel.startsWith("[")
578
+ ? `${scope}${sel}`
579
+ : sel.startsWith(":") || sel.startsWith(".")
580
+ ? `${scope}${sel}`
581
+ : `${scope} ${sel}`;
582
+ emitNested(resolved, sub);
583
+ }
584
+ }
585
+
552
586
  for (const [sel, rules] of Object.entries(nested)) {
553
587
  const resolved = sel.startsWith("&")
554
588
  ? sel.replace("&", `[data-jx="${uid}"]`)
555
589
  : sel.startsWith("[")
556
590
  ? `[data-jx="${uid}"]${sel}`
557
- : `[data-jx="${uid}"] ${sel}`;
558
- css += `${resolved} { ${toCSSText(rules)} }\n`;
591
+ : sel.startsWith(":") || sel.startsWith(".")
592
+ ? `[data-jx="${uid}"]${sel}`
593
+ : `[data-jx="${uid}"] ${sel}`;
594
+ emitNested(resolved, rules);
559
595
  }
560
596
 
561
597
  for (const [key, rules] of Object.entries(media)) {
@@ -565,15 +601,27 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
565
601
  : key.slice(1);
566
602
  const scope = `[data-jx="${uid}"]`;
567
603
  css += `@media ${query} { ${scope} { ${toCSSText(rules)} } }\n`;
568
- for (const [sel, nestedRules] of Object.entries(rules)) {
569
- if (!isNestedSelector(sel)) continue;
570
- const resolved = sel.startsWith("&")
571
- ? sel.replace("&", scope)
572
- : sel.startsWith("[")
573
- ? `${scope}${sel}`
574
- : `${scope} ${sel}`;
575
- css += `@media ${query} { ${resolved} { ${toCSSText(nestedRules)} } }\n`;
604
+
605
+ function emitMediaNested(
606
+ /** @type {string} */ parentSel,
607
+ /** @type {Record<string, any>} */ obj,
608
+ ) {
609
+ for (const [sel, sub] of Object.entries(obj)) {
610
+ if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
611
+ if (sel.startsWith("@")) continue;
612
+ const resolved = sel.startsWith("&")
613
+ ? sel.replace("&", parentSel)
614
+ : sel.startsWith("[")
615
+ ? `${parentSel}${sel}`
616
+ : sel.startsWith(":") || sel.startsWith(".")
617
+ ? `${parentSel}${sel}`
618
+ : `${parentSel} ${sel}`;
619
+ const props = toCSSText(sub);
620
+ if (props) css += `@media ${query} { ${resolved} { ${props} } }\n`;
621
+ emitMediaNested(resolved, sub);
622
+ }
576
623
  }
624
+ emitMediaNested(scope, rules);
577
625
  }
578
626
 
579
627
  const tag = document.createElement("style");
@@ -603,7 +651,7 @@ function applyAttributes(el, attrs, state) {
603
651
  /**
604
652
  * @param {Record<string, any>} def
605
653
  * @param {Record<string, any>} state
606
- * @param {any} [options]
654
+ * @param {JxRenderOptions} [options]
607
655
  * @returns {HTMLElement}
608
656
  */
609
657
  function renderMappedArray(def, state, options) {
@@ -628,11 +676,13 @@ function renderMappedArray(def, state, options) {
628
676
  if (!Array.isArray(items)) return;
629
677
  if (filterRef) {
630
678
  const fn = resolveRef(filterRef.$ref, state);
631
- if (typeof fn === "function") items = items.filter(fn);
679
+ if (typeof fn === "function")
680
+ items = items.filter(/** @type {(v: unknown) => boolean} */ (fn));
632
681
  }
633
682
  if (sortRef) {
634
683
  const fn = resolveRef(sortRef.$ref, state);
635
- if (typeof fn === "function") items = [...items].sort(fn);
684
+ if (typeof fn === "function")
685
+ items = [...items].sort(/** @type {(a: unknown, b: unknown) => number} */ (fn));
636
686
  }
637
687
 
638
688
  items.forEach((item, index) => {
@@ -655,7 +705,7 @@ function renderMappedArray(def, state, options) {
655
705
  /**
656
706
  * @param {Record<string, any>} def
657
707
  * @param {Record<string, any>} state
658
- * @param {any} [options]
708
+ * @param {JxRenderOptions} [options]
659
709
  * @returns {HTMLElement}
660
710
  */
661
711
  function renderSwitch(def, state, options) {
@@ -671,7 +721,7 @@ function renderSwitch(def, state, options) {
671
721
 
672
722
  effect(() => {
673
723
  container.innerHTML = "";
674
- const key = resolveRef(def.$switch.$ref, state);
724
+ const key = /** @type {string} */ (resolveRef(def.$switch.$ref, state));
675
725
  const caseDef = def.cases?.[key];
676
726
  if (!caseDef) return;
677
727
 
@@ -688,7 +738,7 @@ function renderSwitch(def, state, options) {
688
738
  const childOpts = options ? { ...options, _path: [...path, "cases", key] } : undefined;
689
739
  container.appendChild(renderNode(doc, childScope, childOpts));
690
740
  })
691
- .catch((/** @type {any} */ e) =>
741
+ .catch((/** @type {unknown} */ e) =>
692
742
  console.error("Jx $switch: failed to load external case", caseDef.$ref, e),
693
743
  );
694
744
  return;
@@ -713,7 +763,7 @@ function renderSwitch(def, state, options) {
713
763
  * @param {Record<string, any>} state - Reactive scope proxy
714
764
  * @param {string} key - Def key (for diagnostics)
715
765
  * @param {string} [base] - Base URL for resolving $src imports
716
- * @returns {Promise<any>}
766
+ * @returns {Promise<unknown>}
717
767
  */
718
768
  export async function resolvePrototype(def, state, key, base) {
719
769
  // ── External class via $src ─────────────────────────────────────────────────
@@ -723,10 +773,10 @@ export async function resolvePrototype(def, state, key, base) {
723
773
 
724
774
  switch (def.$prototype) {
725
775
  case "Request": {
726
- /** @type {import("@vue/reactivity").Ref<any>} */
776
+ /** @type {import("@vue/reactivity").Ref<unknown>} */
727
777
  const s = ref(null);
728
778
  const debounceMs = def.debounce ?? 0;
729
- /** @type {any} */
779
+ /** @type {ReturnType<typeof setTimeout> | null} */
730
780
  let debounceTimer = null;
731
781
 
732
782
  if (!def.manual) {
@@ -742,7 +792,7 @@ export async function resolvePrototype(def, state, key, base) {
742
792
  const controller = new AbortController();
743
793
  onEffectCleanup(() => {
744
794
  controller.abort();
745
- clearTimeout(debounceTimer);
795
+ if (debounceTimer !== null) clearTimeout(debounceTimer);
746
796
  });
747
797
 
748
798
  const doFetch = () =>
@@ -758,8 +808,8 @@ export async function resolvePrototype(def, state, key, base) {
758
808
  .then((d) => {
759
809
  s.value = d;
760
810
  })
761
- .catch((/** @type {any} */ e) => {
762
- if (e.name !== "AbortError") s.value = { error: String(e) };
811
+ .catch((/** @type {unknown} */ e) => {
812
+ if (/** @type {Error} */ (e).name !== "AbortError") s.value = { error: String(e) };
763
813
  });
764
814
 
765
815
  if (debounceMs > 0) {
@@ -858,22 +908,22 @@ export async function resolvePrototype(def, state, key, base) {
858
908
  } = def;
859
909
  const req = indexedDB.open(database, version);
860
910
  req.onupgradeneeded = (e) => {
861
- /** @type {any} */
862
- const db = /** @type {any} */ (e.target)?.result;
911
+ /** @type {IDBDatabase} */
912
+ const db = /** @type {IDBOpenDBRequest} */ (e.target).result;
863
913
  if (!db.objectStoreNames.contains(store)) {
864
914
  const os = db.createObjectStore(store, { keyPath, autoIncrement });
865
915
  for (const i of indexes) os.createIndex(i.name, i.keyPath, { unique: i.unique ?? false });
866
916
  }
867
917
  };
868
918
  req.onsuccess = (e) => {
869
- /** @type {any} */
870
- const db = /** @type {any} */ (e.target)?.result;
919
+ /** @type {IDBDatabase} */
920
+ const db = /** @type {IDBOpenDBRequest} */ (e.target).result;
871
921
  idbState.value = {
872
922
  database,
873
923
  store,
874
924
  version,
875
925
  isReady: true,
876
- getStore: (/** @type {string} */ mode = "readwrite") =>
926
+ getStore: (/** @type {IDBTransactionMode} */ mode = "readwrite") =>
877
927
  Promise.resolve(db.transaction(store, mode).objectStore(store)),
878
928
  };
879
929
  };
@@ -932,7 +982,7 @@ const EXTERNAL_RESERVED = new Set([
932
982
  * @param {Record<string, any>} state
933
983
  * @param {string} key
934
984
  * @param {string} [base]
935
- * @returns {Promise<any>}
985
+ * @returns {Promise<unknown>}
936
986
  */
937
987
  async function resolveExternalPrototype(def, state, key, base) {
938
988
  const src = def.$src;
@@ -956,7 +1006,7 @@ async function resolveExternalPrototype(def, state, key, base) {
956
1006
  * @param {string} src - JS module URL to import
957
1007
  * @param {string} exportName - Export name to look up
958
1008
  * @param {string} [base] - Base URL for resolution
959
- * @returns {Promise<any>}
1009
+ * @returns {Promise<unknown>}
960
1010
  */
961
1011
  async function importAndInstantiate(def, src, exportName, base) {
962
1012
  let mod;
@@ -1005,7 +1055,7 @@ async function importAndInstantiate(def, src, exportName, base) {
1005
1055
  /** @type {import("@vue/reactivity").Ref<any>} */
1006
1056
  const s = ref(value);
1007
1057
  if (typeof instance.subscribe === "function") {
1008
- instance.subscribe((/** @type {any} */ newVal) => {
1058
+ instance.subscribe((/** @type {unknown} */ newVal) => {
1009
1059
  s.value = newVal;
1010
1060
  });
1011
1061
  }
@@ -1020,7 +1070,7 @@ async function importAndInstantiate(def, src, exportName, base) {
1020
1070
  * @param {Record<string, any>} state
1021
1071
  * @param {string} key
1022
1072
  * @param {string} [base]
1023
- * @returns {Promise<any>}
1073
+ * @returns {Promise<unknown>}
1024
1074
  */
1025
1075
  async function resolveClassJson(def, state, key, base) {
1026
1076
  const src = def.$src;
@@ -1072,7 +1122,7 @@ async function resolveClassJson(def, state, key, base) {
1072
1122
  /** @type {import("@vue/reactivity").Ref<any>} */
1073
1123
  const s = ref(value);
1074
1124
  if (typeof instance.subscribe === "function") {
1075
- instance.subscribe((/** @type {any} */ newVal) => {
1125
+ instance.subscribe((/** @type {unknown} */ newVal) => {
1076
1126
  s.value = newVal;
1077
1127
  });
1078
1128
  }
@@ -1084,7 +1134,7 @@ async function resolveClassJson(def, state, key, base) {
1084
1134
  * fields to _-prefixed public fields.
1085
1135
  *
1086
1136
  * @param {Record<string, any>} classDef
1087
- * @returns {any}
1137
+ * @returns {DynamicClass}
1088
1138
  */
1089
1139
  function classFromSchema(classDef) {
1090
1140
  const fields = classDef.$defs?.fields ?? {};
@@ -1094,16 +1144,34 @@ function classFromSchema(classDef) {
1094
1144
  class DynClass {
1095
1145
  constructor(/** @type {Record<string, any>} */ config = {}) {
1096
1146
  for (const [key, field] of Object.entries(fields)) {
1097
- /** @type {any} */
1098
- const typedField = field;
1147
+ /**
1148
+ * @type {{
1149
+ * identifier?: string;
1150
+ * access?: string;
1151
+ * initializer?: unknown;
1152
+ * default?: unknown;
1153
+ * }}
1154
+ */
1155
+ const typedField =
1156
+ /**
1157
+ * @type {{
1158
+ * identifier?: string;
1159
+ * access?: string;
1160
+ * initializer?: unknown;
1161
+ * default?: unknown;
1162
+ * }}
1163
+ */ (field);
1099
1164
  const id = typedField.identifier ?? key;
1100
1165
  const propName = typedField.access === "private" ? `_${id}` : id;
1101
- if (config[id] !== undefined) /** @type {any} */ (this)[propName] = config[id];
1166
+ if (config[id] !== undefined)
1167
+ /** @type {Record<string, unknown>} */ (this)[propName] = config[id];
1102
1168
  else if (typedField.initializer !== undefined)
1103
- /** @type {any} */ (this)[propName] = typedField.initializer;
1169
+ /** @type {Record<string, unknown>} */ (this)[propName] = typedField.initializer;
1104
1170
  else if (typedField.default !== undefined)
1105
- /** @type {any} */ (this)[propName] = structuredClone(typedField.default);
1106
- else /** @type {any} */ (this)[propName] = null;
1171
+ /** @type {Record<string, unknown>} */ (this)[propName] = structuredClone(
1172
+ typedField.default,
1173
+ );
1174
+ else /** @type {Record<string, unknown>} */ (this)[propName] = null;
1107
1175
  }
1108
1176
  if (ctor?.body) {
1109
1177
  const bodyStr = Array.isArray(ctor.body) ? ctor.body.join("\n") : ctor.body;
@@ -1113,13 +1181,35 @@ function classFromSchema(classDef) {
1113
1181
  }
1114
1182
 
1115
1183
  for (const [key, method] of Object.entries(methods)) {
1116
- /** @type {any} */
1117
- const typedMethod = method;
1184
+ /**
1185
+ * @type {{
1186
+ * identifier?: string;
1187
+ * parameters?: Record<string, unknown>[];
1188
+ * body?: string | string[];
1189
+ * role?: string;
1190
+ * scope?: string;
1191
+ * getter?: { body: string };
1192
+ * setter?: { body: string; parameters?: Record<string, unknown>[] };
1193
+ * }}
1194
+ */
1195
+ const typedMethod = /**
1196
+ * @type {{
1197
+ * identifier?: string;
1198
+ * parameters?: Record<string, unknown>[];
1199
+ * body?: string | string[];
1200
+ * role?: string;
1201
+ * scope?: string;
1202
+ * getter?: { body: string };
1203
+ * setter?: { body: string; parameters?: Record<string, unknown>[] };
1204
+ * }}
1205
+ */ (method);
1118
1206
  const name = typedMethod.identifier ?? key;
1119
- const params = (typedMethod.parameters ?? []).map((/** @type {any} */ p) => {
1120
- if (p.$ref) return p.$ref.split("/").pop();
1121
- return p.identifier ?? p.name ?? "arg";
1122
- });
1207
+ const params = /** @type {string[]} */ (
1208
+ (typedMethod.parameters ?? []).map((/** @type {Record<string, unknown>} */ p) => {
1209
+ if (p.$ref) return /** @type {string} */ (/** @type {string} */ (p.$ref).split("/").pop());
1210
+ return /** @type {string} */ (p.identifier ?? p.name ?? "arg");
1211
+ })
1212
+ );
1123
1213
  const bodyStr = Array.isArray(typedMethod.body)
1124
1214
  ? typedMethod.body.join("\n")
1125
1215
  : (typedMethod.body ?? "");
@@ -1128,23 +1218,27 @@ function classFromSchema(classDef) {
1128
1218
  /** @type {PropertyDescriptor} */
1129
1219
  const descriptor = {};
1130
1220
  if (typedMethod.getter)
1131
- descriptor.get = /** @type {any} */ (new Function(typedMethod.getter.body));
1221
+ descriptor.get = /** @type {() => unknown} */ (new Function(typedMethod.getter.body));
1132
1222
  if (typedMethod.setter) {
1133
1223
  const sp = (typedMethod.setter.parameters ?? []).map(
1134
- (/** @type {any} */ p) => p.$ref?.split("/").pop() ?? "v",
1224
+ (/** @type {Record<string, unknown>} */ p) =>
1225
+ /** @type {string} */ (p.$ref)?.split("/").pop() ?? "v",
1226
+ );
1227
+ descriptor.set = /** @type {(v: unknown) => void} */ (
1228
+ new Function(...sp, typedMethod.setter.body)
1135
1229
  );
1136
- descriptor.set = /** @type {any} */ (new Function(...sp, typedMethod.setter.body));
1137
1230
  }
1138
1231
  Object.defineProperty(DynClass.prototype, name, { ...descriptor, configurable: true });
1139
1232
  } else if (typedMethod.scope === "static") {
1140
- /** @type {any} */ (DynClass)[name] = new Function(...params, bodyStr);
1233
+ /** @type {DynamicClass} */ (DynClass)[name] = new Function(...params, bodyStr);
1141
1234
  } else {
1142
- /** @type {any} */ (DynClass.prototype)[name] = new Function(...params, bodyStr);
1235
+ /** @type {DynamicClass} */ (DynClass).prototype[name] = new Function(...params, bodyStr);
1143
1236
  }
1144
1237
  }
1145
1238
 
1146
1239
  Object.defineProperty(DynClass, "name", { value: classDef.title, configurable: true });
1147
- return DynClass;
1240
+ const dynCtor = /** @type {DynamicClass} */ (/** @type {unknown} */ (DynClass));
1241
+ return dynCtor;
1148
1242
  }
1149
1243
 
1150
1244
  /**
@@ -1156,7 +1250,7 @@ function classFromSchema(classDef) {
1156
1250
  * @param {Record<string, any>} state
1157
1251
  * @param {string} key
1158
1252
  * @param {string} [base]
1159
- * @returns {Promise<any>}
1253
+ * @returns {Promise<unknown>}
1160
1254
  */
1161
1255
  async function resolveViaDevProxy(def, state, key, base) {
1162
1256
  /** @type {Record<string, any>} */
@@ -1165,7 +1259,9 @@ async function resolveViaDevProxy(def, state, key, base) {
1165
1259
  if (!EXTERNAL_RESERVED.has(k)) config[k] = v;
1166
1260
  }
1167
1261
 
1168
- const hasTemplates = Object.values(config).some((/** @type {any} */ v) => isTemplateString(v));
1262
+ const hasTemplates = Object.values(config).some((/** @type {unknown} */ v) =>
1263
+ isTemplateString(v),
1264
+ );
1169
1265
 
1170
1266
  /** @param {Record<string, any>} resolvedConfig */
1171
1267
  const doResolve = (resolvedConfig) =>
@@ -1195,17 +1291,17 @@ async function resolveViaDevProxy(def, state, key, base) {
1195
1291
  resolvedConfig[k] = isTemplateString(v) ? evaluateTemplate(v, state) : v;
1196
1292
  }
1197
1293
  doResolve(resolvedConfig)
1198
- .then((/** @type {any} */ value) => {
1294
+ .then((/** @type {unknown} */ value) => {
1199
1295
  s.value = value;
1200
1296
  })
1201
- .catch((/** @type {any} */ e) => console.error("Jx dev proxy:", e));
1297
+ .catch((/** @type {unknown} */ e) => console.error("Jx dev proxy:", e));
1202
1298
  });
1203
1299
  } else {
1204
1300
  doResolve(config)
1205
- .then((/** @type {any} */ value) => {
1301
+ .then((/** @type {unknown} */ value) => {
1206
1302
  s.value = value;
1207
1303
  })
1208
- .catch((/** @type {any} */ e) => console.error("Jx dev proxy:", e));
1304
+ .catch((/** @type {unknown} */ e) => console.error("Jx dev proxy:", e));
1209
1305
  }
1210
1306
  return s;
1211
1307
  }
@@ -1220,7 +1316,7 @@ async function resolveViaDevProxy(def, state, key, base) {
1220
1316
  * @param {Record<string, any>} state
1221
1317
  * @param {string} key
1222
1318
  * @param {string} [base]
1223
- * @returns {Promise<any>}
1319
+ * @returns {Promise<unknown>}
1224
1320
  */
1225
1321
  async function resolveServerFunction(def, state, key, base) {
1226
1322
  const src = def.$src;
@@ -1254,12 +1350,12 @@ async function resolveServerFunction(def, state, key, base) {
1254
1350
  throw new Error(`Jx: "${exportName}" from "${src}" is not a function`);
1255
1351
 
1256
1352
  const rawArgs = def.arguments ?? {};
1257
- const hasReactiveArg = Object.values(rawArgs).some((/** @type {any} */ v) => isRefObj(v));
1353
+ const hasReactiveArg = Object.values(rawArgs).some((/** @type {unknown} */ v) => isRefObj(v));
1258
1354
  const resolveArgs = () => {
1259
1355
  /** @type {Record<string, any>} */
1260
1356
  const args = {};
1261
1357
  for (const [k, v] of Object.entries(rawArgs)) {
1262
- args[k] = isRefObj(v) ? resolveRef(/** @type {any} */ (v).$ref, state) : v;
1358
+ args[k] = isRefObj(v) ? resolveRef(/** @type {{ $ref: string }} */ (v).$ref, state) : v;
1263
1359
  }
1264
1360
  return args;
1265
1361
  };
@@ -1272,7 +1368,7 @@ async function resolveServerFunction(def, state, key, base) {
1272
1368
  const args = resolveArgs();
1273
1369
  onEffectCleanup(() => {});
1274
1370
  fn(args)
1275
- .then((/** @type {any} */ result) => {
1371
+ .then((/** @type {unknown} */ result) => {
1276
1372
  s.value = result;
1277
1373
  })
1278
1374
  .catch(() => {});
@@ -1292,17 +1388,17 @@ async function resolveServerFunction(def, state, key, base) {
1292
1388
  * @param {Record<string, any>} state
1293
1389
  * @param {string} key
1294
1390
  * @param {string} [base]
1295
- * @returns {Promise<any>}
1391
+ * @returns {Promise<unknown>}
1296
1392
  */
1297
1393
  async function resolveServerFunctionViaProxy(def, state, key, base) {
1298
1394
  const rawArgs = def.arguments ?? {};
1299
- const hasReactiveArg = Object.values(rawArgs).some((/** @type {any} */ v) => isRefObj(v));
1395
+ const hasReactiveArg = Object.values(rawArgs).some((/** @type {unknown} */ v) => isRefObj(v));
1300
1396
 
1301
1397
  const resolveArgs = () => {
1302
1398
  /** @type {Record<string, any>} */
1303
1399
  const args = {};
1304
1400
  for (const [k, v] of Object.entries(rawArgs)) {
1305
- args[k] = isRefObj(v) ? resolveRef(/** @type {any} */ (v).$ref, state) : v;
1401
+ args[k] = isRefObj(v) ? resolveRef(/** @type {{ $ref: string }} */ (v).$ref, state) : v;
1306
1402
  }
1307
1403
  return args;
1308
1404
  };
@@ -1331,17 +1427,17 @@ async function resolveServerFunctionViaProxy(def, state, key, base) {
1331
1427
  const args = resolveArgs();
1332
1428
  onEffectCleanup(() => {});
1333
1429
  doResolve(args)
1334
- .then((/** @type {any} */ result) => {
1430
+ .then((/** @type {unknown} */ result) => {
1335
1431
  s.value = result;
1336
1432
  })
1337
- .catch((/** @type {any} */ e) => console.error("Jx server proxy:", e));
1433
+ .catch((/** @type {unknown} */ e) => console.error("Jx server proxy:", e));
1338
1434
  });
1339
1435
  } else {
1340
1436
  doResolve(resolveArgs())
1341
- .then((/** @type {any} */ result) => {
1437
+ .then((/** @type {unknown} */ result) => {
1342
1438
  s.value = result;
1343
1439
  })
1344
- .catch((/** @type {any} */ e) => console.error("Jx server proxy:", e));
1440
+ .catch((/** @type {unknown} */ e) => console.error("Jx server proxy:", e));
1345
1441
  }
1346
1442
  return s;
1347
1443
  }
@@ -1354,7 +1450,7 @@ async function resolveServerFunctionViaProxy(def, state, key, base) {
1354
1450
  *
1355
1451
  * @param {string} ref
1356
1452
  * @param {Record<string, any>} state - Reactive scope proxy (or child scope)
1357
- * @returns {any}
1453
+ * @returns {unknown}
1358
1454
  */
1359
1455
  export function resolveRef(ref, state) {
1360
1456
  if (typeof ref !== "string") return ref;
@@ -1382,7 +1478,7 @@ export function resolveRef(ref, state) {
1382
1478
  /**
1383
1479
  * Check if v is a Vue ref (including computed).
1384
1480
  *
1385
- * @param {any} v
1481
+ * @param {unknown} v
1386
1482
  * @returns {boolean}
1387
1483
  */
1388
1484
  export function isSignal(v) {
@@ -1390,11 +1486,15 @@ export function isSignal(v) {
1390
1486
  }
1391
1487
 
1392
1488
  /**
1393
- * @param {any} v
1489
+ * @param {unknown} v
1394
1490
  * @returns {boolean}
1395
1491
  */
1396
1492
  function isRefObj(v) {
1397
- return v !== null && typeof v === "object" && typeof v.$ref === "string";
1493
+ return (
1494
+ v !== null &&
1495
+ typeof v === "object" &&
1496
+ typeof (/** @type {Record<string, unknown>} */ (v).$ref) === "string"
1497
+ );
1398
1498
  }
1399
1499
 
1400
1500
  /**
@@ -1406,12 +1506,12 @@ function isNestedSelector(k) {
1406
1506
  }
1407
1507
 
1408
1508
  /**
1409
- * @param {any} obj
1509
+ * @param {unknown} obj
1410
1510
  * @param {string} path
1411
- * @returns {any}
1511
+ * @returns {unknown}
1412
1512
  */
1413
1513
  function getPath(obj, path) {
1414
- return path.split(/[./]/).reduce((o, k) => o?.[k], obj);
1514
+ return path.split(/[./]/).reduce((o, k) => /** @type {Record<string, unknown>} */ (o)?.[k], obj);
1415
1515
  }
1416
1516
 
1417
1517
  /**
@@ -1440,12 +1540,12 @@ export function camelToKebab(s) {
1440
1540
  /**
1441
1541
  * Convert a style rules object to a CSS text string (skipping nested selectors).
1442
1542
  *
1443
- * @param {Record<string, any>} rules
1543
+ * @param {Record<string, unknown> | object} rules
1444
1544
  * @returns {string}
1445
1545
  */
1446
1546
  export function toCSSText(rules) {
1447
1547
  return Object.entries(rules)
1448
- .filter(([k]) => !isNestedSelector(k))
1548
+ .filter(([k, v]) => !isNestedSelector(k) && (v === null || typeof v !== "object"))
1449
1549
  .map(([p, v]) => `${camelToKebab(p)}: ${v}`)
1450
1550
  .join("; ");
1451
1551
  }
@@ -1586,8 +1686,8 @@ export async function defineElement(source, base) {
1586
1686
 
1587
1687
  // Merge $props set as JS properties by parent before connection
1588
1688
  for (const key of Object.keys(def.state ?? {})) {
1589
- if (key in this && /** @type {any} */ (this)[key] !== undefined) {
1590
- state[key] = /** @type {any} */ (this)[key];
1689
+ if (key in this && /** @type {Record<string, unknown>} */ (this)[key] !== undefined) {
1690
+ state[key] = /** @type {Record<string, unknown>} */ (this)[key];
1591
1691
  }
1592
1692
  }
1593
1693
  // Set up property getters/setters that forward into reactive state
@@ -1595,7 +1695,7 @@ export async function defineElement(source, base) {
1595
1695
  if (!(key in HTMLElement.prototype)) {
1596
1696
  Object.defineProperty(this, key, {
1597
1697
  get: () => state[key],
1598
- set: (/** @type {any} */ v) => {
1698
+ set: (/** @type {unknown} */ v) => {
1599
1699
  state[key] = v;
1600
1700
  },
1601
1701
  configurable: true,
@@ -1603,7 +1703,7 @@ export async function defineElement(source, base) {
1603
1703
  }
1604
1704
  }
1605
1705
 
1606
- /** @type {any} */ (this)._state = state;
1706
+ /** @type {Record<string, unknown>} */ (this)._state = state;
1607
1707
 
1608
1708
  // Capture light DOM children (for slot distribution) before rendering
1609
1709
  const slottedChildren = Array.from(this.childNodes);
@@ -1633,16 +1733,16 @@ export async function defineElement(source, base) {
1633
1733
  }
1634
1734
 
1635
1735
  disconnectedCallback() {
1636
- /** @type {any} */
1637
- const self = this;
1736
+ /** @type {{ _state?: Record<string, any> }} */
1737
+ const self = /** @type {{ _state?: Record<string, any> }} */ (/** @type {unknown} */ (this));
1638
1738
  if (typeof self._state?.onUnmount === "function") {
1639
1739
  self._state.onUnmount(self._state);
1640
1740
  }
1641
1741
  }
1642
1742
 
1643
1743
  adoptedCallback() {
1644
- /** @type {any} */
1645
- const self = this;
1744
+ /** @type {{ _state?: Record<string, any> }} */
1745
+ const self = /** @type {{ _state?: Record<string, any> }} */ (/** @type {unknown} */ (this));
1646
1746
  if (typeof self._state?.onAdopted === "function") {
1647
1747
  self._state.onAdopted(self._state);
1648
1748
  }
@@ -1653,8 +1753,8 @@ export async function defineElement(source, base) {
1653
1753
  /** @type {string | null} */ oldVal,
1654
1754
  /** @type {string | null} */ newVal,
1655
1755
  ) {
1656
- /** @type {any} */
1657
- const self = this;
1756
+ /** @type {{ _state?: Record<string, any> }} */
1757
+ const self = /** @type {{ _state?: Record<string, any> }} */ (/** @type {unknown} */ (this));
1658
1758
  if (!self._state || oldVal === newVal) return;
1659
1759
  const camelKey = name.replace(
1660
1760
  /-([a-z])/g,
@@ -1676,31 +1776,30 @@ export async function defineElement(source, base) {
1676
1776
  *
1677
1777
  * @param {Record<string, any>} def
1678
1778
  * @param {Record<string, any>} state
1679
- * @param {any} [options]
1680
- * @param {any[]} [path]
1779
+ * @param {JxRenderOptions} [options]
1780
+ * @param {JxPath} [path]
1681
1781
  * @returns {HTMLElement}
1682
1782
  */
1683
1783
  function renderCustomElementWithProps(def, state, options, path) {
1684
- /** @type {any} */
1685
1784
  const el = document.createElement(def.tagName);
1686
1785
 
1687
- if (options?.onNodeCreated) options.onNodeCreated(el, path, def);
1786
+ if (options?.onNodeCreated) options.onNodeCreated(el, path ?? [], def);
1688
1787
 
1689
1788
  // Set JS properties from $props (before connection)
1690
1789
  for (const [key, val] of Object.entries(def.$props ?? {})) {
1691
1790
  if (isRefObj(val)) {
1692
1791
  const resolved = resolveRef(val.$ref, state);
1693
- el[key] = resolved;
1792
+ /** @type {Record<string, unknown>} */ (el)[key] = resolved;
1694
1793
  // Reactive forwarding: re-set the property when the source changes
1695
1794
  effect(() => {
1696
- el[key] = resolveRef(val.$ref, state);
1795
+ /** @type {Record<string, unknown>} */ (el)[key] = resolveRef(val.$ref, state);
1697
1796
  });
1698
1797
  } else if (isTemplateString(val)) {
1699
1798
  effect(() => {
1700
- el[key] = evaluateTemplate(val, state);
1799
+ /** @type {Record<string, unknown>} */ (el)[key] = evaluateTemplate(val, state);
1701
1800
  });
1702
1801
  } else {
1703
- el[key] = val;
1802
+ /** @type {Record<string, unknown>} */ (el)[key] = val;
1704
1803
  }
1705
1804
  }
1706
1805