@jxsuite/compiler 0.6.2 → 0.8.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 +99 -87
- package/src/site/image-cache.js +1 -8
- package/src/site/image-transform.js +104 -6
- package/src/site/prototype-resolver.js +12 -2
- package/src/site/site-build.js +50 -3
- package/src/targets/compile-element.js +14 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Jx static HTML compiler, island detector, and site builder",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@apidevtools/json-schema-ref-parser": "^15.3.5",
|
|
40
|
-
"@jxsuite/parser": "^0.
|
|
41
|
-
"@jxsuite/runtime": "^0.
|
|
40
|
+
"@jxsuite/parser": "^0.7.0",
|
|
41
|
+
"@jxsuite/runtime": "^0.7.0",
|
|
42
42
|
"remark-gfm": "^4.0.1",
|
|
43
43
|
"remark-stringify": "^11.0.0",
|
|
44
44
|
"sharp": "^0.34.5",
|
package/src/shared.js
CHANGED
|
@@ -393,25 +393,7 @@ export function buildAttrs(def, scope) {
|
|
|
393
393
|
if (lang) out += ` lang="${escapeHtml(lang)}"`;
|
|
394
394
|
if (dir) out += ` dir="${escapeHtml(dir)}"`;
|
|
395
395
|
|
|
396
|
-
if (def.style) {
|
|
397
|
-
// Collect properties that have @media overrides — these must NOT be inline
|
|
398
|
-
// because inline styles (specificity 1,0,0,0) always beat stylesheet @media rules.
|
|
399
|
-
const mediaOverriddenProps = new Set();
|
|
400
|
-
for (const [k, v] of Object.entries(def.style)) {
|
|
401
|
-
if (k.startsWith("@") && v && typeof v === "object") {
|
|
402
|
-
for (const prop of Object.keys(/** @type {Record<string, any>} */ (v))) {
|
|
403
|
-
if (
|
|
404
|
-
!prop.startsWith(":") &&
|
|
405
|
-
!prop.startsWith(".") &&
|
|
406
|
-
!prop.startsWith("&") &&
|
|
407
|
-
!prop.startsWith("[")
|
|
408
|
-
) {
|
|
409
|
-
mediaOverriddenProps.add(prop);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
396
|
+
if (def.style && scope) {
|
|
415
397
|
const inline = Object.entries(def.style)
|
|
416
398
|
.filter(
|
|
417
399
|
([k, v]) =>
|
|
@@ -420,9 +402,10 @@ export function buildAttrs(def, scope) {
|
|
|
420
402
|
!k.startsWith("&") &&
|
|
421
403
|
!k.startsWith("[") &&
|
|
422
404
|
!k.startsWith("@") &&
|
|
423
|
-
!mediaOverriddenProps.has(k) &&
|
|
424
405
|
v !== null &&
|
|
425
|
-
typeof v !== "object"
|
|
406
|
+
typeof v !== "object" &&
|
|
407
|
+
typeof v === "string" &&
|
|
408
|
+
isTemplateString(v),
|
|
426
409
|
)
|
|
427
410
|
.map(([k, v]) => {
|
|
428
411
|
const value = resolveStaticValue(v, scope);
|
|
@@ -432,7 +415,6 @@ export function buildAttrs(def, scope) {
|
|
|
432
415
|
.join("; ");
|
|
433
416
|
if (inline) out += ` style="${inline}"`;
|
|
434
417
|
}
|
|
435
|
-
|
|
436
418
|
if (def.attributes) {
|
|
437
419
|
for (const [k, v] of Object.entries(def.attributes)) {
|
|
438
420
|
const value = resolveStaticValue(v, scope);
|
|
@@ -508,7 +490,16 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
508
490
|
if (projectStyle && typeof projectStyle === "object") {
|
|
509
491
|
for (const [key, val] of Object.entries(projectStyle)) {
|
|
510
492
|
if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
|
|
511
|
-
// Standalone selector (e.g. `.dark`)
|
|
493
|
+
// Standalone selector (e.g. `.dark`, `:focus-visible`)
|
|
494
|
+
rules.push(`${key} { ${toCSSText(/** @type {any} */ (val))} }`);
|
|
495
|
+
} else if (
|
|
496
|
+
val !== null &&
|
|
497
|
+
typeof val === "object" &&
|
|
498
|
+
!Array.isArray(val) &&
|
|
499
|
+
!key.startsWith("@") &&
|
|
500
|
+
!key.startsWith("--")
|
|
501
|
+
) {
|
|
502
|
+
// Element selector with object value (e.g. `html`, `*`)
|
|
512
503
|
rules.push(`${key} { ${toCSSText(/** @type {any} */ (val))} }`);
|
|
513
504
|
} else if (key.startsWith("@")) {
|
|
514
505
|
// @media block
|
|
@@ -557,23 +548,19 @@ export function compileStyles(doc, mediaQueries = {}, projectStyle = null) {
|
|
|
557
548
|
* @param {string} [_parentSel]
|
|
558
549
|
* @param {{ n: number }} [counter]
|
|
559
550
|
*/
|
|
560
|
-
export function collectStyles(
|
|
551
|
+
export function collectStyles(
|
|
552
|
+
def,
|
|
553
|
+
rules,
|
|
554
|
+
mediaQueries,
|
|
555
|
+
_parentSel = "",
|
|
556
|
+
counter = { n: 0 },
|
|
557
|
+
prefix = "jx",
|
|
558
|
+
) {
|
|
561
559
|
if (!def || typeof def !== "object") return;
|
|
562
560
|
|
|
563
561
|
if (def.style) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
(k) =>
|
|
567
|
-
k.startsWith("@") ||
|
|
568
|
-
k.startsWith(":") ||
|
|
569
|
-
k.startsWith(".") ||
|
|
570
|
-
k.startsWith("&") ||
|
|
571
|
-
k.startsWith("["),
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
// Auto-scope elements that need CSS rules but lack a unique selector
|
|
575
|
-
if (needsCSS && !def.id && !def.className) {
|
|
576
|
-
def.className = `jx-${counter.n++}`;
|
|
562
|
+
if (!def.id && !def.className) {
|
|
563
|
+
def.className = `${prefix}-${counter.n++}`;
|
|
577
564
|
}
|
|
578
565
|
}
|
|
579
566
|
|
|
@@ -584,36 +571,22 @@ export function collectStyles(def, rules, mediaQueries, _parentSel = "", counter
|
|
|
584
571
|
: (def.tagName ?? "*");
|
|
585
572
|
|
|
586
573
|
if (def.style) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
574
|
+
const baseDecls = [];
|
|
575
|
+
for (const [prop, value] of Object.entries(def.style)) {
|
|
576
|
+
if (
|
|
577
|
+
prop.startsWith(":") ||
|
|
578
|
+
prop.startsWith(".") ||
|
|
579
|
+
prop.startsWith("&") ||
|
|
580
|
+
prop.startsWith("[") ||
|
|
581
|
+
prop.startsWith("@")
|
|
582
|
+
)
|
|
583
|
+
continue;
|
|
584
|
+
if (value === null || typeof value === "object") continue;
|
|
585
|
+
if (typeof value === "string" && isTemplateString(value)) continue;
|
|
586
|
+
baseDecls.push(` ${camelToKebab(prop)}: ${value};`);
|
|
603
587
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (mediaOverriddenProps.size > 0) {
|
|
607
|
-
const baseDecls = [];
|
|
608
|
-
for (const p of mediaOverriddenProps) {
|
|
609
|
-
const v = def.style[p];
|
|
610
|
-
if (v !== null && v !== undefined && typeof v !== "object") {
|
|
611
|
-
baseDecls.push(`${camelToKebab(p)}: ${v}`);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
if (baseDecls.length > 0) {
|
|
615
|
-
rules.push(`${selector} { ${baseDecls.join("; ")} }`);
|
|
616
|
-
}
|
|
588
|
+
if (baseDecls.length > 0) {
|
|
589
|
+
rules.push(`${selector} {\n${baseDecls.join("\n")}\n}`);
|
|
617
590
|
}
|
|
618
591
|
|
|
619
592
|
for (const [prop, val] of Object.entries(def.style)) {
|
|
@@ -651,7 +624,7 @@ export function collectStyles(def, rules, mediaQueries, _parentSel = "", counter
|
|
|
651
624
|
|
|
652
625
|
if (Array.isArray(def.children)) {
|
|
653
626
|
def.children.forEach((/** @type {any} */ c) => {
|
|
654
|
-
collectStyles(c, rules, mediaQueries, selector, counter);
|
|
627
|
+
collectStyles(c, rules, mediaQueries, selector, counter, prefix);
|
|
655
628
|
});
|
|
656
629
|
}
|
|
657
630
|
}
|
|
@@ -787,7 +760,13 @@ const SELF_CLOSING = new Set(["input", "br", "hr", "img", "meta", "link", "area"
|
|
|
787
760
|
* @returns {string}
|
|
788
761
|
*/
|
|
789
762
|
export function renderStaticNode(node, scope, slotContent = null) {
|
|
790
|
-
if (typeof node === "string")
|
|
763
|
+
if (typeof node === "string") {
|
|
764
|
+
if (isTemplateString(node) && scope) {
|
|
765
|
+
const val = evaluateStaticTemplate(node, scope);
|
|
766
|
+
return val != null ? escapeHtml(String(val)) : escapeHtml(node);
|
|
767
|
+
}
|
|
768
|
+
return escapeHtml(node);
|
|
769
|
+
}
|
|
791
770
|
if (typeof node === "number" || typeof node === "boolean") return escapeHtml(String(node));
|
|
792
771
|
if (Array.isArray(node))
|
|
793
772
|
return node.map((/** @type {any} */ c) => renderStaticNode(c, scope, slotContent)).join("\n");
|
|
@@ -913,30 +892,63 @@ function _isStaticNode(node) {
|
|
|
913
892
|
}
|
|
914
893
|
|
|
915
894
|
/**
|
|
916
|
-
* Generate
|
|
917
|
-
*
|
|
895
|
+
* Generate CSS rules for a component: host-level styles using tag selector, plus inner element
|
|
896
|
+
* styles using .jx-N selectors via collectStyles.
|
|
918
897
|
*
|
|
919
898
|
* @param {string} tagName - The custom element tag name (used as CSS selector)
|
|
920
899
|
* @param {any} styleDef - The component's style object
|
|
900
|
+
* @param {any} [doc] - The full component document (for walking children)
|
|
901
|
+
* @param {Record<string, any>} [mediaQueries] - Project media query definitions
|
|
921
902
|
* @returns {string} CSS text, or empty string if no styles
|
|
922
903
|
*/
|
|
923
|
-
export function buildComponentCSS(tagName, styleDef) {
|
|
924
|
-
if (!styleDef || typeof styleDef !== "object") return "";
|
|
904
|
+
export function buildComponentCSS(tagName, styleDef, doc = null, mediaQueries = {}) {
|
|
925
905
|
/** @type {string[]} */
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
906
|
+
const rules = [];
|
|
907
|
+
|
|
908
|
+
if (styleDef && typeof styleDef === "object") {
|
|
909
|
+
/** @type {string[]} */
|
|
910
|
+
const decls = [];
|
|
911
|
+
for (const [prop, value] of Object.entries(styleDef)) {
|
|
912
|
+
if (
|
|
913
|
+
prop.startsWith(":") ||
|
|
914
|
+
prop.startsWith(".") ||
|
|
915
|
+
prop.startsWith("&") ||
|
|
916
|
+
prop.startsWith("[") ||
|
|
917
|
+
prop.startsWith("@")
|
|
918
|
+
)
|
|
919
|
+
continue;
|
|
920
|
+
if (value === null || typeof value === "object") continue;
|
|
921
|
+
if (typeof value === "string" && isTemplateString(value)) continue;
|
|
922
|
+
decls.push(` ${camelToKebab(prop)}: ${value};`);
|
|
923
|
+
}
|
|
924
|
+
if (decls.length > 0) {
|
|
925
|
+
rules.push(`${tagName} {\n${decls.join("\n")}\n}`);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
for (const [prop, val] of Object.entries(styleDef)) {
|
|
929
|
+
if (prop.startsWith("@")) {
|
|
930
|
+
const query = prop.startsWith("@--")
|
|
931
|
+
? (mediaQueries[prop.slice(1)] ?? prop.slice(1))
|
|
932
|
+
: prop.slice(1);
|
|
933
|
+
rules.push(`@media ${query} { ${tagName} { ${toCSSText(/** @type {any} */ (val))} } }`);
|
|
934
|
+
} else if (
|
|
935
|
+
prop.startsWith(":") ||
|
|
936
|
+
prop.startsWith(".") ||
|
|
937
|
+
prop.startsWith("&") ||
|
|
938
|
+
prop.startsWith("[")
|
|
939
|
+
) {
|
|
940
|
+
const resolved = prop.startsWith("&") ? prop.replace("&", tagName) : `${tagName}${prop}`;
|
|
941
|
+
rules.push(`${resolved} { ${toCSSText(/** @type {any} */ (val))} }`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
939
944
|
}
|
|
940
|
-
|
|
941
|
-
|
|
945
|
+
|
|
946
|
+
if (doc && Array.isArray(doc.children)) {
|
|
947
|
+
const counter = { n: 0 };
|
|
948
|
+
for (const child of doc.children) {
|
|
949
|
+
collectStyles(child, rules, mediaQueries, "", counter, tagName);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
return rules.length > 0 ? rules.join("\n") + "\n" : "";
|
|
942
954
|
}
|
package/src/site/image-cache.js
CHANGED
|
@@ -74,18 +74,11 @@ export function saveCache(projectRoot, cache) {
|
|
|
74
74
|
*
|
|
75
75
|
* @param {CacheManifest} cache
|
|
76
76
|
* @param {string} key
|
|
77
|
-
* @param {string} outDir - Absolute path to build output dir
|
|
78
77
|
* @returns {ImageManifest | null}
|
|
79
78
|
*/
|
|
80
|
-
export function getCached(cache, key
|
|
79
|
+
export function getCached(cache, key) {
|
|
81
80
|
const entry = cache.entries[key];
|
|
82
81
|
if (!entry) return null;
|
|
83
|
-
|
|
84
|
-
const allExist = entry.manifest.variants.every((v) =>
|
|
85
|
-
existsSync(resolve(outDir, v.outputPath.slice(1))),
|
|
86
|
-
);
|
|
87
|
-
if (!allExist) return null;
|
|
88
|
-
|
|
89
82
|
return entry.manifest;
|
|
90
83
|
}
|
|
91
84
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
|
-
import { resolve, extname } from "node:path";
|
|
10
|
+
import { resolve, extname, basename } from "node:path";
|
|
11
11
|
import { processImage, buildSrcset, contentHash, configHash } from "./image-optimizer.js";
|
|
12
12
|
import { getCached, setCached } from "./image-cache.js";
|
|
13
13
|
|
|
@@ -22,6 +22,29 @@ import { getCached, setCached } from "./image-cache.js";
|
|
|
22
22
|
const SKIP_EXTENSIONS = new Set([".svg", ".gif"]);
|
|
23
23
|
const EXTERNAL_PREFIXES = ["http://", "https://", "data:", "//"];
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} absoluteSrc
|
|
27
|
+
* @param {string} src
|
|
28
|
+
* @param {ImageConfig} config
|
|
29
|
+
* @param {string} outDir
|
|
30
|
+
* @param {CacheManifest} cache
|
|
31
|
+
* @returns {Promise<ImageManifest>}
|
|
32
|
+
*/
|
|
33
|
+
async function resolveManifest(absoluteSrc, src, config, outDir, cache) {
|
|
34
|
+
const key = `${contentHash(absoluteSrc)}:${configHash(config)}`;
|
|
35
|
+
const cached = getCached(cache, key);
|
|
36
|
+
|
|
37
|
+
if (cached) {
|
|
38
|
+
const allExist = cached.variants.every((v) => existsSync(v.absolutePath));
|
|
39
|
+
if (allExist) return cached;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!cached) console.log(` Optimizing ${basename(absoluteSrc)}...`);
|
|
43
|
+
const manifest = await processImage(absoluteSrc, outDir, config);
|
|
44
|
+
setCached(cache, key, src, manifest);
|
|
45
|
+
return manifest;
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
/**
|
|
26
49
|
* Check if a src value should be skipped for optimization.
|
|
27
50
|
*
|
|
@@ -91,6 +114,17 @@ async function walkAndTransform(node, config, projectRoot, outDir, cache, imageR
|
|
|
91
114
|
await transformImgNode(node, config, projectRoot, outDir, cache, imageRefs);
|
|
92
115
|
}
|
|
93
116
|
|
|
117
|
+
if (typeof node.innerHTML === "string" && node.innerHTML.includes("<img")) {
|
|
118
|
+
node.innerHTML = await transformInnerHtmlImages(
|
|
119
|
+
node.innerHTML,
|
|
120
|
+
config,
|
|
121
|
+
projectRoot,
|
|
122
|
+
outDir,
|
|
123
|
+
cache,
|
|
124
|
+
imageRefs,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
94
128
|
if (Array.isArray(node.children)) {
|
|
95
129
|
for (const child of node.children) {
|
|
96
130
|
await walkAndTransform(child, config, projectRoot, outDir, cache, imageRefs);
|
|
@@ -119,11 +153,7 @@ async function transformImgNode(node, config, projectRoot, outDir, cache, imageR
|
|
|
119
153
|
let manifest = imageRefs.get(absoluteSrc);
|
|
120
154
|
|
|
121
155
|
if (!manifest) {
|
|
122
|
-
|
|
123
|
-
const cached = getCached(cache, key, outDir);
|
|
124
|
-
manifest = cached ?? (await processImage(absoluteSrc, outDir, config));
|
|
125
|
-
|
|
126
|
-
setCached(cache, key, src, manifest);
|
|
156
|
+
manifest = await resolveManifest(absoluteSrc, src, config, outDir, cache);
|
|
127
157
|
imageRefs.set(absoluteSrc, manifest);
|
|
128
158
|
}
|
|
129
159
|
|
|
@@ -147,3 +177,71 @@ async function transformImgNode(node, config, projectRoot, outDir, cache, imageR
|
|
|
147
177
|
node.attributes.decoding = "async";
|
|
148
178
|
}
|
|
149
179
|
}
|
|
180
|
+
|
|
181
|
+
const IMG_TAG_RE = /<img\b([^>]*)>/gi;
|
|
182
|
+
const SRC_ATTR_RE = /\bsrc="([^"]*)"/;
|
|
183
|
+
const SRCSET_ATTR_RE = /\bsrcset="/;
|
|
184
|
+
const DATA_NO_OPT_RE = /\bdata-no-optimize\b/;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Transform `<img>` tags embedded in pre-rendered innerHTML strings.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} html
|
|
190
|
+
* @param {ImageConfig} config
|
|
191
|
+
* @param {string} projectRoot
|
|
192
|
+
* @param {string} outDir
|
|
193
|
+
* @param {CacheManifest} cache
|
|
194
|
+
* @param {Map<string, ImageManifest>} imageRefs
|
|
195
|
+
* @returns {Promise<string>}
|
|
196
|
+
*/
|
|
197
|
+
async function transformInnerHtmlImages(html, config, projectRoot, outDir, cache, imageRefs) {
|
|
198
|
+
/** @type {{ match: string; replacement: string }[]} */
|
|
199
|
+
const replacements = [];
|
|
200
|
+
|
|
201
|
+
for (const m of html.matchAll(IMG_TAG_RE)) {
|
|
202
|
+
const tag = m[0];
|
|
203
|
+
const attrs = m[1];
|
|
204
|
+
|
|
205
|
+
if (SRCSET_ATTR_RE.test(attrs)) continue;
|
|
206
|
+
if (DATA_NO_OPT_RE.test(attrs)) continue;
|
|
207
|
+
|
|
208
|
+
const srcMatch = attrs.match(SRC_ATTR_RE);
|
|
209
|
+
if (!srcMatch) continue;
|
|
210
|
+
|
|
211
|
+
const src = srcMatch[1];
|
|
212
|
+
if (shouldSkip(src)) continue;
|
|
213
|
+
|
|
214
|
+
const absoluteSrc = resolveImagePath(src, projectRoot);
|
|
215
|
+
if (!existsSync(absoluteSrc)) continue;
|
|
216
|
+
|
|
217
|
+
let manifest = imageRefs.get(absoluteSrc);
|
|
218
|
+
if (!manifest) {
|
|
219
|
+
manifest = await resolveManifest(absoluteSrc, src, config, outDir, cache);
|
|
220
|
+
imageRefs.set(absoluteSrc, manifest);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const preferredFormat = config.formats.includes("avif") ? "avif" : config.formats[0];
|
|
224
|
+
const srcset = buildSrcset(manifest.variants, preferredFormat);
|
|
225
|
+
if (!srcset) continue;
|
|
226
|
+
|
|
227
|
+
let extra = ` srcset="${srcset}" sizes="${config.sizes}"`;
|
|
228
|
+
if (!/\bwidth=/.test(attrs) && manifest.original.width) {
|
|
229
|
+
extra += ` width="${manifest.original.width}"`;
|
|
230
|
+
}
|
|
231
|
+
if (!/\bheight=/.test(attrs) && manifest.original.height) {
|
|
232
|
+
extra += ` height="${manifest.original.height}"`;
|
|
233
|
+
}
|
|
234
|
+
if (config.lazyLoad && !/\bloading="eager"/.test(attrs)) {
|
|
235
|
+
if (!/\bloading=/.test(attrs)) extra += ` loading="lazy"`;
|
|
236
|
+
if (!/\bdecoding=/.test(attrs)) extra += ` decoding="async"`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
replacements.push({ match: tag, replacement: `<img${attrs}${extra}>` });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let result = html;
|
|
243
|
+
for (const { match, replacement } of replacements) {
|
|
244
|
+
result = result.replace(match, replacement);
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
@@ -32,6 +32,16 @@ const SKIP_PROTOTYPES = new Set([
|
|
|
32
32
|
"ContentEntry",
|
|
33
33
|
]);
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Built-in $prototype → .class.json mappings. These resolve automatically without requiring an
|
|
37
|
+
* explicit `imports` entry in project.json. Project-level imports take precedence on collision.
|
|
38
|
+
*/
|
|
39
|
+
/** @type {Record<string, string>} */
|
|
40
|
+
const BUILTIN_CLASS_MAPPINGS = {
|
|
41
|
+
MarkdownFile: "@jxsuite/parser/MarkdownFile.class.json",
|
|
42
|
+
MarkdownCollection: "@jxsuite/parser/MarkdownCollection.class.json",
|
|
43
|
+
};
|
|
44
|
+
|
|
35
45
|
/**
|
|
36
46
|
* Keys reserved by the Jx prototype system — stripped before passing config to the external class
|
|
37
47
|
* constructor. Mirrors runtime's EXTERNAL_RESERVED.
|
|
@@ -73,8 +83,8 @@ export async function resolvePrototypes(doc, route, projectRoot) {
|
|
|
73
83
|
|
|
74
84
|
// Look up in imports if no $src already set
|
|
75
85
|
if (!def.$src) {
|
|
76
|
-
const mapped = imports[def.$prototype];
|
|
77
|
-
if (!mapped) continue;
|
|
86
|
+
const mapped = imports[def.$prototype] ?? BUILTIN_CLASS_MAPPINGS[def.$prototype];
|
|
87
|
+
if (!mapped) continue;
|
|
78
88
|
def.$src = mapped;
|
|
79
89
|
}
|
|
80
90
|
|
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,
|
|
@@ -34,6 +36,7 @@ import {
|
|
|
34
36
|
buildComponentCSS,
|
|
35
37
|
collectServerEntries,
|
|
36
38
|
renderStaticNode,
|
|
39
|
+
resolveStaticValue,
|
|
37
40
|
DEFAULT_REACTIVITY_SRC,
|
|
38
41
|
DEFAULT_LIT_HTML_SRC,
|
|
39
42
|
} from "../shared.js";
|
|
@@ -69,7 +72,19 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
69
72
|
|
|
70
73
|
// ── 2. Clean output directory ───────────────────────────────────────────
|
|
71
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
|
+
}
|
|
72
83
|
rmSync(outDir, { recursive: true, force: true });
|
|
84
|
+
if (hasOptimized) {
|
|
85
|
+
mkdirSync(resolve(outDir, "images"), { recursive: true });
|
|
86
|
+
renameSync(tmpOptimized, optimizedDir);
|
|
87
|
+
}
|
|
73
88
|
}
|
|
74
89
|
mkdirSync(outDir, { recursive: true });
|
|
75
90
|
|
|
@@ -137,7 +152,7 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
137
152
|
: JSON.parse(readFileSync(componentPath, "utf8"));
|
|
138
153
|
if (doc.tagName) {
|
|
139
154
|
componentDefs.set(doc.tagName, doc);
|
|
140
|
-
const css = buildComponentCSS(doc.tagName, doc.style);
|
|
155
|
+
const css = buildComponentCSS(doc.tagName, doc.style, doc, projectConfig.$media ?? {});
|
|
141
156
|
if (css) {
|
|
142
157
|
componentCSS.set(doc.tagName, css);
|
|
143
158
|
writeFileSync(resolve(componentOutDir, `${doc.tagName}.css`), css, "utf8");
|
|
@@ -266,10 +281,20 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
266
281
|
});
|
|
267
282
|
|
|
268
283
|
if (workerSource) {
|
|
269
|
-
const workerPath = resolve(
|
|
284
|
+
const workerPath = resolve(outDir, "worker.js");
|
|
270
285
|
writeFileSync(workerPath, workerSource, "utf8");
|
|
271
286
|
fileCount++;
|
|
272
|
-
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
|
+
}
|
|
273
298
|
}
|
|
274
299
|
}
|
|
275
300
|
|
|
@@ -611,6 +636,28 @@ function expandComponents(node, componentDefs) {
|
|
|
611
636
|
|
|
612
637
|
node.innerHTML = innerHTML;
|
|
613
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
|
+
|
|
614
661
|
delete node.$props;
|
|
615
662
|
|
|
616
663
|
if (isStatic) node.$static = true;
|
|
@@ -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
|
|