@jxsuite/runtime 0.23.2 → 0.25.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.
@@ -1,3 +1,4 @@
1
+ /// <reference lib="dom" />
1
2
  /**
2
3
  * Jx — JSON-native reactive web component runtime
3
4
  * @version 3.0.0
@@ -13,7 +14,8 @@
13
14
  */
14
15
 
15
16
  import { reactive, ref, computed, effect, isRef, onEffectCleanup } from "@vue/reactivity";
16
- import { evaluateExpression, isMutating } from "./expression.js";
17
+ import { evaluateExpression, isMutating } from "./expression.ts";
18
+ import type { JxRenderOptions, DynamicClass, JxPath } from "./types.ts";
17
19
 
18
20
  // ─── Public API ───────────────────────────────────────────────────────────────
19
21
 
@@ -30,7 +32,11 @@ import { evaluateExpression, isMutating } from "./expression.js";
30
32
  * @returns {Promise<Record<string, any>>} Resolves with the live component scope (state reactive
31
33
  * proxy)
32
34
  */
33
- export async function Jx(source, target = document.body, options) {
35
+ export async function Jx(
36
+ source: string | Record<string, any>,
37
+ target: HTMLElement = document.body,
38
+ options?: JxRenderOptions,
39
+ ) {
34
40
  const base = typeof source === "string" ? new URL(source, location.href).href : location.href;
35
41
  const doc = await resolve(source);
36
42
 
@@ -56,17 +62,17 @@ export async function Jx(source, target = document.body, options) {
56
62
 
57
63
  // ─── Step 1: Resolve ──────────────────────────────────────────────────────────
58
64
 
59
- /**
60
- * Fetch and parse a Jx JSON source. Accepts a URL string, absolute URL, or a pre-parsed object.
61
- *
62
- * @param {string | Record<string, any>} source
63
- * @returns {Promise<any>}
64
- */
65
- export async function resolve(source) {
65
+ const _resolveCache = new Map<string, Promise<any>>();
66
+
67
+ export async function resolve(source: string | Record<string, any>) {
66
68
  if (typeof source !== "string") return source;
67
- const res = await fetch(source);
68
- if (!res.ok) throw new Error(`Jx: failed to fetch ${source} (${res.status})`);
69
- return res.json();
69
+ if (_resolveCache.has(source)) return _resolveCache.get(source)!;
70
+ const p = fetch(source).then((res) => {
71
+ if (!res.ok) throw new Error(`Jx: failed to fetch ${source} (${res.status})`);
72
+ return res.json();
73
+ });
74
+ _resolveCache.set(source, p);
75
+ return p;
70
76
  }
71
77
 
72
78
  // ─── Step 2: Build scope ──────────────────────────────────────────────────────
@@ -89,12 +95,12 @@ const SCHEMA_KEYWORDS = new Set([
89
95
  /** /** @type {{ skip: boolean }} */
90
96
  const _serverFnConfig = { skip: false };
91
97
  /** Set to true to suppress timing: "server" resolution (used by Studio edit mode). */
92
- export function setSkipServerFunctions(/** @type {boolean} */ v) {
98
+ export function setSkipServerFunctions(v: boolean) {
93
99
  _serverFnConfig.skip = v;
94
100
  }
95
101
 
96
102
  /** @deprecated No longer needed — ContentCollection/ContentEntry resolve via generic class path */
97
- export function setSkipContentResolution(/** @type {boolean} */ _v) {}
103
+ export function setSkipContentResolution(_v: boolean) {}
98
104
 
99
105
  /**
100
106
  * Build the reactive scope (state) from the document using the five-shape detection algorithm.
@@ -104,9 +110,12 @@ export function setSkipContentResolution(/** @type {boolean} */ _v) {}
104
110
  * @param {string} [base] Base URL for resolving $src imports. Default is `location.href`
105
111
  * @returns {Promise<Record<string, any>>} Reactive proxy (state)
106
112
  */
107
- export async function buildScope(doc, parentScope = {}, base = location.href) {
108
- /** @type {Record<string, any>} */
109
- const raw = {};
113
+ export async function buildScope(
114
+ doc: Record<string, any>,
115
+ parentScope: Record<string, any> = {},
116
+ base: string = location.href,
117
+ ) {
118
+ const raw: Record<string, any> = {};
110
119
 
111
120
  // Merge parent scope properties
112
121
  for (const [key, val] of Object.entries(parentScope)) {
@@ -122,19 +131,19 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
122
131
  def &&
123
132
  typeof def === "object" &&
124
133
  !Array.isArray(def) &&
125
- def.$prototype &&
126
- def.$prototype !== "Function" &&
127
- !def.$src
134
+ (def as Record<string, unknown>).$prototype &&
135
+ (def as Record<string, unknown>).$prototype !== "Function" &&
136
+ !(def as Record<string, unknown>).$src
128
137
  ) {
129
- const mapped = imports[def.$prototype];
138
+ const mapped = imports[(def as Record<string, unknown>).$prototype as string];
130
139
  if (mapped) {
131
140
  if (typeof mapped !== "string" || !mapped.endsWith(".class.json")) {
132
141
  console.warn(
133
- `Jx: import "${def.$prototype}" must map to a .class.json path, got "${mapped}"`,
142
+ `Jx: import "${(def as Record<string, unknown>).$prototype}" must map to a .class.json path, got "${mapped}"`,
134
143
  );
135
144
  continue;
136
145
  }
137
- def.$src = mapped;
146
+ (def as Record<string, unknown>).$src = mapped;
138
147
  }
139
148
  }
140
149
  }
@@ -161,14 +170,15 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
161
170
 
162
171
  // 4. Object
163
172
  if (typeof def === "object") {
164
- if (def.$prototype) continue; // handled in later passes
165
- if ("$expression" in def) continue; // handled in pass 2.5
166
- if (def.timing === "server" && def.$src && def.$export) continue; // handled in fifth pass
167
- if ("default" in def) {
168
- raw[key] = def.default;
173
+ const d = def as Record<string, unknown>;
174
+ if (d.$prototype) continue; // handled in later passes
175
+ if ("$expression" in d) continue; // handled in pass 2.5
176
+ if (d.timing === "server" && d.$src && d.$export) continue; // handled in fifth pass
177
+ if ("default" in d) {
178
+ raw[key] = d.default;
169
179
  continue;
170
180
  } // Shape 2: expanded signal
171
- if (hasSchemaKeywords(def)) continue; // Shape 2b: pure type def
181
+ if (hasSchemaKeywords(d as Record<string, any>)) continue; // Shape 2b: pure type def
172
182
  raw[key] = def; // Shape 1: plain object
173
183
  }
174
184
  }
@@ -186,10 +196,13 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
186
196
  // Pass 2.5: $expression entries (Shape 5)
187
197
  for (const [key, def] of Object.entries(defs)) {
188
198
  if (def && typeof def === "object" && !Array.isArray(def) && "$expression" in def) {
189
- const node = /** @type {any} */ (def).$expression;
199
+ const node = (def as Record<string, unknown>).$expression as {
200
+ operator: string;
201
+ target: unknown;
202
+ value?: unknown;
203
+ };
190
204
  if (isMutating(node.operator)) {
191
- state[key] = (/** @type {any} */ s, /** @type {any} */ event) =>
192
- evaluateExpression(node, s, event);
205
+ state[key] = (s: Record<string, any>, event: Event) => evaluateExpression(node, s, event);
193
206
  } else {
194
207
  state[key] = computed(() => evaluateExpression(node, state, null));
195
208
  }
@@ -198,15 +211,19 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
198
211
 
199
212
  // Third pass: $prototype: "Function" entries
200
213
  for (const [key, def] of Object.entries(defs)) {
201
- if (typeof def === "object" && def?.$prototype === "Function") {
202
- state[key] = await resolveFunction(def, state, key, base);
214
+ if (typeof def === "object" && (def as Record<string, unknown>)?.$prototype === "Function") {
215
+ state[key] = await resolveFunction(def as Record<string, any>, state, key, base);
203
216
  }
204
217
  }
205
218
 
206
219
  // Fourth pass: other $prototype entries (Request, Set, Map, etc.)
207
220
  for (const [key, def] of Object.entries(defs)) {
208
- if (typeof def === "object" && def?.$prototype && def.$prototype !== "Function") {
209
- state[key] = await resolvePrototype(def, state, key, base);
221
+ if (
222
+ typeof def === "object" &&
223
+ (def as Record<string, unknown>)?.$prototype &&
224
+ (def as Record<string, unknown>).$prototype !== "Function"
225
+ ) {
226
+ state[key] = await resolvePrototype(def as Record<string, any>, state, key, base);
210
227
  }
211
228
  }
212
229
 
@@ -216,12 +233,12 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
216
233
  if (
217
234
  def != null &&
218
235
  typeof def === "object" &&
219
- def.timing === "server" &&
220
- def.$src &&
221
- def.$export &&
222
- !def.$prototype
236
+ (def as Record<string, unknown>).timing === "server" &&
237
+ (def as Record<string, unknown>).$src &&
238
+ (def as Record<string, unknown>).$export &&
239
+ !(def as Record<string, unknown>).$prototype
223
240
  ) {
224
- state[key] = await resolveServerFunction(def, state, key, base);
241
+ state[key] = await resolveServerFunction(def as Record<string, any>, state, key, base);
225
242
  }
226
243
  }
227
244
  }
@@ -242,7 +259,7 @@ export async function buildScope(doc, parentScope = {}, base = location.href) {
242
259
  * @param {Record<string, any>} obj
243
260
  * @returns {boolean}
244
261
  */
245
- function hasSchemaKeywords(obj) {
262
+ function hasSchemaKeywords(obj: Record<string, any>) {
246
263
  for (const k of Object.keys(obj)) {
247
264
  if (SCHEMA_KEYWORDS.has(k)) return true;
248
265
  }
@@ -258,7 +275,7 @@ export { hasSchemaKeywords };
258
275
  * @param {Record<string, any>} state
259
276
  * @returns {string}
260
277
  */
261
- function evaluateTemplate(str, state) {
278
+ function evaluateTemplate(str: string, state: Record<string, any>) {
262
279
  const $map = state?.$map;
263
280
  const fn = new Function("state", "$map", "item", "index", `return \`${str}\``);
264
281
  return fn(state, $map, $map?.item, $map?.index);
@@ -281,7 +298,12 @@ const _moduleCache = new Map();
281
298
  * @param {string} [base] - Base URL for resolving $src imports
282
299
  * @returns {Promise<unknown>}
283
300
  */
284
- async function resolveFunction(def, state, key, base) {
301
+ async function resolveFunction(
302
+ def: Record<string, any>,
303
+ state: Record<string, any>,
304
+ key: string,
305
+ base?: string,
306
+ ) {
285
307
  if (def.body && def.$src) {
286
308
  throw new Error(`Jx: '${key}' declares both body and $src — these are mutually exclusive`);
287
309
  }
@@ -352,7 +374,7 @@ async function resolveFunction(def, state, key, base) {
352
374
  * @param {Record<string, any>} def
353
375
  * @returns {string[]}
354
376
  */
355
- function resolveParamNames(def) {
377
+ function resolveParamNames(def: Record<string, any>) {
356
378
  const raw = def.parameters ?? def.arguments ?? [];
357
379
  let names;
358
380
  if (Array.isArray(raw) && raw.length > 0 && typeof raw[0] === "object") {
@@ -413,7 +435,11 @@ export const RESERVED_KEYS = new Set([
413
435
  * @param {JxRenderOptions} [options]
414
436
  * @returns {HTMLElement | Text}
415
437
  */
416
- export function renderNode(def, state, options) {
438
+ export function renderNode(
439
+ def: Record<string, any>,
440
+ state: Record<string, any>,
441
+ options?: JxRenderOptions,
442
+ ) {
417
443
  const path = options?._path ?? [];
418
444
 
419
445
  // Text node children: bare strings/numbers/booleans produce DOM Text nodes
@@ -476,7 +502,7 @@ export function renderNode(def, state, options) {
476
502
  * @param {unknown} val
477
503
  * @returns {boolean}
478
504
  */
479
- function isTemplateString(val) {
505
+ function isTemplateString(val: unknown) {
480
506
  return typeof val === "string" && val.includes("${");
481
507
  }
482
508
 
@@ -487,7 +513,7 @@ function isTemplateString(val) {
487
513
  * @param {Record<string, any>} def
488
514
  * @param {Record<string, any>} state
489
515
  */
490
- function applyProperties(el, def, state) {
516
+ function applyProperties(el: HTMLElement, def: Record<string, any>, state: Record<string, any>) {
491
517
  for (const [key, val] of Object.entries(def)) {
492
518
  if (RESERVED_KEYS.has(key)) continue;
493
519
  if (key.startsWith("$")) continue; // scope bindings — handled in renderNode
@@ -512,7 +538,11 @@ function applyProperties(el, def, state) {
512
538
  }
513
539
  // Event handler: inline $expression
514
540
  if (val && typeof val === "object" && "$expression" in val) {
515
- const node = /** @type {any} */ (val).$expression;
541
+ const node = (val as Record<string, unknown>).$expression as {
542
+ operator: string;
543
+ target: unknown;
544
+ value?: unknown;
545
+ };
516
546
  const scope = state;
517
547
  el.addEventListener(key.slice(2), (e) => evaluateExpression(node, scope, e));
518
548
  continue;
@@ -529,17 +559,16 @@ function applyProperties(el, def, state) {
529
559
  * @param {unknown} val
530
560
  * @param {Record<string, any>} state
531
561
  */
532
- function bindProperty(el, key, val, state) {
533
- const target = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (el));
562
+ function bindProperty(el: HTMLElement, key: string, val: unknown, state: Record<string, any>) {
563
+ const target = el as unknown as Record<string, unknown>;
534
564
  if (isRefObj(val)) {
565
+ const refVal = val as { $ref: string };
535
566
  if (key === "id") {
536
- target[key] = /** @type {string} */ (
537
- resolveRef(/** @type {{ $ref: string }} */ (val).$ref, state)
538
- );
567
+ target[key] = resolveRef(refVal.$ref, state) as string;
539
568
  return;
540
569
  }
541
570
  effect(() => {
542
- target[key] = resolveRef(/** @type {{ $ref: string }} */ (val).$ref, state);
571
+ target[key] = resolveRef(refVal.$ref, state);
543
572
  });
544
573
  return;
545
574
  }
@@ -547,7 +576,7 @@ function bindProperty(el, key, val, state) {
547
576
  // Universal ${} reactivity — template strings in element properties
548
577
  if (isTemplateString(val)) {
549
578
  effect(() => {
550
- target[key] = evaluateTemplate(/** @type {string} */ (val), state);
579
+ target[key] = evaluateTemplate(val as string, state);
551
580
  });
552
581
  return;
553
582
  }
@@ -565,17 +594,18 @@ function bindProperty(el, key, val, state) {
565
594
  * @param {Record<string, any>} [state] Component scope for template string evaluation. Default is
566
595
  * `{}`
567
596
  */
568
- export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
569
- /** @type {Record<string, any>} */
570
- const nested = {};
571
- /** @type {Record<string, any>} */
572
- const media = {};
573
- /** @type {Record<string, string>} */
574
- const baseDecls = {};
597
+ export function applyStyle(
598
+ el: HTMLElement,
599
+ styleDef: Record<string, any>,
600
+ mediaQueries: Record<string, any> = {},
601
+ state: Record<string, any> = {},
602
+ ) {
603
+ const nested: Record<string, any> = {};
604
+ const media: Record<string, any> = {};
605
+ const baseDecls: Record<string, string> = {};
575
606
 
576
607
  // Collect properties overridden by media queries so we can avoid inline styles for them
577
- /** @type {Set<string>} */
578
- const mediaOverriddenProps = new Set();
608
+ const mediaOverriddenProps: Set<string> = new Set();
579
609
  for (const [prop, val] of Object.entries(styleDef)) {
580
610
  if (prop.startsWith("@") && val && typeof val === "object") {
581
611
  for (const k of Object.keys(val)) {
@@ -604,11 +634,10 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
604
634
  else el.style.setProperty(prop, val);
605
635
  } else if (isTemplateString(val))
606
636
  effect(() => {
607
- /** @type {Record<string, string>} */ (/** @type {unknown} */ (el.style))[prop] =
608
- evaluateTemplate(val, state);
637
+ (el.style as unknown as Record<string, string>)[prop] = evaluateTemplate(val, state);
609
638
  });
610
639
  else if (mediaOverriddenProps.has(prop)) baseDecls[prop] = val;
611
- else /** @type {Record<string, string>} */ (/** @type {unknown} */ (el.style))[prop] = val;
640
+ else (el.style as unknown as Record<string, string>)[prop] = val;
612
641
  }
613
642
 
614
643
  const hasNested = Object.keys(nested).length > 0;
@@ -623,7 +652,7 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
623
652
  const baseCSS = toCSSText(baseDecls);
624
653
  if (baseCSS) css += `[data-jx="${uid}"] { ${baseCSS} }\n`;
625
654
 
626
- function emitNested(/** @type {string} */ scope, /** @type {Record<string, any>} */ rules) {
655
+ function emitNested(scope: string, rules: Record<string, any>) {
627
656
  const props = toCSSText(rules);
628
657
  if (props) css += `${scope} { ${props} }\n`;
629
658
  for (const [sel, sub] of Object.entries(rules)) {
@@ -661,10 +690,7 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
661
690
  const scope = `[data-jx="${uid}"]`;
662
691
  css += `${atRule} { ${scope} { ${toCSSText(rules)} } }\n`;
663
692
 
664
- function emitMediaNested(
665
- /** @type {string} */ parentSel,
666
- /** @type {Record<string, any>} */ obj,
667
- ) {
693
+ function emitMediaNested(parentSel: string, obj: Record<string, any>) {
668
694
  for (const [sel, sub] of Object.entries(obj)) {
669
695
  if (sub === null || typeof sub !== "object" || Array.isArray(sub)) continue;
670
696
  if (sel.startsWith("@")) continue;
@@ -693,7 +719,7 @@ export function applyStyle(el, styleDef, mediaQueries = {}, state = {}) {
693
719
  * @param {Record<string, any>} attrs
694
720
  * @param {Record<string, any>} state
695
721
  */
696
- function applyAttributes(el, attrs, state) {
722
+ function applyAttributes(el: HTMLElement, attrs: Record<string, any>, state: Record<string, any>) {
697
723
  for (const [k, v] of Object.entries(attrs)) {
698
724
  if (isRefObj(v)) {
699
725
  effect(() => el.setAttribute(k, String(resolveRef(v.$ref, state) ?? "")));
@@ -713,7 +739,11 @@ function applyAttributes(el, attrs, state) {
713
739
  * @param {JxRenderOptions} [options]
714
740
  * @returns {HTMLElement}
715
741
  */
716
- function renderMappedArray(def, state, options) {
742
+ function renderMappedArray(
743
+ def: Record<string, any>,
744
+ state: Record<string, any>,
745
+ options?: JxRenderOptions,
746
+ ) {
717
747
  const path = options?._path ?? [];
718
748
  const container = document.createElement(def.tagName ?? "div");
719
749
 
@@ -767,7 +797,11 @@ function renderMappedArray(def, state, options) {
767
797
  * @param {JxRenderOptions} [options]
768
798
  * @returns {HTMLElement}
769
799
  */
770
- function renderSwitch(def, state, options) {
800
+ function renderSwitch(
801
+ def: Record<string, any>,
802
+ state: Record<string, any>,
803
+ options?: JxRenderOptions,
804
+ ) {
771
805
  const path = options?._path ?? [];
772
806
  const container = document.createElement(def.tagName ?? "div");
773
807
 
@@ -780,7 +814,7 @@ function renderSwitch(def, state, options) {
780
814
 
781
815
  effect(() => {
782
816
  container.innerHTML = "";
783
- const key = /** @type {string} */ (resolveRef(def.$switch.$ref, state));
817
+ const key = resolveRef(def.$switch.$ref, state) as string;
784
818
  const caseDef = def.cases?.[key];
785
819
  if (!caseDef) return;
786
820
 
@@ -797,7 +831,7 @@ function renderSwitch(def, state, options) {
797
831
  const childOpts = options ? { ...options, _path: [...path, "cases", key] } : undefined;
798
832
  container.appendChild(renderNode(doc, childScope, childOpts));
799
833
  })
800
- .catch((/** @type {unknown} */ e) =>
834
+ .catch((e: unknown) =>
801
835
  console.error("Jx $switch: failed to load external case", caseDef.$ref, e),
802
836
  );
803
837
  return;
@@ -824,7 +858,12 @@ function renderSwitch(def, state, options) {
824
858
  * @param {string} [base] - Base URL for resolving $src imports
825
859
  * @returns {Promise<unknown>}
826
860
  */
827
- export async function resolvePrototype(def, state, key, base) {
861
+ export async function resolvePrototype(
862
+ def: Record<string, any>,
863
+ state: Record<string, any>,
864
+ key: string,
865
+ base?: string,
866
+ ) {
828
867
  // ── External class via $src ─────────────────────────────────────────────────
829
868
  if (def.$src) {
830
869
  return resolveExternalPrototype(def, state, key, base);
@@ -832,11 +871,9 @@ export async function resolvePrototype(def, state, key, base) {
832
871
 
833
872
  switch (def.$prototype) {
834
873
  case "Request": {
835
- /** @type {import("@vue/reactivity").Ref<unknown>} */
836
- const s = ref(null);
874
+ const s: import("@vue/reactivity").Ref<unknown> = ref(null);
837
875
  const debounceMs = def.debounce ?? 0;
838
- /** @type {ReturnType<typeof setTimeout> | null} */
839
- let debounceTimer = null;
876
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
840
877
 
841
878
  if (!def.manual) {
842
879
  effect(() => {
@@ -867,8 +904,8 @@ export async function resolvePrototype(def, state, key, base) {
867
904
  .then((d) => {
868
905
  s.value = d;
869
906
  })
870
- .catch((/** @type {unknown} */ e) => {
871
- if (/** @type {Error} */ (e).name !== "AbortError") s.value = { error: String(e) };
907
+ .catch((e: unknown) => {
908
+ if ((e as Error).name !== "AbortError") s.value = { error: String(e) };
872
909
  });
873
910
 
874
911
  if (debounceMs > 0) {
@@ -884,8 +921,7 @@ export async function resolvePrototype(def, state, key, base) {
884
921
 
885
922
  case "URLSearchParams":
886
923
  return computed(() => {
887
- /** @type {Record<string, string>} */
888
- const p = {};
924
+ const p: Record<string, string> = {};
889
925
  for (const [k, v] of Object.entries(def)) {
890
926
  if (k !== "$prototype") {
891
927
  p[k] = isRefObj(v)
@@ -909,8 +945,7 @@ export async function resolvePrototype(def, state, key, base) {
909
945
  } catch {
910
946
  init = def.default ?? null;
911
947
  }
912
- /** @type {import("@vue/reactivity").Ref<any>} */
913
- const storageState = ref(init);
948
+ const storageState: import("@vue/reactivity").Ref<any> = ref(init);
914
949
  // Persist on change
915
950
  effect(() => {
916
951
  const v = storageState.value;
@@ -938,8 +973,7 @@ export async function resolvePrototype(def, state, key, base) {
938
973
  return m[1];
939
974
  }
940
975
  };
941
- /** @type {import("@vue/reactivity").Ref<any>} */
942
- const cookieState = ref(read() ?? def.default ?? null);
976
+ const cookieState: import("@vue/reactivity").Ref<any> = ref(read() ?? def.default ?? null);
943
977
  // Persist on change
944
978
  effect(() => {
945
979
  const v = cookieState.value;
@@ -955,8 +989,7 @@ export async function resolvePrototype(def, state, key, base) {
955
989
  }
956
990
 
957
991
  case "IndexedDB": {
958
- /** @type {import("@vue/reactivity").Ref<any>} */
959
- const idbState = ref(null);
992
+ const idbState: import("@vue/reactivity").Ref<any> = ref(null);
960
993
  const {
961
994
  database,
962
995
  store,
@@ -967,22 +1000,20 @@ export async function resolvePrototype(def, state, key, base) {
967
1000
  } = def;
968
1001
  const req = indexedDB.open(database, version);
969
1002
  req.onupgradeneeded = (e) => {
970
- /** @type {IDBDatabase} */
971
- const db = /** @type {IDBOpenDBRequest} */ (e.target).result;
1003
+ const db: IDBDatabase = (e.target as IDBOpenDBRequest).result;
972
1004
  if (!db.objectStoreNames.contains(store)) {
973
1005
  const os = db.createObjectStore(store, { keyPath, autoIncrement });
974
1006
  for (const i of indexes) os.createIndex(i.name, i.keyPath, { unique: i.unique ?? false });
975
1007
  }
976
1008
  };
977
1009
  req.onsuccess = (e) => {
978
- /** @type {IDBDatabase} */
979
- const db = /** @type {IDBOpenDBRequest} */ (e.target).result;
1010
+ const db: IDBDatabase = (e.target as IDBOpenDBRequest).result;
980
1011
  idbState.value = {
981
1012
  database,
982
1013
  store,
983
1014
  version,
984
1015
  isReady: true,
985
- getStore: (/** @type {IDBTransactionMode} */ mode = "readwrite") =>
1016
+ getStore: (mode: IDBTransactionMode = "readwrite") =>
986
1017
  Promise.resolve(db.transaction(store, mode).objectStore(store)),
987
1018
  };
988
1019
  };
@@ -1000,7 +1031,7 @@ export async function resolvePrototype(def, state, key, base) {
1000
1031
 
1001
1032
  case "FormData": {
1002
1033
  const fd = new FormData();
1003
- for (const [k, v] of Object.entries(def.fields ?? {})) fd.append(k, v);
1034
+ for (const [k, v] of Object.entries(def.fields ?? {})) fd.append(k, v as string);
1004
1035
  return fd;
1005
1036
  }
1006
1037
 
@@ -1043,7 +1074,12 @@ const EXTERNAL_RESERVED = new Set([
1043
1074
  * @param {string} [base]
1044
1075
  * @returns {Promise<unknown>}
1045
1076
  */
1046
- async function resolveExternalPrototype(def, state, key, base) {
1077
+ async function resolveExternalPrototype(
1078
+ def: Record<string, any>,
1079
+ state: Record<string, any>,
1080
+ key: string,
1081
+ base?: string,
1082
+ ) {
1047
1083
  const src = def.$src;
1048
1084
 
1049
1085
  // Non-Function $prototype must use .class.json as entrypoint
@@ -1067,7 +1103,12 @@ async function resolveExternalPrototype(def, state, key, base) {
1067
1103
  * @param {string} [base] - Base URL for resolution
1068
1104
  * @returns {Promise<unknown>}
1069
1105
  */
1070
- async function importAndInstantiate(def, src, exportName, base) {
1106
+ async function importAndInstantiate(
1107
+ def: Record<string, any>,
1108
+ src: string,
1109
+ exportName: string,
1110
+ base?: string,
1111
+ ) {
1071
1112
  let mod;
1072
1113
  if (_moduleCache.has(src)) {
1073
1114
  mod = _moduleCache.get(src);
@@ -1093,8 +1134,7 @@ async function importAndInstantiate(def, src, exportName, base) {
1093
1134
  throw new Error(`Jx: "${exportName}" from "${src}" is not a class`);
1094
1135
  }
1095
1136
 
1096
- /** @type {Record<string, any>} */
1097
- const config = {};
1137
+ const config: Record<string, any> = {};
1098
1138
  for (const [k, v] of Object.entries(def)) {
1099
1139
  if (!EXTERNAL_RESERVED.has(k)) config[k] = v;
1100
1140
  }
@@ -1111,10 +1151,9 @@ async function importAndInstantiate(def, src, exportName, base) {
1111
1151
  }
1112
1152
 
1113
1153
  // Always wrap in ref for reactivity with external classes
1114
- /** @type {import("@vue/reactivity").Ref<any>} */
1115
- const s = ref(value);
1154
+ const s: import("@vue/reactivity").Ref<any> = ref(value);
1116
1155
  if (typeof instance.subscribe === "function") {
1117
- instance.subscribe((/** @type {unknown} */ newVal) => {
1156
+ instance.subscribe((newVal: unknown) => {
1118
1157
  s.value = newVal;
1119
1158
  });
1120
1159
  }
@@ -1131,10 +1170,26 @@ async function importAndInstantiate(def, src, exportName, base) {
1131
1170
  * @param {string} [base]
1132
1171
  * @returns {Promise<unknown>}
1133
1172
  */
1134
- async function resolveClassJson(def, state, key, base) {
1173
+ async function resolveClassJson(
1174
+ def: Record<string, any>,
1175
+ state: Record<string, any>,
1176
+ key: string,
1177
+ base?: string,
1178
+ ) {
1135
1179
  const src = def.$src;
1136
1180
  let classDef;
1137
1181
 
1182
+ // Bare specifiers (package references like @scope/pkg/file) can't be fetched directly —
1183
+ // go straight to dev proxy which can resolve them via node_modules.
1184
+ const isBareSpecifier =
1185
+ !src.startsWith(".") &&
1186
+ !src.startsWith("/") &&
1187
+ !src.startsWith("http") &&
1188
+ !src.startsWith("file:");
1189
+ if (isBareSpecifier) {
1190
+ return resolveViaDevProxy(def, state, key, base);
1191
+ }
1192
+
1138
1193
  // Try fetching the .class.json file directly
1139
1194
  try {
1140
1195
  const url = base ? new URL(src, base).href : src;
@@ -1166,8 +1221,7 @@ async function resolveClassJson(def, state, key, base) {
1166
1221
 
1167
1222
  // Self-contained: construct class dynamically from schema
1168
1223
  const DynClass = classFromSchema(classDef);
1169
- /** @type {Record<string, any>} */
1170
- const config = {};
1224
+ const config: Record<string, any> = {};
1171
1225
  for (const [k, v] of Object.entries(def)) {
1172
1226
  if (!EXTERNAL_RESERVED.has(k)) config[k] = v;
1173
1227
  }
@@ -1183,10 +1237,9 @@ async function resolveClassJson(def, state, key, base) {
1183
1237
  }
1184
1238
 
1185
1239
  // Always wrap in ref for reactivity
1186
- /** @type {import("@vue/reactivity").Ref<any>} */
1187
- const s = ref(value);
1240
+ const s: import("@vue/reactivity").Ref<any> = ref(value);
1188
1241
  if (typeof instance.subscribe === "function") {
1189
- instance.subscribe((/** @type {unknown} */ newVal) => {
1242
+ instance.subscribe((newVal: unknown) => {
1190
1243
  s.value = newVal;
1191
1244
  });
1192
1245
  }
@@ -1200,42 +1253,40 @@ async function resolveClassJson(def, state, key, base) {
1200
1253
  * @param {Record<string, any>} classDef
1201
1254
  * @returns {DynamicClass}
1202
1255
  */
1203
- function classFromSchema(classDef) {
1256
+ function classFromSchema(classDef: Record<string, any>) {
1204
1257
  const fields = classDef.$defs?.fields ?? {};
1205
1258
  const ctor = classDef.$defs?.constructor;
1206
1259
  const methods = classDef.$defs?.methods ?? {};
1207
1260
 
1261
+ interface FieldDef {
1262
+ identifier?: string;
1263
+ access?: string;
1264
+ initializer?: unknown;
1265
+ default?: unknown;
1266
+ }
1267
+
1268
+ interface MethodDef {
1269
+ identifier?: string;
1270
+ parameters?: Record<string, unknown>[];
1271
+ body?: string | string[];
1272
+ role?: string;
1273
+ scope?: string;
1274
+ getter?: { body: string };
1275
+ setter?: { body: string; parameters?: Record<string, unknown>[] };
1276
+ }
1277
+
1208
1278
  class DynClass {
1209
- constructor(/** @type {Record<string, any>} */ config = {}) {
1279
+ constructor(config: Record<string, unknown> = {}) {
1210
1280
  for (const [key, field] of Object.entries(fields)) {
1211
- /**
1212
- * @type {{
1213
- * identifier?: string;
1214
- * access?: string;
1215
- * initializer?: unknown;
1216
- * default?: unknown;
1217
- * }}
1218
- */
1219
- const typedField =
1220
- /**
1221
- * @type {{
1222
- * identifier?: string;
1223
- * access?: string;
1224
- * initializer?: unknown;
1225
- * default?: unknown;
1226
- * }}
1227
- */ (field);
1281
+ const typedField = field as FieldDef;
1228
1282
  const id = typedField.identifier ?? key;
1229
1283
  const propName = typedField.access === "private" ? `_${id}` : id;
1230
- if (config[id] !== undefined)
1231
- /** @type {Record<string, unknown>} */ (this)[propName] = config[id];
1284
+ if (config[id] !== undefined) (this as Record<string, unknown>)[propName] = config[id];
1232
1285
  else if (typedField.initializer !== undefined)
1233
- /** @type {Record<string, unknown>} */ (this)[propName] = typedField.initializer;
1286
+ (this as Record<string, unknown>)[propName] = typedField.initializer;
1234
1287
  else if (typedField.default !== undefined)
1235
- /** @type {Record<string, unknown>} */ (this)[propName] = structuredClone(
1236
- typedField.default,
1237
- );
1238
- else /** @type {Record<string, unknown>} */ (this)[propName] = null;
1288
+ (this as Record<string, unknown>)[propName] = structuredClone(typedField.default);
1289
+ else (this as Record<string, unknown>)[propName] = null;
1239
1290
  }
1240
1291
  if (ctor?.body) {
1241
1292
  const bodyStr = Array.isArray(ctor.body) ? ctor.body.join("\n") : ctor.body;
@@ -1245,63 +1296,36 @@ function classFromSchema(classDef) {
1245
1296
  }
1246
1297
 
1247
1298
  for (const [key, method] of Object.entries(methods)) {
1248
- /**
1249
- * @type {{
1250
- * identifier?: string;
1251
- * parameters?: Record<string, unknown>[];
1252
- * body?: string | string[];
1253
- * role?: string;
1254
- * scope?: string;
1255
- * getter?: { body: string };
1256
- * setter?: { body: string; parameters?: Record<string, unknown>[] };
1257
- * }}
1258
- */
1259
- const typedMethod = /**
1260
- * @type {{
1261
- * identifier?: string;
1262
- * parameters?: Record<string, unknown>[];
1263
- * body?: string | string[];
1264
- * role?: string;
1265
- * scope?: string;
1266
- * getter?: { body: string };
1267
- * setter?: { body: string; parameters?: Record<string, unknown>[] };
1268
- * }}
1269
- */ (method);
1299
+ const typedMethod = method as MethodDef;
1270
1300
  const name = typedMethod.identifier ?? key;
1271
- const params = /** @type {string[]} */ (
1272
- (typedMethod.parameters ?? []).map((/** @type {Record<string, unknown>} */ p) => {
1273
- if (p.$ref) return /** @type {string} */ (/** @type {string} */ (p.$ref).split("/").pop());
1274
- return /** @type {string} */ (p.identifier ?? p.name ?? "arg");
1275
- })
1276
- );
1301
+ const params = (typedMethod.parameters ?? []).map((p: Record<string, unknown>) => {
1302
+ if (p.$ref) return (p.$ref as string).split("/").pop() as string;
1303
+ return (p.identifier ?? p.name ?? "arg") as string;
1304
+ }) as string[];
1277
1305
  const bodyStr = Array.isArray(typedMethod.body)
1278
1306
  ? typedMethod.body.join("\n")
1279
1307
  : (typedMethod.body ?? "");
1280
1308
 
1281
1309
  if (typedMethod.role === "accessor") {
1282
- /** @type {PropertyDescriptor} */
1283
- const descriptor = {};
1310
+ const descriptor: PropertyDescriptor = {};
1284
1311
  if (typedMethod.getter)
1285
- descriptor.get = /** @type {() => unknown} */ (new Function(typedMethod.getter.body));
1312
+ descriptor.get = new Function(typedMethod.getter.body) as () => unknown;
1286
1313
  if (typedMethod.setter) {
1287
1314
  const sp = (typedMethod.setter.parameters ?? []).map(
1288
- (/** @type {Record<string, unknown>} */ p) =>
1289
- /** @type {string} */ (p.$ref)?.split("/").pop() ?? "v",
1290
- );
1291
- descriptor.set = /** @type {(v: unknown) => void} */ (
1292
- new Function(...sp, typedMethod.setter.body)
1315
+ (p: Record<string, unknown>) => (p.$ref as string)?.split("/").pop() ?? "v",
1293
1316
  );
1317
+ descriptor.set = new Function(...sp, typedMethod.setter.body) as (v: unknown) => void;
1294
1318
  }
1295
1319
  Object.defineProperty(DynClass.prototype, name, { ...descriptor, configurable: true });
1296
1320
  } else if (typedMethod.scope === "static") {
1297
- /** @type {DynamicClass} */ (DynClass)[name] = new Function(...params, bodyStr);
1321
+ (DynClass as unknown as DynamicClass)[name] = new Function(...params, bodyStr);
1298
1322
  } else {
1299
- /** @type {DynamicClass} */ (DynClass).prototype[name] = new Function(...params, bodyStr);
1323
+ (DynClass as unknown as DynamicClass).prototype[name] = new Function(...params, bodyStr);
1300
1324
  }
1301
1325
  }
1302
1326
 
1303
1327
  Object.defineProperty(DynClass, "name", { value: classDef.title, configurable: true });
1304
- const dynCtor = /** @type {DynamicClass} */ (/** @type {unknown} */ (DynClass));
1328
+ const dynCtor = DynClass as unknown as DynamicClass;
1305
1329
  return dynCtor;
1306
1330
  }
1307
1331
 
@@ -1316,19 +1340,21 @@ function classFromSchema(classDef) {
1316
1340
  * @param {string} [base]
1317
1341
  * @returns {Promise<unknown>}
1318
1342
  */
1319
- async function resolveViaDevProxy(def, state, key, base) {
1320
- /** @type {Record<string, any>} */
1321
- const config = {};
1343
+ async function resolveViaDevProxy(
1344
+ def: Record<string, any>,
1345
+ state: Record<string, any>,
1346
+ key: string,
1347
+ base?: string,
1348
+ ) {
1349
+ const config: Record<string, any> = {};
1322
1350
  for (const [k, v] of Object.entries(def)) {
1323
1351
  if (!EXTERNAL_RESERVED.has(k)) config[k] = v;
1324
1352
  }
1325
1353
 
1326
- const hasTemplates = Object.values(config).some((/** @type {unknown} */ v) =>
1327
- isTemplateString(v),
1328
- );
1354
+ const hasTemplates = Object.values(config).some((v: unknown) => isTemplateString(v));
1329
1355
 
1330
1356
  /** @param {Record<string, any>} resolvedConfig */
1331
- const doResolve = (resolvedConfig) =>
1357
+ const doResolve = (resolvedConfig: Record<string, any>) =>
1332
1358
  fetch("/__jx_resolve__", {
1333
1359
  method: "POST",
1334
1360
  headers: { "Content-Type": "application/json" },
@@ -1345,27 +1371,25 @@ async function resolveViaDevProxy(def, state, key, base) {
1345
1371
  });
1346
1372
 
1347
1373
  // Always wrap in ref for reactivity
1348
- /** @type {import("@vue/reactivity").Ref<any>} */
1349
- const s = ref(null);
1374
+ const s: import("@vue/reactivity").Ref<any> = ref(null);
1350
1375
  if (hasTemplates) {
1351
1376
  effect(() => {
1352
- /** @type {Record<string, any>} */
1353
- const resolvedConfig = {};
1377
+ const resolvedConfig: Record<string, any> = {};
1354
1378
  for (const [k, v] of Object.entries(config)) {
1355
1379
  resolvedConfig[k] = isTemplateString(v) ? evaluateTemplate(v, state) : v;
1356
1380
  }
1357
1381
  doResolve(resolvedConfig)
1358
- .then((/** @type {unknown} */ value) => {
1382
+ .then((value: unknown) => {
1359
1383
  s.value = value;
1360
1384
  })
1361
- .catch((/** @type {unknown} */ e) => console.error("Jx dev proxy:", e));
1385
+ .catch((e: unknown) => console.error("Jx dev proxy:", e));
1362
1386
  });
1363
1387
  } else {
1364
1388
  doResolve(config)
1365
- .then((/** @type {unknown} */ value) => {
1389
+ .then((value: unknown) => {
1366
1390
  s.value = value;
1367
1391
  })
1368
- .catch((/** @type {unknown} */ e) => console.error("Jx dev proxy:", e));
1392
+ .catch((e: unknown) => console.error("Jx dev proxy:", e));
1369
1393
  }
1370
1394
  return s;
1371
1395
  }
@@ -1382,7 +1406,12 @@ async function resolveViaDevProxy(def, state, key, base) {
1382
1406
  * @param {string} [base]
1383
1407
  * @returns {Promise<unknown>}
1384
1408
  */
1385
- async function resolveServerFunction(def, state, key, base) {
1409
+ async function resolveServerFunction(
1410
+ def: Record<string, any>,
1411
+ state: Record<string, any>,
1412
+ key: string,
1413
+ base?: string,
1414
+ ) {
1386
1415
  const src = def.$src;
1387
1416
  const exportName = def.$export;
1388
1417
 
@@ -1414,25 +1443,23 @@ async function resolveServerFunction(def, state, key, base) {
1414
1443
  throw new Error(`Jx: "${exportName}" from "${src}" is not a function`);
1415
1444
 
1416
1445
  const rawArgs = def.arguments ?? {};
1417
- const hasReactiveArg = Object.values(rawArgs).some((/** @type {unknown} */ v) => isRefObj(v));
1446
+ const hasReactiveArg = Object.values(rawArgs).some((v: unknown) => isRefObj(v));
1418
1447
  const resolveArgs = () => {
1419
- /** @type {Record<string, any>} */
1420
- const args = {};
1448
+ const args: Record<string, any> = {};
1421
1449
  for (const [k, v] of Object.entries(rawArgs)) {
1422
- args[k] = isRefObj(v) ? resolveRef(/** @type {{ $ref: string }} */ (v).$ref, state) : v;
1450
+ args[k] = isRefObj(v) ? resolveRef((v as { $ref: string }).$ref, state) : v;
1423
1451
  }
1424
1452
  return args;
1425
1453
  };
1426
1454
 
1427
1455
  // Always wrap in ref for reactivity
1428
- /** @type {import("@vue/reactivity").Ref<any>} */
1429
- const s = ref(null);
1456
+ const s: import("@vue/reactivity").Ref<any> = ref(null);
1430
1457
  if (hasReactiveArg) {
1431
1458
  effect(() => {
1432
1459
  const args = resolveArgs();
1433
1460
  onEffectCleanup(() => {});
1434
1461
  fn(args)
1435
- .then((/** @type {unknown} */ result) => {
1462
+ .then((result: unknown) => {
1436
1463
  s.value = result;
1437
1464
  })
1438
1465
  .catch(() => {});
@@ -1454,21 +1481,25 @@ async function resolveServerFunction(def, state, key, base) {
1454
1481
  * @param {string} [base]
1455
1482
  * @returns {Promise<unknown>}
1456
1483
  */
1457
- async function resolveServerFunctionViaProxy(def, state, key, base) {
1484
+ async function resolveServerFunctionViaProxy(
1485
+ def: Record<string, any>,
1486
+ state: Record<string, any>,
1487
+ key: string,
1488
+ base?: string,
1489
+ ) {
1458
1490
  const rawArgs = def.arguments ?? {};
1459
- const hasReactiveArg = Object.values(rawArgs).some((/** @type {unknown} */ v) => isRefObj(v));
1491
+ const hasReactiveArg = Object.values(rawArgs).some((v: unknown) => isRefObj(v));
1460
1492
 
1461
1493
  const resolveArgs = () => {
1462
- /** @type {Record<string, any>} */
1463
- const args = {};
1494
+ const args: Record<string, any> = {};
1464
1495
  for (const [k, v] of Object.entries(rawArgs)) {
1465
- args[k] = isRefObj(v) ? resolveRef(/** @type {{ $ref: string }} */ (v).$ref, state) : v;
1496
+ args[k] = isRefObj(v) ? resolveRef((v as { $ref: string }).$ref, state) : v;
1466
1497
  }
1467
1498
  return args;
1468
1499
  };
1469
1500
 
1470
1501
  /** @param {Record<string, any>} args */
1471
- const doResolve = (args) =>
1502
+ const doResolve = (args: Record<string, any>) =>
1472
1503
  fetch("/__jx_server__", {
1473
1504
  method: "POST",
1474
1505
  headers: { "Content-Type": "application/json" },
@@ -1484,24 +1515,23 @@ async function resolveServerFunctionViaProxy(def, state, key, base) {
1484
1515
  });
1485
1516
 
1486
1517
  // Always wrap in ref for reactivity
1487
- /** @type {import("@vue/reactivity").Ref<any>} */
1488
- const s = ref(null);
1518
+ const s: import("@vue/reactivity").Ref<any> = ref(null);
1489
1519
  if (hasReactiveArg) {
1490
1520
  effect(() => {
1491
1521
  const args = resolveArgs();
1492
1522
  onEffectCleanup(() => {});
1493
1523
  doResolve(args)
1494
- .then((/** @type {unknown} */ result) => {
1524
+ .then((result: unknown) => {
1495
1525
  s.value = result;
1496
1526
  })
1497
- .catch((/** @type {unknown} */ e) => console.error("Jx server proxy:", e));
1527
+ .catch((e: unknown) => console.error("Jx server proxy:", e));
1498
1528
  });
1499
1529
  } else {
1500
1530
  doResolve(resolveArgs())
1501
- .then((/** @type {unknown} */ result) => {
1531
+ .then((result: unknown) => {
1502
1532
  s.value = result;
1503
1533
  })
1504
- .catch((/** @type {unknown} */ e) => console.error("Jx server proxy:", e));
1534
+ .catch((e: unknown) => console.error("Jx server proxy:", e));
1505
1535
  }
1506
1536
  return s;
1507
1537
  }
@@ -1516,7 +1546,7 @@ async function resolveServerFunctionViaProxy(def, state, key, base) {
1516
1546
  * @param {Record<string, any>} state - Reactive scope proxy (or child scope)
1517
1547
  * @returns {unknown}
1518
1548
  */
1519
- export function resolveRef(ref, state) {
1549
+ export function resolveRef(ref: string, state: Record<string, any>) {
1520
1550
  if (typeof ref !== "string") return ref;
1521
1551
  if (ref.startsWith("$map/")) {
1522
1552
  const parts = ref.split("/");
@@ -1545,7 +1575,7 @@ export function resolveRef(ref, state) {
1545
1575
  * @param {unknown} v
1546
1576
  * @returns {boolean}
1547
1577
  */
1548
- export function isSignal(v) {
1578
+ export function isSignal(v: unknown) {
1549
1579
  return isRef(v);
1550
1580
  }
1551
1581
 
@@ -1553,11 +1583,9 @@ export function isSignal(v) {
1553
1583
  * @param {unknown} v
1554
1584
  * @returns {boolean}
1555
1585
  */
1556
- function isRefObj(v) {
1586
+ function isRefObj(v: unknown) {
1557
1587
  return (
1558
- v !== null &&
1559
- typeof v === "object" &&
1560
- typeof (/** @type {Record<string, unknown>} */ (v).$ref) === "string"
1588
+ v !== null && typeof v === "object" && typeof (v as Record<string, unknown>).$ref === "string"
1561
1589
  );
1562
1590
  }
1563
1591
 
@@ -1565,7 +1593,7 @@ function isRefObj(v) {
1565
1593
  * @param {string} k
1566
1594
  * @returns {boolean}
1567
1595
  */
1568
- function isNestedSelector(k) {
1596
+ function isNestedSelector(k: string) {
1569
1597
  return k.startsWith(":") || k.startsWith(".") || k.startsWith("&") || k.startsWith("[");
1570
1598
  }
1571
1599
 
@@ -1574,8 +1602,8 @@ function isNestedSelector(k) {
1574
1602
  * @param {string} path
1575
1603
  * @returns {unknown}
1576
1604
  */
1577
- function getPath(obj, path) {
1578
- return path.split(/[./]/).reduce((o, k) => /** @type {Record<string, unknown>} */ (o)?.[k], obj);
1605
+ function getPath(obj: unknown, path: string) {
1606
+ return path.split(/[./]/).reduce((o, k) => (o as Record<string, unknown>)?.[k], obj);
1579
1607
  }
1580
1608
 
1581
1609
  /**
@@ -1583,10 +1611,10 @@ function getPath(obj, path) {
1583
1611
  * @param {Record<string, any>} parentState
1584
1612
  * @returns {Record<string, any>}
1585
1613
  */
1586
- function mergeProps(def, parentState) {
1614
+ function mergeProps(def: Record<string, any>, parentState: Record<string, any>) {
1587
1615
  const child = Object.create(parentState);
1588
1616
  for (const [k, v] of Object.entries(def.$props ?? {})) {
1589
- child[k] = isRefObj(v) ? resolveRef(v.$ref, parentState) : v;
1617
+ child[k] = isRefObj(v) ? resolveRef((v as { $ref: string }).$ref, parentState) : v;
1590
1618
  }
1591
1619
  return child;
1592
1620
  }
@@ -1597,7 +1625,7 @@ function mergeProps(def, parentState) {
1597
1625
  * @param {string} s
1598
1626
  * @returns {string}
1599
1627
  */
1600
- export function camelToKebab(s) {
1628
+ export function camelToKebab(s: string) {
1601
1629
  return s.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
1602
1630
  }
1603
1631
 
@@ -1607,7 +1635,7 @@ export function camelToKebab(s) {
1607
1635
  * @param {Record<string, unknown> | object} rules
1608
1636
  * @returns {string}
1609
1637
  */
1610
- export function toCSSText(rules) {
1638
+ export function toCSSText(rules: Record<string, unknown> | object) {
1611
1639
  return Object.entries(rules)
1612
1640
  .filter(([k, v]) => !isNestedSelector(k) && (v === null || typeof v !== "object"))
1613
1641
  .map(([p, v]) => `${camelToKebab(p)}: ${v}`)
@@ -1626,7 +1654,7 @@ const _elementDefs = new Map();
1626
1654
  * @param {string} base
1627
1655
  * @returns {Promise<void>}
1628
1656
  */
1629
- async function registerElements(elements, base) {
1657
+ async function registerElements(elements: any[], base: string) {
1630
1658
  for (const entry of elements) {
1631
1659
  // Bare string: npm package side-effect import (registers custom elements)
1632
1660
  if (typeof entry === "string") {
@@ -1663,7 +1691,7 @@ async function registerElements(elements, base) {
1663
1691
  * @param {any[]} entries
1664
1692
  * @param {string} _base - Document base URL for resolving relative paths
1665
1693
  */
1666
- function injectHead(entries, _base) {
1694
+ function injectHead(entries: any[], _base: string) {
1667
1695
  for (const entry of entries) {
1668
1696
  if (!entry || !entry.tagName) continue;
1669
1697
  const tag = entry.tagName.toLowerCase();
@@ -1700,15 +1728,18 @@ function injectHead(entries, _base) {
1700
1728
  * @param {string} [base] - Base URL for resolving $src imports
1701
1729
  * @returns {Promise<void>}
1702
1730
  */
1703
- export async function defineElement(source, base) {
1731
+ const _definedSources = new Set<string>();
1732
+
1733
+ export async function defineElement(source: string | Record<string, any>, base?: string) {
1704
1734
  if (typeof source === "string") {
1705
1735
  base = new URL(source, base ?? location.href).href;
1736
+ if (_definedSources.has(base)) return;
1737
+ _definedSources.add(base);
1706
1738
  source = await resolve(source);
1707
1739
  }
1708
1740
  base = base ?? location.href;
1709
1741
 
1710
- /** @type {Record<string, any>} */
1711
- const source_ = /** @type {Record<string, any>} */ (source);
1742
+ const source_: Record<string, any> = source as Record<string, any>;
1712
1743
 
1713
1744
  const tagName = source_.tagName;
1714
1745
  if (!tagName || !tagName.includes("-")) {
@@ -1727,6 +1758,9 @@ export async function defineElement(source, base) {
1727
1758
  const observedAttrs = def.observedAttributes ?? [];
1728
1759
 
1729
1760
  const ElementClass = class extends HTMLElement {
1761
+ _jxInitialized = false;
1762
+ _state: Record<string, any> | null = null;
1763
+
1730
1764
  static get observedAttributes() {
1731
1765
  return observedAttrs;
1732
1766
  }
@@ -1751,8 +1785,8 @@ export async function defineElement(source, base) {
1751
1785
 
1752
1786
  // Merge $props set as JS properties by parent before connection
1753
1787
  for (const key of Object.keys(def.state ?? {})) {
1754
- if (key in this && /** @type {Record<string, unknown>} */ (this)[key] !== undefined) {
1755
- state[key] = /** @type {Record<string, unknown>} */ (this)[key];
1788
+ if (key in this && (this as Record<string, unknown>)[key] !== undefined) {
1789
+ state[key] = (this as Record<string, unknown>)[key];
1756
1790
  }
1757
1791
  }
1758
1792
  // Set up property getters/setters that forward into reactive state
@@ -1760,7 +1794,7 @@ export async function defineElement(source, base) {
1760
1794
  if (!(key in HTMLElement.prototype)) {
1761
1795
  Object.defineProperty(this, key, {
1762
1796
  get: () => state[key],
1763
- set: (/** @type {unknown} */ v) => {
1797
+ set: (v: unknown) => {
1764
1798
  state[key] = v;
1765
1799
  },
1766
1800
  configurable: true,
@@ -1768,7 +1802,7 @@ export async function defineElement(source, base) {
1768
1802
  }
1769
1803
  }
1770
1804
 
1771
- /** @type {Record<string, unknown>} */ (this)._state = state;
1805
+ this._state = state;
1772
1806
 
1773
1807
  // Capture light DOM children (for slot distribution) before rendering
1774
1808
  const slottedChildren = Array.from(this.childNodes);
@@ -1798,38 +1832,25 @@ export async function defineElement(source, base) {
1798
1832
  }
1799
1833
 
1800
1834
  disconnectedCallback() {
1801
- /** @type {{ _state?: Record<string, any> }} */
1802
- const self = /** @type {{ _state?: Record<string, any> }} */ (/** @type {unknown} */ (this));
1803
- if (typeof self._state?.onUnmount === "function") {
1804
- self._state.onUnmount(self._state);
1835
+ if (typeof this._state?.onUnmount === "function") {
1836
+ this._state.onUnmount(this._state);
1805
1837
  }
1806
1838
  }
1807
1839
 
1808
1840
  adoptedCallback() {
1809
- /** @type {{ _state?: Record<string, any> }} */
1810
- const self = /** @type {{ _state?: Record<string, any> }} */ (/** @type {unknown} */ (this));
1811
- if (typeof self._state?.onAdopted === "function") {
1812
- self._state.onAdopted(self._state);
1841
+ if (typeof this._state?.onAdopted === "function") {
1842
+ this._state.onAdopted(this._state);
1813
1843
  }
1814
1844
  }
1815
1845
 
1816
- attributeChangedCallback(
1817
- /** @type {string} */ name,
1818
- /** @type {string | null} */ oldVal,
1819
- /** @type {string | null} */ newVal,
1820
- ) {
1821
- /** @type {{ _state?: Record<string, any> }} */
1822
- const self = /** @type {{ _state?: Record<string, any> }} */ (/** @type {unknown} */ (this));
1823
- if (!self._state || oldVal === newVal) return;
1824
- const camelKey = name.replace(
1825
- /-([a-z])/g,
1826
- (/** @type {string} */ _, /** @type {string} */ c) => c.toUpperCase(),
1827
- );
1828
- const current = self._state[camelKey];
1829
- if (typeof current === "number") self._state[camelKey] = Number(newVal);
1846
+ attributeChangedCallback(name: string, oldVal: string | null, newVal: string | null) {
1847
+ if (!this._state || oldVal === newVal) return;
1848
+ const camelKey = name.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
1849
+ const current = this._state[camelKey];
1850
+ if (typeof current === "number") this._state[camelKey] = Number(newVal);
1830
1851
  else if (typeof current === "boolean")
1831
- self._state[camelKey] = newVal !== null && newVal !== "false";
1832
- else self._state[camelKey] = newVal;
1852
+ this._state[camelKey] = newVal !== null && newVal !== "false";
1853
+ else this._state[camelKey] = newVal;
1833
1854
  }
1834
1855
  };
1835
1856
 
@@ -1845,7 +1866,12 @@ export async function defineElement(source, base) {
1845
1866
  * @param {JxPath} [path]
1846
1867
  * @returns {HTMLElement}
1847
1868
  */
1848
- function renderCustomElementWithProps(def, state, options, path) {
1869
+ function renderCustomElementWithProps(
1870
+ def: Record<string, any>,
1871
+ state: Record<string, any>,
1872
+ options?: JxRenderOptions,
1873
+ path?: JxPath,
1874
+ ) {
1849
1875
  const el = document.createElement(def.tagName);
1850
1876
 
1851
1877
  if (options?.onNodeCreated) options.onNodeCreated(el, path ?? [], def);
@@ -1853,18 +1879,19 @@ function renderCustomElementWithProps(def, state, options, path) {
1853
1879
  // Set JS properties from $props (before connection)
1854
1880
  for (const [key, val] of Object.entries(def.$props ?? {})) {
1855
1881
  if (isRefObj(val)) {
1856
- const resolved = resolveRef(val.$ref, state);
1857
- /** @type {Record<string, unknown>} */ (el)[key] = resolved;
1882
+ const refVal = val as { $ref: string };
1883
+ const resolved = resolveRef(refVal.$ref, state);
1884
+ (el as Record<string, unknown>)[key] = resolved;
1858
1885
  // Reactive forwarding: re-set the property when the source changes
1859
1886
  effect(() => {
1860
- /** @type {Record<string, unknown>} */ (el)[key] = resolveRef(val.$ref, state);
1887
+ (el as Record<string, unknown>)[key] = resolveRef(refVal.$ref, state);
1861
1888
  });
1862
1889
  } else if (isTemplateString(val)) {
1863
1890
  effect(() => {
1864
- /** @type {Record<string, unknown>} */ (el)[key] = evaluateTemplate(val, state);
1891
+ (el as Record<string, unknown>)[key] = evaluateTemplate(val as string, state);
1865
1892
  });
1866
1893
  } else {
1867
- /** @type {Record<string, unknown>} */ (el)[key] = val;
1894
+ (el as Record<string, unknown>)[key] = val;
1868
1895
  }
1869
1896
  }
1870
1897
 
@@ -1888,25 +1915,20 @@ function renderCustomElementWithProps(def, state, options, path) {
1888
1915
  * @param {HTMLElement} host
1889
1916
  * @param {ChildNode[]} slottedChildren
1890
1917
  */
1891
- function distributeSlots(host, slottedChildren) {
1918
+ function distributeSlots(host: HTMLElement, slottedChildren: ChildNode[]) {
1892
1919
  if (slottedChildren.length === 0) return;
1893
1920
 
1894
1921
  const slots = host.querySelectorAll("slot");
1895
1922
  if (slots.length === 0) return;
1896
1923
 
1897
- /** @type {Map<string | null, ChildNode[]>} */
1898
- const named = new Map();
1899
- /** @type {ChildNode[]} */
1900
- const unnamed = [];
1924
+ const named: Map<string | null, ChildNode[]> = new Map();
1925
+ const unnamed: ChildNode[] = [];
1901
1926
 
1902
1927
  for (const child of slottedChildren) {
1903
- if (
1904
- child.nodeType === Node.ELEMENT_NODE &&
1905
- /** @type {Element} */ (child).getAttribute("slot")
1906
- ) {
1907
- const name = /** @type {Element} */ (child).getAttribute("slot");
1928
+ if (child.nodeType === Node.ELEMENT_NODE && (child as Element).getAttribute("slot")) {
1929
+ const name = (child as Element).getAttribute("slot");
1908
1930
  if (!named.has(name)) named.set(name, []);
1909
- /** @type {ChildNode[]} */ (named.get(name)).push(child);
1931
+ (named.get(name) as ChildNode[]).push(child);
1910
1932
  } else {
1911
1933
  unnamed.push(child);
1912
1934
  }