@jxsuite/compiler 0.19.0 → 0.20.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.
@@ -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 {any} pageDoc - The raw page JSON document
23
- * @param {any} projectConfig - Site configuration (for defaults.layout)
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 {any} The merged document (layout wrapping page content)
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 = pageDoc.$layout ?? projectConfig.defaults?.layout ?? null;
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 {any} */
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 {any} */ (e);
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 = typeof rawChildren === "string" ? [rawChildren] : rawChildren;
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 {any} node - Layout document tree (mutated in place)
106
- * @param {any[]} children - Page children to distribute
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, any[]>} */
118
+ /** @type {Map<string, (JxElement | string)[]>} */
114
119
  const named = new Map(); // slot name → children[]
115
- /** @type {any[]} */
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 {any[]} */ (named.get(slotName)).push(child);
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 {any} node
136
- * @param {Map<string, any[]>} named
137
- * @param {any[]} defaults
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 {any[]} */
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(.../** @type {any[]} */ (named.get(slotName)));
153
- named.delete(slotName); // consumed
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 {any} obj
179
- * @returns {any}
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 {any} */
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 {any} */ entry) => ({
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 {any} */ v) => ({ [param]: v }));
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 {any} */
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 {any} */ (e);
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 {any} */ item) => ({
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 {any} doc - The page document (mutated in place)
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
  */
@@ -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, any>} */
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 {any} */ (e);
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 {any} */ (e);
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 {any} */ (e);
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 {any} route
365
- * @param {any} projectConfig
364
+ * @param {SiteRoute} route
365
+ * @param {ProjectConfig} projectConfig
366
366
  * @param {string} projectRoot
367
- * @param {Map<string, any[]>} [contentTypes]
367
+ * @param {Map<string, ContentLoaderEntry[]>} [contentTypes]
368
368
  * @param {import("./image-cache.js").CacheManifest | null} [imageCache]
369
369
  * @param {string} [outDir]
370
- * @returns {Promise<{ html: string; files: any[]; serverHandler: string | null; doc: any }>}
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 {any} */ (def).timing === "compiler"
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 {any} */ e) => typeof e === "string" && !e.startsWith("./") && !e.startsWith("../"),
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.adapter) {
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 {any[]} headEntries
505
- * @param {any} scope
506
- * @returns {any[]}
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 {any} */ entry) => {
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] = evaluateStaticTemplate(v, scope) ?? v;
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 {any[]} headEntries
534
- * @returns {any[]}
543
+ * @param {JxHeadEntry[]} headEntries
544
+ * @returns {JxHeadEntry[]}
535
545
  */
536
546
  function resolveHeadBareSpecifiers(headEntries) {
537
- return headEntries.map((/** @type {any} */ entry) => {
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 {any} node
571
- * @param {any} scope
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("${", "&#36;{");
592
+ node.innerHTML = String(resolved).replaceAll("${", "&#36;{");
583
593
  }
584
594
  }
585
595
  if (typeof node.textContent === "string" && isTemplateString(node.textContent)) {
586
- node.textContent = evaluateStaticTemplate(node.textContent, scope) ?? 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] = evaluateStaticTemplate(v, scope) ?? v;
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
- if (typeof node.children === "string" && isTemplateString(node.children)) {
603
- const resolved = evaluateStaticTemplate(node.children, scope);
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
- node.children = resolved;
606
- for (const child of node.children) {
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
- node.children.splice(i, 1, ...resolved);
618
- for (const spliced of resolved) {
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 += resolved.length;
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 {any} node
636
- * @param {Map<string, any>} componentDefs
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.map((/** @type {any} */ c) => renderStaticNode(c, {}, null)).join("\n")
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, any>} */
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 {any[]} headEntries
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, any>} redirects
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
  */
@@ -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 {any} */ (e);
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 {any} classDef - Parsed .class.json content (must have $prototype: "Class")
12
- * @param {any} [_opts]
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 fields = classDef.$defs?.fields ?? {};
21
- const ctor = classDef.$defs?.constructor;
22
- const methods = classDef.$defs?.methods ?? {};
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 {any} */ (field);
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 {any} */ (field);
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 {any} */ (field);
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 {any} */ (method);
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 = m.returnType?.$ref?.includes("Promise") || isMethodAsync(m) ? "async " : "";
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 {any} ext
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
- // { $ref: "./Parent.class.json" } — extract title from filename as best guess
150
- if (ext.$ref) {
151
- const ref = ext.$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 {any[]} params
166
+ * @param {unknown[]} params
162
167
  * @returns {string}
163
168
  */
164
169
  function resolveParams(params) {
165
170
  return params
166
- .map((/** @type {any} */ p) => {
167
- if (p.$ref) return p.$ref.split("/").pop();
168
- return p.identifier ?? p.name ?? "arg";
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 {any} body
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 {any} method
194
+ * @param {{ body?: string | string[] }} method
189
195
  * @returns {boolean}
190
196
  */
191
197
  function isMethodAsync(method) {