@jxsuite/runtime 0.23.2 → 0.24.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 +56 -31
- package/dist/runtime.js.map +5 -5
- package/package.json +8 -6
- package/src/{expression.js → expression.ts} +110 -112
- package/src/{runtime.js → runtime.ts} +297 -275
- package/src/types.ts +14 -0
|
@@ -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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
165
|
-
if (
|
|
166
|
-
if (
|
|
167
|
-
if ("
|
|
168
|
-
|
|
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(
|
|
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 =
|
|
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] = (
|
|
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 (
|
|
209
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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] =
|
|
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(
|
|
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(
|
|
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(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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((
|
|
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(
|
|
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
|
-
|
|
836
|
-
const s = ref(null);
|
|
874
|
+
const s: import("@vue/reactivity").Ref<unknown> = ref(null);
|
|
837
875
|
const debounceMs = def.debounce ?? 0;
|
|
838
|
-
|
|
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((
|
|
871
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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(
|
|
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
|
-
|
|
1286
|
+
(this as Record<string, unknown>)[propName] = typedField.initializer;
|
|
1234
1287
|
else if (typedField.default !== undefined)
|
|
1235
|
-
|
|
1236
|
-
|
|
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 =
|
|
1272
|
-
(
|
|
1273
|
-
|
|
1274
|
-
|
|
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
|
-
|
|
1283
|
-
const descriptor = {};
|
|
1310
|
+
const descriptor: PropertyDescriptor = {};
|
|
1284
1311
|
if (typedMethod.getter)
|
|
1285
|
-
descriptor.get =
|
|
1312
|
+
descriptor.get = new Function(typedMethod.getter.body) as () => unknown;
|
|
1286
1313
|
if (typedMethod.setter) {
|
|
1287
1314
|
const sp = (typedMethod.setter.parameters ?? []).map(
|
|
1288
|
-
(
|
|
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
|
-
|
|
1321
|
+
(DynClass as unknown as DynamicClass)[name] = new Function(...params, bodyStr);
|
|
1298
1322
|
} else {
|
|
1299
|
-
|
|
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 =
|
|
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(
|
|
1320
|
-
|
|
1321
|
-
|
|
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((
|
|
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
|
-
|
|
1349
|
-
const s = ref(null);
|
|
1374
|
+
const s: import("@vue/reactivity").Ref<any> = ref(null);
|
|
1350
1375
|
if (hasTemplates) {
|
|
1351
1376
|
effect(() => {
|
|
1352
|
-
|
|
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((
|
|
1382
|
+
.then((value: unknown) => {
|
|
1359
1383
|
s.value = value;
|
|
1360
1384
|
})
|
|
1361
|
-
.catch((
|
|
1385
|
+
.catch((e: unknown) => console.error("Jx dev proxy:", e));
|
|
1362
1386
|
});
|
|
1363
1387
|
} else {
|
|
1364
1388
|
doResolve(config)
|
|
1365
|
-
.then((
|
|
1389
|
+
.then((value: unknown) => {
|
|
1366
1390
|
s.value = value;
|
|
1367
1391
|
})
|
|
1368
|
-
.catch((
|
|
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(
|
|
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((
|
|
1446
|
+
const hasReactiveArg = Object.values(rawArgs).some((v: unknown) => isRefObj(v));
|
|
1418
1447
|
const resolveArgs = () => {
|
|
1419
|
-
|
|
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(
|
|
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
|
-
|
|
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((
|
|
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(
|
|
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((
|
|
1491
|
+
const hasReactiveArg = Object.values(rawArgs).some((v: unknown) => isRefObj(v));
|
|
1460
1492
|
|
|
1461
1493
|
const resolveArgs = () => {
|
|
1462
|
-
|
|
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(
|
|
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
|
-
|
|
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((
|
|
1524
|
+
.then((result: unknown) => {
|
|
1495
1525
|
s.value = result;
|
|
1496
1526
|
})
|
|
1497
|
-
.catch((
|
|
1527
|
+
.catch((e: unknown) => console.error("Jx server proxy:", e));
|
|
1498
1528
|
});
|
|
1499
1529
|
} else {
|
|
1500
1530
|
doResolve(resolveArgs())
|
|
1501
|
-
.then((
|
|
1531
|
+
.then((result: unknown) => {
|
|
1502
1532
|
s.value = result;
|
|
1503
1533
|
})
|
|
1504
|
-
.catch((
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
1755
|
-
state[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: (
|
|
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
|
-
|
|
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
|
-
|
|
1802
|
-
|
|
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
|
-
|
|
1810
|
-
|
|
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
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
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
|
-
|
|
1832
|
-
else
|
|
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(
|
|
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
|
|
1857
|
-
|
|
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
|
-
|
|
1887
|
+
(el as Record<string, unknown>)[key] = resolveRef(refVal.$ref, state);
|
|
1861
1888
|
});
|
|
1862
1889
|
} else if (isTemplateString(val)) {
|
|
1863
1890
|
effect(() => {
|
|
1864
|
-
|
|
1891
|
+
(el as Record<string, unknown>)[key] = evaluateTemplate(val as string, state);
|
|
1865
1892
|
});
|
|
1866
1893
|
} else {
|
|
1867
|
-
|
|
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
|
-
|
|
1898
|
-
const
|
|
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
|
|
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
|
-
|
|
1931
|
+
(named.get(name) as ChildNode[]).push(child);
|
|
1910
1932
|
} else {
|
|
1911
1933
|
unnamed.push(child);
|
|
1912
1934
|
}
|