@jxsuite/compiler 0.28.2 → 0.28.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,15 +9,16 @@
9
9
  */
10
10
 
11
11
  import {
12
- writeFileSync,
13
12
  copyFileSync,
14
- mkdirSync,
15
- existsSync,
16
- rmSync,
17
13
  cpSync,
14
+ existsSync,
15
+ mkdirSync,
18
16
  readdirSync,
17
+ rmSync,
18
+ writeFileSync,
19
19
  } from "node:fs";
20
- import { resolve, dirname, join } from "node:path";
20
+ import { dirname, join, resolve } from "node:path";
21
+ import { isMappedArray, isRef } from "@jxsuite/schema/guards";
21
22
  import { loadProjectConfig } from "./site-loader.ts";
22
23
  import { discoverPages, expandDynamicRoutes, readPageDocument } from "./pages-discovery.ts";
23
24
  import { buildProjectFormatRegistry } from "./format-host.ts";
@@ -28,35 +29,39 @@ import { injectContext } from "./context-injection.ts";
28
29
  import { compile, compileServer, compileSiteServer } from "../compiler.ts";
29
30
  import { compileElement } from "../targets/compile-element.ts";
30
31
  import {
32
+ DEFAULT_LIT_HTML_SRC,
33
+ DEFAULT_REACTIVITY_SRC,
34
+ buildComponentCSS,
31
35
  buildInitialScope,
32
- isTemplateString,
36
+ collectServerEntries,
37
+ collectStyles,
33
38
  evaluateStaticTemplate,
34
- preRenderComponentHtml,
35
39
  isComponentFullyStatic,
36
- buildComponentCSS,
37
- collectServerEntries,
40
+ isTemplateString,
41
+ preRenderComponentHtml,
38
42
  renderStaticNode,
39
- resolveStaticValue,
40
43
  resolveRefValue,
41
- DEFAULT_REACTIVITY_SRC,
42
- DEFAULT_LIT_HTML_SRC,
44
+ resolveStaticValue,
43
45
  } from "../shared.ts";
44
- import { loadContentTypes, loadContentConfig, resolveContentTypeRefs } from "./content-loader.ts";
46
+ import { loadContentConfig, loadContentTypes, resolveContentTypeRefs } from "./content-loader.ts";
45
47
  import { resolvePrototypes } from "./prototype-resolver.ts";
46
48
  import { transformImageNodes } from "./image-transform.ts";
47
- import { loadCache, saveCache, getImageCacheDir } from "./image-cache.ts";
49
+ import { getImageCacheDir, loadCache, saveCache } from "./image-cache.ts";
48
50
  import type { ImageConfig } from "./image-optimizer.ts";
49
51
  import type { ImageMetaCache } from "./image-transform.ts";
50
52
  import type {
53
+ JsonValue,
54
+ JxAttributeValue,
51
55
  JxElement,
52
- JxMutableNode,
53
- JxStyle,
54
56
  JxHeadEntry,
57
+ JxMutableNode,
55
58
  JxStateDefinition,
59
+ JxStyle,
56
60
  ProjectConfig,
57
61
  } from "@jxsuite/schema/types";
58
62
  import type { ContentLoaderEntry } from "@jxsuite/parser/types";
59
63
  import type { SiteRoute } from "../types.ts";
64
+ import type { CacheManifest } from "./image-cache.js";
60
65
 
61
66
  /**
62
67
  * Build an entire Jx site from a project directory.
@@ -99,7 +104,7 @@ export async function buildSite(
99
104
 
100
105
  // ── 2. Clean output directory ───────────────────────────────────────────
101
106
  if (clean && existsSync(outDir)) {
102
- rmSync(outDir, { recursive: true, force: true });
107
+ rmSync(outDir, { force: true, recursive: true });
103
108
  }
104
109
  mkdirSync(outDir, { recursive: true });
105
110
 
@@ -133,8 +138,8 @@ export async function buildSite(
133
138
  // ── 5. Compile site components ──────────────────────────────────────────
134
139
  const componentsDir = resolve(projectRoot, "components");
135
140
  const compiledComponentTags: string[] = [];
136
- const componentCSS: Map<string, string> = new Map(); // tagName → CSS text
137
- const componentDefs: Map<string, JxElement> = new Map(); // tagName → parsed component definition
141
+ const componentCSS = new Map<string, string>(); // TagName → CSS text
142
+ const componentDefs = new Map<string, JxElement>(); // TagName → parsed component definition
138
143
  if (existsSync(componentsDir)) {
139
144
  log("Compiling components...");
140
145
  const componentExtensions = [".json", ...formatRegistry.documentExtensions("component")];
@@ -148,14 +153,16 @@ export async function buildSite(
148
153
  try {
149
154
  const componentPath = resolve(componentsDir, file);
150
155
  const result = await compileElement(componentPath, {
151
- $media: projectConfig.$media,
156
+ ...(projectConfig.$media ? { $media: projectConfig.$media } : {}),
152
157
  formats: formatRegistry,
153
158
  });
154
159
  for (const f of result.files) {
155
160
  const outName = f.path.includes("/") ? (f.path.split("/").pop() as string) : f.path;
156
161
  writeFileSync(resolve(componentOutDir, outName), f.content, "utf8");
157
- if (f.tagName) compiledComponentTags.push(f.tagName);
158
- fileCount++;
162
+ if (f.tagName) {
163
+ compiledComponentTags.push(f.tagName);
164
+ }
165
+ fileCount += 1;
159
166
  }
160
167
 
161
168
  // Pre-render component HTML scaffold and CSS sidecar
@@ -166,11 +173,11 @@ export async function buildSite(
166
173
  if (css) {
167
174
  componentCSS.set(doc.tagName, css);
168
175
  writeFileSync(resolve(componentOutDir, `${doc.tagName}.css`), css, "utf8");
169
- fileCount++;
176
+ fileCount += 1;
170
177
  }
171
178
  }
172
- } catch (e) {
173
- const err = e as Error;
179
+ } catch (error) {
180
+ const err = error as Error;
174
181
  errors.push(`Error compiling component ${file}: ${err.message}`);
175
182
  console.error(`Error compiling component ${file}: ${err.message}`);
176
183
  }
@@ -187,7 +194,7 @@ export async function buildSite(
187
194
  for (const [, doc] of componentDefs) {
188
195
  const entries = collectServerEntries(doc);
189
196
  for (const entry of entries) {
190
- const resolvedSrc = "./components/" + entry.src.replace(/^\.\//, "");
197
+ const resolvedSrc = `./components/${entry.src.replace(/^\.\//, "")}`;
191
198
  siteServerEntries.push({
192
199
  exportName: entry.exportName,
193
200
  src: resolvedSrc,
@@ -225,9 +232,11 @@ export async function buildSite(
225
232
  );
226
233
 
227
234
  // Determine which component tags are fully static (for script omission)
228
- const staticTags: Set<string> = new Set();
235
+ const staticTags = new Set<string>();
229
236
  for (const [tag, def] of componentDefs) {
230
- if (isComponentFullyStatic(def)) staticTags.add(tag);
237
+ if (isComponentFullyStatic(def)) {
238
+ staticTags.add(tag);
239
+ }
231
240
  }
232
241
 
233
242
  // Inject component CSS and JS scripts
@@ -244,29 +253,33 @@ export async function buildSite(
244
253
  const outPath = routeToOutputPath(route.urlPattern, outDir, trailingSlash);
245
254
  mkdirSync(dirname(outPath), { recursive: true });
246
255
  writeFileSync(outPath, result.html, "utf8");
247
- fileCount++;
256
+ fileCount += 1;
248
257
 
249
258
  // Write serialized export sidecars alongside HTML (formats with exportTarget: true)
250
259
  for (const fmt of formatRegistry.withCapability("serialize")) {
251
- if (!fmt.exportTarget) continue;
260
+ if (!fmt.exportTarget) {
261
+ continue;
262
+ }
252
263
  try {
253
264
  const content = (await fmt.call("serialize", result.doc, {
254
- mode: "export",
265
+ buildScope: (state: Record<string, JxStateDefinition>) =>
266
+ buildInitialScope(state, null),
255
267
  componentDefs,
256
268
  evaluateTemplate: (value: string, scope: Record<string, unknown>) => {
257
- if (!isTemplateString(value)) return undefined;
269
+ if (!isTemplateString(value)) {
270
+ return;
271
+ }
258
272
  return evaluateStaticTemplate(value, scope) ?? value;
259
273
  },
260
- buildScope: (state: Record<string, JxStateDefinition>) =>
261
- buildInitialScope(state, null),
274
+ mode: "export",
262
275
  })) as string;
263
276
  if (content) {
264
277
  const sidecarPath = outPath.replace(/\.html$/, fmt.extensions[0]);
265
278
  writeFileSync(sidecarPath, content, "utf8");
266
- fileCount++;
279
+ fileCount += 1;
267
280
  }
268
- } catch (e) {
269
- const err = e as Error;
281
+ } catch (error) {
282
+ const err = error as Error;
270
283
  errors.push(`Error exporting ${fmt.name} for ${route.urlPattern}: ${err.message}`);
271
284
  }
272
285
  }
@@ -276,17 +289,17 @@ export async function buildSite(
276
289
  const filePath = resolve(dirname(outPath), file.path);
277
290
  mkdirSync(dirname(filePath), { recursive: true });
278
291
  writeFileSync(filePath, file.content, "utf8");
279
- fileCount++;
292
+ fileCount += 1;
280
293
  }
281
294
 
282
295
  // Write server handler if present
283
296
  if (result.serverHandler) {
284
297
  const serverPath = resolve(dirname(outPath), "_server.js");
285
298
  writeFileSync(serverPath, result.serverHandler, "utf8");
286
- fileCount++;
299
+ fileCount += 1;
287
300
  }
288
- } catch (e) {
289
- const err = e as Error;
301
+ } catch (error) {
302
+ const err = error as Error;
290
303
  const msg = `Error compiling ${route.urlPattern}: ${err.message}`;
291
304
  errors.push(msg);
292
305
  console.error(msg);
@@ -310,16 +323,18 @@ export async function buildSite(
310
323
 
311
324
  // ── 6c. Generate site-wide server worker ────────────────────────────────
312
325
  if (projectConfig.build.adapter) {
313
- const adapter = projectConfig.build.adapter;
326
+ const { adapter } = projectConfig.build;
314
327
  log("Generating site-wide server worker...");
315
328
 
316
329
  const deduped = new Map();
317
330
  for (const entry of siteServerEntries) {
318
- if (!deduped.has(entry.exportName)) deduped.set(entry.exportName, entry);
331
+ if (!deduped.has(entry.exportName)) {
332
+ deduped.set(entry.exportName, entry);
333
+ }
319
334
  }
320
335
 
321
336
  // Cloudflare Pages uses advanced mode (_worker.js inside the build output) — the
322
- // functions/ directory convention only works from the project root, not from dist/.
337
+ // Functions/ directory convention only works from the project root, not from dist/.
323
338
  // A static-only Pages site needs no worker at all.
324
339
  const skipWorker = adapter === "cloudflare-pages" && deduped.size === 0;
325
340
  const workerSource = skipWorker ? null : compileSiteServer([...deduped.values()], { adapter });
@@ -327,17 +342,17 @@ export async function buildSite(
327
342
  if (workerSource) {
328
343
  const workerName = adapter === "cloudflare-pages" ? "_worker.js" : "worker.js";
329
344
  writeFileSync(resolve(outDir, workerName), workerSource, "utf8");
330
- fileCount++;
345
+ fileCount += 1;
331
346
  log(` Generated dist/${workerName} (${deduped.size} server function(s))`);
332
347
 
333
348
  if (adapter === "cloudflare-pages") {
334
349
  // Only invoke the worker for server routes; everything else stays static.
335
350
  writeFileSync(
336
351
  resolve(outDir, "_routes.json"),
337
- JSON.stringify({ version: 1, include: ["/_jx/*"], exclude: [] }, null, 2) + "\n",
352
+ `${JSON.stringify({ exclude: [], include: ["/_jx/*"], version: 1 }, null, 2)}\n`,
338
353
  "utf8",
339
354
  );
340
- fileCount++;
355
+ fileCount += 1;
341
356
  }
342
357
 
343
358
  // Copy server source files into dist/components/ so worker imports resolve
@@ -383,7 +398,7 @@ export async function buildSite(
383
398
  log(` ${errors.length} error(s)`);
384
399
  }
385
400
 
386
- return { routes: routes.length, files: fileCount, errors };
401
+ return { errors, files: fileCount, routes: routes.length };
387
402
  }
388
403
 
389
404
  /**
@@ -409,9 +424,9 @@ async function compilePage(
409
424
  route: SiteRoute,
410
425
  projectConfig: ProjectConfig,
411
426
  projectRoot: string,
412
- contentTypes: Map<string, ContentLoaderEntry[]> = new Map(),
413
- imageCache: import("./image-cache.js").CacheManifest | null = null,
414
- componentDefs: Map<string, JxElement> = new Map(),
427
+ contentTypes = new Map<string, ContentLoaderEntry[]>(),
428
+ imageCache: CacheManifest | null = null,
429
+ componentDefs = new Map<string, JxElement>(),
415
430
  imageMetaCache: ImageMetaCache | null = null,
416
431
  formatRegistry?: FormatRegistry,
417
432
  ) {
@@ -422,9 +437,9 @@ async function compilePage(
422
437
  const layoutDoc = resolveLayout(pageDoc, projectConfig, projectRoot);
423
438
 
424
439
  // Extract head arrays before they get lost in the merge
425
- const pageHead = pageDoc.$head ?? layoutDoc._pageHead ?? ([] as JxHeadEntry[]);
426
- const layoutHead = layoutDoc.$head ?? [];
427
- const pageTitle = pageDoc.title ?? layoutDoc._pageTitle ?? (null as string | null);
440
+ const pageHead = (pageDoc.$head ?? layoutDoc._pageHead ?? []) as JxHeadEntry[];
441
+ const layoutHead = (layoutDoc.$head ?? []) as JxHeadEntry[];
442
+ const pageTitle = (pageDoc.title ?? layoutDoc._pageTitle ?? null) as string | null;
428
443
 
429
444
  // Clean up internal properties
430
445
  delete layoutDoc._pageHead;
@@ -453,19 +468,28 @@ async function compilePage(
453
468
  const resolvedLayoutHead = resolveHeadTemplates(layoutHead, scope);
454
469
 
455
470
  // Resolve template strings in the document tree (innerHTML, textContent, style, attributes)
456
- // so that timing: "compiler" data is baked into the static HTML
471
+ // So that timing: "compiler" data is baked into the static HTML
457
472
  resolveDocTemplates(layoutDoc, scope);
458
473
 
459
- // Expand registered custom elements (apply $props, pre-render, mark static/prerendered)
460
- expandComponents(layoutDoc, componentDefs);
474
+ // Expand registered custom elements (apply $props, pre-render, mark static/prerendered).
475
+ // Slot children are serialized to HTML strings during expansion — before compileStyles can walk
476
+ // Them — so their static styles are collected here and injected as a page style block below.
477
+ const slotCss: SlotCssCollector = {
478
+ counter: { n: 0 },
479
+ media: { ...projectConfig.$media, ...layoutDoc.$media },
480
+ rules: [],
481
+ };
482
+ expandComponents(layoutDoc, componentDefs, slotCss);
461
483
 
462
484
  // Strip resolved timing: "compiler" state entries — they're now baked into the tree
463
- // and keeping them would cause isDynamic() to misclassify the page as dynamic.
485
+ // And keeping them would cause isDynamic() to misclassify the page as dynamic.
464
486
  // Also strip resolved content arrays (from ContentCollection) that have been
465
- // baked into unrolled map templates.
487
+ // Baked into unrolled map templates.
466
488
  if (layoutDoc.state) {
467
489
  for (const [key, def] of Object.entries(layoutDoc.state)) {
468
- if (key === "$site" || key === "$page") continue;
490
+ if (key === "$site" || key === "$page") {
491
+ continue;
492
+ }
469
493
  if (
470
494
  def &&
471
495
  typeof def === "object" &&
@@ -485,7 +509,7 @@ async function compilePage(
485
509
  // Merge $head from site + layout + page
486
510
  const mergedHead = mergeHead(resolvedSiteHead, resolvedLayoutHead, resolvedPageHead, {
487
511
  title,
488
- charset: projectConfig.defaults?.charset ?? "utf-8",
512
+ charset: projectConfig.defaults?.charset ?? "utf8",
489
513
  ...(projectConfig.name != null && { siteName: projectConfig.name }),
490
514
  ...(projectConfig.url != null && { siteUrl: projectConfig.url }),
491
515
  pageUrl: route.urlPattern,
@@ -509,11 +533,19 @@ async function compilePage(
509
533
 
510
534
  // Compile the document using the existing compiler
511
535
  const result = await compile(layoutDoc, {
512
- title,
513
536
  lang: projectConfig.defaults?.lang ?? "en",
514
537
  projectStyle: projectConfig.style ?? null,
538
+ title,
515
539
  });
516
540
 
541
+ // Inject CSS rules collected from component slot content
542
+ if (slotCss.rules.length > 0) {
543
+ result.html = result.html.replace(
544
+ "</head>",
545
+ `<style>\n${slotCss.rules.join("\n")}\n</style>\n</head>`,
546
+ );
547
+ }
548
+
517
549
  // Post-process: inject merged <head> content into the compiled HTML
518
550
  result.html = injectHead(result.html, mergedHead, projectConfig.defaults?.lang ?? "en");
519
551
 
@@ -522,7 +554,7 @@ async function compilePage(
522
554
  (e: JxElement | string) => typeof e === "string" && !e.startsWith("./") && !e.startsWith("../"),
523
555
  );
524
556
  if (npmElements.length > 0) {
525
- result.html = injectNpmElementScripts(result.html, /** @type {string[]} */ npmElements);
557
+ result.html = injectNpmElementScripts(result.html, npmElements as string[]);
526
558
  }
527
559
 
528
560
  // Compile server handler if applicable (skip when provider bundles site-wide)
@@ -539,10 +571,10 @@ async function compilePage(
539
571
  }
540
572
 
541
573
  return {
542
- html: result.html,
574
+ doc: layoutDoc,
543
575
  files: result.files,
576
+ html: result.html,
544
577
  serverHandler,
545
- doc: layoutDoc,
546
578
  };
547
579
  }
548
580
 
@@ -555,7 +587,9 @@ async function compilePage(
555
587
  */
556
588
  function resolveHeadTemplates(headEntries: JxHeadEntry[], scope: Record<string, unknown>) {
557
589
  return headEntries.map((entry: JxHeadEntry) => {
558
- if (!entry || typeof entry !== "object") return entry;
590
+ if (!entry || typeof entry !== "object") {
591
+ return entry;
592
+ }
559
593
  const resolved = { ...entry };
560
594
  if (resolved.attributes) {
561
595
  resolved.attributes = { ...resolved.attributes };
@@ -584,7 +618,9 @@ function resolveHeadTemplates(headEntries: JxHeadEntry[], scope: Record<string,
584
618
  */
585
619
  function resolveHeadBareSpecifiers(headEntries: JxHeadEntry[]) {
586
620
  return headEntries.map((entry: JxHeadEntry) => {
587
- if (!entry || typeof entry !== "object" || !entry.attributes) return entry;
621
+ if (!entry || typeof entry !== "object" || !entry.attributes) {
622
+ return entry;
623
+ }
588
624
  const resolved = { ...entry, attributes: { ...entry.attributes } };
589
625
  for (const key of ["href", "src"]) {
590
626
  const val = resolved.attributes[key];
@@ -615,41 +651,46 @@ function isBareSpecifier(s: string) {
615
651
  /**
616
652
  * Deep-clone a map template, resolving template strings and $ref values against the given scope.
617
653
  *
618
- * @param {JxMutableNode} template
654
+ * @param {JxElement} template
619
655
  * @param {Record<string, unknown>} scope
620
- * @returns {JxMutableNode}
656
+ * @returns {JxElement}
621
657
  */
622
- function expandMapTemplate(template: JxMutableNode, scope: Record<string, unknown>) {
623
- if (!template || typeof template !== "object") return template;
624
- const node = {} as JxMutableNode;
658
+ function expandMapTemplate(template: JxElement, scope: Record<string, unknown>): JxElement {
659
+ if (!template || typeof template !== "object") {
660
+ return template;
661
+ }
662
+ const node = {} as JxElement;
625
663
  for (const [k, v] of Object.entries(template)) {
626
664
  if (k === "children" && Array.isArray(v)) {
627
665
  node.children = v.map((child) => {
628
- if (typeof child === "string") return child;
666
+ if (typeof child === "string") {
667
+ return child;
668
+ }
629
669
  return expandMapTemplate(/** @type {JxMutableNode} */ child, scope);
630
670
  });
631
671
  } else if (k === "style" && v && typeof v === "object") {
632
- const style = { ...v } as Record<string, unknown>;
672
+ const style: JxStyle = { ...(v as JxStyle) };
633
673
  for (const [sk, sv] of Object.entries(style)) {
634
- if (typeof sv === "string" && isTemplateString(sv)) {
635
- style[sk] = evaluateMapTemplate(sv, scope) ?? sv;
674
+ if (isTemplateString(sv)) {
675
+ // Template evaluation yields a substituted scalar for style values.
676
+ style[sk] = (evaluateMapTemplate(sv, scope) as string | number | undefined) ?? sv;
636
677
  }
637
678
  }
638
679
  node.style = style;
639
680
  } else if (k === "attributes" && v && typeof v === "object") {
640
- const attrs = { ...v } as Record<string, unknown>;
681
+ const attrs = { ...(v as Record<string, JxAttributeValue>) };
641
682
  for (const [ak, av] of Object.entries(attrs)) {
642
- if (typeof av === "string" && isTemplateString(av)) {
643
- attrs[ak] = evaluateMapTemplate(av, scope) ?? av;
683
+ if (isTemplateString(av)) {
684
+ attrs[ak] = (evaluateMapTemplate(av, scope) as JxAttributeValue | undefined) ?? av;
644
685
  }
645
686
  }
646
687
  node.attributes = attrs;
647
688
  } else if (k === "$props" && v && typeof v === "object") {
648
- const props = { ...v } as Record<string, unknown>;
689
+ const props = { ...(v as Record<string, JsonValue>) };
649
690
  for (const [pk, pv] of Object.entries(props)) {
650
- if (typeof pv === "string" && isTemplateString(pv)) {
651
- const resolved = evaluateMapTemplate(pv, scope);
652
- // null = evaluation error → keep template string; undefined = missing data → use null
691
+ if (isTemplateString(pv)) {
692
+ const resolved = evaluateMapTemplate(pv, scope) as JsonValue | undefined;
693
+ // Null = evaluation error → keep template string; undefined = missing data → use null
653
694
  props[pk] = resolved !== null ? (resolved ?? null) : pv;
654
695
  }
655
696
  }
@@ -695,14 +736,16 @@ function evaluateMapTemplate(str: string, scope: Record<string, unknown>) {
695
736
  * @param {Record<string, unknown>} scope
696
737
  */
697
738
  function resolveDocTemplates(node: JxElement | string, scope: Record<string, unknown>) {
698
- if (!node || typeof node !== "object") return;
739
+ if (!node || typeof node !== "object") {
740
+ return;
741
+ }
699
742
 
700
743
  if (typeof node.innerHTML === "string" && isTemplateString(node.innerHTML)) {
701
744
  const resolved = evaluateStaticTemplate(node.innerHTML, scope);
702
745
  if (resolved != null) {
703
746
  // Encode any remaining `${` as HTML entities so the compile phase won't
704
- // re-interpret them as template expressions. After resolution, any `${` in the
705
- // result is literal content (e.g., code examples), not an intentional template.
747
+ // Re-interpret them as template expressions. After resolution, any `${` in the
748
+ // Result is literal content (e.g., code examples), not an intentional template.
706
749
  node.innerHTML = String(resolved).replaceAll("${", "&#36;{");
707
750
  }
708
751
  }
@@ -731,32 +774,26 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
731
774
  }
732
775
  }
733
776
  }
734
- const rawChildren = node.children as unknown;
735
- if (
736
- rawChildren &&
737
- typeof rawChildren === "object" &&
738
- !Array.isArray(rawChildren) &&
739
- (rawChildren as JxMutableNode).$prototype === "Array"
740
- ) {
741
- const arrayDef = rawChildren as JxMutableNode;
742
- const itemsSrc = arrayDef.items;
777
+ const rawChildren = node.children;
778
+ if (isMappedArray(rawChildren)) {
779
+ const itemsSrc = rawChildren.items;
743
780
  let items = null;
744
- if (itemsSrc && typeof itemsSrc === "object" && (itemsSrc as JxMutableNode).$ref) {
745
- const ref = (itemsSrc as JxMutableNode).$ref as string;
746
- items = resolveRefValue(ref, scope);
781
+ if (isRef(itemsSrc)) {
782
+ items = resolveRefValue(itemsSrc.$ref, scope);
747
783
  } else if (Array.isArray(itemsSrc)) {
748
784
  items = itemsSrc;
749
785
  }
750
- if (Array.isArray(items) && arrayDef.map) {
786
+ const mapTemplate = rawChildren.map;
787
+ if (Array.isArray(items) && mapTemplate) {
751
788
  node.children = items.map((item, index) => {
752
789
  const childScope = Object.create(scope);
753
- childScope.$map = { item, index };
790
+ childScope.$map = { index, item };
754
791
  childScope["$map/item"] = item;
755
792
  childScope["$map/index"] = index;
756
- const expanded = expandMapTemplate(arrayDef.map, childScope);
757
- resolveDocTemplates(expanded as JxElement, childScope);
793
+ const expanded = expandMapTemplate(mapTemplate, childScope);
794
+ resolveDocTemplates(expanded, childScope);
758
795
  return expanded;
759
- }) as (string | JxElement)[];
796
+ });
760
797
  return;
761
798
  }
762
799
  }
@@ -786,29 +823,46 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
786
823
  }
787
824
  }
788
825
  resolveDocTemplates(child, scope);
789
- i++;
826
+ i += 1;
790
827
  }
791
828
  }
792
829
  }
793
830
 
831
+ /** Collector for CSS rules extracted from component slot content during expansion. */
832
+ interface SlotCssCollector {
833
+ rules: string[];
834
+ counter: { n: number };
835
+ media: Record<string, string>;
836
+ }
837
+
794
838
  /**
795
839
  * Walk the document tree and expand registered custom elements in-place. Applies $props via
796
- * preRenderComponentHtml, marks static/prerendered.
840
+ * preRenderComponentHtml, marks static/prerendered. Slot children get their static styles collected
841
+ * into `slotCss` (assigning jxs-N classes) before being serialized to HTML.
797
842
  *
798
843
  * @param {JxElement | string} node
799
844
  * @param {Map<string, JxElement>} componentDefs
845
+ * @param {SlotCssCollector} [slotCss]
800
846
  */
801
- function expandComponents(node: JxElement | string, componentDefs: Map<string, JxElement>) {
802
- if (!node || typeof node !== "object") return;
847
+ function expandComponents(
848
+ node: JxElement | string,
849
+ componentDefs: Map<string, JxElement>,
850
+ slotCss?: SlotCssCollector,
851
+ ) {
852
+ if (!node || typeof node !== "object") {
853
+ return;
854
+ }
803
855
  if (Array.isArray(node)) {
804
- node.forEach((n) => expandComponents(n, componentDefs));
856
+ for (const n of node) {
857
+ expandComponents(n, componentDefs, slotCss);
858
+ }
805
859
  return;
806
860
  }
807
861
 
808
862
  // Recurse into children first (bottom-up expansion)
809
863
  if (Array.isArray(node.children)) {
810
864
  for (const child of node.children) {
811
- expandComponents(child, componentDefs);
865
+ expandComponents(child, componentDefs, slotCss);
812
866
  }
813
867
  }
814
868
 
@@ -816,7 +870,14 @@ function expandComponents(node: JxElement | string, componentDefs: Map<string, J
816
870
  if (def) {
817
871
  const slotContent =
818
872
  Array.isArray(node.children) && node.children.length > 0
819
- ? node.children.map((c: JxElement | string) => renderStaticNode(c, {}, null)).join("\n")
873
+ ? node.children
874
+ .map((c: JxElement | string) => {
875
+ if (slotCss && c && typeof c === "object") {
876
+ collectStyles(c, slotCss.rules, slotCss.media, "", slotCss.counter, "jxs");
877
+ }
878
+ return renderStaticNode(c, {}, null);
879
+ })
880
+ .join("\n")
820
881
  : null;
821
882
 
822
883
  const innerHTML = preRenderComponentHtml(def, node.$props || null, slotContent);
@@ -827,17 +888,19 @@ function expandComponents(node: JxElement | string, componentDefs: Map<string, J
827
888
 
828
889
  // Resolve template-string host styles with props (per-instance values like background-image)
829
890
  if (def.style && node.$props) {
830
- let stateDefs: Record<string, JxStateDefinition> = { ...def.state };
891
+ const stateDefs: Record<string, JxStateDefinition> = { ...def.state };
831
892
  for (const [key, value] of Object.entries(node.$props)) {
832
- if (key in stateDefs) stateDefs[key] = value as JxStateDefinition;
833
- else stateDefs[key] = value as JxStateDefinition;
893
+ stateDefs[key] =
894
+ key in stateDefs ? (value as JxStateDefinition) : (value as JxStateDefinition);
834
895
  }
835
896
  const scope = buildInitialScope(stateDefs, null);
836
897
  const resolvedStyle: Record<string, unknown> = {};
837
898
  for (const [prop, value] of Object.entries(def.style)) {
838
899
  if (typeof value === "string" && isTemplateString(value)) {
839
900
  const resolved = resolveStaticValue(value, scope);
840
- if (resolved != null) resolvedStyle[prop] = resolved;
901
+ if (resolved != null) {
902
+ resolvedStyle[prop] = resolved;
903
+ }
841
904
  }
842
905
  }
843
906
  if (Object.keys(resolvedStyle).length > 0) {
@@ -847,8 +910,11 @@ function expandComponents(node: JxElement | string, componentDefs: Map<string, J
847
910
 
848
911
  delete node.$props;
849
912
 
850
- if (isStatic) node.$static = true;
851
- else node.$prerendered = true;
913
+ if (isStatic) {
914
+ node.$static = true;
915
+ } else {
916
+ node.$prerendered = true;
917
+ }
852
918
  }
853
919
  }
854
920
 
@@ -865,27 +931,32 @@ function expandComponents(node: JxElement | string, componentDefs: Map<string, J
865
931
  function injectComponentScripts(
866
932
  html: string,
867
933
  allComponentTags: string[],
868
- cssMap: Map<string, string> = new Map(),
869
- staticTags: Set<string> = new Set(),
934
+ cssMap = new Map<string, string>(),
935
+ staticTags = new Set<string>(),
870
936
  ) {
871
937
  // Find which components are actually referenced in this page
872
938
  const usedTags = allComponentTags.filter(
873
- (tag: string) => html.includes(`<${tag}`), // matches <tag> and <tag ...>
939
+ (tag: string) => html.includes(`<${tag}`), // Matches <tag> and <tag ...>
874
940
  );
875
- if (usedTags.length === 0) return html;
941
+ if (usedTags.length === 0) {
942
+ return html;
943
+ }
876
944
 
877
945
  // Inject CSS links in <head> for ALL components that have CSS sidecars
878
946
  const cssLinks = usedTags
879
947
  .filter((tag: string) => cssMap.has(tag))
880
948
  .map((tag: string) => `<link rel="stylesheet" href="/components/${tag}.css">`)
881
949
  .join("\n ");
950
+ let result = html;
882
951
  if (cssLinks) {
883
- html = html.replace("</head>", ` ${cssLinks}\n</head>`);
952
+ result = result.replace("</head>", ` ${cssLinks}\n</head>`);
884
953
  }
885
954
 
886
955
  // Only inject JS for components that have non-static instances
887
956
  const jsTags = usedTags.filter((tag: string) => !staticTags.has(tag));
888
- if (jsTags.length === 0) return html;
957
+ if (jsTags.length === 0) {
958
+ return result;
959
+ }
889
960
 
890
961
  // Build import map (needed for @vue/reactivity and lit-html)
891
962
  const importMap = `<script type="importmap">
@@ -902,10 +973,10 @@ function injectComponentScripts(
902
973
  .join("\n ");
903
974
 
904
975
  // Check if an import map already exists (from islands etc.)
905
- const hasImportMap = html.includes('<script type="importmap">');
976
+ const hasImportMap = result.includes('<script type="importmap">');
906
977
  const injection = (hasImportMap ? "" : `${importMap}\n `) + moduleScripts;
907
978
 
908
- return html.replace("</body>", ` ${injection}\n</body>`);
979
+ return result.replace("</body>", ` ${injection}\n</body>`);
909
980
  }
910
981
 
911
982
  /**
@@ -942,23 +1013,28 @@ function injectHead(html: string, headEntries: JxHeadEntry[], lang: string) {
942
1013
  let preservedBlocks = "";
943
1014
  if (existingMatch) {
944
1015
  const styles = existingMatch[1].match(/<style>[\s\S]*?<\/style>/gi);
945
- if (styles) preservedBlocks += "\n " + styles.join("\n ");
1016
+ if (styles) {
1017
+ preservedBlocks += `\n ${styles.join("\n ")}`;
1018
+ }
946
1019
  const scripts = existingMatch[1].match(/<script[\s\S]*?<\/script>/gi);
947
- if (scripts) preservedBlocks += "\n " + scripts.join("\n ");
1020
+ if (scripts) {
1021
+ preservedBlocks += `\n ${scripts.join("\n ")}`;
1022
+ }
948
1023
  }
949
- if (headPattern.test(html)) {
950
- html = html.replace(headPattern, `<head>\n ${headHtml}${preservedBlocks}\n</head>`);
1024
+ let result = html;
1025
+ if (headPattern.test(result)) {
1026
+ result = result.replace(headPattern, `<head>\n ${headHtml}${preservedBlocks}\n</head>`);
951
1027
  }
952
1028
 
953
1029
  // Set the lang attribute on <html>
954
- html = html.replace(/<html\s[^>]*>/i, (match: string) => {
1030
+ result = result.replace(/<html\s[^>]*>/i, (match: string) => {
955
1031
  if (/lang=/.test(match)) {
956
1032
  return match.replace(/lang="[^"]*"/, `lang="${lang}"`);
957
1033
  }
958
1034
  return match.replace("<html", `<html lang="${lang}"`);
959
1035
  });
960
1036
 
961
- return html;
1037
+ return result;
962
1038
  }
963
1039
 
964
1040
  /**
@@ -984,7 +1060,7 @@ function routeToOutputPath(urlPattern: string, outDir: string, trailingSlash: st
984
1060
  return join(outDir, segments, "index.html");
985
1061
  }
986
1062
 
987
- // trailingSlash: "never" or default
1063
+ // TrailingSlash: "never" or default
988
1064
  return join(outDir, `${segments}.html`);
989
1065
  }
990
1066
 
@@ -1028,14 +1104,14 @@ function generateRedirects(
1028
1104
  </html>`;
1029
1105
  mkdirSync(dirname(htmlPath), { recursive: true });
1030
1106
  writeFileSync(htmlPath, html, "utf8");
1031
- count++;
1107
+ count += 1;
1032
1108
  redirectLines.push(`${source} ${dest} ${status}`);
1033
1109
  }
1034
1110
 
1035
1111
  // Write _redirects file (Netlify/Cloudflare format)
1036
1112
  if (redirectLines.length > 0) {
1037
- writeFileSync(join(outDir, "_redirects"), redirectLines.join("\n") + "\n", "utf8");
1038
- count++;
1113
+ writeFileSync(join(outDir, "_redirects"), `${redirectLines.join("\n")}\n`, "utf8");
1114
+ count += 1;
1039
1115
  }
1040
1116
 
1041
1117
  return count;
@@ -1046,7 +1122,7 @@ function generateRedirects(
1046
1122
  * @returns {string}
1047
1123
  */
1048
1124
  function escapeHtml(str: string) {
1049
- return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1125
+ return String(str).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1050
1126
  }
1051
1127
 
1052
1128
  /**
@@ -1054,5 +1130,5 @@ function escapeHtml(str: string) {
1054
1130
  * @returns {string}
1055
1131
  */
1056
1132
  function escapeAttr(str: string) {
1057
- return String(str).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
1133
+ return String(str).replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;");
1058
1134
  }