@jxsuite/compiler 0.6.0 → 0.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/compiler",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Jx static HTML compiler, island detector, and site builder",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/shared.js CHANGED
@@ -451,8 +451,10 @@ export function buildAttrs(def, scope) {
451
451
  if (!def.attributes?.decoding) out += ` decoding="async"`;
452
452
  }
453
453
 
454
- if (def.$props && typeof def.$props === "object") {
455
- out += ` data-jx-props="${escapeHtml(JSON.stringify(def.$props))}"`;
454
+ if (def.$static) {
455
+ out += ` data-jx-static`;
456
+ } else if (def.$prerendered) {
457
+ out += ` data-jx-prerendered`;
456
458
  }
457
459
 
458
460
  return out;
@@ -126,12 +126,12 @@ async function getMarkdownModule() {
126
126
  async function loadMarkdownEntry(filePath, directiveOptions) {
127
127
  const { MarkdownFile } = await getMarkdownModule();
128
128
  const file = new MarkdownFile({ src: filePath, directiveOptions });
129
- const result = await file.resolve();
129
+ const result = file.resolve();
130
130
  return {
131
131
  id: result.slug,
132
132
  data: result.frontmatter,
133
133
  body: readFileSync(filePath, "utf-8"),
134
- rendered: result.$body,
134
+ $children: result.$children,
135
135
  _meta: {
136
136
  excerpt: result.$excerpt,
137
137
  toc: result.$toc,
@@ -54,7 +54,8 @@ export function resolveLayout(pageDoc, projectConfig, projectRoot) {
54
54
  }
55
55
 
56
56
  // Distribute page children into layout slots
57
- const pageChildren = pageDoc.children ?? [];
57
+ const rawChildren = pageDoc.children ?? [];
58
+ const pageChildren = typeof rawChildren === "string" ? [rawChildren] : rawChildren;
58
59
  const merged = deepClone(layoutDoc);
59
60
 
60
61
  distributeSlots(merged, pageChildren);
@@ -33,6 +33,7 @@ import {
33
33
  isComponentFullyStatic,
34
34
  buildComponentCSS,
35
35
  collectServerEntries,
36
+ renderStaticNode,
36
37
  DEFAULT_REACTIVITY_SRC,
37
38
  DEFAULT_LIT_HTML_SRC,
38
39
  } from "../shared.js";
@@ -104,8 +105,6 @@ export async function buildSite(projectRoot, options = {}) {
104
105
  /** @type {string[]} */
105
106
  const compiledComponentTags = [];
106
107
  /** @type {Map<string, string>} */
107
- const preRendered = new Map(); // tagName → innerHTML (default state, fallback)
108
- /** @type {Map<string, string>} */
109
108
  const componentCSS = new Map(); // tagName → CSS text
110
109
  /** @type {Map<string, any>} */
111
110
  const componentDefs = new Map(); // tagName → parsed component definition
@@ -138,9 +137,6 @@ export async function buildSite(projectRoot, options = {}) {
138
137
  : JSON.parse(readFileSync(componentPath, "utf8"));
139
138
  if (doc.tagName) {
140
139
  componentDefs.set(doc.tagName, doc);
141
- const innerHTML = preRenderComponentHtml(doc);
142
- if (innerHTML) preRendered.set(doc.tagName, innerHTML);
143
-
144
140
  const css = buildComponentCSS(doc.tagName, doc.style);
145
141
  if (css) {
146
142
  componentCSS.set(doc.tagName, css);
@@ -186,16 +182,14 @@ export async function buildSite(projectRoot, options = {}) {
186
182
  collections,
187
183
  imageCache,
188
184
  outDir,
185
+ componentDefs,
189
186
  );
190
187
 
191
- // Inject pre-rendered component HTML scaffolding (instance-aware)
192
- // Must happen before script injection so we know which tags are fully static
188
+ // Determine which component tags are fully static (for script omission)
193
189
  /** @type {Set<string>} */
194
- let staticTags = new Set();
195
- if (componentDefs.size > 0) {
196
- const preResult = injectPreRenderedComponents(result.html, preRendered, componentDefs);
197
- result.html = preResult.html;
198
- staticTags = preResult.staticTags;
190
+ const staticTags = new Set();
191
+ for (const [tag, def] of componentDefs) {
192
+ if (isComponentFullyStatic(def)) staticTags.add(tag);
199
193
  }
200
194
 
201
195
  // Inject component CSS and JS scripts
@@ -332,6 +326,7 @@ async function compilePage(
332
326
  collections = new Map(),
333
327
  imageCache = null,
334
328
  outDir = "",
329
+ componentDefs = new Map(),
335
330
  ) {
336
331
  // Load the raw page document
337
332
  let pageDoc;
@@ -377,6 +372,9 @@ async function compilePage(
377
372
  // so that timing: "compiler" data is baked into the static HTML
378
373
  resolveDocTemplates(layoutDoc, scope);
379
374
 
375
+ // Expand registered custom elements (apply $props, pre-render, mark static/prerendered)
376
+ expandComponents(layoutDoc, componentDefs);
377
+
380
378
  // Strip resolved timing: "compiler" state entries — they're now baked into the tree
381
379
  // and keeping them would cause isDynamic() to misclassify the page as dynamic
382
380
  if (layoutDoc.state) {
@@ -551,11 +549,73 @@ function resolveDocTemplates(node, scope) {
551
549
  }
552
550
  }
553
551
  }
552
+ if (typeof node.children === "string" && isTemplateString(node.children)) {
553
+ const resolved = evaluateStaticTemplate(node.children, scope);
554
+ if (Array.isArray(resolved)) {
555
+ node.children = resolved;
556
+ for (const child of node.children) {
557
+ resolveDocTemplates(child, scope);
558
+ }
559
+ }
560
+ } else if (Array.isArray(node.children)) {
561
+ let i = 0;
562
+ while (i < node.children.length) {
563
+ const child = node.children[i];
564
+ if (typeof child === "string" && isTemplateString(child)) {
565
+ const resolved = evaluateStaticTemplate(child, scope);
566
+ if (Array.isArray(resolved)) {
567
+ node.children.splice(i, 1, ...resolved);
568
+ for (const spliced of resolved) {
569
+ resolveDocTemplates(spliced, scope);
570
+ }
571
+ i += resolved.length;
572
+ continue;
573
+ }
574
+ }
575
+ resolveDocTemplates(child, scope);
576
+ i++;
577
+ }
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Walk the document tree and expand registered custom elements in-place. Applies $props via
583
+ * preRenderComponentHtml, marks static/prerendered.
584
+ *
585
+ * @param {any} node
586
+ * @param {Map<string, any>} componentDefs
587
+ */
588
+ function expandComponents(node, componentDefs) {
589
+ if (!node || typeof node !== "object") return;
590
+ if (Array.isArray(node)) {
591
+ node.forEach((n) => expandComponents(n, componentDefs));
592
+ return;
593
+ }
594
+
595
+ // Recurse into children first (bottom-up expansion)
554
596
  if (Array.isArray(node.children)) {
555
597
  for (const child of node.children) {
556
- resolveDocTemplates(child, scope);
598
+ expandComponents(child, componentDefs);
557
599
  }
558
600
  }
601
+
602
+ const def = componentDefs.get(node.tagName);
603
+ if (def) {
604
+ const slotContent =
605
+ Array.isArray(node.children) && node.children.length > 0
606
+ ? node.children.map((/** @type {any} */ c) => renderStaticNode(c, {}, null)).join("\n")
607
+ : null;
608
+
609
+ const innerHTML = preRenderComponentHtml(def, node.$props || null, slotContent);
610
+ const isStatic = isComponentFullyStatic(def);
611
+
612
+ node.innerHTML = innerHTML;
613
+ delete node.children;
614
+ delete node.$props;
615
+
616
+ if (isStatic) node.$static = true;
617
+ else node.$prerendered = true;
618
+ }
559
619
  }
560
620
 
561
621
  /**
@@ -616,74 +676,6 @@ function injectComponentScripts(
616
676
  return html.replace("</body>", ` ${injection}\n</body>`);
617
677
  }
618
678
 
619
- /**
620
- * Inject pre-rendered HTML scaffolding into component tags, using instance-specific props.
621
- *
622
- * @param {string} html
623
- * @param {Map<string, string>} preRendered - TagName → default innerHTML (fallback)
624
- * @param {Map<string, any>} componentDefs - TagName → parsed component definition
625
- * @returns {{ html: string; staticTags: Set<string> }}
626
- */
627
- function injectPreRenderedComponents(html, preRendered, componentDefs) {
628
- /** @type {Set<string>} */
629
- const staticTags = new Set();
630
- /** @type {Map<string, boolean>} */
631
- const tagHasNonStaticInstance = new Map();
632
-
633
- for (const [tag] of componentDefs) {
634
- // Match both empty tags and tags with inner content (for slotted components)
635
- const pattern = new RegExp(`<${tag}(\\s[^>]*?)?>([\\s\\S]*?)</${tag}>`, "g");
636
- html = html.replace(pattern, (match, attrsStr, existingInner) => {
637
- const attrs = attrsStr ?? "";
638
- const doc = componentDefs.get(tag);
639
- if (!doc) {
640
- // No definition, use default pre-rendered content
641
- const fallback = preRendered.get(tag) ?? "";
642
- return `<${tag}${attrs}>${fallback}</${tag}>`;
643
- }
644
-
645
- // Extract data-jx-props from the attribute string
646
- /** @type {Record<string, any> | null} */
647
- let props = null;
648
- const propsMatch = attrs.match(/\sdata-jx-props="([^"]*)"/);
649
- if (propsMatch) {
650
- try {
651
- props = JSON.parse(
652
- propsMatch[1]
653
- .replace(/&quot;/g, '"')
654
- .replace(/&amp;/g, "&")
655
- .replace(/&lt;/g, "<")
656
- .replace(/&gt;/g, ">")
657
- .replace(/&#39;/g, "'"),
658
- );
659
- } catch {}
660
- }
661
-
662
- // Pre-render with instance-specific props
663
- const slotContent = existingInner.trim() || null;
664
- const innerHTML = preRenderComponentHtml(doc, props, slotContent);
665
-
666
- const isStatic = isComponentFullyStatic(doc);
667
- if (!isStatic) tagHasNonStaticInstance.set(tag, true);
668
-
669
- if (isStatic) {
670
- // Strip data-jx-props from attrs for fully static instances
671
- let cleanAttrs = attrs.replace(/\s*data-jx-props="[^"]*"/, "");
672
- return `<${tag}${cleanAttrs} data-jx-static>${innerHTML}</${tag}>`;
673
- } else {
674
- return `<${tag}${attrs} data-jx-prerendered>${innerHTML}</${tag}>`;
675
- }
676
- });
677
-
678
- // Track whether ALL instances of this tag are static
679
- if (componentDefs.has(tag) && !tagHasNonStaticInstance.has(tag)) {
680
- staticTags.add(tag);
681
- }
682
- }
683
-
684
- return { html, staticTags };
685
- }
686
-
687
679
  /**
688
680
  * Inject <script type="module"> tags for npm package $elements (cherry-picked component imports).
689
681
  * Bare specifiers are resolved to /node_modules/ paths.