@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/compiler",
3
- "version": "0.25.4",
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.4",
45
- "@jxsuite/runtime": "^0.25.4",
46
- "@jxsuite/schema": "^0.25.4",
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
- function loadCSVEntries(filePath: string, schema?: ContentTypeSchema) {
184
- const csv = readFileSync(filePath, "utf-8");
185
- const rows = parseCSV(csv);
186
- return rows.map((row: Record<string, string>, i: number) => {
187
- // Apply type coercion based on schema if available
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
- if (def.type === "number") data[key] = Number(data[key]);
193
- else if (def.type === "boolean") data[key] = data[key] === "true";
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} outDir - Absolute path to the build output directory (dist/)
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, outDir: string, config: ImageConfig) {
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(outDir, OPTIMIZED_DIR);
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} outDir
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
- outDir: string,
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 manifest = await processImage(absoluteSrc, outDir, config);
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
- * Returns a set of absolute source paths that need processing.
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, outDir, cache, imageRefs);
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, outDir, cache, imageRefs);
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, outDir, cache, imageRefs);
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, outDir, cache);
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, outDir, cache);
243
+ manifest = await resolveManifest(absoluteSrc, src, config, projectRoot, cache);
251
244
  imageRefs.set(absoluteSrc, manifest);
252
245
  }
253
246
 
@@ -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 && outDir) {
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
  }