@jxsuite/compiler 0.28.2 → 0.28.3
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 +144 -63
- 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
|
@@ -5,67 +5,100 @@
|
|
|
5
5
|
* state and lit-html for rendering.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { RESERVED_KEYS, camelToKebab } from "@jxsuite/runtime";
|
|
9
9
|
import {
|
|
10
|
-
escapeHtml,
|
|
11
|
-
tagNameToClassName,
|
|
12
|
-
isSchemaOnly,
|
|
13
10
|
collectStyles,
|
|
14
11
|
compileExpression,
|
|
12
|
+
escapeHtml,
|
|
15
13
|
isMutating,
|
|
14
|
+
isSchemaOnly,
|
|
15
|
+
tagNameToClassName,
|
|
16
16
|
} from "../shared.ts";
|
|
17
|
+
import {
|
|
18
|
+
isExpressionDef,
|
|
19
|
+
isFunctionDef,
|
|
20
|
+
isMappedArray,
|
|
21
|
+
isRef,
|
|
22
|
+
paramNames,
|
|
23
|
+
} from "@jxsuite/schema/guards";
|
|
24
|
+
import { parseJxDocument } from "@jxsuite/schema/parse";
|
|
17
25
|
import type { ExpressionNode } from "../shared.ts";
|
|
18
|
-
import type {
|
|
26
|
+
import type { FormatRegistry } from "@jxsuite/schema/format-registry";
|
|
27
|
+
import type {
|
|
28
|
+
JxDocument,
|
|
29
|
+
JxExpressionDef,
|
|
30
|
+
JxFunctionDef,
|
|
31
|
+
JxMappedArray,
|
|
32
|
+
JxMutableNode,
|
|
33
|
+
JxStyle,
|
|
34
|
+
} from "@jxsuite/schema/types";
|
|
35
|
+
|
|
36
|
+
/** Options accepted by compileElement. */
|
|
37
|
+
export interface CompileElementOptions {
|
|
38
|
+
/** Resolve an `$elements` ref path to its emitted JS module path. */
|
|
39
|
+
resolveElementPath?: (refPath: string, currentDir: string | null) => string;
|
|
40
|
+
$media?: Record<string, string>;
|
|
41
|
+
formats?: FormatRegistry;
|
|
42
|
+
basePath?: string | null;
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}
|
|
19
45
|
|
|
20
46
|
/**
|
|
21
47
|
* Compile a Jx custom element document to a JS module string.
|
|
22
48
|
*
|
|
23
|
-
* @param {string |
|
|
24
|
-
* @param {
|
|
49
|
+
* @param {string | JxDocument} sourcePath - Path to .json file or raw object
|
|
50
|
+
* @param {CompileElementOptions} [opts]
|
|
25
51
|
* @returns {Promise<{ files: { path: string; content: string; tagName: string }[] }>}
|
|
26
52
|
*/
|
|
27
|
-
export async function compileElement(
|
|
28
|
-
|
|
53
|
+
export async function compileElement(
|
|
54
|
+
sourcePath: string | JxDocument,
|
|
55
|
+
opts: CompileElementOptions = {},
|
|
56
|
+
) {
|
|
57
|
+
const { resolveElementPath, $media: optsMedia, formats } = opts;
|
|
29
58
|
/** @type {{ path: string; content: string; tagName: string }[]} */
|
|
30
59
|
const files: { path: string; content: string; tagName: string }[] = [];
|
|
31
|
-
const visited
|
|
60
|
+
const visited = new Set<string>();
|
|
32
61
|
|
|
33
62
|
/**
|
|
34
|
-
* @param {string |
|
|
63
|
+
* @param {string | JxDocument} srcPath
|
|
35
64
|
* @param {string | null} parentDir
|
|
36
65
|
*/
|
|
37
|
-
async function processElement(srcPath: string |
|
|
38
|
-
|
|
39
|
-
let
|
|
40
|
-
/** @type {string | null} */
|
|
41
|
-
let filePath;
|
|
66
|
+
async function processElement(srcPath: string | JxDocument, parentDir: string | null) {
|
|
67
|
+
let doc: JxDocument;
|
|
68
|
+
let filePath: string | null;
|
|
42
69
|
if (typeof srcPath === "string") {
|
|
43
70
|
const { readFileSync } = await import("node:fs");
|
|
44
71
|
const { resolve } = await import("node:path");
|
|
45
72
|
filePath = parentDir ? resolve(parentDir, srcPath) : resolve(srcPath);
|
|
46
|
-
if (visited.has(filePath))
|
|
73
|
+
if (visited.has(filePath)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
47
76
|
visited.add(filePath);
|
|
48
77
|
if (filePath.endsWith(".json")) {
|
|
49
|
-
doc =
|
|
78
|
+
doc = parseJxDocument(readFileSync(filePath, "utf8"), filePath);
|
|
50
79
|
} else {
|
|
51
80
|
const { extname } = await import("node:path");
|
|
52
81
|
const ext = extname(filePath).toLowerCase();
|
|
53
|
-
const
|
|
54
|
-
const entry = registry?.byExtension?.(ext, "parse");
|
|
82
|
+
const entry = formats?.byExtension?.(ext, "parse");
|
|
55
83
|
if (!entry) {
|
|
56
84
|
const { unknownFormatError } = await import("../site/format-host.ts");
|
|
57
85
|
throw unknownFormatError(filePath, ext);
|
|
58
86
|
}
|
|
59
|
-
|
|
87
|
+
// Format plugins contractually parse source text into a Jx document.
|
|
88
|
+
doc = (await entry.call("parse", readFileSync(filePath, "utf8"))) as JxDocument;
|
|
60
89
|
}
|
|
61
90
|
} else {
|
|
62
91
|
doc = srcPath;
|
|
63
92
|
filePath = null;
|
|
64
|
-
if (visited.has(doc.tagName
|
|
65
|
-
|
|
93
|
+
if (doc.tagName && visited.has(doc.tagName)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (doc.tagName) {
|
|
97
|
+
visited.add(doc.tagName);
|
|
98
|
+
}
|
|
66
99
|
}
|
|
67
100
|
|
|
68
|
-
const tagName = doc
|
|
101
|
+
const { tagName } = doc;
|
|
69
102
|
if (!tagName || !tagName.includes("-")) {
|
|
70
103
|
throw new Error(`compileElement: tagName "${tagName}" must contain a hyphen`);
|
|
71
104
|
}
|
|
@@ -77,20 +110,19 @@ export async function compileElement(sourcePath: string | any, opts: Record<stri
|
|
|
77
110
|
const elementImports: string[] = [];
|
|
78
111
|
if (Array.isArray(doc.$elements)) {
|
|
79
112
|
for (const elRef of doc.$elements) {
|
|
80
|
-
const refPath = (elRef
|
|
81
|
-
if (typeof refPath !== "string")
|
|
113
|
+
const refPath = isRef(elRef) ? elRef.$ref : elRef;
|
|
114
|
+
if (typeof refPath !== "string") {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
82
117
|
|
|
83
118
|
if (currentDir) {
|
|
84
119
|
await processElement(refPath, currentDir);
|
|
85
120
|
}
|
|
86
121
|
|
|
87
122
|
/** @type {string} */
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
} else {
|
|
92
|
-
importPath = refPath.replace(/\.json$/, ".js");
|
|
93
|
-
}
|
|
123
|
+
const importPath = resolveElementPath
|
|
124
|
+
? resolveElementPath(refPath, currentDir)
|
|
125
|
+
: refPath.replace(/\.json$/, ".js");
|
|
94
126
|
elementImports.push(importPath);
|
|
95
127
|
}
|
|
96
128
|
}
|
|
@@ -101,10 +133,10 @@ export async function compileElement(sourcePath: string | any, opts: Record<stri
|
|
|
101
133
|
}
|
|
102
134
|
const jsContent = emitElementModule(doc, className, elementImports);
|
|
103
135
|
const outputPath = filePath ? filePath.replace(/\.json$/, ".js") : `${tagName}.js`;
|
|
104
|
-
files.push({
|
|
136
|
+
files.push({ content: jsContent, path: outputPath, tagName });
|
|
105
137
|
}
|
|
106
138
|
|
|
107
|
-
await processElement(sourcePath,
|
|
139
|
+
await processElement(sourcePath, opts.basePath ?? null);
|
|
108
140
|
return { files };
|
|
109
141
|
}
|
|
110
142
|
|
|
@@ -120,17 +152,25 @@ export async function compileElement(sourcePath: string | any, opts: Record<stri
|
|
|
120
152
|
* }>}
|
|
121
153
|
*/
|
|
122
154
|
export async function compileElementPage(
|
|
123
|
-
sourcePath: string |
|
|
124
|
-
opts:
|
|
155
|
+
sourcePath: string | JxDocument,
|
|
156
|
+
opts: {
|
|
157
|
+
title?: string;
|
|
158
|
+
reactivitySrc?: string;
|
|
159
|
+
litHtmlSrc?: string;
|
|
160
|
+
[key: string]: unknown;
|
|
161
|
+
} = {},
|
|
125
162
|
) {
|
|
126
163
|
const {
|
|
127
164
|
title = "Jx App",
|
|
128
165
|
reactivitySrc = "https://esm.sh/@vue/reactivity@3.5.13",
|
|
129
166
|
litHtmlSrc = "https://esm.sh/lit-html@3.3.0",
|
|
130
|
-
} = opts
|
|
167
|
+
} = opts;
|
|
131
168
|
|
|
132
169
|
const result = await compileElement(sourcePath, opts);
|
|
133
|
-
const root = result.files
|
|
170
|
+
const root = result.files.at(-1);
|
|
171
|
+
if (!root) {
|
|
172
|
+
throw new Error("compileElementPage: no element modules were produced");
|
|
173
|
+
}
|
|
134
174
|
|
|
135
175
|
const { basename } = await import("node:path");
|
|
136
176
|
const rootScript = basename(root.path);
|
|
@@ -156,7 +196,7 @@ export async function compileElementPage(
|
|
|
156
196
|
</body>
|
|
157
197
|
</html>`;
|
|
158
198
|
|
|
159
|
-
return {
|
|
199
|
+
return { files: result.files, html: htmlContent };
|
|
160
200
|
}
|
|
161
201
|
|
|
162
202
|
// ─── Element code generation helpers ──────────────────────────────────────────
|
|
@@ -179,7 +219,7 @@ function extractInitialValue(def: JxMutableNode) {
|
|
|
179
219
|
}
|
|
180
220
|
// Pure schema-only type definitions — skip (no runtime value)
|
|
181
221
|
if (isSchemaOnly(def)) {
|
|
182
|
-
return
|
|
222
|
+
return; // Caller should skip this entry
|
|
183
223
|
}
|
|
184
224
|
// $prototype entries (LocalStorage, SessionStorage, Request, etc.)
|
|
185
225
|
if (def.$prototype === "LocalStorage" || def.$prototype === "SessionStorage") {
|
|
@@ -195,33 +235,32 @@ function extractInitialValue(def: JxMutableNode) {
|
|
|
195
235
|
/**
|
|
196
236
|
* Generate a complete ES module string for a custom element.
|
|
197
237
|
*
|
|
198
|
-
* @param {
|
|
238
|
+
* @param {JxDocument} doc
|
|
199
239
|
* @param {string} className
|
|
200
240
|
* @param {string[]} elementImports
|
|
201
241
|
* @returns {string}
|
|
202
242
|
*/
|
|
203
|
-
export function emitElementModule(
|
|
204
|
-
|
|
205
|
-
className: string,
|
|
206
|
-
elementImports: string[],
|
|
207
|
-
) {
|
|
208
|
-
const lines: string[] = [];
|
|
243
|
+
export function emitElementModule(doc: JxDocument, className: string, elementImports: string[]) {
|
|
244
|
+
const lines: string[] = ["// Generated by @jxsuite/compiler — do not edit manually"];
|
|
209
245
|
|
|
210
|
-
|
|
211
|
-
|
|
246
|
+
if (doc.$id) {
|
|
247
|
+
lines.push(`// Source: ${doc.$id}`);
|
|
248
|
+
}
|
|
212
249
|
|
|
213
250
|
for (const imp of elementImports) {
|
|
214
251
|
lines.push(`import '${imp}';`);
|
|
215
252
|
}
|
|
216
253
|
|
|
217
254
|
// Collect $src imports from state entries before emitting other imports
|
|
218
|
-
const srcImportMap
|
|
255
|
+
const srcImportMap = new Map<string, string[]>();
|
|
219
256
|
const defs = doc.state ?? {};
|
|
220
257
|
for (const [key, def] of Object.entries(defs)) {
|
|
221
258
|
const d = def as JxMutableNode;
|
|
222
259
|
if (d && typeof d === "object" && !Array.isArray(d) && d.$prototype === "Function" && d.$src) {
|
|
223
260
|
const srcPath = d.$src;
|
|
224
|
-
if (!srcImportMap.has(srcPath))
|
|
261
|
+
if (!srcImportMap.has(srcPath)) {
|
|
262
|
+
srcImportMap.set(srcPath, []);
|
|
263
|
+
}
|
|
225
264
|
(srcImportMap.get(srcPath) as string[]).push(key);
|
|
226
265
|
}
|
|
227
266
|
}
|
|
@@ -241,19 +280,19 @@ export function emitElementModule(
|
|
|
241
280
|
lines.push(" super();");
|
|
242
281
|
|
|
243
282
|
const stateEntries: [string, string][] = [];
|
|
244
|
-
const computedEntries: [string,
|
|
245
|
-
const functionEntries: [string,
|
|
283
|
+
const computedEntries: [string, JxExpressionDef | JxFunctionDef][] = [];
|
|
284
|
+
const functionEntries: [string, JxExpressionDef | JxFunctionDef][] = [];
|
|
246
285
|
|
|
247
286
|
for (const [key, def] of Object.entries(defs)) {
|
|
248
287
|
const d = def as JxMutableNode;
|
|
249
|
-
if (
|
|
250
|
-
const node =
|
|
288
|
+
if (isExpressionDef(d)) {
|
|
289
|
+
const node = d.$expression as ExpressionNode;
|
|
251
290
|
if (isMutating(node.operator)) {
|
|
252
291
|
functionEntries.push([key, d]);
|
|
253
292
|
} else {
|
|
254
293
|
computedEntries.push([key, d]);
|
|
255
294
|
}
|
|
256
|
-
} else if (
|
|
295
|
+
} else if (isFunctionDef(d)) {
|
|
257
296
|
if (typeof d.body === "string" && d.body.includes("return")) {
|
|
258
297
|
computedEntries.push([key, d]);
|
|
259
298
|
} else {
|
|
@@ -278,46 +317,46 @@ export function emitElementModule(
|
|
|
278
317
|
// Emit functions: this.state.fnName = (state) => { body } or imported $src function
|
|
279
318
|
for (const [key, def] of functionEntries) {
|
|
280
319
|
lines.push("");
|
|
281
|
-
if (
|
|
282
|
-
const compiled = compileExpression(def.$expression, {
|
|
283
|
-
statePrefix: "s",
|
|
320
|
+
if (isExpressionDef(def)) {
|
|
321
|
+
const compiled = compileExpression(def.$expression as ExpressionNode, {
|
|
284
322
|
eventParam: "e",
|
|
323
|
+
statePrefix: "s",
|
|
285
324
|
});
|
|
286
325
|
lines.push(` this.state.${key} = (s, e) => { ${compiled}; };`);
|
|
287
|
-
} else if (def.$src) {
|
|
288
|
-
// $src function — wrap imported function so it receives state
|
|
289
|
-
const args = def.parameters ?? def.arguments ?? ["state"];
|
|
290
|
-
const paramList = args.join(", ");
|
|
291
|
-
lines.push(` this.state.${key} = (${paramList}) => ${key}(${paramList});`);
|
|
292
326
|
} else {
|
|
293
|
-
const args = def.parameters
|
|
327
|
+
const args = def.parameters ? paramNames(def.parameters) : (def.arguments ?? ["state"]);
|
|
294
328
|
const paramList = args.join(", ");
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
329
|
+
if (def.$src) {
|
|
330
|
+
// $src function — wrap imported function so it receives state
|
|
331
|
+
lines.push(` this.state.${key} = (${paramList}) => ${key}(${paramList});`);
|
|
332
|
+
} else {
|
|
333
|
+
lines.push(` this.state.${key} = (${paramList}) => {`);
|
|
334
|
+
lines.push(` ${def.body ?? ""}`);
|
|
335
|
+
lines.push(" };");
|
|
336
|
+
}
|
|
298
337
|
}
|
|
299
338
|
}
|
|
300
339
|
|
|
301
340
|
// Emit computed signals — $src or inline body
|
|
302
341
|
for (const [key, def] of computedEntries) {
|
|
303
342
|
lines.push("");
|
|
304
|
-
if (
|
|
305
|
-
const compiled = compileExpression(def.$expression, {
|
|
306
|
-
statePrefix: "this.state",
|
|
343
|
+
if (isExpressionDef(def)) {
|
|
344
|
+
const compiled = compileExpression(def.$expression as ExpressionNode, {
|
|
307
345
|
eventParam: "e",
|
|
346
|
+
statePrefix: "this.state",
|
|
308
347
|
});
|
|
309
348
|
lines.push(` this.state.${key} = computed(() => ${compiled});`);
|
|
310
349
|
} else if (def.$src) {
|
|
311
350
|
lines.push(` this.state.${key} = computed(() => ${key}(this.state));`);
|
|
312
351
|
} else {
|
|
313
352
|
lines.push(` this.state.${key} = computed(() => {`);
|
|
314
|
-
const body = def.body.
|
|
353
|
+
const body = (def.body ?? "").replaceAll("state.", "this.state.");
|
|
315
354
|
lines.push(` ${body}`);
|
|
316
355
|
lines.push(" });");
|
|
317
356
|
}
|
|
318
357
|
}
|
|
319
358
|
|
|
320
|
-
lines.push(" }"); //
|
|
359
|
+
lines.push(" }"); // End constructor
|
|
321
360
|
lines.push("");
|
|
322
361
|
|
|
323
362
|
// Collect CSS rules from children tree (assigns .jx-N classes to defs)
|
|
@@ -338,7 +377,7 @@ export function emitElementModule(
|
|
|
338
377
|
lines.push(" }");
|
|
339
378
|
lines.push("");
|
|
340
379
|
|
|
341
|
-
//
|
|
380
|
+
// ConnectedCallback
|
|
342
381
|
lines.push(" connectedCallback() {");
|
|
343
382
|
// Read $props from data-jx-props attribute (set by compiler for pre-rendered instances)
|
|
344
383
|
lines.push(" const _pa = this.getAttribute('data-jx-props');");
|
|
@@ -367,9 +406,12 @@ export function emitElementModule(
|
|
|
367
406
|
prop.startsWith("&") ||
|
|
368
407
|
prop.startsWith("[") ||
|
|
369
408
|
prop.startsWith("@")
|
|
370
|
-
)
|
|
409
|
+
) {
|
|
371
410
|
continue;
|
|
372
|
-
|
|
411
|
+
}
|
|
412
|
+
if (value === null || typeof value === "object") {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
373
415
|
const cssProp = camelToKebab(prop);
|
|
374
416
|
if (typeof value === "string" && value.includes("${")) {
|
|
375
417
|
dynamicStyles.push([cssProp, value]);
|
|
@@ -378,9 +420,9 @@ export function emitElementModule(
|
|
|
378
420
|
if (dynamicStyles.length > 0) {
|
|
379
421
|
lines.push(" effect(() => {");
|
|
380
422
|
for (const [cssProp, value] of dynamicStyles) {
|
|
381
|
-
const expr = value.
|
|
423
|
+
const expr = value.replaceAll(
|
|
382
424
|
/\$\{([^}]+)\}/g,
|
|
383
|
-
(_: string, e: string) =>
|
|
425
|
+
(_: string, e: string) => `\${${e.replaceAll("state.", "this.state.")}}`,
|
|
384
426
|
);
|
|
385
427
|
lines.push(` this.style['${cssProp}'] = \`${expr}\`;`);
|
|
386
428
|
}
|
|
@@ -410,7 +452,7 @@ export function emitElementModule(
|
|
|
410
452
|
lines.push(" }");
|
|
411
453
|
lines.push("");
|
|
412
454
|
|
|
413
|
-
//
|
|
455
|
+
// DisconnectedCallback
|
|
414
456
|
lines.push(" disconnectedCallback() {");
|
|
415
457
|
lines.push(" if (this.#dispose) { this.#dispose(); this.#dispose = null; }");
|
|
416
458
|
lines.push(" }");
|
|
@@ -426,23 +468,27 @@ export function emitElementModule(
|
|
|
426
468
|
/**
|
|
427
469
|
* Convert Jx children to lit-html template content.
|
|
428
470
|
*
|
|
429
|
-
* @param {
|
|
430
|
-
* @param {JxStyle |
|
|
471
|
+
* @param {JxMutableNode["children"]} children
|
|
472
|
+
* @param {JxStyle | null | undefined} parentStyle
|
|
431
473
|
* @param {string} indent
|
|
432
474
|
* @returns {string}
|
|
433
475
|
*/
|
|
434
476
|
function emitLitChildren(
|
|
435
|
-
children:
|
|
436
|
-
parentStyle: JxStyle |
|
|
477
|
+
children: JxMutableNode["children"],
|
|
478
|
+
parentStyle: JxStyle | null | undefined,
|
|
437
479
|
indent: string,
|
|
438
480
|
) {
|
|
439
|
-
if (!children)
|
|
481
|
+
if (!children) {
|
|
482
|
+
return "";
|
|
483
|
+
}
|
|
440
484
|
|
|
441
|
-
if ((children
|
|
442
|
-
return emitMappedArray(
|
|
485
|
+
if (isMappedArray(children)) {
|
|
486
|
+
return emitMappedArray(children, indent);
|
|
443
487
|
}
|
|
444
488
|
|
|
445
|
-
if (!Array.isArray(children))
|
|
489
|
+
if (!Array.isArray(children)) {
|
|
490
|
+
return "";
|
|
491
|
+
}
|
|
446
492
|
|
|
447
493
|
return children.map((child: JxMutableNode | string) => emitLitNode(child, indent)).join("\n");
|
|
448
494
|
}
|
|
@@ -455,13 +501,17 @@ function emitLitChildren(
|
|
|
455
501
|
function emitLitNode(def: JxMutableNode | string, indent: string) {
|
|
456
502
|
// String children are text nodes
|
|
457
503
|
if (typeof def === "string") {
|
|
458
|
-
if (def.includes("${"))
|
|
504
|
+
if (def.includes("${")) {
|
|
505
|
+
return `${indent}${toLitTextContent(def)}`;
|
|
506
|
+
}
|
|
459
507
|
return `${indent}${escapeHtml(def)}`;
|
|
460
508
|
}
|
|
461
509
|
if (typeof def === "number" || typeof def === "boolean") {
|
|
462
510
|
return `${indent}${escapeHtml(String(def))}`;
|
|
463
511
|
}
|
|
464
|
-
if (!def || typeof def !== "object")
|
|
512
|
+
if (!def || typeof def !== "object") {
|
|
513
|
+
return "";
|
|
514
|
+
}
|
|
465
515
|
|
|
466
516
|
const tag = def.tagName ?? "div";
|
|
467
517
|
|
|
@@ -469,7 +519,9 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
|
|
|
469
519
|
|
|
470
520
|
if (def.attributes) {
|
|
471
521
|
for (const [key, val] of Object.entries(def.attributes)) {
|
|
472
|
-
if (typeof val === "
|
|
522
|
+
if (val && typeof val === "object" && isRef(val)) {
|
|
523
|
+
parts.push(`${key}="\${${refToExpr(val.$ref)}}"`);
|
|
524
|
+
} else if (typeof val === "string" && val.includes("${")) {
|
|
473
525
|
parts.push(`${key}="${toLitExpr(val)}"`);
|
|
474
526
|
} else {
|
|
475
527
|
parts.push(`${key}="${val}"`);
|
|
@@ -477,8 +529,12 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
|
|
|
477
529
|
}
|
|
478
530
|
}
|
|
479
531
|
|
|
480
|
-
if (def.id)
|
|
481
|
-
|
|
532
|
+
if (def.id) {
|
|
533
|
+
parts.push(`id="${def.id}"`);
|
|
534
|
+
}
|
|
535
|
+
if (def.className) {
|
|
536
|
+
parts.push(`class="${def.className}"`);
|
|
537
|
+
}
|
|
482
538
|
|
|
483
539
|
for (const [key, val] of Object.entries(def)) {
|
|
484
540
|
if (
|
|
@@ -493,8 +549,9 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
|
|
|
493
549
|
key === "textContent" ||
|
|
494
550
|
key === "innerHTML" ||
|
|
495
551
|
key === "attributes"
|
|
496
|
-
)
|
|
552
|
+
) {
|
|
497
553
|
continue;
|
|
554
|
+
}
|
|
498
555
|
|
|
499
556
|
if (val && typeof val === "object" && (val as JxMutableNode).$ref) {
|
|
500
557
|
parts.push(`.${key}="\${${refToExpr((val as JxMutableNode).$ref as string)}}"`);
|
|
@@ -514,7 +571,9 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
|
|
|
514
571
|
}
|
|
515
572
|
|
|
516
573
|
for (const [key, val] of Object.entries(def)) {
|
|
517
|
-
if (!key.startsWith("on") || key === "observedAttributes")
|
|
574
|
+
if (!key.startsWith("on") || key === "observedAttributes") {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
518
577
|
const eventName = key.slice(2).toLowerCase();
|
|
519
578
|
if (val && typeof val === "object" && (val as JxMutableNode).$ref) {
|
|
520
579
|
parts.push(
|
|
@@ -524,22 +583,22 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
|
|
|
524
583
|
const compiled = compileExpression(
|
|
525
584
|
(val as Record<string, unknown>).$expression as ExpressionNode,
|
|
526
585
|
{
|
|
527
|
-
statePrefix: "s",
|
|
528
586
|
eventParam: "e",
|
|
587
|
+
statePrefix: "s",
|
|
529
588
|
},
|
|
530
589
|
);
|
|
531
590
|
parts.push(`@${eventName}="\${(e) => { ${compiled}; }}"`);
|
|
532
|
-
} else if (
|
|
533
|
-
parts.push(
|
|
534
|
-
`@${eventName}="\${(e) => { ${inlineHandlerBody(/** @type {JxMutableNode} */ val)} }}"`,
|
|
535
|
-
);
|
|
591
|
+
} else if (isFunctionDef(val)) {
|
|
592
|
+
parts.push(`@${eventName}="\${(e) => { ${inlineHandlerBody(val)} }}"`);
|
|
536
593
|
}
|
|
537
594
|
}
|
|
538
595
|
|
|
539
596
|
const styleStr = emitStyleString(def.style);
|
|
540
|
-
if (styleStr)
|
|
597
|
+
if (styleStr) {
|
|
598
|
+
parts.push(`style="${styleStr}"`);
|
|
599
|
+
}
|
|
541
600
|
|
|
542
|
-
const attrsStr = parts.length > 0 ?
|
|
601
|
+
const attrsStr = parts.length > 0 ? `\n${indent} ${parts.join(`\n${indent} `)}` : "";
|
|
543
602
|
|
|
544
603
|
const selfClosing = new Set(["input", "br", "hr", "img", "meta", "link"]);
|
|
545
604
|
if (selfClosing.has(tag)) {
|
|
@@ -547,35 +606,50 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
|
|
|
547
606
|
}
|
|
548
607
|
|
|
549
608
|
let inner = "";
|
|
550
|
-
if (def
|
|
609
|
+
if (def.$switch) {
|
|
610
|
+
const switchRef = isRef(def.$switch) ? def.$switch.$ref : (def.$switch as string);
|
|
611
|
+
const switchExpr = refToExpr(switchRef);
|
|
612
|
+
const cases = (def.cases ?? {}) as Record<string, JxMutableNode>;
|
|
613
|
+
const caseEntries: string[] = [];
|
|
614
|
+
for (const [key, caseDef] of Object.entries(cases)) {
|
|
615
|
+
if (!caseDef || isRef(caseDef)) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const renderedCase = emitLitNode(caseDef, `${indent} `);
|
|
619
|
+
caseEntries.push(` ${JSON.stringify(key)}: html\`\n${renderedCase}\n \``);
|
|
620
|
+
}
|
|
621
|
+
inner = `\${{\n${caseEntries.join(",\n")}\n}[${switchExpr}]}`;
|
|
622
|
+
} else if (def.textContent !== undefined) {
|
|
551
623
|
inner = toLitTextContent(def.textContent);
|
|
552
624
|
} else if (def.innerHTML !== undefined) {
|
|
553
625
|
inner = def.innerHTML;
|
|
554
626
|
} else if (def.children) {
|
|
555
|
-
inner =
|
|
627
|
+
inner = `\n${emitLitChildren(def.children, def.style, `${indent} `)}\n${indent}`;
|
|
556
628
|
}
|
|
557
629
|
|
|
558
630
|
return `${indent}<${tag}${attrsStr}\n${indent}>${inner}</${tag}>`;
|
|
559
631
|
}
|
|
560
632
|
|
|
561
633
|
/**
|
|
562
|
-
* @param {
|
|
634
|
+
* @param {JxMappedArray} arrayDef
|
|
563
635
|
* @param {string} indent
|
|
564
636
|
* @returns {string}
|
|
565
637
|
*/
|
|
566
|
-
function emitMappedArray(arrayDef:
|
|
567
|
-
const itemsExpr = arrayDef.items
|
|
638
|
+
function emitMappedArray(arrayDef: JxMappedArray, indent: string) {
|
|
639
|
+
const itemsExpr = isRef(arrayDef.items) ? refToExpr(arrayDef.items.$ref) : "ITEMS";
|
|
568
640
|
const mapDef = arrayDef.map;
|
|
569
641
|
|
|
570
|
-
if (!mapDef)
|
|
642
|
+
if (!mapDef) {
|
|
643
|
+
return "";
|
|
644
|
+
}
|
|
571
645
|
|
|
572
646
|
const tag = mapDef.tagName ?? "div";
|
|
573
647
|
const parts: string[] = [];
|
|
574
648
|
|
|
575
649
|
if (mapDef.$props) {
|
|
576
650
|
for (const [key, val] of Object.entries(mapDef.$props)) {
|
|
577
|
-
if (
|
|
578
|
-
parts.push(`.${key}="\${${mapRefToExpr(
|
|
651
|
+
if (isRef(val)) {
|
|
652
|
+
parts.push(`.${key}="\${${mapRefToExpr(val.$ref)}}"`);
|
|
579
653
|
} else {
|
|
580
654
|
parts.push(`.${key}="\${${JSON.stringify(val)}}"`);
|
|
581
655
|
}
|
|
@@ -583,36 +657,33 @@ function emitMappedArray(arrayDef: JxMutableNode, indent: string) {
|
|
|
583
657
|
}
|
|
584
658
|
|
|
585
659
|
const styleStr = emitStyleString(mapDef.style);
|
|
586
|
-
if (styleStr)
|
|
660
|
+
if (styleStr) {
|
|
661
|
+
parts.push(`style="${styleStr}"`);
|
|
662
|
+
}
|
|
587
663
|
|
|
588
664
|
for (const [key, val] of Object.entries(mapDef)) {
|
|
589
|
-
if (!key.startsWith("on"))
|
|
665
|
+
if (!key.startsWith("on")) {
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
590
668
|
const eventName = key.slice(2).toLowerCase();
|
|
591
|
-
if (
|
|
592
|
-
parts.push(
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
{
|
|
599
|
-
statePrefix: "s",
|
|
600
|
-
eventParam: "e",
|
|
601
|
-
},
|
|
602
|
-
);
|
|
669
|
+
if (isRef(val)) {
|
|
670
|
+
parts.push(`@${eventName}="\${(e) => ${refToExpr(val.$ref)}(s, e)}"`);
|
|
671
|
+
} else if (isExpressionDef(val)) {
|
|
672
|
+
const compiled = compileExpression(val.$expression as ExpressionNode, {
|
|
673
|
+
eventParam: "e",
|
|
674
|
+
statePrefix: "s",
|
|
675
|
+
});
|
|
603
676
|
parts.push(`@${eventName}="\${(e) => { ${compiled}; }}"`);
|
|
604
677
|
}
|
|
605
678
|
}
|
|
606
679
|
|
|
607
|
-
const attrsStr =
|
|
608
|
-
parts.length > 0 ? "\n" + indent + " " + parts.join("\n" + indent + " ") : "";
|
|
680
|
+
const attrsStr = parts.length > 0 ? `\n${indent} ${parts.join(`\n${indent} `)}` : "";
|
|
609
681
|
|
|
610
682
|
let inner = "";
|
|
611
683
|
if (mapDef.textContent !== undefined) {
|
|
612
684
|
inner = toLitTextContent(mapDef.textContent);
|
|
613
685
|
} else if (mapDef.children) {
|
|
614
|
-
inner =
|
|
615
|
-
"\n" + emitLitChildren(mapDef.children, null, indent + " ") + "\n" + indent + " ";
|
|
686
|
+
inner = `\n${emitLitChildren(mapDef.children, null, `${indent} `)}\n${indent} `;
|
|
616
687
|
}
|
|
617
688
|
|
|
618
689
|
return `${indent}\${${itemsExpr}.map((item, index) => html\`\n${indent} <${tag}${attrsStr}\n${indent} >${inner}</${tag}>\n${indent}\`)}`;
|
|
@@ -627,13 +698,13 @@ function emitMappedArray(arrayDef: JxMutableNode, indent: string) {
|
|
|
627
698
|
function refToExpr(ref: string) {
|
|
628
699
|
if (ref.startsWith("#/state/")) {
|
|
629
700
|
const path = ref.slice("#/state/".length);
|
|
630
|
-
return
|
|
701
|
+
return `s.${path.replaceAll("/", ".")}`;
|
|
631
702
|
}
|
|
632
703
|
if (ref.startsWith("$map/")) {
|
|
633
704
|
const path = ref.slice("$map/".length);
|
|
634
|
-
return path.
|
|
705
|
+
return path.replaceAll("/", ".");
|
|
635
706
|
}
|
|
636
|
-
return
|
|
707
|
+
return `s.${ref}`;
|
|
637
708
|
}
|
|
638
709
|
|
|
639
710
|
/**
|
|
@@ -642,7 +713,7 @@ function refToExpr(ref: string) {
|
|
|
642
713
|
*/
|
|
643
714
|
function mapRefToExpr(ref: string) {
|
|
644
715
|
if (ref.startsWith("$map/")) {
|
|
645
|
-
return ref.slice("$map/".length).
|
|
716
|
+
return ref.slice("$map/".length).replaceAll("/", ".");
|
|
646
717
|
}
|
|
647
718
|
return refToExpr(ref);
|
|
648
719
|
}
|
|
@@ -652,7 +723,7 @@ function mapRefToExpr(ref: string) {
|
|
|
652
723
|
* @returns {string}
|
|
653
724
|
*/
|
|
654
725
|
function toLitExpr(str: string) {
|
|
655
|
-
return str.
|
|
726
|
+
return str.replaceAll("state.", "s.");
|
|
656
727
|
}
|
|
657
728
|
|
|
658
729
|
/**
|
|
@@ -678,20 +749,22 @@ function toLitTextContent(value: unknown) {
|
|
|
678
749
|
}
|
|
679
750
|
|
|
680
751
|
/**
|
|
681
|
-
* @param {
|
|
752
|
+
* @param {import("@jxsuite/schema/types").JxFunctionDef} def
|
|
682
753
|
* @returns {string}
|
|
683
754
|
*/
|
|
684
|
-
function inlineHandlerBody(def:
|
|
755
|
+
function inlineHandlerBody(def: JxFunctionDef) {
|
|
685
756
|
const body = def.body ?? "";
|
|
686
|
-
return body.
|
|
757
|
+
return body.replaceAll(/(?<!this\.)state\./g, "s.").replaceAll(/(?<!this\.)state(?!\.)/g, "s");
|
|
687
758
|
}
|
|
688
759
|
|
|
689
760
|
/**
|
|
690
|
-
* @param {JxStyle |
|
|
761
|
+
* @param {JxStyle | null | undefined} styleDef
|
|
691
762
|
* @returns {string}
|
|
692
763
|
*/
|
|
693
|
-
function emitStyleString(styleDef: JxStyle |
|
|
694
|
-
if (!styleDef || typeof styleDef !== "object")
|
|
764
|
+
function emitStyleString(styleDef: JxStyle | null | undefined) {
|
|
765
|
+
if (!styleDef || typeof styleDef !== "object") {
|
|
766
|
+
return "";
|
|
767
|
+
}
|
|
695
768
|
|
|
696
769
|
const parts: string[] = [];
|
|
697
770
|
for (const [prop, value] of Object.entries(styleDef)) {
|
|
@@ -701,10 +774,13 @@ function emitStyleString(styleDef: JxStyle | Record<string, any> | null | undefi
|
|
|
701
774
|
prop.startsWith("&") ||
|
|
702
775
|
prop.startsWith("[") ||
|
|
703
776
|
prop.startsWith("@")
|
|
704
|
-
)
|
|
777
|
+
) {
|
|
705
778
|
continue;
|
|
779
|
+
}
|
|
706
780
|
|
|
707
|
-
if (value === null || typeof value === "object")
|
|
781
|
+
if (value === null || typeof value === "object") {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
708
784
|
|
|
709
785
|
if (typeof value === "string" && value.includes("${")) {
|
|
710
786
|
const cssProp = camelToKebab(prop);
|
|
@@ -718,15 +794,23 @@ function emitStyleString(styleDef: JxStyle | Record<string, any> | null | undefi
|
|
|
718
794
|
/**
|
|
719
795
|
* Check if a children tree contains a `<slot>` element.
|
|
720
796
|
*
|
|
721
|
-
* @param {
|
|
797
|
+
* @param {JxMutableNode["children"]} children
|
|
722
798
|
* @returns {boolean}
|
|
723
799
|
*/
|
|
724
|
-
function treeHasSlot(children:
|
|
725
|
-
if (!Array.isArray(children))
|
|
800
|
+
function treeHasSlot(children: JxMutableNode["children"]): boolean {
|
|
801
|
+
if (!Array.isArray(children)) {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
726
804
|
for (const child of children) {
|
|
727
|
-
if (!child || typeof child !== "object")
|
|
728
|
-
|
|
729
|
-
|
|
805
|
+
if (!child || typeof child !== "object") {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (child.tagName === "slot") {
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
if (treeHasSlot(child.children)) {
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
730
814
|
}
|
|
731
815
|
return false;
|
|
732
816
|
}
|