@jxsuite/compiler 0.25.4 → 0.26.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/cli.js +213 -213
- package/package.json +4 -4
- package/src/site/content-loader.ts +49 -10
- package/src/site/image-cache.ts +61 -4
- package/src/site/image-optimizer.ts +5 -4
- package/src/site/image-transform.ts +11 -19
- package/src/site/site-build.ts +9 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
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.
|
|
45
|
-
"@jxsuite/runtime": "^0.
|
|
46
|
-
"@jxsuite/schema": "^0.
|
|
44
|
+
"@jxsuite/parser": "^0.26.0",
|
|
45
|
+
"@jxsuite/runtime": "^0.26.0",
|
|
46
|
+
"@jxsuite/schema": "^0.26.0",
|
|
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,11 +2,14 @@
|
|
|
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 the npm global cache directory under jxsuite-images/ so that
|
|
6
|
+
* CI environments (e.g. Cloudflare Pages) can persist it via their npm cache layer.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
9
|
-
import { resolve } from "node:path";
|
|
10
|
+
import { resolve, basename } from "node:path";
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
10
13
|
import { contentHash, configHash } from "./image-optimizer.ts";
|
|
11
14
|
|
|
12
15
|
import type { ImageManifest } from "./image-optimizer.ts";
|
|
@@ -23,6 +26,56 @@ export interface CacheManifest {
|
|
|
23
26
|
entries: Record<string, CacheEntry>; // Cached entries keyed by content+config hash
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
// ─── Cache directory resolution ──────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
let _npmCacheBase: string | null | undefined = undefined; // undefined = not yet resolved
|
|
32
|
+
|
|
33
|
+
/** Reset the memoized npm cache base so the next call re-evaluates. Tests only. */
|
|
34
|
+
export function _testResetNpmCacheBase() {
|
|
35
|
+
_npmCacheBase = undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Directly set the memoized npm cache base. Pass null to force the project-local fallback. Tests
|
|
40
|
+
* only.
|
|
41
|
+
*/
|
|
42
|
+
export function _testSetNpmCacheBase(value: string | null) {
|
|
43
|
+
_npmCacheBase = value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Return the image cache directory for a project.
|
|
48
|
+
*
|
|
49
|
+
* Prefers the npm global cache (`npm config get cache`) so CI environments that cache ~/.npm (e.g.
|
|
50
|
+
* Cloudflare Pages) automatically persist optimized images across builds. Falls back to
|
|
51
|
+
* .cache/images inside the project root when npm is unavailable.
|
|
52
|
+
*
|
|
53
|
+
* Result is memoized — `npm config get cache` is only spawned once per process.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} projectRoot
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function getImageCacheDir(projectRoot: string): string {
|
|
59
|
+
if (_npmCacheBase === undefined) {
|
|
60
|
+
try {
|
|
61
|
+
const result = execSync("npm config get cache", {
|
|
62
|
+
encoding: "utf-8",
|
|
63
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
64
|
+
cwd: tmpdir(), // avoid ENOWORKSPACES when cwd is inside an npm workspace
|
|
65
|
+
}).trim();
|
|
66
|
+
_npmCacheBase = result && result !== "undefined" ? result : null;
|
|
67
|
+
} catch {
|
|
68
|
+
_npmCacheBase = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (_npmCacheBase) {
|
|
72
|
+
return resolve(_npmCacheBase, "jxsuite-images", basename(projectRoot));
|
|
73
|
+
}
|
|
74
|
+
return resolve(projectRoot, ".cache/images");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Cache key ───────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
26
79
|
/**
|
|
27
80
|
* Build a cache key from source file content and config.
|
|
28
81
|
*
|
|
@@ -34,6 +87,8 @@ export function cacheKey(srcPath: string, config: ImageConfig) {
|
|
|
34
87
|
return `${contentHash(srcPath)}:${configHash(config)}`;
|
|
35
88
|
}
|
|
36
89
|
|
|
90
|
+
// ─── Load / save ─────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
37
92
|
/**
|
|
38
93
|
* Load the cache manifest from disk, or return an empty one.
|
|
39
94
|
*
|
|
@@ -41,7 +96,7 @@ export function cacheKey(srcPath: string, config: ImageConfig) {
|
|
|
41
96
|
* @returns {CacheManifest}
|
|
42
97
|
*/
|
|
43
98
|
export function loadCache(projectRoot: string) {
|
|
44
|
-
const manifestPath = resolve(projectRoot, "
|
|
99
|
+
const manifestPath = resolve(getImageCacheDir(projectRoot), "manifest.json");
|
|
45
100
|
if (!existsSync(manifestPath)) {
|
|
46
101
|
return { version: 1, entries: {} };
|
|
47
102
|
}
|
|
@@ -59,11 +114,13 @@ export function loadCache(projectRoot: string) {
|
|
|
59
114
|
* @param {CacheManifest} cache
|
|
60
115
|
*/
|
|
61
116
|
export function saveCache(projectRoot: string, cache: CacheManifest) {
|
|
62
|
-
const cacheDir =
|
|
117
|
+
const cacheDir = getImageCacheDir(projectRoot);
|
|
63
118
|
mkdirSync(cacheDir, { recursive: true });
|
|
64
119
|
writeFileSync(resolve(cacheDir, "manifest.json"), JSON.stringify(cache, null, 2), "utf8");
|
|
65
120
|
}
|
|
66
121
|
|
|
122
|
+
// ─── Entry access ─────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
67
124
|
/**
|
|
68
125
|
* Check if a cache entry exists and its output files are still present.
|
|
69
126
|
*
|
|
@@ -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[] = [];
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import { resolve, extname, basename } from "node:path";
|
|
11
11
|
import { processImage, buildSrcset, contentHash, configHash } from "./image-optimizer.ts";
|
|
12
|
-
import { getCached, setCached } from "./image-cache.ts";
|
|
12
|
+
import { getCached, setCached, getImageCacheDir } from "./image-cache.ts";
|
|
13
13
|
|
|
14
14
|
import type { ImageConfig } from "./image-optimizer.ts";
|
|
15
15
|
import type { ImageManifest } from "./image-optimizer.ts";
|
|
@@ -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,7 @@ async function resolveManifest(
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (!cached) console.log(` Optimizing ${basename(absoluteSrc)}...`);
|
|
47
|
-
const manifest = await processImage(absoluteSrc,
|
|
47
|
+
const manifest = await processImage(absoluteSrc, getImageCacheDir(projectRoot), config);
|
|
48
48
|
setCached(cache, key, src, manifest);
|
|
49
49
|
return manifest;
|
|
50
50
|
}
|
|
@@ -83,12 +83,12 @@ function resolveImagePath(src: string, projectRoot: string) {
|
|
|
83
83
|
* Transform image nodes in a Jx document tree.
|
|
84
84
|
*
|
|
85
85
|
* Mutates img nodes in place, injecting srcset, sizes, width, height, loading, and decoding.
|
|
86
|
-
*
|
|
86
|
+
* Optimized variants are written to .cache/images/_optimized/ — the caller is responsible for
|
|
87
|
+
* copying them to the dist directory after all pages are compiled.
|
|
87
88
|
*
|
|
88
89
|
* @param {JxMutableNode | JxDocument} doc - The Jx document tree (mutated in place)
|
|
89
90
|
* @param {ImageConfig} config
|
|
90
91
|
* @param {string} projectRoot
|
|
91
|
-
* @param {string} outDir
|
|
92
92
|
* @param {CacheManifest} cache
|
|
93
93
|
* @returns {Promise<{ imageRefs: Map<string, ImageManifest> }>}
|
|
94
94
|
*/
|
|
@@ -96,14 +96,13 @@ export async function transformImageNodes(
|
|
|
96
96
|
doc: JxMutableNode | JxDocument,
|
|
97
97
|
config: ImageConfig,
|
|
98
98
|
projectRoot: string,
|
|
99
|
-
outDir: string,
|
|
100
99
|
cache: CacheManifest,
|
|
101
100
|
) {
|
|
102
101
|
const imageRefs: Map<string, ImageManifest> = new Map();
|
|
103
102
|
|
|
104
103
|
if (!config.optimize) return { imageRefs };
|
|
105
104
|
|
|
106
|
-
await walkAndTransform(doc, config, projectRoot,
|
|
105
|
+
await walkAndTransform(doc, config, projectRoot, cache, imageRefs);
|
|
107
106
|
|
|
108
107
|
return { imageRefs };
|
|
109
108
|
}
|
|
@@ -112,7 +111,6 @@ export async function transformImageNodes(
|
|
|
112
111
|
* @param {JxMutableNode | JxDocument} node
|
|
113
112
|
* @param {ImageConfig} config
|
|
114
113
|
* @param {string} projectRoot
|
|
115
|
-
* @param {string} outDir
|
|
116
114
|
* @param {CacheManifest} cache
|
|
117
115
|
* @param {Map<string, ImageManifest>} imageRefs
|
|
118
116
|
*/
|
|
@@ -120,14 +118,13 @@ async function walkAndTransform(
|
|
|
120
118
|
node: JxMutableNode | JxDocument,
|
|
121
119
|
config: ImageConfig,
|
|
122
120
|
projectRoot: string,
|
|
123
|
-
outDir: string,
|
|
124
121
|
cache: CacheManifest,
|
|
125
122
|
imageRefs: Map<string, ImageManifest>,
|
|
126
123
|
) {
|
|
127
124
|
if (!node || typeof node !== "object") return;
|
|
128
125
|
|
|
129
126
|
if (node.tagName === "img") {
|
|
130
|
-
await transformImgNode(node, config, projectRoot,
|
|
127
|
+
await transformImgNode(node, config, projectRoot, cache, imageRefs);
|
|
131
128
|
}
|
|
132
129
|
|
|
133
130
|
if (typeof node.innerHTML === "string" && node.innerHTML.includes("<img")) {
|
|
@@ -135,7 +132,6 @@ async function walkAndTransform(
|
|
|
135
132
|
node.innerHTML,
|
|
136
133
|
config,
|
|
137
134
|
projectRoot,
|
|
138
|
-
outDir,
|
|
139
135
|
cache,
|
|
140
136
|
imageRefs,
|
|
141
137
|
);
|
|
@@ -144,7 +140,7 @@ async function walkAndTransform(
|
|
|
144
140
|
if (Array.isArray(node.children)) {
|
|
145
141
|
for (const child of node.children) {
|
|
146
142
|
if (typeof child === "string") continue;
|
|
147
|
-
await walkAndTransform(child, config, projectRoot,
|
|
143
|
+
await walkAndTransform(child, config, projectRoot, cache, imageRefs);
|
|
148
144
|
}
|
|
149
145
|
}
|
|
150
146
|
}
|
|
@@ -153,7 +149,6 @@ async function walkAndTransform(
|
|
|
153
149
|
* @param {JxMutableNode | JxDocument} node
|
|
154
150
|
* @param {ImageConfig} config
|
|
155
151
|
* @param {string} projectRoot
|
|
156
|
-
* @param {string} outDir
|
|
157
152
|
* @param {CacheManifest} cache
|
|
158
153
|
* @param {Map<string, ImageManifest>} imageRefs
|
|
159
154
|
*/
|
|
@@ -161,7 +156,6 @@ async function transformImgNode(
|
|
|
161
156
|
node: JxMutableNode | JxDocument,
|
|
162
157
|
config: ImageConfig,
|
|
163
158
|
projectRoot: string,
|
|
164
|
-
outDir: string,
|
|
165
159
|
cache: CacheManifest,
|
|
166
160
|
imageRefs: Map<string, ImageManifest>,
|
|
167
161
|
) {
|
|
@@ -177,7 +171,7 @@ async function transformImgNode(
|
|
|
177
171
|
let manifest = imageRefs.get(absoluteSrc);
|
|
178
172
|
|
|
179
173
|
if (!manifest) {
|
|
180
|
-
manifest = await resolveManifest(absoluteSrc, src, config,
|
|
174
|
+
manifest = await resolveManifest(absoluteSrc, src, config, projectRoot, cache);
|
|
181
175
|
imageRefs.set(absoluteSrc, manifest);
|
|
182
176
|
}
|
|
183
177
|
|
|
@@ -213,7 +207,6 @@ const DATA_NO_OPT_RE = /\bdata-no-optimize\b/;
|
|
|
213
207
|
* @param {string} html
|
|
214
208
|
* @param {ImageConfig} config
|
|
215
209
|
* @param {string} projectRoot
|
|
216
|
-
* @param {string} outDir
|
|
217
210
|
* @param {CacheManifest} cache
|
|
218
211
|
* @param {Map<string, ImageManifest>} imageRefs
|
|
219
212
|
* @returns {Promise<string>}
|
|
@@ -222,7 +215,6 @@ async function transformInnerHtmlImages(
|
|
|
222
215
|
html: string,
|
|
223
216
|
config: ImageConfig,
|
|
224
217
|
projectRoot: string,
|
|
225
|
-
outDir: string,
|
|
226
218
|
cache: CacheManifest,
|
|
227
219
|
imageRefs: Map<string, ImageManifest>,
|
|
228
220
|
) {
|
|
@@ -247,7 +239,7 @@ async function transformInnerHtmlImages(
|
|
|
247
239
|
|
|
248
240
|
let manifest = imageRefs.get(absoluteSrc);
|
|
249
241
|
if (!manifest) {
|
|
250
|
-
manifest = await resolveManifest(absoluteSrc, src, config,
|
|
242
|
+
manifest = await resolveManifest(absoluteSrc, src, config, projectRoot, cache);
|
|
251
243
|
imageRefs.set(absoluteSrc, manifest);
|
|
252
244
|
}
|
|
253
245
|
|
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,
|
|
@@ -45,7 +44,7 @@ import { loadContentTypes, loadContentConfig, resolveContentTypeRefs } from "./c
|
|
|
45
44
|
import { resolvePrototypes } from "./prototype-resolver.ts";
|
|
46
45
|
import { compileMarkdown } from "../targets/compile-markdown.ts";
|
|
47
46
|
import { transformImageNodes } from "./image-transform.ts";
|
|
48
|
-
import { loadCache, saveCache } from "./image-cache.ts";
|
|
47
|
+
import { loadCache, saveCache, getImageCacheDir } from "./image-cache.ts";
|
|
49
48
|
import type {
|
|
50
49
|
JxElement,
|
|
51
50
|
JxMutableNode,
|
|
@@ -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(getImageCacheDir(projectRoot), "_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
|
}
|