@jxsuite/compiler 0.25.4 → 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 +170 -170
- package/package.json +4 -4
- package/src/site/content-loader.ts +49 -10
- package/src/site/image-optimizer.ts +5 -4
- package/src/site/image-transform.ts +11 -18
- package/src/site/site-build.ts +8 -20
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[]} */
|
|
@@ -118,20 +118,21 @@ export function variantFilename(stem: string, width: number, hash8: string, form
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
* 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/).
|
|
122
123
|
*
|
|
123
124
|
* @param {string} srcPath - Absolute path to source image
|
|
124
|
-
* @param {string}
|
|
125
|
+
* @param {string} cacheImgDir - Absolute path to the image cache directory (.cache/images/)
|
|
125
126
|
* @param {ImageConfig} config
|
|
126
127
|
* @returns {Promise<ImageManifest>}
|
|
127
128
|
*/
|
|
128
|
-
export async function processImage(srcPath: string,
|
|
129
|
+
export async function processImage(srcPath: string, cacheImgDir: string, config: ImageConfig) {
|
|
129
130
|
const sharp = await getSharp();
|
|
130
131
|
const meta = await getImageMetadata(srcPath);
|
|
131
132
|
const hash8 = contentHash(srcPath);
|
|
132
133
|
const stem = basename(srcPath, extname(srcPath));
|
|
133
134
|
|
|
134
|
-
const optimizedDir = resolve(
|
|
135
|
+
const optimizedDir = resolve(cacheImgDir, "_optimized");
|
|
135
136
|
mkdirSync(optimizedDir, { recursive: true });
|
|
136
137
|
|
|
137
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,20 +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, ".cache/_optimized_tmp");
|
|
97
|
-
mkdirSync(resolve(projectRoot, ".cache"), { recursive: true });
|
|
98
|
-
renameSync(optimizedDir, tmpOptimized);
|
|
99
|
-
}
|
|
100
90
|
rmSync(outDir, { recursive: true, force: true });
|
|
101
|
-
if (hasOptimized) {
|
|
102
|
-
mkdirSync(resolve(outDir, "images"), { recursive: true });
|
|
103
|
-
renameSync(tmpOptimized, optimizedDir);
|
|
104
|
-
}
|
|
105
91
|
}
|
|
106
92
|
mkdirSync(outDir, { recursive: true });
|
|
107
93
|
|
|
@@ -208,7 +194,6 @@ export async function buildSite(
|
|
|
208
194
|
projectRoot,
|
|
209
195
|
contentTypes,
|
|
210
196
|
imageCache,
|
|
211
|
-
outDir,
|
|
212
197
|
componentDefs,
|
|
213
198
|
);
|
|
214
199
|
|
|
@@ -269,9 +254,15 @@ export async function buildSite(
|
|
|
269
254
|
}
|
|
270
255
|
}
|
|
271
256
|
|
|
272
|
-
// ── 6b. Save image cache
|
|
257
|
+
// ── 6b. Save image cache and copy variants to dist ──────────────────────
|
|
273
258
|
if (imageCache && projectConfig.images.optimize) {
|
|
274
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
|
+
}
|
|
275
266
|
const totalImages = Object.keys(imageCache.entries).length;
|
|
276
267
|
if (totalImages > 0) {
|
|
277
268
|
log(` Optimized ${totalImages} image(s)`);
|
|
@@ -377,7 +368,6 @@ export async function buildSite(
|
|
|
377
368
|
* @param {string} projectRoot
|
|
378
369
|
* @param {Map<string, ContentLoaderEntry[]>} [contentTypes]
|
|
379
370
|
* @param {import("./image-cache.js").CacheManifest | null} [imageCache]
|
|
380
|
-
* @param {string} [outDir]
|
|
381
371
|
* @param {Map<string, JxElement>} [componentDefs]
|
|
382
372
|
* @returns {Promise<{
|
|
383
373
|
* html: string;
|
|
@@ -392,7 +382,6 @@ async function compilePage(
|
|
|
392
382
|
projectRoot: string,
|
|
393
383
|
contentTypes: Map<string, ContentLoaderEntry[]> = new Map(),
|
|
394
384
|
imageCache: import("./image-cache.js").CacheManifest | null = null,
|
|
395
|
-
outDir: string = "",
|
|
396
385
|
componentDefs: Map<string, JxElement> = new Map(),
|
|
397
386
|
) {
|
|
398
387
|
// Load the raw page document
|
|
@@ -483,12 +472,11 @@ async function compilePage(
|
|
|
483
472
|
}
|
|
484
473
|
|
|
485
474
|
// Transform <img> nodes for responsive image optimization
|
|
486
|
-
if (imageCache && projectConfig.images?.optimize
|
|
475
|
+
if (imageCache && projectConfig.images?.optimize) {
|
|
487
476
|
await transformImageNodes(
|
|
488
477
|
layoutDoc,
|
|
489
478
|
projectConfig.images as import("./image-optimizer").ImageConfig,
|
|
490
479
|
projectRoot,
|
|
491
|
-
outDir,
|
|
492
480
|
imageCache,
|
|
493
481
|
);
|
|
494
482
|
}
|