@jxsuite/compiler 0.25.3 → 0.25.5
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/cli.js +186 -186
- package/package.json +4 -4
- package/src/site/content-loader.ts +49 -10
- package/src/site/image-cache.ts +3 -3
- package/src/site/image-optimizer.ts +20 -5
- package/src/site/image-transform.ts +11 -18
- package/src/site/site-build.ts +25 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.5",
|
|
4
4
|
"description": "Jx static HTML compiler, island detector, and site builder",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@apidevtools/json-schema-ref-parser": "^15.3.5",
|
|
44
|
-
"@jxsuite/parser": "^0.25.
|
|
45
|
-
"@jxsuite/runtime": "^0.25.
|
|
46
|
-
"@jxsuite/schema": "^0.25.
|
|
44
|
+
"@jxsuite/parser": "^0.25.5",
|
|
45
|
+
"@jxsuite/runtime": "^0.25.5",
|
|
46
|
+
"@jxsuite/schema": "^0.25.5",
|
|
47
47
|
"remark-gfm": "^4.0.1",
|
|
48
48
|
"remark-stringify": "^11.0.0",
|
|
49
49
|
"sharp": "^0.34.5",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Content-loader.js — Content type loader
|
|
3
3
|
*
|
|
4
4
|
* Loads content types defined in project.json's `contentTypes` key. Supports Markdown (.md), JSON
|
|
5
|
-
* (.json), and CSV (.csv) source files.
|
|
5
|
+
* (.json), and CSV (.csv) source files or remote URLs.
|
|
6
6
|
*
|
|
7
7
|
* Phase 2 implementation of site-architecture spec §6.
|
|
8
8
|
*
|
|
@@ -180,26 +180,53 @@ function loadJSONEntries(filePath: string) {
|
|
|
180
180
|
* @param {ContentTypeSchema} [schema] - Content type schema (for type coercion)
|
|
181
181
|
* @returns {ContentLoaderEntry[]} Array of ContentEntry shapes
|
|
182
182
|
*/
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Coerce raw CSV row strings to typed values per the schema. - number: strips currency
|
|
185
|
+
* symbols/commas, returns null for empty cells - boolean: "true" → true, everything else → false -
|
|
186
|
+
* string: trimmed (already done by parseCSV, but defensively applied)
|
|
187
|
+
*/
|
|
188
|
+
function coerceCSVRows(
|
|
189
|
+
rows: Record<string, string>[],
|
|
190
|
+
schema?: ContentTypeSchema,
|
|
191
|
+
): ContentLoaderEntry[] {
|
|
192
|
+
return rows.map((row, i) => {
|
|
188
193
|
const data: Record<string, unknown> = { ...row };
|
|
189
194
|
if (schema?.properties) {
|
|
190
195
|
for (const [key, def] of Object.entries(schema.properties)) {
|
|
191
|
-
if (key in data)
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
if (!(key in data)) continue;
|
|
197
|
+
if (def.type === "number") {
|
|
198
|
+
const raw = String(data[key] ?? "").trim();
|
|
199
|
+
if (raw === "") {
|
|
200
|
+
data[key] = null;
|
|
201
|
+
} else {
|
|
202
|
+
const cleaned = raw.replace(/[$€£¥,\s]/g, "");
|
|
203
|
+
const n = Number(cleaned);
|
|
204
|
+
data[key] = isNaN(n) ? null : n;
|
|
205
|
+
}
|
|
206
|
+
} else if (def.type === "boolean") {
|
|
207
|
+
data[key] = data[key] === "true";
|
|
194
208
|
}
|
|
195
209
|
}
|
|
196
210
|
}
|
|
197
|
-
// Use `id` column, `sku` column, or row index as the entry ID
|
|
198
211
|
const id = (data.id as string | undefined) ?? (data.sku as string | undefined) ?? String(i);
|
|
199
212
|
return { id, data, body: null };
|
|
200
213
|
});
|
|
201
214
|
}
|
|
202
215
|
|
|
216
|
+
function loadCSVEntries(filePath: string, schema?: ContentTypeSchema) {
|
|
217
|
+
const rows = parseCSV(readFileSync(filePath, "utf-8"));
|
|
218
|
+
return coerceCSVRows(rows, schema);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Fetch a remote CSV URL and return ContentLoaderEntry[]. */
|
|
222
|
+
async function loadRemoteCSV(url: string, schema?: ContentTypeSchema) {
|
|
223
|
+
const response = await fetch(url);
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
throw new Error(`Failed to fetch CSV from ${url}: ${response.status} ${response.statusText}`);
|
|
226
|
+
}
|
|
227
|
+
return coerceCSVRows(parseCSV(await response.text()), schema);
|
|
228
|
+
}
|
|
229
|
+
|
|
203
230
|
// ─── Content Config ───────────────────────────────────────────────────────────
|
|
204
231
|
|
|
205
232
|
/**
|
|
@@ -289,6 +316,18 @@ async function loadContentType(name: string, contentTypeDef: ContentTypeDef, pro
|
|
|
289
316
|
}
|
|
290
317
|
: undefined;
|
|
291
318
|
|
|
319
|
+
// Remote URL source (e.g. Google Sheets CSV export)
|
|
320
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
321
|
+
try {
|
|
322
|
+
const entries = await loadRemoteCSV(source, schema);
|
|
323
|
+
if (schema) validateEntries(entries, schema, name);
|
|
324
|
+
return entries;
|
|
325
|
+
} catch (e) {
|
|
326
|
+
console.warn(`Content type "${name}": ${(e as Error).message}`);
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
292
331
|
// Resolve source path and discover files
|
|
293
332
|
const resolvedSource = resolve(projectRoot, source).split("\\").join("/");
|
|
294
333
|
/** @type {string[]} */
|
package/src/site/image-cache.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Image-cache.js — Content-hash based cache for processed image variants.
|
|
3
3
|
*
|
|
4
4
|
* Stores a manifest of previously processed images so that unchanged sources can skip re-encoding
|
|
5
|
-
* on subsequent builds. Cache lives in .
|
|
5
|
+
* on subsequent builds. Cache lives in .cache/images/.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
@@ -41,7 +41,7 @@ export function cacheKey(srcPath: string, config: ImageConfig) {
|
|
|
41
41
|
* @returns {CacheManifest}
|
|
42
42
|
*/
|
|
43
43
|
export function loadCache(projectRoot: string) {
|
|
44
|
-
const manifestPath = resolve(projectRoot, ".
|
|
44
|
+
const manifestPath = resolve(projectRoot, ".cache/images/manifest.json");
|
|
45
45
|
if (!existsSync(manifestPath)) {
|
|
46
46
|
return { version: 1, entries: {} };
|
|
47
47
|
}
|
|
@@ -59,7 +59,7 @@ export function loadCache(projectRoot: string) {
|
|
|
59
59
|
* @param {CacheManifest} cache
|
|
60
60
|
*/
|
|
61
61
|
export function saveCache(projectRoot: string, cache: CacheManifest) {
|
|
62
|
-
const cacheDir = resolve(projectRoot, ".
|
|
62
|
+
const cacheDir = resolve(projectRoot, ".cache/images");
|
|
63
63
|
mkdirSync(cacheDir, { recursive: true });
|
|
64
64
|
writeFileSync(resolve(cacheDir, "manifest.json"), JSON.stringify(cache, null, 2), "utf8");
|
|
65
65
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
import { readFileSync, mkdirSync, existsSync } from "node:fs";
|
|
10
10
|
import { resolve, basename, extname } from "node:path";
|
|
11
|
+
import { createRequire } from "node:module";
|
|
11
12
|
|
|
12
13
|
export interface ImageVariant {
|
|
13
14
|
width: number; // Pixel width of the variant
|
|
@@ -35,9 +36,22 @@ let _sharp: typeof import("sharp") | null = null;
|
|
|
35
36
|
|
|
36
37
|
async function getSharp() {
|
|
37
38
|
if (_sharp) return _sharp;
|
|
39
|
+
// Primary: dynamic import — works in tests (mock.module intercepts it) and in
|
|
40
|
+
// production installs where @img/sharp-* native packages are adjacent to cli.js.
|
|
38
41
|
try {
|
|
39
42
|
const sharpMod = await import("sharp");
|
|
40
|
-
_sharp = sharpMod.default;
|
|
43
|
+
_sharp = sharpMod.default as typeof import("sharp");
|
|
44
|
+
return _sharp;
|
|
45
|
+
} catch {
|
|
46
|
+
// Fall through to CJS fallback below.
|
|
47
|
+
}
|
|
48
|
+
// Fallback: resolve from the project being compiled. This covers symlinked
|
|
49
|
+
// monorepo dev environments (e.g. NixOS) where Node.js resolves import.meta.url
|
|
50
|
+
// to the compiler package's real path, making the @img/sharp-* packages
|
|
51
|
+
// unreachable via the primary import path.
|
|
52
|
+
try {
|
|
53
|
+
const req = createRequire(resolve(process.cwd(), "package.json"));
|
|
54
|
+
_sharp = req("sharp") as typeof import("sharp");
|
|
41
55
|
return _sharp;
|
|
42
56
|
} catch (e) {
|
|
43
57
|
throw new Error(
|
|
@@ -104,20 +118,21 @@ export function variantFilename(stem: string, width: number, hash8: string, form
|
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
/**
|
|
107
|
-
* Process a single source image: resize to each configured width, encode to each format.
|
|
121
|
+
* Process a single source image: resize to each configured width, encode to each format. Writes
|
|
122
|
+
* variants to cacheImgDir/_optimized/ (e.g. .cache/images/_optimized/).
|
|
108
123
|
*
|
|
109
124
|
* @param {string} srcPath - Absolute path to source image
|
|
110
|
-
* @param {string}
|
|
125
|
+
* @param {string} cacheImgDir - Absolute path to the image cache directory (.cache/images/)
|
|
111
126
|
* @param {ImageConfig} config
|
|
112
127
|
* @returns {Promise<ImageManifest>}
|
|
113
128
|
*/
|
|
114
|
-
export async function processImage(srcPath: string,
|
|
129
|
+
export async function processImage(srcPath: string, cacheImgDir: string, config: ImageConfig) {
|
|
115
130
|
const sharp = await getSharp();
|
|
116
131
|
const meta = await getImageMetadata(srcPath);
|
|
117
132
|
const hash8 = contentHash(srcPath);
|
|
118
133
|
const stem = basename(srcPath, extname(srcPath));
|
|
119
134
|
|
|
120
|
-
const optimizedDir = resolve(
|
|
135
|
+
const optimizedDir = resolve(cacheImgDir, "_optimized");
|
|
121
136
|
mkdirSync(optimizedDir, { recursive: true });
|
|
122
137
|
|
|
123
138
|
const variants: ImageVariant[] = [];
|
|
@@ -24,7 +24,7 @@ const EXTERNAL_PREFIXES = ["http://", "https://", "data:", "//"];
|
|
|
24
24
|
* @param {string} absoluteSrc
|
|
25
25
|
* @param {string} src
|
|
26
26
|
* @param {ImageConfig} config
|
|
27
|
-
* @param {string}
|
|
27
|
+
* @param {string} projectRoot
|
|
28
28
|
* @param {CacheManifest} cache
|
|
29
29
|
* @returns {Promise<ImageManifest>}
|
|
30
30
|
*/
|
|
@@ -32,7 +32,7 @@ async function resolveManifest(
|
|
|
32
32
|
absoluteSrc: string,
|
|
33
33
|
src: string,
|
|
34
34
|
config: ImageConfig,
|
|
35
|
-
|
|
35
|
+
projectRoot: string,
|
|
36
36
|
cache: CacheManifest,
|
|
37
37
|
) {
|
|
38
38
|
const key = `${contentHash(absoluteSrc)}:${configHash(config)}`;
|
|
@@ -44,7 +44,8 @@ async function resolveManifest(
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (!cached) console.log(` Optimizing ${basename(absoluteSrc)}...`);
|
|
47
|
-
const
|
|
47
|
+
const cacheImgDir = resolve(projectRoot, ".cache/images");
|
|
48
|
+
const manifest = await processImage(absoluteSrc, cacheImgDir, config);
|
|
48
49
|
setCached(cache, key, src, manifest);
|
|
49
50
|
return manifest;
|
|
50
51
|
}
|
|
@@ -83,12 +84,12 @@ function resolveImagePath(src: string, projectRoot: string) {
|
|
|
83
84
|
* Transform image nodes in a Jx document tree.
|
|
84
85
|
*
|
|
85
86
|
* Mutates img nodes in place, injecting srcset, sizes, width, height, loading, and decoding.
|
|
86
|
-
*
|
|
87
|
+
* Optimized variants are written to .cache/images/_optimized/ — the caller is responsible for
|
|
88
|
+
* copying them to the dist directory after all pages are compiled.
|
|
87
89
|
*
|
|
88
90
|
* @param {JxMutableNode | JxDocument} doc - The Jx document tree (mutated in place)
|
|
89
91
|
* @param {ImageConfig} config
|
|
90
92
|
* @param {string} projectRoot
|
|
91
|
-
* @param {string} outDir
|
|
92
93
|
* @param {CacheManifest} cache
|
|
93
94
|
* @returns {Promise<{ imageRefs: Map<string, ImageManifest> }>}
|
|
94
95
|
*/
|
|
@@ -96,14 +97,13 @@ export async function transformImageNodes(
|
|
|
96
97
|
doc: JxMutableNode | JxDocument,
|
|
97
98
|
config: ImageConfig,
|
|
98
99
|
projectRoot: string,
|
|
99
|
-
outDir: string,
|
|
100
100
|
cache: CacheManifest,
|
|
101
101
|
) {
|
|
102
102
|
const imageRefs: Map<string, ImageManifest> = new Map();
|
|
103
103
|
|
|
104
104
|
if (!config.optimize) return { imageRefs };
|
|
105
105
|
|
|
106
|
-
await walkAndTransform(doc, config, projectRoot,
|
|
106
|
+
await walkAndTransform(doc, config, projectRoot, cache, imageRefs);
|
|
107
107
|
|
|
108
108
|
return { imageRefs };
|
|
109
109
|
}
|
|
@@ -112,7 +112,6 @@ export async function transformImageNodes(
|
|
|
112
112
|
* @param {JxMutableNode | JxDocument} node
|
|
113
113
|
* @param {ImageConfig} config
|
|
114
114
|
* @param {string} projectRoot
|
|
115
|
-
* @param {string} outDir
|
|
116
115
|
* @param {CacheManifest} cache
|
|
117
116
|
* @param {Map<string, ImageManifest>} imageRefs
|
|
118
117
|
*/
|
|
@@ -120,14 +119,13 @@ async function walkAndTransform(
|
|
|
120
119
|
node: JxMutableNode | JxDocument,
|
|
121
120
|
config: ImageConfig,
|
|
122
121
|
projectRoot: string,
|
|
123
|
-
outDir: string,
|
|
124
122
|
cache: CacheManifest,
|
|
125
123
|
imageRefs: Map<string, ImageManifest>,
|
|
126
124
|
) {
|
|
127
125
|
if (!node || typeof node !== "object") return;
|
|
128
126
|
|
|
129
127
|
if (node.tagName === "img") {
|
|
130
|
-
await transformImgNode(node, config, projectRoot,
|
|
128
|
+
await transformImgNode(node, config, projectRoot, cache, imageRefs);
|
|
131
129
|
}
|
|
132
130
|
|
|
133
131
|
if (typeof node.innerHTML === "string" && node.innerHTML.includes("<img")) {
|
|
@@ -135,7 +133,6 @@ async function walkAndTransform(
|
|
|
135
133
|
node.innerHTML,
|
|
136
134
|
config,
|
|
137
135
|
projectRoot,
|
|
138
|
-
outDir,
|
|
139
136
|
cache,
|
|
140
137
|
imageRefs,
|
|
141
138
|
);
|
|
@@ -144,7 +141,7 @@ async function walkAndTransform(
|
|
|
144
141
|
if (Array.isArray(node.children)) {
|
|
145
142
|
for (const child of node.children) {
|
|
146
143
|
if (typeof child === "string") continue;
|
|
147
|
-
await walkAndTransform(child, config, projectRoot,
|
|
144
|
+
await walkAndTransform(child, config, projectRoot, cache, imageRefs);
|
|
148
145
|
}
|
|
149
146
|
}
|
|
150
147
|
}
|
|
@@ -153,7 +150,6 @@ async function walkAndTransform(
|
|
|
153
150
|
* @param {JxMutableNode | JxDocument} node
|
|
154
151
|
* @param {ImageConfig} config
|
|
155
152
|
* @param {string} projectRoot
|
|
156
|
-
* @param {string} outDir
|
|
157
153
|
* @param {CacheManifest} cache
|
|
158
154
|
* @param {Map<string, ImageManifest>} imageRefs
|
|
159
155
|
*/
|
|
@@ -161,7 +157,6 @@ async function transformImgNode(
|
|
|
161
157
|
node: JxMutableNode | JxDocument,
|
|
162
158
|
config: ImageConfig,
|
|
163
159
|
projectRoot: string,
|
|
164
|
-
outDir: string,
|
|
165
160
|
cache: CacheManifest,
|
|
166
161
|
imageRefs: Map<string, ImageManifest>,
|
|
167
162
|
) {
|
|
@@ -177,7 +172,7 @@ async function transformImgNode(
|
|
|
177
172
|
let manifest = imageRefs.get(absoluteSrc);
|
|
178
173
|
|
|
179
174
|
if (!manifest) {
|
|
180
|
-
manifest = await resolveManifest(absoluteSrc, src, config,
|
|
175
|
+
manifest = await resolveManifest(absoluteSrc, src, config, projectRoot, cache);
|
|
181
176
|
imageRefs.set(absoluteSrc, manifest);
|
|
182
177
|
}
|
|
183
178
|
|
|
@@ -213,7 +208,6 @@ const DATA_NO_OPT_RE = /\bdata-no-optimize\b/;
|
|
|
213
208
|
* @param {string} html
|
|
214
209
|
* @param {ImageConfig} config
|
|
215
210
|
* @param {string} projectRoot
|
|
216
|
-
* @param {string} outDir
|
|
217
211
|
* @param {CacheManifest} cache
|
|
218
212
|
* @param {Map<string, ImageManifest>} imageRefs
|
|
219
213
|
* @returns {Promise<string>}
|
|
@@ -222,7 +216,6 @@ async function transformInnerHtmlImages(
|
|
|
222
216
|
html: string,
|
|
223
217
|
config: ImageConfig,
|
|
224
218
|
projectRoot: string,
|
|
225
|
-
outDir: string,
|
|
226
219
|
cache: CacheManifest,
|
|
227
220
|
imageRefs: Map<string, ImageManifest>,
|
|
228
221
|
) {
|
|
@@ -247,7 +240,7 @@ async function transformInnerHtmlImages(
|
|
|
247
240
|
|
|
248
241
|
let manifest = imageRefs.get(absoluteSrc);
|
|
249
242
|
if (!manifest) {
|
|
250
|
-
manifest = await resolveManifest(absoluteSrc, src, config,
|
|
243
|
+
manifest = await resolveManifest(absoluteSrc, src, config, projectRoot, cache);
|
|
251
244
|
imageRefs.set(absoluteSrc, manifest);
|
|
252
245
|
}
|
|
253
246
|
|
package/src/site/site-build.ts
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
copyFileSync,
|
|
15
15
|
mkdirSync,
|
|
16
16
|
existsSync,
|
|
17
|
-
renameSync,
|
|
18
17
|
rmSync,
|
|
19
18
|
cpSync,
|
|
20
19
|
readdirSync,
|
|
@@ -88,19 +87,7 @@ export async function buildSite(
|
|
|
88
87
|
|
|
89
88
|
// ── 2. Clean output directory ───────────────────────────────────────────
|
|
90
89
|
if (clean && existsSync(outDir)) {
|
|
91
|
-
// Preserve optimized images across builds — sharp re-encoding is expensive
|
|
92
|
-
const optimizedDir = resolve(outDir, "images/_optimized");
|
|
93
|
-
const hasOptimized = existsSync(optimizedDir);
|
|
94
|
-
let tmpOptimized = "";
|
|
95
|
-
if (hasOptimized) {
|
|
96
|
-
tmpOptimized = resolve(projectRoot, ".jx-cache/_optimized_tmp");
|
|
97
|
-
renameSync(optimizedDir, tmpOptimized);
|
|
98
|
-
}
|
|
99
90
|
rmSync(outDir, { recursive: true, force: true });
|
|
100
|
-
if (hasOptimized) {
|
|
101
|
-
mkdirSync(resolve(outDir, "images"), { recursive: true });
|
|
102
|
-
renameSync(tmpOptimized, optimizedDir);
|
|
103
|
-
}
|
|
104
91
|
}
|
|
105
92
|
mkdirSync(outDir, { recursive: true });
|
|
106
93
|
|
|
@@ -207,7 +194,6 @@ export async function buildSite(
|
|
|
207
194
|
projectRoot,
|
|
208
195
|
contentTypes,
|
|
209
196
|
imageCache,
|
|
210
|
-
outDir,
|
|
211
197
|
componentDefs,
|
|
212
198
|
);
|
|
213
199
|
|
|
@@ -268,9 +254,15 @@ export async function buildSite(
|
|
|
268
254
|
}
|
|
269
255
|
}
|
|
270
256
|
|
|
271
|
-
// ── 6b. Save image cache
|
|
257
|
+
// ── 6b. Save image cache and copy variants to dist ──────────────────────
|
|
272
258
|
if (imageCache && projectConfig.images.optimize) {
|
|
273
259
|
saveCache(projectRoot, imageCache);
|
|
260
|
+
const cacheOptimizedDir = resolve(projectRoot, ".cache/images/_optimized");
|
|
261
|
+
if (existsSync(cacheOptimizedDir)) {
|
|
262
|
+
const distOptimizedDir = resolve(outDir, "images/_optimized");
|
|
263
|
+
mkdirSync(distOptimizedDir, { recursive: true });
|
|
264
|
+
cpSync(cacheOptimizedDir, distOptimizedDir, { recursive: true });
|
|
265
|
+
}
|
|
274
266
|
const totalImages = Object.keys(imageCache.entries).length;
|
|
275
267
|
if (totalImages > 0) {
|
|
276
268
|
log(` Optimized ${totalImages} image(s)`);
|
|
@@ -376,7 +368,6 @@ export async function buildSite(
|
|
|
376
368
|
* @param {string} projectRoot
|
|
377
369
|
* @param {Map<string, ContentLoaderEntry[]>} [contentTypes]
|
|
378
370
|
* @param {import("./image-cache.js").CacheManifest | null} [imageCache]
|
|
379
|
-
* @param {string} [outDir]
|
|
380
371
|
* @param {Map<string, JxElement>} [componentDefs]
|
|
381
372
|
* @returns {Promise<{
|
|
382
373
|
* html: string;
|
|
@@ -391,7 +382,6 @@ async function compilePage(
|
|
|
391
382
|
projectRoot: string,
|
|
392
383
|
contentTypes: Map<string, ContentLoaderEntry[]> = new Map(),
|
|
393
384
|
imageCache: import("./image-cache.js").CacheManifest | null = null,
|
|
394
|
-
outDir: string = "",
|
|
395
385
|
componentDefs: Map<string, JxElement> = new Map(),
|
|
396
386
|
) {
|
|
397
387
|
// Load the raw page document
|
|
@@ -482,12 +472,11 @@ async function compilePage(
|
|
|
482
472
|
}
|
|
483
473
|
|
|
484
474
|
// Transform <img> nodes for responsive image optimization
|
|
485
|
-
if (imageCache && projectConfig.images?.optimize
|
|
475
|
+
if (imageCache && projectConfig.images?.optimize) {
|
|
486
476
|
await transformImageNodes(
|
|
487
477
|
layoutDoc,
|
|
488
478
|
projectConfig.images as import("./image-optimizer").ImageConfig,
|
|
489
479
|
projectRoot,
|
|
490
|
-
outDir,
|
|
491
480
|
imageCache,
|
|
492
481
|
);
|
|
493
482
|
}
|
|
@@ -624,6 +613,16 @@ function expandMapTemplate(template: JxMutableNode, scope: Record<string, unknow
|
|
|
624
613
|
}
|
|
625
614
|
}
|
|
626
615
|
node.attributes = attrs;
|
|
616
|
+
} else if (k === "$props" && v && typeof v === "object") {
|
|
617
|
+
const props = { ...v } as Record<string, unknown>;
|
|
618
|
+
for (const [pk, pv] of Object.entries(props)) {
|
|
619
|
+
if (typeof pv === "string" && isTemplateString(pv)) {
|
|
620
|
+
const resolved = evaluateMapTemplate(pv, scope);
|
|
621
|
+
// null = evaluation error → keep template string; undefined = missing data → use null
|
|
622
|
+
props[pk] = resolved !== null ? (resolved ?? null) : pv;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
node.$props = props;
|
|
627
626
|
} else if (typeof v === "string" && isTemplateString(v)) {
|
|
628
627
|
node[k] = evaluateMapTemplate(v, scope) ?? v;
|
|
629
628
|
} else {
|
|
@@ -694,6 +693,13 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
|
|
|
694
693
|
}
|
|
695
694
|
}
|
|
696
695
|
}
|
|
696
|
+
if (node.$props && typeof node.$props === "object") {
|
|
697
|
+
for (const [k, v] of Object.entries(node.$props)) {
|
|
698
|
+
if (typeof v === "string" && isTemplateString(v)) {
|
|
699
|
+
node.$props[k] = evaluateStaticTemplate(v, scope) ?? v;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
697
703
|
const rawChildren = node.children as unknown;
|
|
698
704
|
if (
|
|
699
705
|
rawChildren &&
|