@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/dist/runtime.js +73 -36
- package/dist/runtime.js.map +6 -6
- package/package.json +2 -2
- package/src/runtime.js +216 -117
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 {
|
|
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 {
|
|
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<
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
}
|
|
291
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
495
|
+
* @param {HTMLElement} el
|
|
484
496
|
* @param {string} key
|
|
485
|
-
* @param {
|
|
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
|
-
|
|
504
|
+
target[key] = /** @type {string} */ (
|
|
505
|
+
resolveRef(/** @type {{ $ref: string }} */ (val).$ref, state)
|
|
506
|
+
);
|
|
492
507
|
return;
|
|
493
508
|
}
|
|
494
509
|
effect(() => {
|
|
495
|
-
|
|
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
|
-
|
|
518
|
+
target[key] = evaluateTemplate(/** @type {string} */ (val), state);
|
|
504
519
|
});
|
|
505
520
|
return;
|
|
506
521
|
}
|
|
507
522
|
|
|
508
|
-
|
|
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 {
|
|
554
|
+
/** @type {Record<string, string>} */ (/** @type {unknown} */ (el.style))[prop] =
|
|
555
|
+
evaluateTemplate(val, state);
|
|
539
556
|
});
|
|
540
|
-
else /** @type {
|
|
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
|
-
:
|
|
558
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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 {
|
|
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")
|
|
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")
|
|
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 {
|
|
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 {
|
|
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<
|
|
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<
|
|
776
|
+
/** @type {import("@vue/reactivity").Ref<unknown>} */
|
|
727
777
|
const s = ref(null);
|
|
728
778
|
const debounceMs = def.debounce ?? 0;
|
|
729
|
-
/** @type {
|
|
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 {
|
|
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 {
|
|
862
|
-
const db = /** @type {
|
|
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 {
|
|
870
|
-
const db = /** @type {
|
|
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 {
|
|
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<
|
|
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<
|
|
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 {
|
|
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<
|
|
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 {
|
|
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 {
|
|
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
|
-
/**
|
|
1098
|
-
|
|
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)
|
|
1166
|
+
if (config[id] !== undefined)
|
|
1167
|
+
/** @type {Record<string, unknown>} */ (this)[propName] = config[id];
|
|
1102
1168
|
else if (typedField.initializer !== undefined)
|
|
1103
|
-
/** @type {
|
|
1169
|
+
/** @type {Record<string, unknown>} */ (this)[propName] = typedField.initializer;
|
|
1104
1170
|
else if (typedField.default !== undefined)
|
|
1105
|
-
/** @type {
|
|
1106
|
-
|
|
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
|
-
/**
|
|
1117
|
-
|
|
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 =
|
|
1120
|
-
|
|
1121
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
1233
|
+
/** @type {DynamicClass} */ (DynClass)[name] = new Function(...params, bodyStr);
|
|
1141
1234
|
} else {
|
|
1142
|
-
/** @type {
|
|
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
|
-
|
|
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<
|
|
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 {
|
|
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 {
|
|
1294
|
+
.then((/** @type {unknown} */ value) => {
|
|
1199
1295
|
s.value = value;
|
|
1200
1296
|
})
|
|
1201
|
-
.catch((/** @type {
|
|
1297
|
+
.catch((/** @type {unknown} */ e) => console.error("Jx dev proxy:", e));
|
|
1202
1298
|
});
|
|
1203
1299
|
} else {
|
|
1204
1300
|
doResolve(config)
|
|
1205
|
-
.then((/** @type {
|
|
1301
|
+
.then((/** @type {unknown} */ value) => {
|
|
1206
1302
|
s.value = value;
|
|
1207
1303
|
})
|
|
1208
|
-
.catch((/** @type {
|
|
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<
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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<
|
|
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 {
|
|
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 {
|
|
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 {
|
|
1430
|
+
.then((/** @type {unknown} */ result) => {
|
|
1335
1431
|
s.value = result;
|
|
1336
1432
|
})
|
|
1337
|
-
.catch((/** @type {
|
|
1433
|
+
.catch((/** @type {unknown} */ e) => console.error("Jx server proxy:", e));
|
|
1338
1434
|
});
|
|
1339
1435
|
} else {
|
|
1340
1436
|
doResolve(resolveArgs())
|
|
1341
|
-
.then((/** @type {
|
|
1437
|
+
.then((/** @type {unknown} */ result) => {
|
|
1342
1438
|
s.value = result;
|
|
1343
1439
|
})
|
|
1344
|
-
.catch((/** @type {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
1489
|
+
* @param {unknown} v
|
|
1394
1490
|
* @returns {boolean}
|
|
1395
1491
|
*/
|
|
1396
1492
|
function isRefObj(v) {
|
|
1397
|
-
return
|
|
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 {
|
|
1509
|
+
* @param {unknown} obj
|
|
1410
1510
|
* @param {string} path
|
|
1411
|
-
* @returns {
|
|
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,
|
|
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 {
|
|
1590
|
-
state[key] = /** @type {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
1680
|
-
* @param {
|
|
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
|
|