@jxsuite/runtime 6.2.1 → 6.3.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 +322 -29
- package/dist/runtime.js.map +7 -5
- package/package.json +3 -2
- package/src/body-eraser.ts +201 -0
- package/src/runtime.ts +72 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/runtime",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"description": "JSON-native reactive web component runtime",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,11 +33,12 @@
|
|
|
33
33
|
"upgrade": "bun update --latest && bun install"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@jxsuite/schema": "^5.
|
|
36
|
+
"@jxsuite/schema": "^5.6.0",
|
|
37
37
|
"@vue/reactivity": "3.5.44"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@happy-dom/global-registrator": "^21.0.4",
|
|
41
|
+
"@jxsuite/erase": "^0.2.0",
|
|
41
42
|
"@jxsuite/test-support": "^0.0.0"
|
|
42
43
|
}
|
|
43
44
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime's seam for TypeScript function bodies (`spec.md` §23.1).
|
|
3
|
+
*
|
|
4
|
+
* A string `body` may use erasable TypeScript, and only JavaScript can run, so every body the
|
|
5
|
+
* runtime compiles goes through {@link compileBody}. The runtime carries no stripper: a host hands
|
|
6
|
+
* it one, eagerly with {@link setBodyEraser} (`eraseBody` from `@jxsuite/erase/browser`), or lazily
|
|
7
|
+
* with {@link setBodyEraserLoader}, which is fetched only when a document has a body that needs it.
|
|
8
|
+
* With neither, a body compiles as written, and one that does not compile says what would fix it.
|
|
9
|
+
*
|
|
10
|
+
* @docs framework/concepts/functions
|
|
11
|
+
*/
|
|
12
|
+
import { isFunctionEntry, isJsonObject, isString } from "@jxsuite/schema/guards";
|
|
13
|
+
import type { JsonValue, JxClassDef, JxDocument } from "@jxsuite/schema/types";
|
|
14
|
+
import type { JxScopeValue } from "./types.ts";
|
|
15
|
+
|
|
16
|
+
/** Something erasing found. Only `unsupported` stops a body from running. */
|
|
17
|
+
export interface BodyEraserProblem {
|
|
18
|
+
kind: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** What `eraseBody` from `@jxsuite/erase/browser` is: TypeScript in, JavaScript out. */
|
|
23
|
+
export type BodyEraser = (text: string) => {
|
|
24
|
+
code: string;
|
|
25
|
+
problems: readonly BodyEraserProblem[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** What importing `@jxsuite/erase/browser` gives. */
|
|
29
|
+
export interface BodyEraserModule {
|
|
30
|
+
eraseBody: BodyEraser;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A compiled body: called with what its caller passes, returning what its body returns. */
|
|
34
|
+
export type CompiledBody = (...args: never[]) => JxScopeValue;
|
|
35
|
+
|
|
36
|
+
/** Fetches the eraser, once, when a document first needs it. */
|
|
37
|
+
export type BodyEraserLoader = () => Promise<BodyEraserModule>;
|
|
38
|
+
|
|
39
|
+
let eraser: BodyEraser | null = null;
|
|
40
|
+
let loader: BodyEraserLoader | null = null;
|
|
41
|
+
let loading: Promise<void> | null = null;
|
|
42
|
+
|
|
43
|
+
/** Erased bodies by authored text, so a body erases once however often it compiles. */
|
|
44
|
+
const erased = new Map<string, string>();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Install (or with `null`, remove) the function that erases TypeScript from a body.
|
|
48
|
+
*
|
|
49
|
+
* @param fn `eraseBody` from `@jxsuite/erase/browser`.
|
|
50
|
+
*/
|
|
51
|
+
export function setBodyEraser(fn: BodyEraser | null): void {
|
|
52
|
+
eraser = fn;
|
|
53
|
+
erased.clear();
|
|
54
|
+
if (!fn) {
|
|
55
|
+
// Taken away, so a later document that needs one loads it again.
|
|
56
|
+
loading = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Say how to fetch the eraser when a document needs it, so a page with no typed body downloads
|
|
62
|
+
* nothing. The runtime installs what the promise gives.
|
|
63
|
+
*
|
|
64
|
+
* @param load Typically `() => import("/path/to/erase.js")`, or null to take it away.
|
|
65
|
+
*/
|
|
66
|
+
export function setBodyEraserLoader(load: BodyEraserLoader | null): void {
|
|
67
|
+
loader = load;
|
|
68
|
+
loading = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Whether text parses as a function body, sync or async. Parsed, never called. */
|
|
72
|
+
function compiles(text: string): boolean {
|
|
73
|
+
for (const source of [text, `return async function () {\n${text}\n}`]) {
|
|
74
|
+
try {
|
|
75
|
+
Reflect.construct(Function, [source]);
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
// Not as this kind of function; the other may still take it.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `<…>` followed by a call or a tagged template: a generic call, which JavaScript reads otherwise. */
|
|
85
|
+
const GENERIC_CALL = /<[^<>]*>\s*[(`]/u;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Whether a body needs the eraser before it can run as meant: it does not compile as JavaScript, or
|
|
89
|
+
* it holds what TypeScript reads as a generic call.
|
|
90
|
+
*
|
|
91
|
+
* @param text The body as authored.
|
|
92
|
+
*/
|
|
93
|
+
export function bodyNeedsEraser(text: string): boolean {
|
|
94
|
+
return GENERIC_CALL.test(text) || !compiles(text);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Every function entry's string body in a tree, accessor halves and `string[]` lines included.
|
|
99
|
+
*
|
|
100
|
+
* @param value A document, a class definition, or a part of one.
|
|
101
|
+
* @yields {string} Each body's text.
|
|
102
|
+
*/
|
|
103
|
+
function* bodiesOf(value: JsonValue): Generator<string> {
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
for (const item of value) {
|
|
106
|
+
yield* bodiesOf(item);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!isJsonObject(value)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const isFunction = isFunctionEntry(value);
|
|
114
|
+
for (const [key, child] of Object.entries(value)) {
|
|
115
|
+
if (isFunction && key === "body") {
|
|
116
|
+
if (isString(child)) {
|
|
117
|
+
yield child;
|
|
118
|
+
} else if (Array.isArray(child) && child.every((line) => isString(line))) {
|
|
119
|
+
yield child.join("\n");
|
|
120
|
+
} else {
|
|
121
|
+
yield* bodiesOf(child);
|
|
122
|
+
}
|
|
123
|
+
} else if (isFunction && (key === "getter" || key === "setter") && isJsonObject(child)) {
|
|
124
|
+
if (isString(child.body)) {
|
|
125
|
+
yield child.body;
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
yield* bodiesOf(child);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Load the eraser first when anything in a document needs it. Called where the runtime already
|
|
135
|
+
* waits (a fetched document, a fetched class), so nothing that compiles later has to.
|
|
136
|
+
*
|
|
137
|
+
* @param doc A document or class definition.
|
|
138
|
+
*/
|
|
139
|
+
export async function prepareBodies(doc: JsonValue | JxDocument | JxClassDef): Promise<void> {
|
|
140
|
+
const load = loader;
|
|
141
|
+
if (eraser || !load) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// SAFETY: a Jx document and a class definition are JSON data; their types name what they hold.
|
|
145
|
+
const tree = doc as JsonValue;
|
|
146
|
+
if (!bodiesOf(tree).some((body) => bodyNeedsEraser(body))) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
loading ??= load().then((module) => {
|
|
150
|
+
if (loader === load) {
|
|
151
|
+
setBodyEraser(module.eraseBody);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
await loading;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** A body's JavaScript, through the eraser when one is installed. */
|
|
158
|
+
function javaScriptOf(body: string, where: string): string {
|
|
159
|
+
if (!eraser) {
|
|
160
|
+
return body;
|
|
161
|
+
}
|
|
162
|
+
let code = erased.get(body);
|
|
163
|
+
if (code === undefined) {
|
|
164
|
+
const result = eraser(body);
|
|
165
|
+
const refused = result.problems.find((problem) => problem.kind === "unsupported");
|
|
166
|
+
if (refused) {
|
|
167
|
+
throw new SyntaxError(`Jx: ${where} cannot run: ${refused.message}`);
|
|
168
|
+
}
|
|
169
|
+
({ code } = result);
|
|
170
|
+
erased.set(body, code);
|
|
171
|
+
}
|
|
172
|
+
return code;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Compile a function body, erasing its TypeScript first when an eraser is installed.
|
|
177
|
+
*
|
|
178
|
+
* @param params The parameter names.
|
|
179
|
+
* @param body The body as authored.
|
|
180
|
+
* @param where Names the body in an error, as `'save'` or `the constructor of Counter`.
|
|
181
|
+
* @throws {SyntaxError} When the body does not compile, naming it, and the call that installs an
|
|
182
|
+
* eraser when none is.
|
|
183
|
+
*/
|
|
184
|
+
export function compileBody(params: readonly string[], body: string, where: string): CompiledBody {
|
|
185
|
+
const code = javaScriptOf(body, where);
|
|
186
|
+
try {
|
|
187
|
+
/* SAFETY: `new Function` returns an ordinary function of the named parameters; each caller
|
|
188
|
+
says what it is called with and what it returns. */
|
|
189
|
+
return new Function(...params, code) as CompiledBody;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (!(error instanceof SyntaxError) || eraser) {
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
throw new SyntaxError(
|
|
195
|
+
`Jx: ${where} does not compile as JavaScript (${error.message}). A function body may be ` +
|
|
196
|
+
`TypeScript: give this page an eraser with setBodyEraser(eraseBody), eraseBody from ` +
|
|
197
|
+
`@jxsuite/erase/browser.`,
|
|
198
|
+
{ cause: error },
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -52,7 +52,6 @@ import type {
|
|
|
52
52
|
JxScopeValue,
|
|
53
53
|
} from "./types.ts";
|
|
54
54
|
import {
|
|
55
|
-
bodyReturnsValue,
|
|
56
55
|
hasStructuredBody,
|
|
57
56
|
isBoolean,
|
|
58
57
|
isExpressionDef,
|
|
@@ -71,11 +70,18 @@ import {
|
|
|
71
70
|
isString,
|
|
72
71
|
isTagExpression,
|
|
73
72
|
isTemplateString,
|
|
74
|
-
paramNames,
|
|
75
73
|
} from "@jxsuite/schema/guards";
|
|
76
74
|
import type { TagNameSlot } from "@jxsuite/schema/guards";
|
|
75
|
+
import {
|
|
76
|
+
classifyFunctionEntries,
|
|
77
|
+
declaresBodyAndSrc,
|
|
78
|
+
handlerParamNames,
|
|
79
|
+
} from "@jxsuite/schema/function-role";
|
|
80
|
+
import type { FunctionRole } from "@jxsuite/schema/function-role";
|
|
77
81
|
import { formatSrcset, parseSrcset } from "@jxsuite/schema/asset-paths";
|
|
78
82
|
import { runStatements } from "./statements.ts";
|
|
83
|
+
import { compileBody, prepareBodies } from "./body-eraser.ts";
|
|
84
|
+
|
|
79
85
|
import { readCookie, serializeCookie } from "./cookie.ts";
|
|
80
86
|
import type {
|
|
81
87
|
CemParameter,
|
|
@@ -96,6 +102,14 @@ import type {
|
|
|
96
102
|
} from "@jxsuite/schema/types";
|
|
97
103
|
import type { EffectScope, Ref } from "@vue/reactivity";
|
|
98
104
|
|
|
105
|
+
export {
|
|
106
|
+
bodyNeedsEraser,
|
|
107
|
+
prepareBodies,
|
|
108
|
+
setBodyEraser,
|
|
109
|
+
setBodyEraserLoader,
|
|
110
|
+
} from "./body-eraser.ts";
|
|
111
|
+
export type { BodyEraser, BodyEraserLoader, BodyEraserModule } from "./body-eraser.ts";
|
|
112
|
+
|
|
99
113
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
100
114
|
|
|
101
115
|
/**
|
|
@@ -357,6 +371,7 @@ function responseContentType(res: Partial<Pick<Response, "headers">>): string {
|
|
|
357
371
|
|
|
358
372
|
export async function resolve(source: string | JxDocument, ctx?: JxContext): Promise<JxDocument> {
|
|
359
373
|
if (!isString(source)) {
|
|
374
|
+
await prepareBodies(source);
|
|
360
375
|
return source;
|
|
361
376
|
}
|
|
362
377
|
// A mount's resolver answers for THIS mount and its hit never enters the shared cache.
|
|
@@ -367,7 +382,7 @@ export async function resolve(source: string | JxDocument, ctx?: JxContext): Pro
|
|
|
367
382
|
if (_resolveCache.has(source)) {
|
|
368
383
|
return _resolveCache.get(source)!;
|
|
369
384
|
}
|
|
370
|
-
const
|
|
385
|
+
const fetched = fetch(source).then(async (res) => {
|
|
371
386
|
if (!res.ok) {
|
|
372
387
|
throw new Error(`Jx: failed to fetch ${source} (${res.status})`);
|
|
373
388
|
}
|
|
@@ -406,6 +421,11 @@ export async function resolve(source: string | JxDocument, ctx?: JxContext): Pro
|
|
|
406
421
|
the JSON is that document; the runtime does not schema-validate documents by design. */
|
|
407
422
|
return (await res.json()) as JxDocument;
|
|
408
423
|
});
|
|
424
|
+
// A body that needs the eraser has it before anything compiles, which happens synchronously.
|
|
425
|
+
const p = fetched.then(async (doc) => {
|
|
426
|
+
await prepareBodies(doc);
|
|
427
|
+
return doc;
|
|
428
|
+
});
|
|
409
429
|
_resolveCache.set(source, p);
|
|
410
430
|
return p;
|
|
411
431
|
}
|
|
@@ -1007,8 +1027,15 @@ export async function buildScope(
|
|
|
1007
1027
|
}
|
|
1008
1028
|
}
|
|
1009
1029
|
|
|
1010
|
-
// Third pass: $prototype: "Function" entries
|
|
1030
|
+
// Third pass: $prototype: "Function" entries. One verdict per entry, from the document: the same
|
|
1031
|
+
// One every compiled target reads (spec.md §5.3 4b, 4d).
|
|
1032
|
+
const roles = classifyFunctionEntries(doc);
|
|
1011
1033
|
for (const [key, def] of Object.entries(defs)) {
|
|
1034
|
+
/* Refused ahead of the structured-body branch, so a structured body beside a `$src` is caught
|
|
1035
|
+
too, and by presence, so `body: ""` is (spec.md §5.3 4d). */
|
|
1036
|
+
if (isFunctionDef(def) && declaresBodyAndSrc(def)) {
|
|
1037
|
+
throw new Error(`Jx: '${key}' declares both body and $src — these are mutually exclusive`);
|
|
1038
|
+
}
|
|
1012
1039
|
if (hasStructuredBody(def)) {
|
|
1013
1040
|
// Structured body (spec §20): with parameters → a callable mapping positional args onto
|
|
1014
1041
|
// $args names (invoked via the call operator); without → an event handler.
|
|
@@ -1029,7 +1056,7 @@ export async function buildScope(
|
|
|
1029
1056
|
state[key] = handler;
|
|
1030
1057
|
}
|
|
1031
1058
|
} else if (isFunctionDef(def)) {
|
|
1032
|
-
state[key] = await resolveFunction(def, state, key, base);
|
|
1059
|
+
state[key] = await resolveFunction(def, state, key, roles.get(key) ?? "callable", base);
|
|
1033
1060
|
}
|
|
1034
1061
|
}
|
|
1035
1062
|
|
|
@@ -1589,21 +1616,25 @@ type ScopeFunction = (...args: JxScopeValue[]) => JxScopeValue;
|
|
|
1589
1616
|
/**
|
|
1590
1617
|
* Resolve a $prototype: "Function" entry into a function or computed.
|
|
1591
1618
|
*
|
|
1592
|
-
* Functions receive state as their first parameter at call time.
|
|
1593
|
-
*
|
|
1619
|
+
* Functions receive state as their first parameter at call time. A `computed` role (decided by
|
|
1620
|
+
* `classifyFunctionEntries` from the document) is wrapped in computed() for reactive evaluation.
|
|
1594
1621
|
*
|
|
1595
1622
|
* @param {JxFunctionDef} def - State entry with $prototype: "Function"
|
|
1596
1623
|
* @param {JxScope} state - Reactive scope proxy
|
|
1597
1624
|
* @param {string} key - Def key name
|
|
1625
|
+
* @param {FunctionRole} role - The document's verdict for this entry
|
|
1598
1626
|
* @param {string} [base] - Base URL for resolving $src imports
|
|
1599
1627
|
* @returns {Promise<JxScopeValue>}
|
|
1600
1628
|
*/
|
|
1601
|
-
async function resolveFunction(
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1629
|
+
async function resolveFunction(
|
|
1630
|
+
def: JxFunctionDef,
|
|
1631
|
+
state: JxScope,
|
|
1632
|
+
key: string,
|
|
1633
|
+
role: FunctionRole,
|
|
1634
|
+
base?: string,
|
|
1635
|
+
) {
|
|
1605
1636
|
if (!def.body && !def.$src) {
|
|
1606
|
-
const params =
|
|
1637
|
+
const params = handlerParamNames(def);
|
|
1607
1638
|
const noop = new Function(...params, "");
|
|
1608
1639
|
Object.defineProperty(noop, "name", {
|
|
1609
1640
|
configurable: true,
|
|
@@ -1615,10 +1646,10 @@ async function resolveFunction(def: JxFunctionDef, state: JxScope, key: string,
|
|
|
1615
1646
|
let fn: ScopeFunction;
|
|
1616
1647
|
|
|
1617
1648
|
if (isString(def.body)) {
|
|
1618
|
-
const params =
|
|
1649
|
+
const params = handlerParamNames(def);
|
|
1619
1650
|
/* SAFETY: `new Function` returns an ordinary function of the named parameters, `state` first.
|
|
1620
1651
|
A call never checks its arguments, and what the body returns is one more scope member. */
|
|
1621
|
-
fn =
|
|
1652
|
+
fn = compileBody(params, def.body, `'${key}'`) as ScopeFunction;
|
|
1622
1653
|
Object.defineProperty(fn, "name", {
|
|
1623
1654
|
configurable: true,
|
|
1624
1655
|
value: def.name ?? key,
|
|
@@ -1649,16 +1680,8 @@ async function resolveFunction(def: JxFunctionDef, state: JxScope, key: string,
|
|
|
1649
1680
|
fn = candidate as ScopeFunction;
|
|
1650
1681
|
}
|
|
1651
1682
|
|
|
1652
|
-
//
|
|
1653
|
-
|
|
1654
|
-
const hasParams = (def.parameters ?? def.arguments ?? []).length > 0;
|
|
1655
|
-
let isComputed = false;
|
|
1656
|
-
if (!hasParams) {
|
|
1657
|
-
isComputed = isString(def.body)
|
|
1658
|
-
? bodyReturnsValue(def.body)
|
|
1659
|
-
: fn.length <= 1 && bodyReturnsValue(fn.toString());
|
|
1660
|
-
}
|
|
1661
|
-
if (isComputed) {
|
|
1683
|
+
// The role comes from the document (spec.md §5.3 4b, 4d), never from the import's arity or source.
|
|
1684
|
+
if (role === "computed") {
|
|
1662
1685
|
return computed(() => fn(state));
|
|
1663
1686
|
}
|
|
1664
1687
|
|
|
@@ -1667,19 +1690,6 @@ async function resolveFunction(def: JxFunctionDef, state: JxScope, key: string,
|
|
|
1667
1690
|
|
|
1668
1691
|
// ─── Step 3: Render ───────────────────────────────────────────────────────────
|
|
1669
1692
|
|
|
1670
|
-
/**
|
|
1671
|
-
* Extract parameter names from a function definition. Supports both legacy "arguments" (string
|
|
1672
|
-
* array) and CEM-compatible "parameters" (object array). Always ensures "state" is the first
|
|
1673
|
-
* parameter.
|
|
1674
|
-
*
|
|
1675
|
-
* @param {JxFunctionDef | JxPrototypeDef} def
|
|
1676
|
-
* @returns {string[]}
|
|
1677
|
-
*/
|
|
1678
|
-
function resolveParamNames(def: JxFunctionDef | JxPrototypeDef) {
|
|
1679
|
-
const names = def.parameters ? paramNames(def.parameters) : (def.arguments ?? []);
|
|
1680
|
-
return names.length > 0 && names[0] === "state" ? names : ["state", ...names];
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
1693
|
/**
|
|
1684
1694
|
* Reserved Jx keys — never set as DOM properties.
|
|
1685
1695
|
*
|
|
@@ -2121,6 +2131,11 @@ function bindHandler(
|
|
|
2121
2131
|
}
|
|
2122
2132
|
return true;
|
|
2123
2133
|
}
|
|
2134
|
+
if (isFunctionDef(val) && declaresBodyAndSrc(val)) {
|
|
2135
|
+
throw new Error(
|
|
2136
|
+
`Jx: inline '${key}' handler declares both body and $src — these are mutually exclusive`,
|
|
2137
|
+
);
|
|
2138
|
+
}
|
|
2124
2139
|
if (hasStructuredBody(val)) {
|
|
2125
2140
|
const { body } = val;
|
|
2126
2141
|
el.addEventListener(type, (e) => {
|
|
@@ -2129,10 +2144,10 @@ function bindHandler(
|
|
|
2129
2144
|
return true;
|
|
2130
2145
|
}
|
|
2131
2146
|
if (isFunctionDef(val) && isString(val.body)) {
|
|
2132
|
-
const params =
|
|
2147
|
+
const params = handlerParamNames(val);
|
|
2133
2148
|
/* SAFETY: `new Function` returns an ordinary function of the named parameters, `state` first;
|
|
2134
2149
|
the listener calls it with the scope and the event, and discards what it returns. */
|
|
2135
|
-
const fn =
|
|
2150
|
+
const fn = compileBody(params, val.body, `the ${type} handler`) as JxEventHandler;
|
|
2136
2151
|
el.addEventListener(type, (e) => fn(scope, e));
|
|
2137
2152
|
return true;
|
|
2138
2153
|
}
|
|
@@ -3867,6 +3882,7 @@ async function resolveClassJson(def: JxPrototypeDef, state: JxScope, key: string
|
|
|
3867
3882
|
// Fall back to dev proxy (server will handle .class.json resolution)
|
|
3868
3883
|
return resolveViaDevProxy(def, state, key, base);
|
|
3869
3884
|
}
|
|
3885
|
+
await prepareBodies(classDef);
|
|
3870
3886
|
|
|
3871
3887
|
// Hybrid mode: $implementation points to the real JS module
|
|
3872
3888
|
if (classDef.$implementation) {
|
|
@@ -3934,6 +3950,7 @@ function classFromSchema(classDef: JxClassDef) {
|
|
|
3934
3950
|
const rawCtor = classDef.$defs?.constructor;
|
|
3935
3951
|
const ctor = isCtorDef(rawCtor) ? rawCtor : undefined;
|
|
3936
3952
|
const methods = classDef.$defs?.methods ?? {};
|
|
3953
|
+
const className = classDef.title ?? "a class";
|
|
3937
3954
|
|
|
3938
3955
|
// oxlint-disable-next-line typescript/no-extraneous-class -- methods are attached to the prototype dynamically below
|
|
3939
3956
|
class DynClass {
|
|
@@ -3961,7 +3978,10 @@ function classFromSchema(classDef: JxClassDef) {
|
|
|
3961
3978
|
/* SAFETY: `new Function` returns an ordinary function of the one named parameter; `call`
|
|
3962
3979
|
runs it with the instance under construction as `this`, which is the constructor
|
|
3963
3980
|
body's contract. */
|
|
3964
|
-
const init =
|
|
3981
|
+
const init = compileBody(["config"], bodyStr, `the constructor of ${className}`) as (
|
|
3982
|
+
this: DynClass,
|
|
3983
|
+
config: JxScope,
|
|
3984
|
+
) => void;
|
|
3965
3985
|
init.call(this, config);
|
|
3966
3986
|
}
|
|
3967
3987
|
}
|
|
@@ -3986,7 +4006,11 @@ function classFromSchema(classDef: JxClassDef) {
|
|
|
3986
4006
|
if (typedMethod.getter) {
|
|
3987
4007
|
/* SAFETY: `new Function` returns an ordinary function; as a getter it is called with no
|
|
3988
4008
|
arguments and the instance as `this`, and what it returns is the property's value. */
|
|
3989
|
-
descriptor.get =
|
|
4009
|
+
descriptor.get = compileBody(
|
|
4010
|
+
[],
|
|
4011
|
+
typedMethod.getter.body ?? "",
|
|
4012
|
+
`the getter ${name} of ${className}`,
|
|
4013
|
+
) as () => JxScopeValue;
|
|
3990
4014
|
}
|
|
3991
4015
|
if (typedMethod.setter) {
|
|
3992
4016
|
const sp = (typedMethod.setter.parameters ?? []).map(
|
|
@@ -3994,18 +4018,20 @@ function classFromSchema(classDef: JxClassDef) {
|
|
|
3994
4018
|
);
|
|
3995
4019
|
/* SAFETY: `new Function` returns an ordinary function of the named parameters; as a setter
|
|
3996
4020
|
it receives the assigned value as its one argument, and its return is discarded. */
|
|
3997
|
-
descriptor.set =
|
|
3998
|
-
|
|
3999
|
-
|
|
4021
|
+
descriptor.set = compileBody(
|
|
4022
|
+
sp,
|
|
4023
|
+
typedMethod.setter.body ?? "",
|
|
4024
|
+
`the setter ${name} of ${className}`,
|
|
4025
|
+
) as (v: JxScopeValue) => void;
|
|
4000
4026
|
}
|
|
4001
4027
|
Object.defineProperty(DynClass.prototype, name, {
|
|
4002
4028
|
...descriptor,
|
|
4003
4029
|
configurable: true,
|
|
4004
4030
|
});
|
|
4005
4031
|
} else if (typedMethod.scope === "static") {
|
|
4006
|
-
DynClass[name] =
|
|
4032
|
+
DynClass[name] = compileBody(params, bodyStr, `${name} of ${className}`);
|
|
4007
4033
|
} else {
|
|
4008
|
-
DynClass.prototype[name] =
|
|
4034
|
+
DynClass.prototype[name] = compileBody(params, bodyStr, `${name} of ${className}`);
|
|
4009
4035
|
}
|
|
4010
4036
|
}
|
|
4011
4037
|
|