@jxsuite/runtime 0.28.4 → 0.29.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/runtime",
3
- "version": "0.28.4",
3
+ "version": "0.29.0",
4
4
  "description": "JSON-native reactive web component runtime",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,12 +23,13 @@
23
23
  },
24
24
  "scripts": {
25
25
  "build": "bun build ./src/runtime.ts --outdir dist --target browser --sourcemap=linked",
26
+ "build:metafile": "bun build ./src/runtime.ts --outdir dist --target browser --sourcemap=linked --metafile=metafile.json",
26
27
  "test": "bun test --isolate",
27
28
  "test:coverage": "bun test --isolate --coverage",
28
29
  "upgrade": "bunx npm-check-updates -u && bun install"
29
30
  },
30
31
  "dependencies": {
31
- "@jxsuite/schema": "^0.28.4",
32
+ "@jxsuite/schema": "^0.29.0",
32
33
  "@vue/reactivity": "^3.5.38"
33
34
  },
34
35
  "devDependencies": {
package/src/expression.ts CHANGED
@@ -139,7 +139,7 @@ function resolveExprRef(ref: string, state: JxScope, event: Event | null, iterCt
139
139
  } else if (key === "index") {
140
140
  base = iterCtx?.index ?? map?.index ?? state["$map/index"];
141
141
  } else {
142
- base = map?.[key] ?? state[`$map/${key}`];
142
+ base = map?.[key!] ?? state[`$map/${key}`];
143
143
  }
144
144
  return parts.length > 2 ? getPath(base, parts.slice(2).join("/")) : base;
145
145
  }
@@ -167,7 +167,7 @@ function resolveExprRef(ref: string, state: JxScope, event: Event | null, iterCt
167
167
  function resolveWritableRef(
168
168
  ref: string,
169
169
  state: JxScope,
170
- event: Event | null,
170
+ _event: Event | null,
171
171
  iterCtx?: IterCtx,
172
172
  ): { obj: JxScope; key: string } {
173
173
  if (ref.startsWith("$map/")) {
@@ -180,7 +180,7 @@ function resolveWritableRef(
180
180
  } else if (key === "index") {
181
181
  base = iterCtx?.index ?? map?.index ?? state["$map/index"];
182
182
  } else {
183
- base = map?.[key] ?? state[`$map/${key}`];
183
+ base = map?.[key!] ?? state[`$map/${key}`];
184
184
  }
185
185
  if (parts.length > 2) {
186
186
  const pathParts = parts.slice(2);
package/src/runtime.ts CHANGED
@@ -13,7 +13,15 @@
13
13
  * @module jx
14
14
  */
15
15
 
16
- import { computed, effect, isRef, onEffectCleanup, reactive, ref } from "@vue/reactivity";
16
+ import {
17
+ computed,
18
+ effect,
19
+ effectScope,
20
+ isRef,
21
+ onEffectCleanup,
22
+ reactive,
23
+ ref,
24
+ } from "@vue/reactivity";
17
25
  import { evaluateExpression, isMutating } from "./expression.ts";
18
26
  import type { DynamicClass, JxEventHandler, JxPath, JxRenderOptions, JxScope } from "./types.ts";
19
27
  import {
@@ -81,7 +89,7 @@ export async function Jx(
81
89
  const state = await buildScope(doc, {}, base);
82
90
  target.append(renderNode(doc, state, options));
83
91
  if (typeof state.onMount === "function") {
84
- state.onMount(state);
92
+ (state.onMount as (s: JxScope) => unknown)(state);
85
93
  }
86
94
  return state;
87
95
  }
@@ -300,16 +308,31 @@ export { hasSchemaKeywords };
300
308
  * @param {JxScope} state
301
309
  * @returns {string}
302
310
  */
303
- function evaluateTemplate(str: string, state: JxScope) {
311
+ function evaluateTemplate(str: string, state: JxScope): string {
304
312
  const $map = state?.$map as { item?: unknown; index?: number } | undefined;
305
- const fn = new Function("state", "$map", "item", "index", `return \`${str}\``);
313
+ const fn = new Function("state", "$map", "item", "index", `return \`${str}\``) as (
314
+ state: JxScope,
315
+ $map: unknown,
316
+ item: unknown,
317
+ index: number | undefined,
318
+ ) => string;
306
319
  return fn(state, $map, $map?.item, $map?.index);
307
320
  }
308
321
 
309
322
  // ─── Step 2b: Function resolution (Shape 4) ─────────────────────────────────
310
323
 
324
+ /** Shape of a dynamically imported module: named exports plus an optional default. */
325
+ type ImportedModule = Record<string, unknown> & { default?: Record<string, unknown> };
326
+
327
+ /** Minimal contract an externally-imported resolver class may implement. */
328
+ interface ExternalClassInstance {
329
+ value?: unknown;
330
+ resolve?: () => unknown;
331
+ subscribe?: (cb: (newVal: unknown) => void) => void;
332
+ }
333
+
311
334
  /** Module cache for $src imports (shared with external class resolution). */
312
- const _moduleCache = new Map();
335
+ const _moduleCache = new Map<string, ImportedModule>();
313
336
 
314
337
  /**
315
338
  * Resolve a $prototype: "Function" entry into a function or computed.
@@ -337,11 +360,11 @@ async function resolveFunction(def: JxFunctionDef, state: JxScope, key: string,
337
360
  return noop;
338
361
  }
339
362
 
340
- let fn;
363
+ let fn: ((...args: unknown[]) => unknown) | undefined;
341
364
 
342
365
  if (def.body) {
343
366
  const params = resolveParamNames(def);
344
- fn = new Function(...params, def.body);
367
+ fn = new Function(...params, def.body) as (...args: unknown[]) => unknown;
345
368
  Object.defineProperty(fn, "name", {
346
369
  configurable: true,
347
370
  value: def.name ?? key,
@@ -353,26 +376,27 @@ async function resolveFunction(def: JxFunctionDef, state: JxScope, key: string,
353
376
  throw new Error(`Jx: '${key}' has neither body nor $src`);
354
377
  }
355
378
  const exportName = def.$export ?? key;
356
- let mod;
379
+ let mod: ImportedModule;
357
380
  if (_moduleCache.has(src)) {
358
- mod = _moduleCache.get(src);
381
+ mod = _moduleCache.get(src)!;
359
382
  } else {
360
383
  if (base) {
361
384
  const resolvedSrc = new URL(src, base).href;
362
385
  try {
363
- mod = await import(resolvedSrc);
386
+ mod = (await import(resolvedSrc)) as ImportedModule;
364
387
  } catch {
365
- mod = await import(src);
388
+ mod = (await import(src)) as ImportedModule;
366
389
  }
367
390
  } else {
368
- mod = await import(src);
391
+ mod = (await import(src)) as ImportedModule;
369
392
  }
370
393
  _moduleCache.set(src, mod);
371
394
  }
372
- fn = mod[exportName] ?? mod.default?.[exportName];
373
- if (typeof fn !== "function") {
395
+ const candidate = mod[exportName] ?? mod.default?.[exportName];
396
+ if (typeof candidate !== "function") {
374
397
  throw new TypeError(`Jx: export "${exportName}" not found or not a function in "${src}"`);
375
398
  }
399
+ fn = candidate as (...args: unknown[]) => unknown;
376
400
  }
377
401
 
378
402
  // Detect computed: body contains a return statement, or $src function introspection.
@@ -475,11 +499,11 @@ export function renderNode(
475
499
  }
476
500
 
477
501
  // Extend scope with any $-prefixed local bindings declared on this node
478
- let localState = state;
502
+ let localState: JxScope = state;
479
503
  for (const [key, val] of Object.entries(def)) {
480
504
  if (key.startsWith("$") && !RESERVED_KEYS.has(key)) {
481
505
  if (localState === state) {
482
- localState = Object.create(state);
506
+ localState = Object.create(state) as JxScope;
483
507
  }
484
508
  localState[key] = isRefObj(val) ? resolveRef(val.$ref, state) : val;
485
509
  }
@@ -500,14 +524,11 @@ export function renderNode(
500
524
  if (def.$switch) {
501
525
  return renderSwitch(def, localState, options);
502
526
  }
503
- if (isMappedArray(def.children)) {
504
- return renderMappedArray(def, def.children, localState, options);
505
- }
506
527
 
507
528
  const el = document.createElement(tagName);
508
529
 
509
530
  if (options?.onNodeCreated) {
510
- options.onNodeCreated(el, path, def);
531
+ options.onNodeCreated(el, path, def, localState);
511
532
  }
512
533
 
513
534
  applyProperties(el, def, localState);
@@ -519,10 +540,23 @@ export function renderNode(
519
540
  );
520
541
  applyAttributes(el, def.attributes ?? {}, localState);
521
542
 
522
- const children = Array.isArray(def.children) ? def.children : [];
523
- for (let i = 0; i < children.length; i++) {
524
- const childOpts = options ? { ...options, _path: [...path, "children", i] } : undefined;
525
- el.append(renderNode(children[i], localState, childOpts));
543
+ const kids = def.children;
544
+ if (isMappedArray(kids)) {
545
+ // Legacy whole-children repeater: the items render directly into `el` (which keeps its own
546
+ // TagName, e.g. <ul>), with no extra wrapper element.
547
+ const arrOpts = options ? { ...options, _path: [...path, "children"] } : undefined;
548
+ renderMappedArrayInto(el, kids, localState, arrOpts);
549
+ } else if (Array.isArray(kids)) {
550
+ for (let i = 0; i < kids.length; i++) {
551
+ const child = kids[i]!;
552
+ const childOpts = options ? { ...options, _path: [...path, "children", i] } : undefined;
553
+ if (isMappedArray(child)) {
554
+ // Array pseudo-element among siblings: expand inline, no wrapper.
555
+ renderMappedArrayInto(el, child, localState, childOpts);
556
+ } else {
557
+ el.append(renderNode(child, localState, childOpts));
558
+ }
559
+ }
526
560
  }
527
561
 
528
562
  return el;
@@ -562,14 +596,15 @@ function applyProperties(el: HTMLElement, def: JxElement, state: JxScope) {
562
596
  const handler = resolveRef(val.$ref, state);
563
597
  if (typeof handler === "function") {
564
598
  const scope = state;
565
- el.addEventListener(key.slice(2), (e) => handler(scope, e));
599
+ const handlerFn = handler as (s: JxScope, e: Event) => unknown;
600
+ el.addEventListener(key.slice(2), (e) => handlerFn(scope, e));
566
601
  }
567
602
  continue;
568
603
  }
569
604
  // Event handler: inline $prototype: "Function"
570
605
  if (isFunctionDef(val) && val.body) {
571
606
  const params = resolveParamNames(val);
572
- const fn = new Function(...params, val.body);
607
+ const fn = new Function(...params, val.body) as (s: JxScope, e: Event) => unknown;
573
608
  const scope = state;
574
609
  el.addEventListener(key.slice(2), (e) => fn(scope, e));
575
610
  continue;
@@ -781,7 +816,40 @@ export function applyStyle(
781
816
 
782
817
  const tag = document.createElement("style");
783
818
  tag.textContent = css;
819
+ tag.dataset.jxOwner = uid;
784
820
  document.head.append(tag);
821
+ elementStyleTags.set(el, tag);
822
+ }
823
+
824
+ /**
825
+ * Scoped <style> tags emitted per element by applyStyle (latest wins). Lets callers replace an
826
+ * element's emitted styles instead of accumulating orphaned tags.
827
+ */
828
+ export const elementStyleTags = new WeakMap<HTMLElement, HTMLStyleElement>();
829
+
830
+ /**
831
+ * Re-apply an element's style definition in place: removes the previously emitted scoped <style>
832
+ * tag (if any), clears inline styles, and applies the new definition.
833
+ *
834
+ * @param {HTMLElement} el
835
+ * @param {JxStyle | undefined} styleDef
836
+ * @param {Record<string, string>} [mediaQueries]
837
+ * @param {JxScope} [state]
838
+ */
839
+ export function reapplyStyle(
840
+ el: HTMLElement,
841
+ styleDef: JxStyle | undefined,
842
+ mediaQueries: Record<string, string> = {},
843
+ state: JxScope = {},
844
+ ) {
845
+ const prev = elementStyleTags.get(el);
846
+ if (prev) {
847
+ prev.remove();
848
+ elementStyleTags.delete(el);
849
+ }
850
+ el.style.cssText = "";
851
+ delete el.dataset.jx;
852
+ applyStyle(el, styleDef ?? {}, mediaQueries, state);
785
853
  }
786
854
 
787
855
  /**
@@ -804,65 +872,72 @@ function applyAttributes(el: HTMLElement, attrs: Record<string, JxAttributeValue
804
872
  // ─── Array mapping ────────────────────────────────────────────────────────────
805
873
 
806
874
  /**
807
- * @param {JxElement} def
875
+ * Render a mapped array (repeater) wrapper-less: its item instances are inserted directly into
876
+ * `parentEl`, in place, ahead of an anchor comment that marks the array's position among the
877
+ * parent's other children. Re-renders reactively when `items` (or the filter/sort sources) change;
878
+ * each generation's item renders live in their own detached effect scope so nested arrays and
879
+ * template bindings are disposed — not leaked or double-fired — on the next change.
880
+ *
881
+ * `options._path` is the array node's own document path (`[…, "children", i]`, or `[…, "children"]`
882
+ * for a legacy whole-children repeater); item instances render at `[…that…, "map", index]`.
883
+ *
884
+ * @param {HTMLElement} parentEl
808
885
  * @param {import("@jxsuite/schema/types").JxMappedArray} arrayDef
809
886
  * @param {JxScope} state
810
887
  * @param {JxRenderOptions} [options]
811
- * @returns {HTMLElement}
812
888
  */
813
- function renderMappedArray(
814
- def: JxElement,
889
+ function renderMappedArrayInto(
890
+ parentEl: HTMLElement,
815
891
  arrayDef: JxMappedArray,
816
892
  state: JxScope,
817
893
  options?: JxRenderOptions,
818
894
  ) {
819
895
  const path = options?._path ?? [];
820
- const container = document.createElement(def.tagName ?? "div");
821
-
822
- if (options?.onNodeCreated) {
823
- options.onNodeCreated(container, path, def);
824
- }
825
-
826
- applyProperties(container, def, state);
827
- applyStyle(container, def.style ?? {}, (state["$media"] as Record<string, string>) ?? {}, state);
828
- applyAttributes(container, def.attributes ?? {}, state);
896
+ const anchor = document.createComment("jx-array");
897
+ parentEl.append(anchor);
829
898
  const { items: itemsSrc, map: mapDef, filter: filterRef, sort: sortRef } = arrayDef;
830
899
 
831
900
  effect(() => {
832
- container.innerHTML = "";
833
- let items: unknown;
834
- items = isRefObj(itemsSrc) ? resolveRef(itemsSrc.$ref, state) : itemsSrc;
835
- if (!Array.isArray(items)) {
836
- return;
837
- }
838
- if (isRefObj(filterRef)) {
901
+ let items: unknown = isRefObj(itemsSrc) ? resolveRef(itemsSrc.$ref, state) : itemsSrc;
902
+ if (Array.isArray(items) && isRefObj(filterRef)) {
839
903
  const fn = resolveRef(filterRef.$ref, state);
840
904
  if (typeof fn === "function") {
841
905
  items = items.filter(fn as (v: unknown) => boolean);
842
906
  }
843
907
  }
844
- if (isRefObj(sortRef)) {
908
+ if (Array.isArray(items) && isRefObj(sortRef)) {
845
909
  const fn = resolveRef(sortRef.$ref, state);
846
910
  if (typeof fn === "function") {
847
911
  items = [...(items as unknown[])].toSorted(fn as (a: unknown, b: unknown) => number);
848
912
  }
849
913
  }
914
+ if (!Array.isArray(items) || !mapDef) {
915
+ return;
916
+ }
850
917
 
851
- for (const [index, item] of (items as unknown[]).entries()) {
852
- const child = Object.create(state);
853
- child.$map = { index, item };
854
- child["$map/item"] = item;
855
- child["$map/index"] = index;
856
- const childOpts = options
857
- ? { ...options, _path: [...path, "children", "map", index] }
858
- : undefined;
859
- if (mapDef) {
860
- container.append(renderNode(mapDef, child, childOpts));
918
+ // Render this generation's items inside a detached scope; the cleanup (run before the next
919
+ // Re-render and when the enclosing render scope stops) tears it down and removes its nodes.
920
+ const scope = effectScope(true);
921
+ const nodes: ChildNode[] = [];
922
+ scope.run(() => {
923
+ for (const [index, item] of (items as unknown[]).entries()) {
924
+ const child = Object.create(state) as JxScope;
925
+ child.$map = { index, item };
926
+ child["$map/item"] = item;
927
+ child["$map/index"] = index;
928
+ const childOpts = options ? { ...options, _path: [...path, "map", index] } : undefined;
929
+ const node = renderNode(mapDef, child, childOpts);
930
+ anchor.before(node);
931
+ nodes.push(node);
861
932
  }
862
- }
933
+ });
934
+ onEffectCleanup(() => {
935
+ scope.stop();
936
+ for (const n of nodes) {
937
+ n.remove();
938
+ }
939
+ });
863
940
  });
864
-
865
- return container;
866
941
  }
867
942
 
868
943
  // ─── $switch ──────────────────────────────────────────────────────────────────
@@ -878,7 +953,7 @@ function renderSwitch(def: JxElement, state: JxScope, options?: JxRenderOptions)
878
953
  const container = document.createElement(def.tagName ?? "div");
879
954
 
880
955
  if (options?.onNodeCreated) {
881
- options.onNodeCreated(container, path, def);
956
+ options.onNodeCreated(container, path, def, state);
882
957
  }
883
958
 
884
959
  applyProperties(container, def, state);
@@ -960,7 +1035,7 @@ export async function resolvePrototype(
960
1035
 
961
1036
  if (!def.manual) {
962
1037
  effect(() => {
963
- let url;
1038
+ let url: string | undefined;
964
1039
  if (isTemplateString(def.url)) {
965
1040
  url = evaluateTemplate(def.url, state);
966
1041
  } else {
@@ -1000,7 +1075,7 @@ export async function resolvePrototype(
1000
1075
  if (debounceMs > 0) {
1001
1076
  debounceTimer = setTimeout(doFetch, debounceMs);
1002
1077
  } else {
1003
- doFetch();
1078
+ void doFetch();
1004
1079
  }
1005
1080
  });
1006
1081
  }
@@ -1013,11 +1088,13 @@ export async function resolvePrototype(
1013
1088
  const p: Record<string, string> = {};
1014
1089
  for (const [k, v] of Object.entries(def)) {
1015
1090
  if (k !== "$prototype") {
1016
- p[k] = isRefObj(v)
1017
- ? resolveRef(v.$ref, state)
1018
- : isTemplateString(v)
1019
- ? evaluateTemplate(v, state)
1020
- : v;
1091
+ p[k] = (
1092
+ isRefObj(v)
1093
+ ? resolveRef(v.$ref, state)
1094
+ : isTemplateString(v)
1095
+ ? evaluateTemplate(v, state)
1096
+ : v
1097
+ ) as string;
1021
1098
  }
1022
1099
  }
1023
1100
  return new URLSearchParams(p).toString();
@@ -1028,10 +1105,10 @@ export async function resolvePrototype(
1028
1105
  case "SessionStorage": {
1029
1106
  const store = def.$prototype === "LocalStorage" ? localStorage : sessionStorage;
1030
1107
  const k = def.key ?? key;
1031
- let init;
1108
+ let init: unknown;
1032
1109
  try {
1033
1110
  const s = store.getItem(k);
1034
- init = s !== null ? JSON.parse(s) : (def.default ?? null);
1111
+ init = s !== null ? (JSON.parse(s) as unknown) : (def.default ?? null);
1035
1112
  } catch {
1036
1113
  init = def.default ?? null;
1037
1114
  }
@@ -1060,7 +1137,7 @@ export async function resolvePrototype(
1060
1137
  return null;
1061
1138
  }
1062
1139
  try {
1063
- return JSON.parse(decodeURIComponent(m[1]));
1140
+ return JSON.parse(decodeURIComponent(m[1]!));
1064
1141
  } catch {
1065
1142
  return m[1];
1066
1143
  }
@@ -1221,16 +1298,16 @@ async function resolveExternalPrototype(
1221
1298
  * @returns {Promise<unknown>}
1222
1299
  */
1223
1300
  async function importAndInstantiate(def: JxScope, src: string, exportName: string, base?: string) {
1224
- let mod;
1301
+ let mod: ImportedModule;
1225
1302
  if (_moduleCache.has(src)) {
1226
- mod = _moduleCache.get(src);
1303
+ mod = _moduleCache.get(src)!;
1227
1304
  } else {
1228
1305
  try {
1229
- mod = await import(src);
1306
+ mod = (await import(src)) as ImportedModule;
1230
1307
  } catch {
1231
1308
  if (base) {
1232
1309
  const resolvedSrc = new URL(src, base).href;
1233
- mod = await import(resolvedSrc);
1310
+ mod = (await import(resolvedSrc)) as ImportedModule;
1234
1311
  } else {
1235
1312
  throw new Error(`Failed to import "${src}"`);
1236
1313
  }
@@ -1253,9 +1330,10 @@ async function importAndInstantiate(def: JxScope, src: string, exportName: strin
1253
1330
  }
1254
1331
  }
1255
1332
 
1256
- const instance = new ExportedClass(config);
1333
+ const Ctor = ExportedClass as new (config: JxScope) => ExternalClassInstance;
1334
+ const instance = new Ctor(config);
1257
1335
 
1258
- let value;
1336
+ let value: unknown;
1259
1337
  if (typeof instance.resolve === "function") {
1260
1338
  value = await instance.resolve();
1261
1339
  } else if ("value" in instance) {
@@ -1342,9 +1420,9 @@ async function resolveClassJson(def: JxPrototypeDef, state: JxScope, key: string
1342
1420
  config[k] = v;
1343
1421
  }
1344
1422
  }
1345
- const instance = new DynClass(config);
1423
+ const instance = new DynClass(config) as ExternalClassInstance;
1346
1424
 
1347
- let value;
1425
+ let value: unknown;
1348
1426
  if (typeof instance.resolve === "function") {
1349
1427
  value = await instance.resolve();
1350
1428
  } else if ("value" in instance) {
@@ -1395,7 +1473,12 @@ function classFromSchema(classDef: JxClassDef) {
1395
1473
  }
1396
1474
  if (ctor?.body) {
1397
1475
  const bodyStr = Array.isArray(ctor.body) ? ctor.body.join("\n") : ctor.body;
1398
- new Function("config", bodyStr).call(this, config);
1476
+ (
1477
+ new Function("config", bodyStr) as (
1478
+ this: unknown,
1479
+ config: Record<string, unknown>,
1480
+ ) => void
1481
+ ).call(this, config);
1399
1482
  }
1400
1483
  }
1401
1484
  }
@@ -1528,17 +1611,17 @@ async function resolveServerFunction(
1528
1611
  const src = def.$src;
1529
1612
  const exportName = def.$export;
1530
1613
 
1531
- let mod;
1614
+ let mod: ImportedModule;
1532
1615
  if (_moduleCache.has(src)) {
1533
- mod = _moduleCache.get(src);
1616
+ mod = _moduleCache.get(src)!;
1534
1617
  } else {
1535
1618
  try {
1536
- mod = await import(src);
1619
+ mod = (await import(src)) as ImportedModule;
1537
1620
  } catch {
1538
1621
  if (base) {
1539
1622
  try {
1540
1623
  const resolvedSrc = new URL(src, base).href;
1541
- mod = await import(resolvedSrc);
1624
+ mod = (await import(resolvedSrc)) as ImportedModule;
1542
1625
  } catch {
1543
1626
  // Module cannot run in the browser — fall back to dev server proxy
1544
1627
  return resolveServerFunctionViaProxy(def, state, key, base);
@@ -1550,13 +1633,14 @@ async function resolveServerFunction(
1550
1633
  _moduleCache.set(src, mod);
1551
1634
  }
1552
1635
 
1553
- const fn = mod[exportName] ?? mod.default?.[exportName];
1554
- if (!fn) {
1636
+ const candidate = mod[exportName] ?? mod.default?.[exportName];
1637
+ if (!candidate) {
1555
1638
  throw new Error(`Jx: export "${exportName}" not found in "${src}" for "${key}"`);
1556
1639
  }
1557
- if (typeof fn !== "function") {
1640
+ if (typeof candidate !== "function") {
1558
1641
  throw new TypeError(`Jx: "${exportName}" from "${src}" is not a function`);
1559
1642
  }
1643
+ const fn = candidate as (args: JxScope) => Promise<unknown>;
1560
1644
 
1561
1645
  const rawArgs = def.arguments ?? {};
1562
1646
  const hasReactiveArg = Object.values(rawArgs).some((v: unknown) => isRefObj(v));
@@ -1672,7 +1756,7 @@ export function resolveRef(refPath: string, state: JxScope) {
1672
1756
  const parts = refPath.split("/");
1673
1757
  const [, key] = parts; // "item" or "index"
1674
1758
  const map = state.$map as Record<string, unknown> | undefined;
1675
- const base = map?.[key] ?? state[`$map/${key}`];
1759
+ const base = map?.[key!] ?? state[`$map/${key}`];
1676
1760
  return parts.length > 2 ? getPath(base, parts.slice(2).join("/")) : base;
1677
1761
  }
1678
1762
  if (refPath.startsWith("#/state/")) {
@@ -1742,7 +1826,7 @@ function getPath(obj: unknown, path: string) {
1742
1826
  * @returns {JxScope}
1743
1827
  */
1744
1828
  function mergeProps(def: JxElement, parentState: JxScope): JxScope {
1745
- const child = Object.create(parentState);
1829
+ const child = Object.create(parentState) as JxScope;
1746
1830
  for (const [k, v] of Object.entries(def.$props ?? {})) {
1747
1831
  child[k] = isRefObj(v) ? resolveRef(v.$ref, parentState) : v;
1748
1832
  }
@@ -1927,7 +2011,7 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
1927
2011
  const propsAttr = this.dataset.jxProps;
1928
2012
  if (propsAttr) {
1929
2013
  try {
1930
- const props = JSON.parse(propsAttr);
2014
+ const props = JSON.parse(propsAttr) as Record<string, unknown>;
1931
2015
  for (const [key, val] of Object.entries(props)) {
1932
2016
  if (key in (def.state ?? {})) {
1933
2017
  state[key] = val;
@@ -1984,19 +2068,19 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
1984
2068
  // Lifecycle: onMount
1985
2069
  const { onMount } = state;
1986
2070
  if (typeof onMount === "function") {
1987
- queueMicrotask(() => onMount(state));
2071
+ queueMicrotask(() => (onMount as (s: JxScope) => unknown)(state));
1988
2072
  }
1989
2073
  }
1990
2074
 
1991
2075
  disconnectedCallback() {
1992
2076
  if (typeof this._state?.onUnmount === "function") {
1993
- this._state.onUnmount(this._state);
2077
+ (this._state.onUnmount as (s: JxScope) => unknown)(this._state);
1994
2078
  }
1995
2079
  }
1996
2080
 
1997
2081
  adoptedCallback() {
1998
2082
  if (typeof this._state?.onAdopted === "function") {
1999
- this._state.onAdopted(this._state);
2083
+ (this._state.onAdopted as (s: JxScope) => unknown)(this._state);
2000
2084
  }
2001
2085
  }
2002
2086
 
@@ -2037,7 +2121,7 @@ function renderCustomElementWithProps(
2037
2121
  const el = document.createElement(def.tagName ?? "div");
2038
2122
 
2039
2123
  if (options?.onNodeCreated) {
2040
- options.onNodeCreated(el, path ?? [], def);
2124
+ options.onNodeCreated(el, path ?? [], def, state);
2041
2125
  }
2042
2126
 
2043
2127
  // Set JS properties from $props (before connection)
@@ -2067,7 +2151,7 @@ function renderCustomElementWithProps(
2067
2151
  const children = Array.isArray(def.children) ? def.children : [];
2068
2152
  for (let i = 0; i < children.length; i++) {
2069
2153
  const childOpts = options && path ? { ...options, _path: [...path, "children", i] } : undefined;
2070
- el.append(renderNode(children[i], state, childOpts));
2154
+ el.append(renderNode(children[i]!, state, childOpts));
2071
2155
  }
2072
2156
 
2073
2157
  return el;
package/src/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="dom" />
2
2
  import type { JxElement, JxPath } from "@jxsuite/schema/types";
3
3
 
4
- export type { JxPath };
4
+ export type { JxPath } from "@jxsuite/schema/types";
5
5
 
6
6
  /**
7
7
  * The live reactive scope: a prototype-chained object whose keys are user-defined state names.
@@ -15,7 +15,16 @@ export type JxEventHandler = (scope: JxScope, event: Event) => unknown;
15
15
 
16
16
  export interface JxRenderOptions {
17
17
  _path?: JxPath;
18
- onNodeCreated?: (el: HTMLElement | Text, path: JxPath, def: JxElement | string) => void;
18
+ /**
19
+ * Called for each created node. `state` is the local scope the node's children render with —
20
+ * callers can capture it to re-render a subtree in isolation later.
21
+ */
22
+ onNodeCreated?: (
23
+ el: HTMLElement | Text,
24
+ path: JxPath,
25
+ def: JxElement | string,
26
+ state?: JxScope,
27
+ ) => void;
19
28
  }
20
29
 
21
30
  export interface DynamicClass {