@jxsuite/compiler 0.19.0 → 0.21.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/compiler.js +4 -4
- package/package.json +6 -4
- package/src/cli.js +1 -1
- package/src/compiler.js +2 -2
- package/src/shared.js +183 -145
- package/src/site/content-loader.js +106 -76
- package/src/site/context-injection.js +30 -24
- package/src/site/head-merger.js +13 -12
- package/src/site/image-optimizer.js +2 -2
- package/src/site/image-transform.js +5 -4
- package/src/site/layout-resolver.js +32 -25
- package/src/site/pages-discovery.js +6 -6
- package/src/site/prototype-resolver.js +1 -1
- package/src/site/site-build.js +71 -51
- package/src/site/site-loader.js +1 -1
- package/src/targets/compile-class.js +27 -21
- package/src/targets/compile-client.js +75 -33
- package/src/targets/compile-element.js +57 -39
- package/src/targets/compile-markdown.js +99 -78
- package/src/targets/compile-server.js +1 -1
- package/src/targets/compile-static.js +24 -13
|
@@ -19,14 +19,17 @@ import { resolve } from "node:path";
|
|
|
19
19
|
/**
|
|
20
20
|
* Resolve a page's layout, wrapping the page content in the layout structure.
|
|
21
21
|
*
|
|
22
|
-
* @param {
|
|
23
|
-
* @param {
|
|
22
|
+
* @param {JxDocument} pageDoc - The raw page JSON document
|
|
23
|
+
* @param {Record<string, unknown>} projectConfig - Site configuration (for defaults.layout)
|
|
24
24
|
* @param {string} projectRoot - Project root directory
|
|
25
|
-
* @returns {
|
|
25
|
+
* @returns {JxDocument} The merged document (layout wrapping page content)
|
|
26
26
|
*/
|
|
27
27
|
export function resolveLayout(pageDoc, projectConfig, projectRoot) {
|
|
28
28
|
// Determine which layout to use
|
|
29
|
-
const layoutRef =
|
|
29
|
+
const layoutRef =
|
|
30
|
+
pageDoc.$layout ??
|
|
31
|
+
/** @type {Record<string, unknown>} */ (projectConfig.defaults)?.layout ??
|
|
32
|
+
null;
|
|
30
33
|
|
|
31
34
|
if (!layoutRef) {
|
|
32
35
|
// No layout — return page as-is
|
|
@@ -34,17 +37,17 @@ export function resolveLayout(pageDoc, projectConfig, projectRoot) {
|
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
// Load the layout file
|
|
37
|
-
const layoutPath = resolve(projectRoot, layoutRef);
|
|
40
|
+
const layoutPath = resolve(projectRoot, /** @type {string} */ (layoutRef));
|
|
38
41
|
if (!existsSync(layoutPath)) {
|
|
39
42
|
throw new Error(`Layout not found: ${layoutRef} (resolved to ${layoutPath})`);
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
/** @type {
|
|
45
|
+
/** @type {JxDocument} */
|
|
43
46
|
let layoutDoc;
|
|
44
47
|
try {
|
|
45
48
|
layoutDoc = JSON.parse(readFileSync(layoutPath, "utf8"));
|
|
46
49
|
} catch (e) {
|
|
47
|
-
const err = /** @type {
|
|
50
|
+
const err = /** @type {Error} */ (e);
|
|
48
51
|
throw new Error(`Invalid layout JSON at ${layoutPath}: ${err.message}`);
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -55,7 +58,9 @@ export function resolveLayout(pageDoc, projectConfig, projectRoot) {
|
|
|
55
58
|
|
|
56
59
|
// Distribute page children into layout slots
|
|
57
60
|
const rawChildren = pageDoc.children ?? [];
|
|
58
|
-
const pageChildren =
|
|
61
|
+
const pageChildren = /** @type {(JxElement | string)[]} */ (
|
|
62
|
+
typeof rawChildren === "string" ? [rawChildren] : Array.isArray(rawChildren) ? rawChildren : []
|
|
63
|
+
);
|
|
59
64
|
const merged = deepClone(layoutDoc);
|
|
60
65
|
|
|
61
66
|
distributeSlots(merged, pageChildren);
|
|
@@ -102,24 +107,24 @@ export function resolveLayout(pageDoc, projectConfig, projectRoot) {
|
|
|
102
107
|
* 3. Remaining children go into the default (unnamed) slot
|
|
103
108
|
* 4. Replace each <slot> element with its distributed children
|
|
104
109
|
*
|
|
105
|
-
* @param {
|
|
106
|
-
* @param {
|
|
110
|
+
* @param {JxElement} node - Layout document tree (mutated in place)
|
|
111
|
+
* @param {(JxElement | string)[]} children - Page children to distribute
|
|
107
112
|
*/
|
|
108
113
|
function distributeSlots(node, children) {
|
|
109
114
|
if (!node || typeof node !== "object") return;
|
|
110
115
|
if (!Array.isArray(node.children)) return;
|
|
111
116
|
|
|
112
117
|
// Collect named and default children
|
|
113
|
-
/** @type {Map<string,
|
|
118
|
+
/** @type {Map<string, (JxElement | string)[]>} */
|
|
114
119
|
const named = new Map(); // slot name → children[]
|
|
115
|
-
/** @type {
|
|
120
|
+
/** @type {(JxElement | string)[]} */
|
|
116
121
|
const defaults = []; // children without a slot target
|
|
117
122
|
|
|
118
123
|
for (const child of children) {
|
|
119
124
|
if (child && typeof child === "object" && child.attributes?.slot) {
|
|
120
|
-
const slotName = child.attributes.slot;
|
|
125
|
+
const slotName = /** @type {string} */ (child.attributes.slot);
|
|
121
126
|
if (!named.has(slotName)) named.set(slotName, []);
|
|
122
|
-
/** @type {
|
|
127
|
+
/** @type {(JxElement | string)[]} */ (named.get(slotName)).push(child);
|
|
123
128
|
} else {
|
|
124
129
|
defaults.push(child);
|
|
125
130
|
}
|
|
@@ -132,25 +137,27 @@ function distributeSlots(node, children) {
|
|
|
132
137
|
/**
|
|
133
138
|
* Recursively walk the tree and replace <slot> elements with distributed content.
|
|
134
139
|
*
|
|
135
|
-
* @param {
|
|
136
|
-
* @param {Map<string,
|
|
137
|
-
* @param {
|
|
140
|
+
* @param {JxElement} node
|
|
141
|
+
* @param {Map<string, (JxElement | string)[]>} named
|
|
142
|
+
* @param {(JxElement | string)[]} defaults
|
|
138
143
|
*/
|
|
139
144
|
function fillSlots(node, named, defaults) {
|
|
140
145
|
if (!node || typeof node !== "object") return;
|
|
141
146
|
if (!Array.isArray(node.children)) return;
|
|
142
147
|
|
|
143
|
-
/** @type {
|
|
148
|
+
/** @type {(JxElement | string)[]} */
|
|
144
149
|
const newChildren = [];
|
|
145
150
|
|
|
146
151
|
for (const child of node.children) {
|
|
147
152
|
if (child && typeof child === "object" && child.tagName === "slot") {
|
|
148
153
|
const slotName = child.attributes?.name;
|
|
149
154
|
|
|
150
|
-
if (slotName && named.has(slotName)) {
|
|
155
|
+
if (slotName && named.has(/** @type {string} */ (slotName))) {
|
|
151
156
|
// Named slot — replace with matching children
|
|
152
|
-
newChildren.push(
|
|
153
|
-
|
|
157
|
+
newChildren.push(
|
|
158
|
+
.../** @type {(JxElement | string)[]} */ (named.get(/** @type {string} */ (slotName))),
|
|
159
|
+
);
|
|
160
|
+
named.delete(/** @type {string} */ (slotName)); // consumed
|
|
154
161
|
} else if (!slotName && defaults.length > 0) {
|
|
155
162
|
// Default slot — replace with unassigned children
|
|
156
163
|
newChildren.push(...defaults);
|
|
@@ -158,13 +165,13 @@ function fillSlots(node, named, defaults) {
|
|
|
158
165
|
// but if there are multiple, the first one wins
|
|
159
166
|
} else {
|
|
160
167
|
// No matching content — keep slot's fallback children
|
|
161
|
-
if (child.children) {
|
|
168
|
+
if (Array.isArray(child.children)) {
|
|
162
169
|
newChildren.push(...child.children);
|
|
163
170
|
}
|
|
164
171
|
}
|
|
165
172
|
} else {
|
|
166
173
|
// Not a slot — recurse into it
|
|
167
|
-
fillSlots(child, named, defaults);
|
|
174
|
+
fillSlots(/** @type {JxElement} */ (child), named, defaults);
|
|
168
175
|
newChildren.push(child);
|
|
169
176
|
}
|
|
170
177
|
}
|
|
@@ -175,8 +182,8 @@ function fillSlots(node, named, defaults) {
|
|
|
175
182
|
/**
|
|
176
183
|
* Deep clone a JSON-serializable object.
|
|
177
184
|
*
|
|
178
|
-
* @param {
|
|
179
|
-
* @returns {
|
|
185
|
+
* @param {JxDocument} obj
|
|
186
|
+
* @returns {JxDocument}
|
|
180
187
|
*/
|
|
181
188
|
function deepClone(obj) {
|
|
182
189
|
return JSON.parse(JSON.stringify(obj));
|
|
@@ -183,7 +183,7 @@ export async function expandDynamicRoutes(routes, projectRoot, contentTypes = ne
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
// Read the page to look for $paths
|
|
186
|
-
/** @type {
|
|
186
|
+
/** @type {Record<string, unknown>} */
|
|
187
187
|
let raw;
|
|
188
188
|
try {
|
|
189
189
|
raw = JSON.parse(readFileSync(route.sourcePath, "utf8"));
|
|
@@ -245,7 +245,7 @@ function resolvePathEntries($paths, projectRoot, contentTypes) {
|
|
|
245
245
|
}
|
|
246
246
|
const param = $paths.param ?? "slug";
|
|
247
247
|
const field = $paths.field ?? "id";
|
|
248
|
-
return entries.map((/** @type {
|
|
248
|
+
return entries.map((/** @type {ContentLoaderEntry} */ entry) => ({
|
|
249
249
|
[param]: field === "id" ? entry.id : (entry.data[field] ?? entry.id),
|
|
250
250
|
}));
|
|
251
251
|
}
|
|
@@ -253,18 +253,18 @@ function resolvePathEntries($paths, projectRoot, contentTypes) {
|
|
|
253
253
|
// Explicit values: { values: ["en", "fr"], param: "lang" }
|
|
254
254
|
if (Array.isArray($paths.values)) {
|
|
255
255
|
const param = $paths.param ?? "value";
|
|
256
|
-
return $paths.values.map((/** @type {
|
|
256
|
+
return $paths.values.map((/** @type {string} */ v) => ({ [param]: v }));
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
// Data file ref: { "$ref": "./data/products.json", param: "id", field: "sku" }
|
|
260
260
|
if ($paths.$ref) {
|
|
261
261
|
const filePath = resolve(projectRoot, $paths.$ref);
|
|
262
|
-
/** @type {
|
|
262
|
+
/** @type {Record<string, unknown>[]} */
|
|
263
263
|
let data;
|
|
264
264
|
try {
|
|
265
265
|
data = JSON.parse(readFileSync(filePath, "utf8"));
|
|
266
266
|
} catch (e) {
|
|
267
|
-
const err = /** @type {
|
|
267
|
+
const err = /** @type {Error} */ (e);
|
|
268
268
|
console.warn(`Warning: $paths.$ref could not load "${$paths.$ref}": ${err.message}`);
|
|
269
269
|
return [];
|
|
270
270
|
}
|
|
@@ -274,7 +274,7 @@ function resolvePathEntries($paths, projectRoot, contentTypes) {
|
|
|
274
274
|
}
|
|
275
275
|
const param = $paths.param ?? "id";
|
|
276
276
|
const field = $paths.field ?? "id";
|
|
277
|
-
return data.map((/** @type {
|
|
277
|
+
return data.map((/** @type {Record<string, unknown>} */ item) => ({
|
|
278
278
|
[param]: item[field] ?? item.id ?? String(item),
|
|
279
279
|
}));
|
|
280
280
|
}
|
|
@@ -65,7 +65,7 @@ const RESERVED_KEYS = new Set([
|
|
|
65
65
|
* - Instantiates the class with the config - Calls .resolve() and replaces the state entry with the
|
|
66
66
|
* result
|
|
67
67
|
*
|
|
68
|
-
* @param {
|
|
68
|
+
* @param {JxMutableNode | JxDocument} doc - The page document (mutated in place)
|
|
69
69
|
* @param {{ sourcePath?: string }} route - Route info (sourcePath = absolute path to page .json)
|
|
70
70
|
* @param {string} projectRoot - Absolute path to the project root
|
|
71
71
|
*/
|
package/src/site/site-build.js
CHANGED
|
@@ -121,7 +121,7 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
121
121
|
const compiledComponentTags = [];
|
|
122
122
|
/** @type {Map<string, string>} */
|
|
123
123
|
const componentCSS = new Map(); // tagName → CSS text
|
|
124
|
-
/** @type {Map<string,
|
|
124
|
+
/** @type {Map<string, JxElement>} */
|
|
125
125
|
const componentDefs = new Map(); // tagName → parsed component definition
|
|
126
126
|
if (existsSync(componentsDir)) {
|
|
127
127
|
log("Compiling components...");
|
|
@@ -160,7 +160,7 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
} catch (e) {
|
|
163
|
-
const err = /** @type {
|
|
163
|
+
const err = /** @type {Error} */ (e);
|
|
164
164
|
errors.push(`Error compiling component ${file}: ${err.message}`);
|
|
165
165
|
console.error(`Error compiling component ${file}: ${err.message}`);
|
|
166
166
|
}
|
|
@@ -232,7 +232,7 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
232
232
|
fileCount++;
|
|
233
233
|
}
|
|
234
234
|
} catch (e) {
|
|
235
|
-
const err = /** @type {
|
|
235
|
+
const err = /** @type {Error} */ (e);
|
|
236
236
|
errors.push(`Error exporting markdown for ${route.urlPattern}: ${err.message}`);
|
|
237
237
|
}
|
|
238
238
|
|
|
@@ -251,7 +251,7 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
251
251
|
fileCount++;
|
|
252
252
|
}
|
|
253
253
|
} catch (e) {
|
|
254
|
-
const err = /** @type {
|
|
254
|
+
const err = /** @type {Error} */ (e);
|
|
255
255
|
const msg = `Error compiling ${route.urlPattern}: ${err.message}`;
|
|
256
256
|
errors.push(msg);
|
|
257
257
|
console.error(msg);
|
|
@@ -361,13 +361,19 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
361
361
|
*
|
|
362
362
|
* Pipeline: load JSON → resolve layout → inject context → merge head → compile
|
|
363
363
|
*
|
|
364
|
-
* @param {
|
|
365
|
-
* @param {
|
|
364
|
+
* @param {SiteRoute} route
|
|
365
|
+
* @param {ProjectConfig} projectConfig
|
|
366
366
|
* @param {string} projectRoot
|
|
367
|
-
* @param {Map<string,
|
|
367
|
+
* @param {Map<string, ContentLoaderEntry[]>} [contentTypes]
|
|
368
368
|
* @param {import("./image-cache.js").CacheManifest | null} [imageCache]
|
|
369
369
|
* @param {string} [outDir]
|
|
370
|
-
* @
|
|
370
|
+
* @param {Map<string, JxElement>} [componentDefs]
|
|
371
|
+
* @returns {Promise<{
|
|
372
|
+
* html: string;
|
|
373
|
+
* files: { path: string; content: string; tagName?: string }[];
|
|
374
|
+
* serverHandler: string | null;
|
|
375
|
+
* doc: JxDocument;
|
|
376
|
+
* }>}
|
|
371
377
|
*/
|
|
372
378
|
async function compilePage(
|
|
373
379
|
route,
|
|
@@ -380,20 +386,20 @@ async function compilePage(
|
|
|
380
386
|
) {
|
|
381
387
|
// Load the raw page document
|
|
382
388
|
let pageDoc;
|
|
383
|
-
if (route.sourcePath.endsWith(".md")) {
|
|
389
|
+
if (/** @type {string} */ (route.sourcePath).endsWith(".md")) {
|
|
384
390
|
const { transpileJxMarkdown } = await import("@jxsuite/parser/transpile");
|
|
385
|
-
pageDoc = transpileJxMarkdown(readFileSync(route.sourcePath, "utf8"));
|
|
391
|
+
pageDoc = transpileJxMarkdown(readFileSync(/** @type {string} */ (route.sourcePath), "utf8"));
|
|
386
392
|
} else {
|
|
387
|
-
pageDoc = JSON.parse(readFileSync(route.sourcePath, "utf8"));
|
|
393
|
+
pageDoc = JSON.parse(readFileSync(/** @type {string} */ (route.sourcePath), "utf8"));
|
|
388
394
|
}
|
|
389
395
|
|
|
390
396
|
// Resolve layout (wraps page in layout with slot distribution)
|
|
391
397
|
const layoutDoc = resolveLayout(pageDoc, projectConfig, projectRoot);
|
|
392
398
|
|
|
393
399
|
// Extract head arrays before they get lost in the merge
|
|
394
|
-
const pageHead = pageDoc.$head ?? layoutDoc._pageHead ?? [];
|
|
400
|
+
const pageHead = /** @type {JxHeadEntry[]} */ (pageDoc.$head ?? layoutDoc._pageHead ?? []);
|
|
395
401
|
const layoutHead = layoutDoc.$head ?? [];
|
|
396
|
-
const pageTitle = pageDoc.title ?? layoutDoc._pageTitle ?? null;
|
|
402
|
+
const pageTitle = /** @type {string | null} */ (pageDoc.title ?? layoutDoc._pageTitle ?? null);
|
|
397
403
|
|
|
398
404
|
// Clean up internal properties
|
|
399
405
|
delete layoutDoc._pageHead;
|
|
@@ -411,7 +417,7 @@ async function compilePage(
|
|
|
411
417
|
// Determine the page title — resolve template strings against the scope
|
|
412
418
|
let title = pageTitle ?? projectConfig.name ?? "Jx Site";
|
|
413
419
|
if (typeof title === "string" && isTemplateString(title)) {
|
|
414
|
-
title = evaluateStaticTemplate(title, scope) ?? title;
|
|
420
|
+
title = /** @type {string} */ (evaluateStaticTemplate(title, scope) ?? title);
|
|
415
421
|
}
|
|
416
422
|
|
|
417
423
|
// Resolve template strings in $head entries
|
|
@@ -434,7 +440,7 @@ async function compilePage(
|
|
|
434
440
|
def &&
|
|
435
441
|
typeof def === "object" &&
|
|
436
442
|
!Array.isArray(def) &&
|
|
437
|
-
/** @type {
|
|
443
|
+
/** @type {JxMutableNode} */ (def).timing === "compiler"
|
|
438
444
|
) {
|
|
439
445
|
delete layoutDoc.state[key];
|
|
440
446
|
}
|
|
@@ -475,18 +481,19 @@ async function compilePage(
|
|
|
475
481
|
|
|
476
482
|
// Inject <script type="module"> for npm $elements (cherry-picked component imports)
|
|
477
483
|
const npmElements = (layoutDoc.$elements ?? []).filter(
|
|
478
|
-
(/** @type {
|
|
484
|
+
(/** @type {JxElement | string} */ e) =>
|
|
485
|
+
typeof e === "string" && !e.startsWith("./") && !e.startsWith("../"),
|
|
479
486
|
);
|
|
480
487
|
if (npmElements.length > 0) {
|
|
481
|
-
result.html = injectNpmElementScripts(result.html, npmElements);
|
|
488
|
+
result.html = injectNpmElementScripts(result.html, /** @type {string[]} */ (npmElements));
|
|
482
489
|
}
|
|
483
490
|
|
|
484
491
|
// Compile server handler if applicable (skip when provider bundles site-wide)
|
|
485
492
|
/** @type {string | null} */
|
|
486
493
|
let serverHandler = null;
|
|
487
|
-
if (!projectConfig.build
|
|
494
|
+
if (!projectConfig.build?.adapter) {
|
|
488
495
|
try {
|
|
489
|
-
const serverResult = await compileServer(route.sourcePath);
|
|
496
|
+
const serverResult = await compileServer(/** @type {string} */ (route.sourcePath));
|
|
490
497
|
if (serverResult) {
|
|
491
498
|
serverHandler = serverResult;
|
|
492
499
|
}
|
|
@@ -501,25 +508,28 @@ async function compilePage(
|
|
|
501
508
|
/**
|
|
502
509
|
* Resolve template strings in $head entries against the compiled scope.
|
|
503
510
|
*
|
|
504
|
-
* @param {
|
|
505
|
-
* @param {
|
|
506
|
-
* @returns {
|
|
511
|
+
* @param {JxHeadEntry[]} headEntries
|
|
512
|
+
* @param {Record<string, unknown>} scope
|
|
513
|
+
* @returns {JxHeadEntry[]}
|
|
507
514
|
*/
|
|
508
515
|
function resolveHeadTemplates(headEntries, scope) {
|
|
509
|
-
return headEntries.map((/** @type {
|
|
516
|
+
return headEntries.map((/** @type {JxHeadEntry} */ entry) => {
|
|
510
517
|
if (!entry || typeof entry !== "object") return entry;
|
|
511
518
|
const resolved = { ...entry };
|
|
512
519
|
if (resolved.attributes) {
|
|
513
520
|
resolved.attributes = { ...resolved.attributes };
|
|
514
521
|
for (const [k, v] of Object.entries(resolved.attributes)) {
|
|
515
522
|
if (typeof v === "string" && isTemplateString(v)) {
|
|
516
|
-
resolved.attributes[k] =
|
|
523
|
+
resolved.attributes[k] = /** @type {string | boolean} */ (
|
|
524
|
+
evaluateStaticTemplate(v, scope) ?? v
|
|
525
|
+
);
|
|
517
526
|
}
|
|
518
527
|
}
|
|
519
528
|
}
|
|
520
529
|
if (typeof resolved.textContent === "string" && isTemplateString(resolved.textContent)) {
|
|
521
|
-
resolved.textContent =
|
|
522
|
-
evaluateStaticTemplate(resolved.textContent, scope) ?? resolved.textContent
|
|
530
|
+
resolved.textContent = /** @type {string | undefined} */ (
|
|
531
|
+
evaluateStaticTemplate(resolved.textContent, scope) ?? resolved.textContent
|
|
532
|
+
);
|
|
523
533
|
}
|
|
524
534
|
return resolved;
|
|
525
535
|
});
|
|
@@ -530,11 +540,11 @@ function resolveHeadTemplates(headEntries, scope) {
|
|
|
530
540
|
* "@shoelace-style/shoelace/dist/themes/light.css" →
|
|
531
541
|
* "/node_modules/@shoelace-style/shoelace/dist/themes/light.css"
|
|
532
542
|
*
|
|
533
|
-
* @param {
|
|
534
|
-
* @returns {
|
|
543
|
+
* @param {JxHeadEntry[]} headEntries
|
|
544
|
+
* @returns {JxHeadEntry[]}
|
|
535
545
|
*/
|
|
536
546
|
function resolveHeadBareSpecifiers(headEntries) {
|
|
537
|
-
return headEntries.map((/** @type {
|
|
547
|
+
return headEntries.map((/** @type {JxHeadEntry} */ entry) => {
|
|
538
548
|
if (!entry || typeof entry !== "object" || !entry.attributes) return entry;
|
|
539
549
|
const resolved = { ...entry, attributes: { ...entry.attributes } };
|
|
540
550
|
for (const key of ["href", "src"]) {
|
|
@@ -567,8 +577,8 @@ function isBareSpecifier(s) {
|
|
|
567
577
|
* Recursively resolve template strings in a document tree against a scope. Mutates the document in
|
|
568
578
|
* place — evaluates ${...} in innerHTML, textContent, style values, and attribute values.
|
|
569
579
|
*
|
|
570
|
-
* @param {
|
|
571
|
-
* @param {
|
|
580
|
+
* @param {JxElement | string} node
|
|
581
|
+
* @param {Record<string, unknown>} scope
|
|
572
582
|
*/
|
|
573
583
|
function resolveDocTemplates(node, scope) {
|
|
574
584
|
if (!node || typeof node !== "object") return;
|
|
@@ -579,16 +589,20 @@ function resolveDocTemplates(node, scope) {
|
|
|
579
589
|
// Encode any remaining `${` as HTML entities so the compile phase won't
|
|
580
590
|
// re-interpret them as template expressions. After resolution, any `${` in the
|
|
581
591
|
// result is literal content (e.g., code examples), not an intentional template.
|
|
582
|
-
node.innerHTML = resolved.replaceAll("${", "${");
|
|
592
|
+
node.innerHTML = String(resolved).replaceAll("${", "${");
|
|
583
593
|
}
|
|
584
594
|
}
|
|
585
595
|
if (typeof node.textContent === "string" && isTemplateString(node.textContent)) {
|
|
586
|
-
node.textContent =
|
|
596
|
+
node.textContent = /** @type {string | null} */ (
|
|
597
|
+
evaluateStaticTemplate(node.textContent, scope) ?? node.textContent
|
|
598
|
+
);
|
|
587
599
|
}
|
|
588
600
|
if (node.style && typeof node.style === "object") {
|
|
589
601
|
for (const [k, v] of Object.entries(node.style)) {
|
|
590
602
|
if (typeof v === "string" && isTemplateString(v)) {
|
|
591
|
-
node.style[k] =
|
|
603
|
+
node.style[k] = /** @type {string | number | JxStyle} */ (
|
|
604
|
+
evaluateStaticTemplate(v, scope) ?? v
|
|
605
|
+
);
|
|
592
606
|
}
|
|
593
607
|
}
|
|
594
608
|
}
|
|
@@ -599,11 +613,13 @@ function resolveDocTemplates(node, scope) {
|
|
|
599
613
|
}
|
|
600
614
|
}
|
|
601
615
|
}
|
|
602
|
-
|
|
603
|
-
|
|
616
|
+
const rawChildren = /** @type {unknown} */ (node.children);
|
|
617
|
+
if (typeof rawChildren === "string" && isTemplateString(rawChildren)) {
|
|
618
|
+
const resolved = evaluateStaticTemplate(rawChildren, scope);
|
|
604
619
|
if (Array.isArray(resolved)) {
|
|
605
|
-
|
|
606
|
-
|
|
620
|
+
const resolvedNodes = /** @type {(JxElement | string)[]} */ (resolved);
|
|
621
|
+
node.children = resolvedNodes;
|
|
622
|
+
for (const child of resolvedNodes) {
|
|
607
623
|
resolveDocTemplates(child, scope);
|
|
608
624
|
}
|
|
609
625
|
}
|
|
@@ -614,11 +630,12 @@ function resolveDocTemplates(node, scope) {
|
|
|
614
630
|
if (typeof child === "string" && isTemplateString(child)) {
|
|
615
631
|
const resolved = evaluateStaticTemplate(child, scope);
|
|
616
632
|
if (Array.isArray(resolved)) {
|
|
617
|
-
|
|
618
|
-
|
|
633
|
+
const resolvedNodes = /** @type {(JxElement | string)[]} */ (resolved);
|
|
634
|
+
node.children.splice(i, 1, ...resolvedNodes);
|
|
635
|
+
for (const spliced of resolvedNodes) {
|
|
619
636
|
resolveDocTemplates(spliced, scope);
|
|
620
637
|
}
|
|
621
|
-
i +=
|
|
638
|
+
i += resolvedNodes.length;
|
|
622
639
|
continue;
|
|
623
640
|
}
|
|
624
641
|
}
|
|
@@ -632,8 +649,8 @@ function resolveDocTemplates(node, scope) {
|
|
|
632
649
|
* Walk the document tree and expand registered custom elements in-place. Applies $props via
|
|
633
650
|
* preRenderComponentHtml, marks static/prerendered.
|
|
634
651
|
*
|
|
635
|
-
* @param {
|
|
636
|
-
* @param {Map<string,
|
|
652
|
+
* @param {JxElement | string} node
|
|
653
|
+
* @param {Map<string, JxElement>} componentDefs
|
|
637
654
|
*/
|
|
638
655
|
function expandComponents(node, componentDefs) {
|
|
639
656
|
if (!node || typeof node !== "object") return;
|
|
@@ -649,11 +666,13 @@ function expandComponents(node, componentDefs) {
|
|
|
649
666
|
}
|
|
650
667
|
}
|
|
651
668
|
|
|
652
|
-
const def = componentDefs.get(node.tagName);
|
|
669
|
+
const def = componentDefs.get(/** @type {string} */ (node.tagName));
|
|
653
670
|
if (def) {
|
|
654
671
|
const slotContent =
|
|
655
672
|
Array.isArray(node.children) && node.children.length > 0
|
|
656
|
-
? node.children
|
|
673
|
+
? node.children
|
|
674
|
+
.map((/** @type {JxElement | string} */ c) => renderStaticNode(c, {}, null))
|
|
675
|
+
.join("\n")
|
|
657
676
|
: null;
|
|
658
677
|
|
|
659
678
|
const innerHTML = preRenderComponentHtml(def, node.$props || null, slotContent);
|
|
@@ -664,13 +683,14 @@ function expandComponents(node, componentDefs) {
|
|
|
664
683
|
|
|
665
684
|
// Resolve template-string host styles with props (per-instance values like background-image)
|
|
666
685
|
if (def.style && node.$props) {
|
|
686
|
+
/** @type {Record<string, JxStateDefinition>} */
|
|
667
687
|
let stateDefs = { ...def.state };
|
|
668
688
|
for (const [key, value] of Object.entries(node.$props)) {
|
|
669
|
-
if (key in stateDefs) stateDefs[key] = value;
|
|
670
|
-
else stateDefs[key] = value;
|
|
689
|
+
if (key in stateDefs) stateDefs[key] = /** @type {JxStateDefinition} */ (value);
|
|
690
|
+
else stateDefs[key] = /** @type {JxStateDefinition} */ (value);
|
|
671
691
|
}
|
|
672
692
|
const scope = buildInitialScope(stateDefs, null);
|
|
673
|
-
/** @type {Record<string,
|
|
693
|
+
/** @type {Record<string, unknown>} */
|
|
674
694
|
const resolvedStyle = {};
|
|
675
695
|
for (const [prop, value] of Object.entries(def.style)) {
|
|
676
696
|
if (typeof value === "string" && isTemplateString(value)) {
|
|
@@ -679,7 +699,7 @@ function expandComponents(node, componentDefs) {
|
|
|
679
699
|
}
|
|
680
700
|
}
|
|
681
701
|
if (Object.keys(resolvedStyle).length > 0) {
|
|
682
|
-
node.style = { ...node.style, ...resolvedStyle };
|
|
702
|
+
node.style = /** @type {JxStyle} */ ({ ...node.style, ...resolvedStyle });
|
|
683
703
|
}
|
|
684
704
|
}
|
|
685
705
|
|
|
@@ -771,7 +791,7 @@ function injectNpmElementScripts(html, npmElements) {
|
|
|
771
791
|
* Replaces the compiler's default <head> section with our merged version.
|
|
772
792
|
*
|
|
773
793
|
* @param {string} html
|
|
774
|
-
* @param {
|
|
794
|
+
* @param {JxHeadEntry[]} headEntries
|
|
775
795
|
* @param {string} lang
|
|
776
796
|
* @returns {string}
|
|
777
797
|
*/
|
|
@@ -833,7 +853,7 @@ function routeToOutputPath(urlPattern, outDir, trailingSlash) {
|
|
|
833
853
|
/**
|
|
834
854
|
* Generate redirect files (HTML meta refresh and _redirects).
|
|
835
855
|
*
|
|
836
|
-
* @param {Record<string,
|
|
856
|
+
* @param {Record<string, string | { destination: string; status?: number }>} redirects
|
|
837
857
|
* @param {string} outDir
|
|
838
858
|
* @returns {number} Number of files written
|
|
839
859
|
*/
|
package/src/site/site-loader.js
CHANGED
|
@@ -61,7 +61,7 @@ export function loadProjectConfig(projectRoot) {
|
|
|
61
61
|
try {
|
|
62
62
|
raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
63
63
|
} catch (e) {
|
|
64
|
-
const err = /** @type {
|
|
64
|
+
const err = /** @type {Error} */ (e);
|
|
65
65
|
throw new Error(`Invalid JSON in ${configPath}: ${err.message}`);
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
/**
|
|
9
9
|
* Compile a .class.json schema to a JavaScript ES module string.
|
|
10
10
|
*
|
|
11
|
-
* @param {
|
|
12
|
-
* @param {
|
|
11
|
+
* @param {JxMutableNode} classDef - Parsed .class.json content (must have $prototype: "Class")
|
|
12
|
+
* @param {Record<string, unknown>} [_opts]
|
|
13
13
|
* @returns {string} JavaScript module source code
|
|
14
14
|
*/
|
|
15
15
|
export function compileClassJson(classDef, _opts = {}) {
|
|
@@ -17,9 +17,10 @@ export function compileClassJson(classDef, _opts = {}) {
|
|
|
17
17
|
if (!className) throw new Error("compileClassJson: missing title (class name)");
|
|
18
18
|
|
|
19
19
|
const baseClass = resolveBaseClass(classDef.extends);
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
20
|
+
const defs = /** @type {Record<string, unknown> | undefined} */ (classDef.$defs);
|
|
21
|
+
const fields = /** @type {Record<string, JxMutableNode>} */ (defs?.fields ?? {});
|
|
22
|
+
const ctor = /** @type {JxMutableNode | undefined} */ (defs?.constructor);
|
|
23
|
+
const methods = /** @type {Record<string, JxMutableNode>} */ (defs?.methods ?? {});
|
|
23
24
|
|
|
24
25
|
/** @type {string[]} */
|
|
25
26
|
const lines = [];
|
|
@@ -33,7 +34,7 @@ export function compileClassJson(classDef, _opts = {}) {
|
|
|
33
34
|
|
|
34
35
|
// Static fields
|
|
35
36
|
for (const [key, field] of Object.entries(fields)) {
|
|
36
|
-
const f = /** @type {
|
|
37
|
+
const f = /** @type {JxMutableNode} */ (field);
|
|
37
38
|
if (f.scope !== "static") continue;
|
|
38
39
|
const name = f.identifier ?? key;
|
|
39
40
|
const prefix = f.access === "private" ? "#" : "";
|
|
@@ -48,7 +49,7 @@ export function compileClassJson(classDef, _opts = {}) {
|
|
|
48
49
|
|
|
49
50
|
// Instance field declarations (private)
|
|
50
51
|
for (const [key, field] of Object.entries(fields)) {
|
|
51
|
-
const f = /** @type {
|
|
52
|
+
const f = /** @type {JxMutableNode} */ (field);
|
|
52
53
|
if (f.scope === "static") continue;
|
|
53
54
|
if (f.access !== "private") continue;
|
|
54
55
|
const name = f.identifier ?? key;
|
|
@@ -66,7 +67,7 @@ export function compileClassJson(classDef, _opts = {}) {
|
|
|
66
67
|
|
|
67
68
|
// Initialize instance fields from config or defaults
|
|
68
69
|
for (const [key, field] of Object.entries(fields)) {
|
|
69
|
-
const f = /** @type {
|
|
70
|
+
const f = /** @type {JxMutableNode} */ (field);
|
|
70
71
|
if (f.scope === "static") continue;
|
|
71
72
|
const name = f.identifier ?? key;
|
|
72
73
|
const prefix = f.access === "private" ? "#" : "";
|
|
@@ -92,13 +93,17 @@ export function compileClassJson(classDef, _opts = {}) {
|
|
|
92
93
|
|
|
93
94
|
// Methods
|
|
94
95
|
for (const [key, method] of Object.entries(methods)) {
|
|
95
|
-
const m = /** @type {
|
|
96
|
+
const m = /** @type {JxMutableNode} */ (method);
|
|
96
97
|
const name = m.identifier ?? key;
|
|
97
98
|
const isStatic = m.scope === "static";
|
|
98
99
|
const isPrivate = m.access === "private";
|
|
99
100
|
const prefix = isPrivate ? "#" : "";
|
|
100
101
|
const staticPrefix = isStatic ? "static " : "";
|
|
101
|
-
const asyncPrefix =
|
|
102
|
+
const asyncPrefix =
|
|
103
|
+
m.returnType?.$ref?.includes("Promise") ||
|
|
104
|
+
isMethodAsync(/** @type {{ body?: string | string[] }} */ (/** @type {unknown} */ (m)))
|
|
105
|
+
? "async "
|
|
106
|
+
: "";
|
|
102
107
|
|
|
103
108
|
const params = resolveParams(m.parameters ?? []);
|
|
104
109
|
const bodyStr = resolveBody(m.body);
|
|
@@ -140,15 +145,15 @@ export function compileClassJson(classDef, _opts = {}) {
|
|
|
140
145
|
/**
|
|
141
146
|
* Resolve the base class name from an extends value.
|
|
142
147
|
*
|
|
143
|
-
* @param {
|
|
148
|
+
* @param {unknown} ext
|
|
144
149
|
* @returns {string}
|
|
145
150
|
*/
|
|
146
151
|
function resolveBaseClass(ext) {
|
|
147
152
|
if (!ext) return "Object";
|
|
148
153
|
if (typeof ext === "string") return ext;
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
const ref =
|
|
154
|
+
const e = /** @type {JxMutableNode} */ (ext);
|
|
155
|
+
if (e.$ref) {
|
|
156
|
+
const ref = e.$ref;
|
|
152
157
|
const match = ref.match(/([A-Za-z0-9_]+)\.class\.json/);
|
|
153
158
|
return match ? match[1] : "Object";
|
|
154
159
|
}
|
|
@@ -158,14 +163,15 @@ function resolveBaseClass(ext) {
|
|
|
158
163
|
/**
|
|
159
164
|
* Resolve parameter names from $ref or inline definitions.
|
|
160
165
|
*
|
|
161
|
-
* @param {
|
|
166
|
+
* @param {unknown[]} params
|
|
162
167
|
* @returns {string}
|
|
163
168
|
*/
|
|
164
169
|
function resolveParams(params) {
|
|
165
170
|
return params
|
|
166
|
-
.map((
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
.map((p) => {
|
|
172
|
+
const param = /** @type {JxMutableNode} */ (p);
|
|
173
|
+
if (param.$ref) return param.$ref.split("/").pop();
|
|
174
|
+
return param.identifier ?? param.name ?? "arg";
|
|
169
175
|
})
|
|
170
176
|
.join(", ");
|
|
171
177
|
}
|
|
@@ -173,19 +179,19 @@ function resolveParams(params) {
|
|
|
173
179
|
/**
|
|
174
180
|
* Resolve body from string or array of strings.
|
|
175
181
|
*
|
|
176
|
-
* @param {
|
|
182
|
+
* @param {unknown} body
|
|
177
183
|
* @returns {string[]}
|
|
178
184
|
*/
|
|
179
185
|
function resolveBody(body) {
|
|
180
186
|
if (!body) return [""];
|
|
181
187
|
if (Array.isArray(body)) return body;
|
|
182
|
-
return [body];
|
|
188
|
+
return [/** @type {string} */ (body)];
|
|
183
189
|
}
|
|
184
190
|
|
|
185
191
|
/**
|
|
186
192
|
* Simple heuristic: does the method body contain await?
|
|
187
193
|
*
|
|
188
|
-
* @param {
|
|
194
|
+
* @param {{ body?: string | string[] }} method
|
|
189
195
|
* @returns {boolean}
|
|
190
196
|
*/
|
|
191
197
|
function isMethodAsync(method) {
|