@jxsuite/compiler 0.6.1 → 0.7.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 +256 -252
- package/package.json +3 -3
- package/src/shared.js +103 -89
- package/src/site/content-loader.js +2 -2
- package/src/site/image-cache.js +1 -8
- package/src/site/image-transform.js +104 -6
- package/src/site/layout-resolver.js +2 -1
- package/src/site/prototype-resolver.js +12 -2
- package/src/site/site-build.js +123 -84
- package/src/targets/compile-element.js +14 -16
package/src/site/site-build.js
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
import {
|
|
12
12
|
readFileSync,
|
|
13
13
|
writeFileSync,
|
|
14
|
+
copyFileSync,
|
|
14
15
|
mkdirSync,
|
|
15
16
|
existsSync,
|
|
17
|
+
renameSync,
|
|
16
18
|
rmSync,
|
|
17
19
|
cpSync,
|
|
18
20
|
readdirSync,
|
|
@@ -33,6 +35,8 @@ import {
|
|
|
33
35
|
isComponentFullyStatic,
|
|
34
36
|
buildComponentCSS,
|
|
35
37
|
collectServerEntries,
|
|
38
|
+
renderStaticNode,
|
|
39
|
+
resolveStaticValue,
|
|
36
40
|
DEFAULT_REACTIVITY_SRC,
|
|
37
41
|
DEFAULT_LIT_HTML_SRC,
|
|
38
42
|
} from "../shared.js";
|
|
@@ -68,7 +72,19 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
68
72
|
|
|
69
73
|
// ── 2. Clean output directory ───────────────────────────────────────────
|
|
70
74
|
if (clean && existsSync(outDir)) {
|
|
75
|
+
// Preserve optimized images across builds — sharp re-encoding is expensive
|
|
76
|
+
const optimizedDir = resolve(outDir, "images/_optimized");
|
|
77
|
+
const hasOptimized = existsSync(optimizedDir);
|
|
78
|
+
let tmpOptimized = "";
|
|
79
|
+
if (hasOptimized) {
|
|
80
|
+
tmpOptimized = resolve(projectRoot, ".jx-cache/_optimized_tmp");
|
|
81
|
+
renameSync(optimizedDir, tmpOptimized);
|
|
82
|
+
}
|
|
71
83
|
rmSync(outDir, { recursive: true, force: true });
|
|
84
|
+
if (hasOptimized) {
|
|
85
|
+
mkdirSync(resolve(outDir, "images"), { recursive: true });
|
|
86
|
+
renameSync(tmpOptimized, optimizedDir);
|
|
87
|
+
}
|
|
72
88
|
}
|
|
73
89
|
mkdirSync(outDir, { recursive: true });
|
|
74
90
|
|
|
@@ -104,8 +120,6 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
104
120
|
/** @type {string[]} */
|
|
105
121
|
const compiledComponentTags = [];
|
|
106
122
|
/** @type {Map<string, string>} */
|
|
107
|
-
const preRendered = new Map(); // tagName → innerHTML (default state, fallback)
|
|
108
|
-
/** @type {Map<string, string>} */
|
|
109
123
|
const componentCSS = new Map(); // tagName → CSS text
|
|
110
124
|
/** @type {Map<string, any>} */
|
|
111
125
|
const componentDefs = new Map(); // tagName → parsed component definition
|
|
@@ -138,10 +152,7 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
138
152
|
: JSON.parse(readFileSync(componentPath, "utf8"));
|
|
139
153
|
if (doc.tagName) {
|
|
140
154
|
componentDefs.set(doc.tagName, doc);
|
|
141
|
-
const
|
|
142
|
-
if (innerHTML) preRendered.set(doc.tagName, innerHTML);
|
|
143
|
-
|
|
144
|
-
const css = buildComponentCSS(doc.tagName, doc.style);
|
|
155
|
+
const css = buildComponentCSS(doc.tagName, doc.style, doc, projectConfig.$media ?? {});
|
|
145
156
|
if (css) {
|
|
146
157
|
componentCSS.set(doc.tagName, css);
|
|
147
158
|
writeFileSync(resolve(componentOutDir, `${doc.tagName}.css`), css, "utf8");
|
|
@@ -186,16 +197,14 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
186
197
|
collections,
|
|
187
198
|
imageCache,
|
|
188
199
|
outDir,
|
|
200
|
+
componentDefs,
|
|
189
201
|
);
|
|
190
202
|
|
|
191
|
-
//
|
|
192
|
-
// Must happen before script injection so we know which tags are fully static
|
|
203
|
+
// Determine which component tags are fully static (for script omission)
|
|
193
204
|
/** @type {Set<string>} */
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
result.html = preResult.html;
|
|
198
|
-
staticTags = preResult.staticTags;
|
|
205
|
+
const staticTags = new Set();
|
|
206
|
+
for (const [tag, def] of componentDefs) {
|
|
207
|
+
if (isComponentFullyStatic(def)) staticTags.add(tag);
|
|
199
208
|
}
|
|
200
209
|
|
|
201
210
|
// Inject component CSS and JS scripts
|
|
@@ -272,10 +281,20 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
272
281
|
});
|
|
273
282
|
|
|
274
283
|
if (workerSource) {
|
|
275
|
-
const workerPath = resolve(
|
|
284
|
+
const workerPath = resolve(outDir, "worker.js");
|
|
276
285
|
writeFileSync(workerPath, workerSource, "utf8");
|
|
277
286
|
fileCount++;
|
|
278
|
-
log(` Generated
|
|
287
|
+
log(` Generated dist/worker.js (${deduped.size} server function(s))`);
|
|
288
|
+
|
|
289
|
+
// Copy server source files into dist/components/ so worker imports resolve
|
|
290
|
+
const distComponentsDir = resolve(outDir, "components");
|
|
291
|
+
for (const { src } of deduped.values()) {
|
|
292
|
+
const srcFile = resolve(projectRoot, src.replace(/^\.\//, ""));
|
|
293
|
+
const destFile = resolve(distComponentsDir, src.replace(/^\.\/components\//, ""));
|
|
294
|
+
if (existsSync(srcFile)) {
|
|
295
|
+
copyFileSync(srcFile, destFile);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
279
298
|
}
|
|
280
299
|
}
|
|
281
300
|
|
|
@@ -332,6 +351,7 @@ async function compilePage(
|
|
|
332
351
|
collections = new Map(),
|
|
333
352
|
imageCache = null,
|
|
334
353
|
outDir = "",
|
|
354
|
+
componentDefs = new Map(),
|
|
335
355
|
) {
|
|
336
356
|
// Load the raw page document
|
|
337
357
|
let pageDoc;
|
|
@@ -377,6 +397,9 @@ async function compilePage(
|
|
|
377
397
|
// so that timing: "compiler" data is baked into the static HTML
|
|
378
398
|
resolveDocTemplates(layoutDoc, scope);
|
|
379
399
|
|
|
400
|
+
// Expand registered custom elements (apply $props, pre-render, mark static/prerendered)
|
|
401
|
+
expandComponents(layoutDoc, componentDefs);
|
|
402
|
+
|
|
380
403
|
// Strip resolved timing: "compiler" state entries — they're now baked into the tree
|
|
381
404
|
// and keeping them would cause isDynamic() to misclassify the page as dynamic
|
|
382
405
|
if (layoutDoc.state) {
|
|
@@ -551,11 +574,95 @@ function resolveDocTemplates(node, scope) {
|
|
|
551
574
|
}
|
|
552
575
|
}
|
|
553
576
|
}
|
|
577
|
+
if (typeof node.children === "string" && isTemplateString(node.children)) {
|
|
578
|
+
const resolved = evaluateStaticTemplate(node.children, scope);
|
|
579
|
+
if (Array.isArray(resolved)) {
|
|
580
|
+
node.children = resolved;
|
|
581
|
+
for (const child of node.children) {
|
|
582
|
+
resolveDocTemplates(child, scope);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
} else if (Array.isArray(node.children)) {
|
|
586
|
+
let i = 0;
|
|
587
|
+
while (i < node.children.length) {
|
|
588
|
+
const child = node.children[i];
|
|
589
|
+
if (typeof child === "string" && isTemplateString(child)) {
|
|
590
|
+
const resolved = evaluateStaticTemplate(child, scope);
|
|
591
|
+
if (Array.isArray(resolved)) {
|
|
592
|
+
node.children.splice(i, 1, ...resolved);
|
|
593
|
+
for (const spliced of resolved) {
|
|
594
|
+
resolveDocTemplates(spliced, scope);
|
|
595
|
+
}
|
|
596
|
+
i += resolved.length;
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
resolveDocTemplates(child, scope);
|
|
601
|
+
i++;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Walk the document tree and expand registered custom elements in-place. Applies $props via
|
|
608
|
+
* preRenderComponentHtml, marks static/prerendered.
|
|
609
|
+
*
|
|
610
|
+
* @param {any} node
|
|
611
|
+
* @param {Map<string, any>} componentDefs
|
|
612
|
+
*/
|
|
613
|
+
function expandComponents(node, componentDefs) {
|
|
614
|
+
if (!node || typeof node !== "object") return;
|
|
615
|
+
if (Array.isArray(node)) {
|
|
616
|
+
node.forEach((n) => expandComponents(n, componentDefs));
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Recurse into children first (bottom-up expansion)
|
|
554
621
|
if (Array.isArray(node.children)) {
|
|
555
622
|
for (const child of node.children) {
|
|
556
|
-
|
|
623
|
+
expandComponents(child, componentDefs);
|
|
557
624
|
}
|
|
558
625
|
}
|
|
626
|
+
|
|
627
|
+
const def = componentDefs.get(node.tagName);
|
|
628
|
+
if (def) {
|
|
629
|
+
const slotContent =
|
|
630
|
+
Array.isArray(node.children) && node.children.length > 0
|
|
631
|
+
? node.children.map((/** @type {any} */ c) => renderStaticNode(c, {}, null)).join("\n")
|
|
632
|
+
: null;
|
|
633
|
+
|
|
634
|
+
const innerHTML = preRenderComponentHtml(def, node.$props || null, slotContent);
|
|
635
|
+
const isStatic = isComponentFullyStatic(def);
|
|
636
|
+
|
|
637
|
+
node.innerHTML = innerHTML;
|
|
638
|
+
delete node.children;
|
|
639
|
+
|
|
640
|
+
// Resolve template-string host styles with props (per-instance values like background-image)
|
|
641
|
+
if (def.style && node.$props) {
|
|
642
|
+
let stateDefs = { ...def.state };
|
|
643
|
+
for (const [key, value] of Object.entries(node.$props)) {
|
|
644
|
+
if (key in stateDefs) stateDefs[key] = value;
|
|
645
|
+
else stateDefs[key] = value;
|
|
646
|
+
}
|
|
647
|
+
const scope = buildInitialScope(stateDefs, null);
|
|
648
|
+
/** @type {Record<string, any>} */
|
|
649
|
+
const resolvedStyle = {};
|
|
650
|
+
for (const [prop, value] of Object.entries(def.style)) {
|
|
651
|
+
if (typeof value === "string" && isTemplateString(value)) {
|
|
652
|
+
const resolved = resolveStaticValue(value, scope);
|
|
653
|
+
if (resolved != null) resolvedStyle[prop] = resolved;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (Object.keys(resolvedStyle).length > 0) {
|
|
657
|
+
node.style = { ...node.style, ...resolvedStyle };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
delete node.$props;
|
|
662
|
+
|
|
663
|
+
if (isStatic) node.$static = true;
|
|
664
|
+
else node.$prerendered = true;
|
|
665
|
+
}
|
|
559
666
|
}
|
|
560
667
|
|
|
561
668
|
/**
|
|
@@ -616,74 +723,6 @@ function injectComponentScripts(
|
|
|
616
723
|
return html.replace("</body>", ` ${injection}\n</body>`);
|
|
617
724
|
}
|
|
618
725
|
|
|
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(/"/g, '"')
|
|
654
|
-
.replace(/&/g, "&")
|
|
655
|
-
.replace(/</g, "<")
|
|
656
|
-
.replace(/>/g, ">")
|
|
657
|
-
.replace(/'/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
726
|
/**
|
|
688
727
|
* Inject <script type="module"> tags for npm package $elements (cherry-picked component imports).
|
|
689
728
|
* Bare specifiers are resolved to /node_modules/ paths.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { camelToKebab, RESERVED_KEYS } from "@jxsuite/runtime";
|
|
9
|
-
import { escapeHtml, tagNameToClassName, isSchemaOnly } from "../shared.js";
|
|
9
|
+
import { escapeHtml, tagNameToClassName, isSchemaOnly, collectStyles } from "../shared.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Compile a Jx custom element document to a JS module string.
|
|
@@ -282,6 +282,16 @@ export function emitElementModule(doc, className, elementImports) {
|
|
|
282
282
|
lines.push(" }"); // end constructor
|
|
283
283
|
lines.push("");
|
|
284
284
|
|
|
285
|
+
// Collect CSS rules from children tree (assigns .jx-N classes to defs)
|
|
286
|
+
/** @type {string[]} */
|
|
287
|
+
const cssRules = [];
|
|
288
|
+
if (Array.isArray(doc.children)) {
|
|
289
|
+
const counter = { n: 0 };
|
|
290
|
+
for (const child of doc.children) {
|
|
291
|
+
collectStyles(child, cssRules, doc.$media ?? {}, "", counter, doc.tagName);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
285
295
|
// Template method
|
|
286
296
|
lines.push(" template() {");
|
|
287
297
|
lines.push(" const s = this.state;");
|
|
@@ -312,8 +322,6 @@ export function emitElementModule(doc, className, elementImports) {
|
|
|
312
322
|
lines.push(" }");
|
|
313
323
|
lines.push(" }");
|
|
314
324
|
if (doc.style && typeof doc.style === "object") {
|
|
315
|
-
/** @type {[string, any][]} */
|
|
316
|
-
const staticStyles = [];
|
|
317
325
|
/** @type {[string, string][]} */
|
|
318
326
|
const dynamicStyles = [];
|
|
319
327
|
for (const [prop, value] of Object.entries(doc.style)) {
|
|
@@ -329,13 +337,6 @@ export function emitElementModule(doc, className, elementImports) {
|
|
|
329
337
|
const cssProp = camelToKebab(prop);
|
|
330
338
|
if (typeof value === "string" && value.includes("${")) {
|
|
331
339
|
dynamicStyles.push([cssProp, value]);
|
|
332
|
-
} else {
|
|
333
|
-
staticStyles.push([cssProp, value]);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
if (staticStyles.length > 0) {
|
|
337
|
-
for (const [cssProp, value] of staticStyles) {
|
|
338
|
-
lines.push(` this.style['${cssProp}'] = ${JSON.stringify(value)};`);
|
|
339
340
|
}
|
|
340
341
|
}
|
|
341
342
|
if (dynamicStyles.length > 0) {
|
|
@@ -358,12 +359,10 @@ export function emitElementModule(doc, className, elementImports) {
|
|
|
358
359
|
" const _slotted = Array.from(this.childNodes).filter(n => n.nodeType === 1 || (n.nodeType === 3 && n.textContent.trim()));",
|
|
359
360
|
);
|
|
360
361
|
}
|
|
361
|
-
// Skip clearing innerHTML if content was pre-rendered with correct props
|
|
362
362
|
lines.push(" if (this.hasAttribute('data-jx-prerendered')) {");
|
|
363
363
|
lines.push(" this.removeAttribute('data-jx-prerendered');");
|
|
364
|
-
lines.push(" } else {");
|
|
365
|
-
lines.push(" this.innerHTML = '';");
|
|
366
364
|
lines.push(" }");
|
|
365
|
+
lines.push(" this.innerHTML = '';");
|
|
367
366
|
lines.push(" this.#dispose = effect(() => render(this.template(), this));");
|
|
368
367
|
if (hasSlot) {
|
|
369
368
|
// Replace <slot> placeholder with saved slotted content
|
|
@@ -417,6 +416,7 @@ function emitLitChildren(children, parentStyle, indent) {
|
|
|
417
416
|
function emitLitNode(def, indent) {
|
|
418
417
|
// String children are text nodes
|
|
419
418
|
if (typeof def === "string") {
|
|
419
|
+
if (def.includes("${")) return `${indent}${toLitTextContent(def)}`;
|
|
420
420
|
return `${indent}${escapeHtml(def)}`;
|
|
421
421
|
}
|
|
422
422
|
if (typeof def === "number" || typeof def === "boolean") {
|
|
@@ -646,11 +646,9 @@ function emitStyleString(styleDef) {
|
|
|
646
646
|
|
|
647
647
|
if (value === null || typeof value === "object") continue;
|
|
648
648
|
|
|
649
|
-
const cssProp = camelToKebab(prop);
|
|
650
649
|
if (typeof value === "string" && value.includes("${")) {
|
|
650
|
+
const cssProp = camelToKebab(prop);
|
|
651
651
|
parts.push(`${cssProp}: ${toLitExpr(value)}`);
|
|
652
|
-
} else {
|
|
653
|
-
parts.push(`${cssProp}: ${value}`);
|
|
654
652
|
}
|
|
655
653
|
}
|
|
656
654
|
|