@jxsuite/compiler 0.28.2 → 0.28.4
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/cli.js +159 -150
- package/dist/compiler.js +100 -95
- package/package.json +5 -5
- package/src/cli.ts +6 -6
- package/src/compile-cli.ts +6 -4
- package/src/compiler.ts +34 -23
- package/src/shared.ts +402 -231
- package/src/site/content-loader.ts +54 -29
- package/src/site/context-injection.ts +13 -9
- package/src/site/format-host.ts +26 -15
- package/src/site/head-merger.ts +37 -18
- package/src/site/image-cache.ts +13 -12
- package/src/site/image-optimizer.ts +30 -20
- package/src/site/image-transform.ts +136 -78
- package/src/site/layout-resolver.ts +33 -18
- package/src/site/pages-discovery.ts +61 -39
- package/src/site/prototype-resolver.ts +45 -21
- package/src/site/site-build.ts +212 -136
- package/src/site/site-loader.ts +70 -43
- package/src/targets/compile-class.ts +70 -51
- package/src/targets/compile-client.ts +271 -227
- package/src/targets/compile-element.ts +230 -146
- package/src/targets/compile-server.ts +9 -5
- package/src/targets/compile-static.ts +27 -19
|
@@ -11,41 +11,75 @@
|
|
|
11
11
|
* state + computed signals), bind (DOM getters), on (event handlers), hydrate()
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { RESERVED_KEYS, camelToKebab } from "@jxsuite/runtime";
|
|
15
15
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
isRefObject,
|
|
19
|
-
resolveStaticValue,
|
|
20
|
-
createCompileContext,
|
|
16
|
+
DEFAULT_LIT_HTML_SRC,
|
|
17
|
+
DEFAULT_REACTIVITY_SRC,
|
|
21
18
|
buildAttrs,
|
|
19
|
+
compileExpression,
|
|
22
20
|
compileStyles,
|
|
21
|
+
createCompileContext,
|
|
23
22
|
escapeHtml,
|
|
24
|
-
DEFAULT_REACTIVITY_SRC,
|
|
25
|
-
DEFAULT_LIT_HTML_SRC,
|
|
26
|
-
compileExpression,
|
|
27
23
|
isMutating,
|
|
24
|
+
isRefObject,
|
|
25
|
+
isSchemaOnly,
|
|
26
|
+
isTemplateString,
|
|
27
|
+
resolveStaticValue,
|
|
28
28
|
} from "../shared.ts";
|
|
29
|
-
import
|
|
30
|
-
|
|
29
|
+
import {
|
|
30
|
+
isExpandedSignal,
|
|
31
|
+
isExpressionDef,
|
|
32
|
+
isFunctionDef,
|
|
33
|
+
isMappedArray,
|
|
34
|
+
isPrototypeDef,
|
|
35
|
+
paramNames,
|
|
36
|
+
} from "@jxsuite/schema/guards";
|
|
37
|
+
import type { CompileContext, ExpressionNode } from "../shared.ts";
|
|
38
|
+
import type {
|
|
39
|
+
JsonObject,
|
|
40
|
+
JsonValue,
|
|
41
|
+
JxDocument,
|
|
42
|
+
JxMutableNode,
|
|
43
|
+
JxPrototypeDef,
|
|
44
|
+
JxStyle,
|
|
45
|
+
} from "@jxsuite/schema/types";
|
|
46
|
+
|
|
47
|
+
/** A compiled event handler entry: parameter names + function body. */
|
|
48
|
+
interface HandlerDef {
|
|
49
|
+
args?: string[] | undefined;
|
|
50
|
+
body?: string | undefined;
|
|
51
|
+
}
|
|
31
52
|
|
|
32
53
|
/**
|
|
33
54
|
* Compile a Jx document to pre-rendered HTML + reactive JS module.
|
|
34
55
|
*
|
|
35
|
-
* @param {
|
|
36
|
-
* @param {
|
|
56
|
+
* @param {JxDocument} raw
|
|
57
|
+
* @param {{
|
|
58
|
+
* title?: string;
|
|
59
|
+
* reactivitySrc?: string;
|
|
60
|
+
* litHtmlSrc?: string;
|
|
61
|
+
* modulePath?: string;
|
|
62
|
+
* projectStyle?: JxStyle | null;
|
|
63
|
+
* }} opts
|
|
37
64
|
* @returns {{ html: string; files: { path: string; content: string }[] }}
|
|
38
65
|
*/
|
|
39
66
|
export function compileClient(
|
|
40
|
-
raw:
|
|
41
|
-
opts:
|
|
67
|
+
raw: JxDocument,
|
|
68
|
+
opts: {
|
|
69
|
+
title?: string;
|
|
70
|
+
reactivitySrc?: string;
|
|
71
|
+
litHtmlSrc?: string;
|
|
72
|
+
modulePath?: string;
|
|
73
|
+
projectStyle?: JxStyle | null;
|
|
74
|
+
[key: string]: unknown;
|
|
75
|
+
},
|
|
42
76
|
) {
|
|
43
77
|
const {
|
|
44
|
-
title,
|
|
78
|
+
title = "Jx App",
|
|
45
79
|
reactivitySrc = DEFAULT_REACTIVITY_SRC,
|
|
46
80
|
litHtmlSrc = DEFAULT_LIT_HTML_SRC,
|
|
47
81
|
modulePath = "app.js",
|
|
48
|
-
} = opts
|
|
82
|
+
} = opts;
|
|
49
83
|
|
|
50
84
|
const context = createCompileContext(raw, null, raw.state ?? {}, raw.$media ?? {});
|
|
51
85
|
const styleBlock = compileStyles(
|
|
@@ -55,19 +89,19 @@ export function compileClient(
|
|
|
55
89
|
);
|
|
56
90
|
|
|
57
91
|
// Collectors for bindings and handlers
|
|
58
|
-
const counter = {
|
|
59
|
-
const bindings
|
|
60
|
-
const handlers
|
|
92
|
+
const counter = { h: 0, l: 0, m: 0, needsLit: false, s: 0, sw: 0, t: 0 };
|
|
93
|
+
const bindings = new Map<string, string>(); // Key → expression string
|
|
94
|
+
const handlers = new Map<string, HandlerDef>(); // Key → { body, args }
|
|
61
95
|
|
|
62
96
|
// Classify state entries into reactive state, computed, bind, on, and init blocks
|
|
63
|
-
const stateEntries: [string,
|
|
97
|
+
const stateEntries: [string, JsonValue | undefined][] = []; // [key, initValue] → reactive({...})
|
|
64
98
|
const computedEntries: [string, string][] = []; // [key, bodyExpr] → state.key = computed(...)
|
|
65
99
|
const bindEntries: [string, string][] = []; // [key, bodyExpr] → bind = {...}
|
|
66
|
-
const onEntries: [string,
|
|
67
|
-
const initBlocks: string[] = []; //
|
|
100
|
+
const onEntries: [string, HandlerDef][] = []; // [key, { args, body }] → on = {...}
|
|
101
|
+
const initBlocks: string[] = []; // Lines emitted after state for prototype init
|
|
68
102
|
|
|
69
103
|
// Map $src path → Set of function names to import
|
|
70
|
-
const srcImportMap
|
|
104
|
+
const srcImportMap = new Map<string, Set<string>>();
|
|
71
105
|
|
|
72
106
|
const defs = raw.state ?? {};
|
|
73
107
|
for (const [key, def] of Object.entries(defs)) {
|
|
@@ -75,88 +109,88 @@ export function compileClient(
|
|
|
75
109
|
// Naked primitive or array → reactive state
|
|
76
110
|
if (typeof def === "string" && isTemplateString(def)) {
|
|
77
111
|
// Template string → computed on state so other computeds can ref it
|
|
78
|
-
computedEntries.push([key,
|
|
112
|
+
computedEntries.push([key, `() => \`${def}\``]);
|
|
79
113
|
} else {
|
|
80
114
|
stateEntries.push([key, def]);
|
|
81
115
|
}
|
|
82
116
|
continue;
|
|
83
117
|
}
|
|
84
118
|
|
|
85
|
-
const d = def as JxMutableNode;
|
|
86
|
-
|
|
87
119
|
// $expression: Shape 5
|
|
88
|
-
if (
|
|
89
|
-
const node =
|
|
120
|
+
if (isExpressionDef(def)) {
|
|
121
|
+
const node = def.$expression as ExpressionNode;
|
|
122
|
+
const compiled = compileExpression(node, {
|
|
123
|
+
eventParam: "e",
|
|
124
|
+
statePrefix: "state",
|
|
125
|
+
});
|
|
90
126
|
if (isMutating(node.operator)) {
|
|
91
|
-
const compiled = compileExpression(node, {
|
|
92
|
-
statePrefix: "state",
|
|
93
|
-
eventParam: "e",
|
|
94
|
-
});
|
|
95
127
|
onEntries.push([key, { args: ["state", "e"], body: compiled }]);
|
|
96
128
|
} else {
|
|
97
|
-
const compiled = compileExpression(node, {
|
|
98
|
-
statePrefix: "state",
|
|
99
|
-
eventParam: "e",
|
|
100
|
-
});
|
|
101
129
|
computedEntries.push([key, `() => ${compiled}`]);
|
|
102
130
|
}
|
|
103
131
|
continue;
|
|
104
132
|
}
|
|
105
133
|
|
|
106
134
|
// $prototype: "Function"
|
|
107
|
-
if (
|
|
108
|
-
const args =
|
|
109
|
-
if (
|
|
110
|
-
if (!srcImportMap.has(
|
|
111
|
-
|
|
135
|
+
if (isFunctionDef(def)) {
|
|
136
|
+
const args = def.parameters ? paramNames(def.parameters) : def.arguments;
|
|
137
|
+
if (def.$src) {
|
|
138
|
+
if (!srcImportMap.has(def.$src)) {
|
|
139
|
+
srcImportMap.set(def.$src, new Set());
|
|
140
|
+
}
|
|
141
|
+
(srcImportMap.get(def.$src) as Set<string>).add(key);
|
|
112
142
|
|
|
113
143
|
// $src functions always produce computed entries (they return values)
|
|
114
|
-
computedEntries.push([key,
|
|
115
|
-
} else if (
|
|
144
|
+
computedEntries.push([key, `() => { return ${key}(state); }`]);
|
|
145
|
+
} else if (def.body && def.body.includes("return")) {
|
|
116
146
|
// Body contains return → computed
|
|
117
|
-
computedEntries.push([key,
|
|
147
|
+
computedEntries.push([key, `() => { ${def.body} }`]);
|
|
118
148
|
} else {
|
|
119
149
|
// No return → event handler
|
|
120
|
-
onEntries.push([key, { args: args ?? ["state"], body:
|
|
150
|
+
onEntries.push([key, { args: args ?? ["state"], body: def.body }]);
|
|
121
151
|
}
|
|
122
152
|
continue;
|
|
123
153
|
}
|
|
124
154
|
|
|
125
155
|
// Pure schema-only type def → skip
|
|
126
|
-
if (isSchemaOnly(
|
|
127
|
-
|
|
128
|
-
// Expanded signal with default
|
|
129
|
-
if ("default" in d && !d.$prototype) {
|
|
130
|
-
stateEntries.push([key, d.default]);
|
|
156
|
+
if (isSchemaOnly(def)) {
|
|
131
157
|
continue;
|
|
132
158
|
}
|
|
133
159
|
|
|
134
|
-
//
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
const storageKey = d.key ?? key;
|
|
138
|
-
const defaultVal = d.default ?? null;
|
|
139
|
-
stateEntries.push([key, null]);
|
|
140
|
-
initBlocks.push(emitStorageInit(key, storeName, storageKey, defaultVal));
|
|
160
|
+
// Expanded signal with default
|
|
161
|
+
if (isExpandedSignal(def)) {
|
|
162
|
+
stateEntries.push([key, def.default]);
|
|
141
163
|
continue;
|
|
142
164
|
}
|
|
143
165
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
166
|
+
if (isPrototypeDef(def)) {
|
|
167
|
+
// $prototype: "LocalStorage" / "SessionStorage"
|
|
168
|
+
if (def.$prototype === "LocalStorage" || def.$prototype === "SessionStorage") {
|
|
169
|
+
const storeName = def.$prototype === "LocalStorage" ? "localStorage" : "sessionStorage";
|
|
170
|
+
const storageKey = def.key ?? key;
|
|
171
|
+
const defaultVal = def.default ?? null;
|
|
172
|
+
stateEntries.push([key, null]);
|
|
173
|
+
initBlocks.push(emitStorageInit(key, storeName, storageKey, defaultVal));
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
150
176
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
177
|
+
// $prototype: "Request"
|
|
178
|
+
if (def.$prototype === "Request") {
|
|
179
|
+
stateEntries.push([key, null]);
|
|
180
|
+
initBlocks.push(emitRequestInit(key, def));
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// $prototype: "Cookie"
|
|
185
|
+
if (def.$prototype === "Cookie") {
|
|
186
|
+
stateEntries.push([key, null]);
|
|
187
|
+
initBlocks.push(emitCookieInit(key, def.name ?? key, def.default ?? null));
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
156
190
|
}
|
|
157
191
|
|
|
158
|
-
// Plain object → reactive state
|
|
159
|
-
stateEntries.push([key,
|
|
192
|
+
// Plain object → reactive state (parsed JSON, so structurally a JsonObject)
|
|
193
|
+
stateEntries.push([key, def as JsonObject]);
|
|
160
194
|
}
|
|
161
195
|
|
|
162
196
|
// Build HTML tree with data-bind markers
|
|
@@ -213,7 +247,7 @@ ${importmapEntries.join(",\n")}
|
|
|
213
247
|
</body>
|
|
214
248
|
</html>`;
|
|
215
249
|
|
|
216
|
-
return {
|
|
250
|
+
return { files: [{ content: moduleContent, path: modulePath }], html };
|
|
217
251
|
}
|
|
218
252
|
|
|
219
253
|
// ─── HTML tree walker ─────────────────────────────────────────────────────────
|
|
@@ -221,9 +255,9 @@ ${importmapEntries.join(",\n")}
|
|
|
221
255
|
/**
|
|
222
256
|
* @param {JxMutableNode} def
|
|
223
257
|
* @param {JxMutableNode} raw
|
|
224
|
-
* @param {
|
|
258
|
+
* @param {CompileContext} context
|
|
225
259
|
* @param {Map<string, string>} bindings
|
|
226
|
-
* @param {Map<string,
|
|
260
|
+
* @param {Map<string, HandlerDef>} handlers
|
|
227
261
|
* @param {{
|
|
228
262
|
* t: number;
|
|
229
263
|
* s: number;
|
|
@@ -238,9 +272,9 @@ ${importmapEntries.join(",\n")}
|
|
|
238
272
|
function buildClientNode(
|
|
239
273
|
def: JxMutableNode,
|
|
240
274
|
raw: JxMutableNode,
|
|
241
|
-
context:
|
|
275
|
+
context: CompileContext,
|
|
242
276
|
bindings: Map<string, string>,
|
|
243
|
-
handlers: Map<string,
|
|
277
|
+
handlers: Map<string, HandlerDef>,
|
|
244
278
|
counter: {
|
|
245
279
|
t: number;
|
|
246
280
|
s: number;
|
|
@@ -258,7 +292,9 @@ function buildClientNode(
|
|
|
258
292
|
if (typeof def === "number" || typeof def === "boolean") {
|
|
259
293
|
return escapeHtml(String(def));
|
|
260
294
|
}
|
|
261
|
-
if (!def || typeof def !== "object")
|
|
295
|
+
if (!def || typeof def !== "object") {
|
|
296
|
+
return "";
|
|
297
|
+
}
|
|
262
298
|
|
|
263
299
|
const nextContext = createCompileContext(
|
|
264
300
|
raw,
|
|
@@ -271,7 +307,7 @@ function buildClientNode(
|
|
|
271
307
|
const bindAttrs = [];
|
|
272
308
|
let needsBind = false;
|
|
273
309
|
|
|
274
|
-
//
|
|
310
|
+
// TextContent bindings
|
|
275
311
|
if (def.textContent !== undefined) {
|
|
276
312
|
const tc = raw?.textContent ?? (def.textContent as unknown as JxMutableNode);
|
|
277
313
|
if (isRefObject(tc)) {
|
|
@@ -280,40 +316,41 @@ function buildClientNode(
|
|
|
280
316
|
addRefBinding(bindings, key, (tc as JxMutableNode).$ref as string);
|
|
281
317
|
needsBind = true;
|
|
282
318
|
} else if (isTemplateString(tc)) {
|
|
283
|
-
const key = `_t${counter.t
|
|
319
|
+
const key = `_t${counter.t}`;
|
|
320
|
+
counter.t += 1;
|
|
284
321
|
bindAttrs.push(`:text-content="${key}"`);
|
|
285
|
-
bindings.set(key,
|
|
322
|
+
bindings.set(key, `() => \`${tc}\``);
|
|
286
323
|
needsBind = true;
|
|
287
324
|
}
|
|
288
325
|
}
|
|
289
326
|
|
|
290
327
|
// Event handlers (onclick, oninput, etc.)
|
|
291
328
|
for (const [prop, val] of Object.entries(def)) {
|
|
292
|
-
if (!prop.startsWith("on") || prop === "observedAttributes")
|
|
329
|
+
if (!prop.startsWith("on") || prop === "observedAttributes") {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
293
332
|
const eventName = prop.slice(2).toLowerCase();
|
|
294
333
|
if (isRefObject(val)) {
|
|
295
|
-
const key = refToBindingKey(
|
|
334
|
+
const key = refToBindingKey(val.$ref);
|
|
296
335
|
bindAttrs.push(`@${eventName}="${key}"`);
|
|
297
336
|
needsBind = true;
|
|
298
|
-
} else if (
|
|
299
|
-
const
|
|
300
|
-
|
|
337
|
+
} else if (isFunctionDef(val)) {
|
|
338
|
+
const key = `_h${counter.h}`;
|
|
339
|
+
counter.h += 1;
|
|
301
340
|
bindAttrs.push(`@${eventName}="${key}"`);
|
|
302
341
|
handlers.set(key, {
|
|
303
|
-
args:
|
|
304
|
-
body:
|
|
342
|
+
args: val.parameters ? paramNames(val.parameters) : (val.arguments ?? ["state", "event"]),
|
|
343
|
+
body: val.body,
|
|
305
344
|
});
|
|
306
345
|
needsBind = true;
|
|
307
|
-
} else if (val
|
|
308
|
-
const key = `_h${counter.h
|
|
346
|
+
} else if (isExpressionDef(val)) {
|
|
347
|
+
const key = `_h${counter.h}`;
|
|
348
|
+
counter.h += 1;
|
|
309
349
|
bindAttrs.push(`@${eventName}="${key}"`);
|
|
310
|
-
const compiled = compileExpression(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
eventParam: "e",
|
|
315
|
-
},
|
|
316
|
-
);
|
|
350
|
+
const compiled = compileExpression(val.$expression as ExpressionNode, {
|
|
351
|
+
eventParam: "e",
|
|
352
|
+
statePrefix: "state",
|
|
353
|
+
});
|
|
317
354
|
handlers.set(key, { args: ["state", "e"], body: compiled });
|
|
318
355
|
needsBind = true;
|
|
319
356
|
}
|
|
@@ -328,13 +365,17 @@ function buildClientNode(
|
|
|
328
365
|
prop.startsWith("&") ||
|
|
329
366
|
prop.startsWith("[") ||
|
|
330
367
|
prop.startsWith("@")
|
|
331
|
-
)
|
|
368
|
+
) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (val === null || typeof val === "object") {
|
|
332
372
|
continue;
|
|
333
|
-
|
|
373
|
+
}
|
|
334
374
|
if (isTemplateString(val)) {
|
|
335
|
-
const key = `_s${counter.s
|
|
375
|
+
const key = `_s${counter.s}`;
|
|
376
|
+
counter.s += 1;
|
|
336
377
|
bindAttrs.push(`:style.${camelToKebab(prop)}="${key}"`);
|
|
337
|
-
bindings.set(key,
|
|
378
|
+
bindings.set(key, `() => \`${val}\``);
|
|
338
379
|
needsBind = true;
|
|
339
380
|
}
|
|
340
381
|
}
|
|
@@ -349,9 +390,10 @@ function buildClientNode(
|
|
|
349
390
|
addRefBinding(bindings, key, (val as JxMutableNode).$ref as string);
|
|
350
391
|
needsBind = true;
|
|
351
392
|
} else if (isTemplateString(val)) {
|
|
352
|
-
const key = `_t${counter.t
|
|
393
|
+
const key = `_t${counter.t}`;
|
|
394
|
+
counter.t += 1;
|
|
353
395
|
bindAttrs.push(`:attr.${attr}="${key}"`);
|
|
354
|
-
bindings.set(key,
|
|
396
|
+
bindings.set(key, `() => \`${val}\``);
|
|
355
397
|
needsBind = true;
|
|
356
398
|
}
|
|
357
399
|
}
|
|
@@ -371,17 +413,19 @@ function buildClientNode(
|
|
|
371
413
|
prop === "textContent" ||
|
|
372
414
|
prop === "innerHTML" ||
|
|
373
415
|
prop === "attributes"
|
|
374
|
-
)
|
|
416
|
+
) {
|
|
375
417
|
continue;
|
|
418
|
+
}
|
|
376
419
|
if (isRefObject(val)) {
|
|
377
420
|
const key = refToBindingKey((val as JxMutableNode).$ref as string);
|
|
378
421
|
bindAttrs.push(`:${camelToKebab(prop)}="${key}"`);
|
|
379
422
|
addRefBinding(bindings, key, (val as JxMutableNode).$ref as string);
|
|
380
423
|
needsBind = true;
|
|
381
424
|
} else if (isTemplateString(val)) {
|
|
382
|
-
const key = `_t${counter.t
|
|
425
|
+
const key = `_t${counter.t}`;
|
|
426
|
+
counter.t += 1;
|
|
383
427
|
bindAttrs.push(`:${camelToKebab(prop)}="${key}"`);
|
|
384
|
-
bindings.set(key,
|
|
428
|
+
bindings.set(key, `() => \`${val}\``);
|
|
385
429
|
needsBind = true;
|
|
386
430
|
}
|
|
387
431
|
}
|
|
@@ -389,7 +433,7 @@ function buildClientNode(
|
|
|
389
433
|
// Build static attrs
|
|
390
434
|
const staticAttrs = buildAttrs(def, nextContext.scope);
|
|
391
435
|
const dataBindAttr = needsBind ? " data-bind" : "";
|
|
392
|
-
const bindAttrStr = bindAttrs.length > 0 ?
|
|
436
|
+
const bindAttrStr = bindAttrs.length > 0 ? ` ${bindAttrs.join(" ")}` : "";
|
|
393
437
|
|
|
394
438
|
// Inner content
|
|
395
439
|
let inner = "";
|
|
@@ -401,25 +445,21 @@ function buildClientNode(
|
|
|
401
445
|
const value = resolveStaticValue(source.textContent, nextContext.scope);
|
|
402
446
|
inner = value == null ? "" : escapeHtml(String(value));
|
|
403
447
|
} else if (source.innerHTML) {
|
|
404
|
-
//
|
|
448
|
+
// ResolveStaticValue may return null if innerHTML contains `${` from rendered content
|
|
405
449
|
// (e.g., code examples) that isn't an actual template expression. Fall back to raw value.
|
|
406
450
|
inner = (resolveStaticValue(source.innerHTML, nextContext.scope) as string) ?? source.innerHTML;
|
|
407
|
-
} else if (
|
|
408
|
-
source.children &&
|
|
409
|
-
typeof source.children === "object" &&
|
|
410
|
-
!Array.isArray(source.children) &&
|
|
411
|
-
(source.children as JxMutableNode).$prototype === "Array"
|
|
412
|
-
) {
|
|
451
|
+
} else if (isMappedArray(source.children)) {
|
|
413
452
|
// ─── Mapped array → lit-html render binding ───
|
|
414
453
|
counter.needsLit = true;
|
|
415
|
-
const listKey = `_list${counter.l
|
|
416
|
-
|
|
454
|
+
const listKey = `_list${counter.l}`;
|
|
455
|
+
counter.l += 1;
|
|
456
|
+
const arrayDef = source.children;
|
|
417
457
|
|
|
418
458
|
// Resolve items source expression
|
|
419
459
|
let itemsExpr;
|
|
420
460
|
if (isRefObject(arrayDef.items)) {
|
|
421
461
|
const path = refToBindingKey(arrayDef.items.$ref);
|
|
422
|
-
itemsExpr =
|
|
462
|
+
itemsExpr = `state.${path}`;
|
|
423
463
|
} else {
|
|
424
464
|
itemsExpr = JSON.stringify(arrayDef.items);
|
|
425
465
|
}
|
|
@@ -428,27 +468,27 @@ function buildClientNode(
|
|
|
428
468
|
const litTemplate = emitLitMapTemplate(arrayDef.map);
|
|
429
469
|
bindings.set(
|
|
430
470
|
listKey,
|
|
431
|
-
|
|
471
|
+
`() => (${itemsExpr} ?? []).map((item, index) => html\`${litTemplate}\`)`,
|
|
432
472
|
);
|
|
433
473
|
|
|
434
474
|
bindAttrs.push(`:render="${listKey}"`);
|
|
435
475
|
needsBind = true;
|
|
436
476
|
// Re-derive the data-bind/attr strings since we added to bindAttrs
|
|
437
477
|
const dataBindAttr2 = " data-bind";
|
|
438
|
-
const bindAttrStr2 =
|
|
478
|
+
const bindAttrStr2 = ` ${bindAttrs.join(" ")}`;
|
|
439
479
|
const selfClosing = new Set(["input", "br", "hr", "img", "meta", "link"]);
|
|
440
480
|
if (selfClosing.has(tag)) {
|
|
441
481
|
return `<${tag}${staticAttrs}${dataBindAttr2}${bindAttrStr2}>`;
|
|
442
482
|
}
|
|
443
483
|
return `<${tag}${staticAttrs}${dataBindAttr2}${bindAttrStr2}></${tag}>`;
|
|
444
484
|
} else if (Array.isArray(source.children)) {
|
|
445
|
-
const rawChildren = raw?.children;
|
|
485
|
+
const rawChildren = Array.isArray(raw?.children) ? raw.children : undefined;
|
|
446
486
|
inner = source.children
|
|
447
487
|
.map((c, i) => {
|
|
448
488
|
const childRaw = rawChildren?.[i] ?? c;
|
|
449
489
|
return buildClientNode(
|
|
450
|
-
c as
|
|
451
|
-
childRaw as
|
|
490
|
+
c as JxMutableNode,
|
|
491
|
+
childRaw as JxMutableNode,
|
|
452
492
|
nextContext,
|
|
453
493
|
bindings,
|
|
454
494
|
handlers,
|
|
@@ -476,26 +516,31 @@ function buildClientNode(
|
|
|
476
516
|
* @param {JxMutableNode} def
|
|
477
517
|
* @returns {string}
|
|
478
518
|
*/
|
|
479
|
-
function emitLitMapTemplate(def: JxMutableNode) {
|
|
480
|
-
if (!def)
|
|
519
|
+
function emitLitMapTemplate(def: JxMutableNode | undefined) {
|
|
520
|
+
if (!def) {
|
|
521
|
+
return "";
|
|
522
|
+
}
|
|
481
523
|
const tag = def.tagName ?? "div";
|
|
482
524
|
let attrs = "";
|
|
483
525
|
|
|
484
|
-
if (def.id)
|
|
485
|
-
|
|
526
|
+
if (def.id) {
|
|
527
|
+
attrs += ` id="${def.id}"`;
|
|
528
|
+
}
|
|
529
|
+
if (def.className) {
|
|
530
|
+
attrs += ` class="${mapRefsToLit(def.className)}"`;
|
|
531
|
+
}
|
|
486
532
|
|
|
487
|
-
//
|
|
533
|
+
// Attributes object
|
|
488
534
|
if (def.attributes && typeof def.attributes === "object") {
|
|
489
535
|
for (const [k, v] of Object.entries(def.attributes)) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
536
|
+
attrs +=
|
|
537
|
+
typeof v === "string" && isTemplateString(v)
|
|
538
|
+
? ` ${k}="${mapRefsToLit(v)}"`
|
|
539
|
+
: ` ${k}="${escapeHtml(String(v))}"`;
|
|
495
540
|
}
|
|
496
541
|
}
|
|
497
542
|
|
|
498
|
-
//
|
|
543
|
+
// Style → inline CSS
|
|
499
544
|
if (def.style && typeof def.style === "object") {
|
|
500
545
|
const parts: string[] = [];
|
|
501
546
|
for (const [k, v] of Object.entries(def.style)) {
|
|
@@ -505,46 +550,51 @@ function emitLitMapTemplate(def: JxMutableNode) {
|
|
|
505
550
|
k.startsWith("&") ||
|
|
506
551
|
k.startsWith("[") ||
|
|
507
552
|
k.startsWith("@")
|
|
508
|
-
)
|
|
553
|
+
) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (v === null || typeof v === "object") {
|
|
509
557
|
continue;
|
|
510
|
-
|
|
558
|
+
}
|
|
511
559
|
const cssProp = camelToKebab(k);
|
|
512
560
|
if (isTemplateString(String(v))) {
|
|
513
|
-
parts.push(cssProp
|
|
561
|
+
parts.push(`${cssProp}: ${mapRefsToLit(String(v))}`);
|
|
514
562
|
} else {
|
|
515
|
-
parts.push(cssProp
|
|
563
|
+
parts.push(`${cssProp}: ${v}`);
|
|
516
564
|
}
|
|
517
565
|
}
|
|
518
566
|
if (parts.length > 0) {
|
|
519
|
-
attrs +=
|
|
567
|
+
attrs += ` style="${parts.join("; ")}"`;
|
|
520
568
|
}
|
|
521
569
|
}
|
|
522
570
|
|
|
523
571
|
// Event handlers in map template
|
|
524
572
|
for (const [prop, val] of Object.entries(def)) {
|
|
525
|
-
if (!prop.startsWith("on") || prop === "observedAttributes")
|
|
573
|
+
if (!prop.startsWith("on") || prop === "observedAttributes") {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
526
576
|
const eventName = prop.slice(2).toLowerCase();
|
|
527
577
|
if (isRefObject(val)) {
|
|
528
578
|
const key = refToBindingKey((val as JxMutableNode).$ref as string);
|
|
529
|
-
attrs +=
|
|
579
|
+
attrs += ` @${eventName}=\${(e) => { state.$map = { item, index }; on.${key}(e); }}`;
|
|
530
580
|
} else if (val && typeof val === "object" && (val as JxMutableNode).$prototype === "Function") {
|
|
531
581
|
const body = mapRefsToLit((val as JxMutableNode).body as string);
|
|
532
|
-
attrs +=
|
|
582
|
+
attrs += ` @${eventName}=\${(e) => { ${body} }}`;
|
|
533
583
|
} else if (val && typeof val === "object" && "$expression" in /** @type {any} */ val) {
|
|
534
584
|
const compiled = compileExpression(
|
|
535
585
|
(val as Record<string, unknown>).$expression as ExpressionNode,
|
|
536
586
|
{
|
|
537
|
-
statePrefix: "state",
|
|
538
587
|
eventParam: "e",
|
|
588
|
+
statePrefix: "state",
|
|
539
589
|
},
|
|
540
590
|
);
|
|
541
|
-
attrs +=
|
|
591
|
+
attrs += ` @${eventName}=\${(e) => { state.$map = { item, index }; ${compiled}; }}`;
|
|
542
592
|
}
|
|
543
593
|
}
|
|
544
594
|
|
|
545
595
|
// Non-reserved properties that render as attributes
|
|
546
596
|
if (def.contentEditable) {
|
|
547
|
-
attrs +=
|
|
597
|
+
attrs += ` contenteditable="${def.contentEditable}"`;
|
|
548
598
|
}
|
|
549
599
|
|
|
550
600
|
// Inner content
|
|
@@ -555,22 +605,23 @@ function emitLitMapTemplate(def: JxMutableNode) {
|
|
|
555
605
|
inner = mapRefsToLit(tc);
|
|
556
606
|
} else if (isRefObject(def.textContent)) {
|
|
557
607
|
const path = refToBindingKey((def.textContent as unknown as JxMutableNode).$ref as string);
|
|
558
|
-
inner =
|
|
608
|
+
inner = `\${state.${path}}`;
|
|
559
609
|
} else {
|
|
560
610
|
inner = escapeHtml(tc);
|
|
561
611
|
}
|
|
562
612
|
} else if (def.innerHTML) {
|
|
563
613
|
inner = mapRefsToLit(String(def.innerHTML));
|
|
564
614
|
} else if (Array.isArray(def.children)) {
|
|
565
|
-
inner =
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
"\n ";
|
|
615
|
+
inner = `\n ${def.children
|
|
616
|
+
.map((c) => emitLitMapTemplate(c as JxMutableNode))
|
|
617
|
+
.join("\n ")}\n `;
|
|
569
618
|
}
|
|
570
619
|
|
|
571
620
|
const voidTags = new Set(["input", "br", "hr", "img", "meta", "link"]);
|
|
572
|
-
if (voidTags.has(tag))
|
|
573
|
-
|
|
621
|
+
if (voidTags.has(tag)) {
|
|
622
|
+
return `<${tag}${attrs}>`;
|
|
623
|
+
}
|
|
624
|
+
return `<${tag}${attrs}>${inner}</${tag}>`;
|
|
574
625
|
}
|
|
575
626
|
|
|
576
627
|
/**
|
|
@@ -580,7 +631,7 @@ function emitLitMapTemplate(def: JxMutableNode) {
|
|
|
580
631
|
* @returns {string}
|
|
581
632
|
*/
|
|
582
633
|
function mapRefsToLit(str: string) {
|
|
583
|
-
return str.
|
|
634
|
+
return str.replaceAll("$map.", "");
|
|
584
635
|
}
|
|
585
636
|
|
|
586
637
|
// ─── JS module generation ─────────────────────────────────────────────────────
|
|
@@ -605,10 +656,10 @@ function mapRefsToLit(str: string) {
|
|
|
605
656
|
* @returns {string}
|
|
606
657
|
*/
|
|
607
658
|
function emitClientModule(
|
|
608
|
-
stateEntries: [string,
|
|
659
|
+
stateEntries: [string, JsonValue | undefined][],
|
|
609
660
|
computedEntries: [string, string][],
|
|
610
661
|
bindEntries: [string, string][],
|
|
611
|
-
onEntries: [string,
|
|
662
|
+
onEntries: [string, HandlerDef][],
|
|
612
663
|
initBlocks: string[],
|
|
613
664
|
srcImportMap: Map<string, Set<string>>,
|
|
614
665
|
counter: {
|
|
@@ -623,32 +674,34 @@ function emitClientModule(
|
|
|
623
674
|
_reactivitySrc: string,
|
|
624
675
|
) {
|
|
625
676
|
const lines: string[] = [];
|
|
626
|
-
const needsLit = counter
|
|
677
|
+
const { needsLit } = counter;
|
|
627
678
|
const needsComputed = computedEntries.length > 0;
|
|
628
679
|
|
|
629
680
|
lines.push("// Generated by @jxsuite/compiler — do not edit manually");
|
|
630
681
|
|
|
631
682
|
// Reactivity imports
|
|
632
683
|
const reactivityImports = ["reactive", "effect"];
|
|
633
|
-
if (needsComputed)
|
|
634
|
-
|
|
684
|
+
if (needsComputed) {
|
|
685
|
+
reactivityImports.push("computed");
|
|
686
|
+
}
|
|
687
|
+
lines.push(`import { ${reactivityImports.join(", ")} } from '@vue/reactivity';`);
|
|
635
688
|
|
|
636
|
-
//
|
|
689
|
+
// Lit-html imports (only when arrays are present)
|
|
637
690
|
if (needsLit) {
|
|
638
691
|
lines.push("import { html, render } from 'lit-html';");
|
|
639
692
|
}
|
|
640
693
|
|
|
641
694
|
// $src imports
|
|
642
695
|
for (const [src, names] of srcImportMap) {
|
|
643
|
-
lines.push(
|
|
696
|
+
lines.push(`import { ${[...names].join(", ")} } from '${src}';`);
|
|
644
697
|
}
|
|
645
698
|
|
|
646
699
|
lines.push("");
|
|
647
700
|
|
|
648
|
-
//
|
|
701
|
+
// State — reactive state
|
|
649
702
|
lines.push("const state = reactive({");
|
|
650
703
|
for (const [key, val] of stateEntries) {
|
|
651
|
-
lines.push(
|
|
704
|
+
lines.push(` ${key}: ${JSON.stringify(val)},`);
|
|
652
705
|
}
|
|
653
706
|
lines.push("});");
|
|
654
707
|
lines.push("");
|
|
@@ -664,16 +717,16 @@ function emitClientModule(
|
|
|
664
717
|
// Computed signals on state
|
|
665
718
|
if (computedEntries.length > 0) {
|
|
666
719
|
for (const [key, expr] of computedEntries) {
|
|
667
|
-
lines.push(
|
|
720
|
+
lines.push(`state.${key} = computed(${expr});`);
|
|
668
721
|
}
|
|
669
722
|
lines.push("");
|
|
670
723
|
}
|
|
671
724
|
|
|
672
|
-
//
|
|
725
|
+
// Bind — DOM getters
|
|
673
726
|
if (bindEntries.length > 0) {
|
|
674
727
|
lines.push("const bind = {");
|
|
675
728
|
for (const [key, expr] of bindEntries) {
|
|
676
|
-
lines.push(
|
|
729
|
+
lines.push(` ${key}: ${expr},`);
|
|
677
730
|
}
|
|
678
731
|
lines.push("};");
|
|
679
732
|
} else {
|
|
@@ -681,22 +734,16 @@ function emitClientModule(
|
|
|
681
734
|
}
|
|
682
735
|
lines.push("");
|
|
683
736
|
|
|
684
|
-
//
|
|
737
|
+
// On — event handlers
|
|
685
738
|
if (onEntries.length > 0) {
|
|
686
739
|
lines.push("const on = {");
|
|
687
740
|
for (const [key, def] of onEntries) {
|
|
688
741
|
const argNames = def.args ?? ["state"];
|
|
689
742
|
const callArgs = argNames.map((a: string) => (a === "state" ? "state" : "e")).join(", ");
|
|
690
743
|
lines.push(
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
argNames.join(", ") +
|
|
695
|
-
") => { " +
|
|
696
|
-
def.body +
|
|
697
|
-
" }; fn(" +
|
|
698
|
-
callArgs +
|
|
699
|
-
"); },",
|
|
744
|
+
` ${key}: (e) => { const fn = (${argNames.join(", ")}) => { ${def.body} }; fn(${
|
|
745
|
+
callArgs
|
|
746
|
+
}); },`,
|
|
700
747
|
);
|
|
701
748
|
}
|
|
702
749
|
lines.push("};");
|
|
@@ -746,42 +793,47 @@ function emitClientModule(
|
|
|
746
793
|
* @param {JxMutableNode} def
|
|
747
794
|
* @returns {string}
|
|
748
795
|
*/
|
|
749
|
-
function emitRequestInit(key: string, def:
|
|
750
|
-
const url = def
|
|
796
|
+
function emitRequestInit(key: string, def: JxPrototypeDef) {
|
|
797
|
+
const { url } = def;
|
|
751
798
|
const method = def.method ?? "GET";
|
|
752
799
|
const isTemplateUrl = isTemplateString(url);
|
|
753
800
|
|
|
754
801
|
if (def.manual) {
|
|
755
|
-
return
|
|
802
|
+
return `// ${key}: manual Request — fetch triggered by user action`;
|
|
756
803
|
}
|
|
757
804
|
|
|
758
|
-
const lines: string[] = [
|
|
759
|
-
|
|
760
|
-
|
|
805
|
+
const lines: string[] = [
|
|
806
|
+
`// ${key}: auto-fetch from ${isTemplateUrl ? "(dynamic URL)" : url}`,
|
|
807
|
+
"effect(() => {",
|
|
808
|
+
];
|
|
761
809
|
|
|
762
810
|
if (isTemplateUrl) {
|
|
763
|
-
lines.push(
|
|
811
|
+
lines.push(` const url = \`${url}\`;`);
|
|
764
812
|
lines.push(' if (!url || url === "undefined" || url.includes("undefined")) return;');
|
|
765
813
|
} else {
|
|
766
|
-
lines.push(
|
|
814
|
+
lines.push(` const url = ${JSON.stringify(url)};`);
|
|
767
815
|
}
|
|
768
816
|
|
|
769
817
|
const fetchOpts: string[] = [];
|
|
770
|
-
if (method !== "GET")
|
|
771
|
-
|
|
818
|
+
if (method !== "GET") {
|
|
819
|
+
fetchOpts.push(`method: ${JSON.stringify(method)}`);
|
|
820
|
+
}
|
|
821
|
+
if (def.headers) {
|
|
822
|
+
fetchOpts.push(`headers: ${JSON.stringify(def.headers)}`);
|
|
823
|
+
}
|
|
772
824
|
if (def.body) {
|
|
773
825
|
const bodyStr =
|
|
774
826
|
typeof def.body === "object"
|
|
775
827
|
? JSON.stringify(JSON.stringify(def.body))
|
|
776
828
|
: JSON.stringify(def.body);
|
|
777
|
-
fetchOpts.push(
|
|
829
|
+
fetchOpts.push(`body: ${bodyStr}`);
|
|
778
830
|
}
|
|
779
831
|
|
|
780
|
-
const optsStr = fetchOpts.length > 0 ?
|
|
781
|
-
lines.push(
|
|
832
|
+
const optsStr = fetchOpts.length > 0 ? `, { ${fetchOpts.join(", ")} }` : "";
|
|
833
|
+
lines.push(` fetch(url${optsStr})`);
|
|
782
834
|
lines.push(" .then(r => r.ok ? r.json() : Promise.reject(r.statusText))");
|
|
783
|
-
lines.push(
|
|
784
|
-
lines.push(
|
|
835
|
+
lines.push(` .then(d => { state.${key} = d; })`);
|
|
836
|
+
lines.push(` .catch(e => { state.${key} = { error: String(e) }; });`);
|
|
785
837
|
lines.push("});");
|
|
786
838
|
|
|
787
839
|
return lines.join("\n");
|
|
@@ -795,25 +847,20 @@ function emitRequestInit(key: string, def: JxMutableNode) {
|
|
|
795
847
|
* @returns {string}
|
|
796
848
|
*/
|
|
797
849
|
function emitStorageInit(key: string, storeName: string, storageKey: string, defaultVal: unknown) {
|
|
798
|
-
const lines: string[] = [
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
"
|
|
811
|
-
|
|
812
|
-
lines.push(
|
|
813
|
-
" else " + storeName + ".setItem(" + JSON.stringify(storageKey) + ", JSON.stringify(v));",
|
|
814
|
-
);
|
|
815
|
-
lines.push(" } catch {}");
|
|
816
|
-
lines.push("});");
|
|
850
|
+
const lines: string[] = [
|
|
851
|
+
`// ${key}: ${storeName} (key: "${storageKey}")`,
|
|
852
|
+
"try {",
|
|
853
|
+
` const _s = ${storeName}.getItem(${JSON.stringify(storageKey)});`,
|
|
854
|
+
` state.${key} = _s !== null ? JSON.parse(_s) : ${JSON.stringify(defaultVal)};`,
|
|
855
|
+
`} catch { state.${key} = ${JSON.stringify(defaultVal)}; }`,
|
|
856
|
+
"effect(() => {",
|
|
857
|
+
` const v = state.${key};`,
|
|
858
|
+
" try {",
|
|
859
|
+
` if (v === null) ${storeName}.removeItem(${JSON.stringify(storageKey)});`,
|
|
860
|
+
` else ${storeName}.setItem(${JSON.stringify(storageKey)}, JSON.stringify(v));`,
|
|
861
|
+
" } catch {}",
|
|
862
|
+
"});",
|
|
863
|
+
];
|
|
817
864
|
return lines.join("\n");
|
|
818
865
|
}
|
|
819
866
|
|
|
@@ -824,21 +871,16 @@ function emitStorageInit(key: string, storeName: string, storageKey: string, def
|
|
|
824
871
|
* @returns {string}
|
|
825
872
|
*/
|
|
826
873
|
function emitCookieInit(key: string, cookieName: string, defaultVal: unknown) {
|
|
827
|
-
const lines: string[] = [
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
JSON.stringify(defaultVal) +
|
|
838
|
-
"; }",
|
|
839
|
-
);
|
|
840
|
-
lines.push(" catch { state." + key + " = _m ? _m[1] : " + JSON.stringify(defaultVal) + "; }");
|
|
841
|
-
lines.push("}");
|
|
874
|
+
const lines: string[] = [
|
|
875
|
+
`// ${key}: Cookie (name: "${cookieName}")`,
|
|
876
|
+
"{",
|
|
877
|
+
` const _m = document.cookie.match(new RegExp("(?:^|; )${cookieName}=([^;]*)"));`,
|
|
878
|
+
` try { state.${key} = _m ? JSON.parse(decodeURIComponent(_m[1])) : ${JSON.stringify(
|
|
879
|
+
defaultVal,
|
|
880
|
+
)}; }`,
|
|
881
|
+
` catch { state.${key} = _m ? _m[1] : ${JSON.stringify(defaultVal)}; }`,
|
|
882
|
+
"}",
|
|
883
|
+
];
|
|
842
884
|
return lines.join("\n");
|
|
843
885
|
}
|
|
844
886
|
|
|
@@ -850,9 +892,9 @@ function emitCookieInit(key: string, cookieName: string, defaultVal: unknown) {
|
|
|
850
892
|
*/
|
|
851
893
|
function refToBindingKey(ref: string) {
|
|
852
894
|
if (ref.startsWith("#/state/")) {
|
|
853
|
-
return ref.slice("#/state/".length).
|
|
895
|
+
return ref.slice("#/state/".length).replaceAll("/", "_");
|
|
854
896
|
}
|
|
855
|
-
return ref.
|
|
897
|
+
return ref.replaceAll("/", "_");
|
|
856
898
|
}
|
|
857
899
|
|
|
858
900
|
/**
|
|
@@ -861,12 +903,14 @@ function refToBindingKey(ref: string) {
|
|
|
861
903
|
* @param {string} ref
|
|
862
904
|
*/
|
|
863
905
|
function addRefBinding(bindings: Map<string, string>, key: string, ref: string) {
|
|
864
|
-
if (bindings.has(key))
|
|
906
|
+
if (bindings.has(key)) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
865
909
|
if (ref.startsWith("#/state/")) {
|
|
866
910
|
const path = ref.slice("#/state/".length);
|
|
867
911
|
const parts = path.split("/");
|
|
868
|
-
bindings.set(key,
|
|
912
|
+
bindings.set(key, `() => state.${parts.join(".")}`);
|
|
869
913
|
} else {
|
|
870
|
-
bindings.set(key,
|
|
914
|
+
bindings.set(key, `() => state.${ref}`);
|
|
871
915
|
}
|
|
872
916
|
}
|