@koehler8/cms 1.0.0-beta.26 → 1.0.0-beta.28
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/bin/cms-migrate-image-variants.js +11 -0
- package/package.json +2 -2
- package/scripts/image-variants/cache.js +40 -0
- package/scripts/image-variants/classifier.js +53 -0
- package/scripts/image-variants/config.js +54 -0
- package/scripts/image-variants/index.js +20 -0
- package/scripts/image-variants/manifest.js +71 -0
- package/scripts/image-variants/planner.js +95 -0
- package/scripts/image-variants/reconcile.js +181 -0
- package/scripts/image-variants/renderer.js +41 -0
- package/scripts/migrate-image-variants.mjs +316 -0
- package/vite-plugin.js +121 -5
- package/bin/cms-generate-image-variants.js +0 -33
- package/scripts/generate-image-variants.js +0 -268
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-site migration helper for the cms@1.0.0-beta.27 in-plugin image
|
|
3
|
+
* variant pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Run from a site directory:
|
|
6
|
+
* npx @koehler8/cms migrate-image-variants
|
|
7
|
+
*
|
|
8
|
+
* What it does (idempotent — running it on an already-migrated site is a
|
|
9
|
+
* series of no-ops):
|
|
10
|
+
* 1. Validate this is a cms-using site (check package.json deps).
|
|
11
|
+
* 2. Promote site/assets/img/_source/* originals to flat dir, delete
|
|
12
|
+
* duplicate copies left over from beta.26's auto-copy step, then
|
|
13
|
+
* remove the now-empty _source/ directory.
|
|
14
|
+
* 3. git rm --cached any committed variants matching {name}-{N}.{ext}
|
|
15
|
+
* and append the patterns to .gitignore for future-proofing.
|
|
16
|
+
* 4. Drop the `generate:image-variants` entry from package.json scripts.
|
|
17
|
+
* 5. Drop the `npm run generate:image-variants` line from amplify.yml.
|
|
18
|
+
* 6. Bump @koehler8/cms range to ^1.0.0-beta.27.
|
|
19
|
+
* 7. Run npm install --prefer-offline --no-audit --no-fund.
|
|
20
|
+
* 8. Print verification instructions.
|
|
21
|
+
*
|
|
22
|
+
* Does NOT auto-commit or auto-push — humans review the diff.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { execSync } from 'node:child_process';
|
|
28
|
+
|
|
29
|
+
const RESET = '\x1b[0m';
|
|
30
|
+
const DIM = '\x1b[2m';
|
|
31
|
+
const BOLD = '\x1b[1m';
|
|
32
|
+
const GREEN = '\x1b[32m';
|
|
33
|
+
const YELLOW = '\x1b[33m';
|
|
34
|
+
const RED = '\x1b[31m';
|
|
35
|
+
|
|
36
|
+
const TARGET_CMS_VERSION = '^1.0.0-beta.27';
|
|
37
|
+
const VARIANT_NAME_RE = /-\d+\.(?:avif|webp|jpg|jpeg)$/i;
|
|
38
|
+
|
|
39
|
+
function log(msg) {
|
|
40
|
+
console.log(msg);
|
|
41
|
+
}
|
|
42
|
+
function step(n, msg) {
|
|
43
|
+
console.log(`${BOLD}[${n}]${RESET} ${msg}`);
|
|
44
|
+
}
|
|
45
|
+
function ok(msg) {
|
|
46
|
+
console.log(` ${GREEN}✓${RESET} ${msg}`);
|
|
47
|
+
}
|
|
48
|
+
function info(msg) {
|
|
49
|
+
console.log(` ${DIM}·${RESET} ${DIM}${msg}${RESET}`);
|
|
50
|
+
}
|
|
51
|
+
function warn(msg) {
|
|
52
|
+
console.log(` ${YELLOW}!${RESET} ${msg}`);
|
|
53
|
+
}
|
|
54
|
+
function fail(msg) {
|
|
55
|
+
console.log(` ${RED}✗${RESET} ${RED}${msg}${RESET}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readJsonOrNull(p) {
|
|
59
|
+
if (!fs.existsSync(p)) return null;
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function writeJson(p, obj) {
|
|
68
|
+
fs.writeFileSync(p, JSON.stringify(obj, null, 2) + '\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function* walkRecursive(dir) {
|
|
72
|
+
if (!fs.existsSync(dir)) return;
|
|
73
|
+
const stack = [dir];
|
|
74
|
+
while (stack.length) {
|
|
75
|
+
const cur = stack.pop();
|
|
76
|
+
for (const entry of fs.readdirSync(cur, { withFileTypes: true })) {
|
|
77
|
+
const full = path.join(cur, entry.name);
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
stack.push(full);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (entry.isFile()) yield full;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function gitTryRm(siteDir, files) {
|
|
88
|
+
if (!files.length) return 0;
|
|
89
|
+
try {
|
|
90
|
+
const args = files.map((f) => `'${path.relative(siteDir, f).replace(/'/g, "'\\''")}'`).join(' ');
|
|
91
|
+
execSync(`git rm --cached --quiet ${args}`, { cwd: siteDir, stdio: 'pipe' });
|
|
92
|
+
return files.length;
|
|
93
|
+
} catch {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isCmsSite(siteDir) {
|
|
99
|
+
const pkg = readJsonOrNull(path.join(siteDir, 'package.json'));
|
|
100
|
+
if (!pkg) return false;
|
|
101
|
+
return Boolean(pkg.dependencies?.['@koehler8/cms'] || pkg.devDependencies?.['@koehler8/cms']);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function migrateSourceDir(siteDir) {
|
|
105
|
+
const imgDir = path.join(siteDir, 'assets', 'img');
|
|
106
|
+
const altImgDir = path.join(siteDir, 'site', 'assets', 'img');
|
|
107
|
+
const target = fs.existsSync(altImgDir) ? altImgDir : imgDir;
|
|
108
|
+
const sourceDir = path.join(target, '_source');
|
|
109
|
+
if (!fs.existsSync(sourceDir)) {
|
|
110
|
+
info('no site/assets/img/_source/ to migrate');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
let promoted = 0;
|
|
114
|
+
let deletedDup = 0;
|
|
115
|
+
for (const sourcePath of walkRecursive(sourceDir)) {
|
|
116
|
+
const rel = path.relative(sourceDir, sourcePath);
|
|
117
|
+
const flatPath = path.join(target, rel);
|
|
118
|
+
if (fs.existsSync(flatPath)) {
|
|
119
|
+
fs.rmSync(sourcePath, { force: true });
|
|
120
|
+
deletedDup += 1;
|
|
121
|
+
} else {
|
|
122
|
+
fs.mkdirSync(path.dirname(flatPath), { recursive: true });
|
|
123
|
+
fs.renameSync(sourcePath, flatPath);
|
|
124
|
+
promoted += 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function removeEmptyDirs(dir) {
|
|
128
|
+
if (!fs.existsSync(dir)) return;
|
|
129
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
130
|
+
if (entry.isDirectory()) removeEmptyDirs(path.join(dir, entry.name));
|
|
131
|
+
}
|
|
132
|
+
if (!fs.readdirSync(dir).length) {
|
|
133
|
+
fs.rmdirSync(dir);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
removeEmptyDirs(sourceDir);
|
|
137
|
+
if (promoted) ok(`promoted ${promoted} originals from _source/ to flat dir`);
|
|
138
|
+
if (deletedDup) ok(`removed ${deletedDup} duplicate copies left in _source/`);
|
|
139
|
+
if (!fs.existsSync(sourceDir)) ok('removed empty _source/ directory');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function untrackVariants(siteDir) {
|
|
143
|
+
const candidates = [
|
|
144
|
+
path.join(siteDir, 'assets', 'img'),
|
|
145
|
+
path.join(siteDir, 'site', 'assets', 'img'),
|
|
146
|
+
].filter((d) => fs.existsSync(d));
|
|
147
|
+
const variantPaths = [];
|
|
148
|
+
for (const dir of candidates) {
|
|
149
|
+
for (const f of walkRecursive(dir)) {
|
|
150
|
+
if (f.includes(`${path.sep}_source${path.sep}`)) continue;
|
|
151
|
+
if (VARIANT_NAME_RE.test(f)) variantPaths.push(f);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!variantPaths.length) {
|
|
155
|
+
info('no committed variants to untrack');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const untracked = gitTryRm(siteDir, variantPaths);
|
|
159
|
+
if (untracked) {
|
|
160
|
+
ok(`git rm --cached on ${untracked} variant file(s) (kept on disk)`);
|
|
161
|
+
for (const v of variantPaths) {
|
|
162
|
+
try { fs.rmSync(v, { force: true }); } catch { /* noop */ }
|
|
163
|
+
}
|
|
164
|
+
ok(`removed ${variantPaths.length} variant file(s) from working tree`);
|
|
165
|
+
} else {
|
|
166
|
+
warn(`found ${variantPaths.length} variant-shaped files but git rm failed (not in git or git not present); deleting from disk anyway`);
|
|
167
|
+
for (const v of variantPaths) {
|
|
168
|
+
try { fs.rmSync(v, { force: true }); } catch { /* noop */ }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function appendGitignore(siteDir) {
|
|
174
|
+
const gi = path.join(siteDir, '.gitignore');
|
|
175
|
+
const block = [
|
|
176
|
+
'',
|
|
177
|
+
'# Image variants are generated at build time into node_modules/.cache/',
|
|
178
|
+
'# (cms@1.0.0-beta.27+ pipeline). Defensive ignore so accidental local',
|
|
179
|
+
'# variants in the source tree don’t get committed.',
|
|
180
|
+
'site/assets/img/*-[0-9]*.avif',
|
|
181
|
+
'site/assets/img/*-[0-9]*.webp',
|
|
182
|
+
'site/assets/img/*-[0-9]*.jpg',
|
|
183
|
+
'site/assets/img/*-[0-9]*.jpeg',
|
|
184
|
+
'site/assets/img/**/*-[0-9]*.avif',
|
|
185
|
+
'site/assets/img/**/*-[0-9]*.webp',
|
|
186
|
+
'site/assets/img/**/*-[0-9]*.jpg',
|
|
187
|
+
'site/assets/img/**/*-[0-9]*.jpeg',
|
|
188
|
+
'assets/img/*-[0-9]*.avif',
|
|
189
|
+
'assets/img/*-[0-9]*.webp',
|
|
190
|
+
'assets/img/*-[0-9]*.jpg',
|
|
191
|
+
'assets/img/*-[0-9]*.jpeg',
|
|
192
|
+
'',
|
|
193
|
+
].join('\n');
|
|
194
|
+
let cur = '';
|
|
195
|
+
if (fs.existsSync(gi)) cur = fs.readFileSync(gi, 'utf-8');
|
|
196
|
+
if (cur.includes('# Image variants are generated at build time')) {
|
|
197
|
+
info('.gitignore already has image-variant patterns');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
fs.writeFileSync(gi, cur + (cur.endsWith('\n') ? '' : '\n') + block);
|
|
201
|
+
ok('appended image-variant patterns to .gitignore');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function dropPackageJsonScript(siteDir) {
|
|
205
|
+
const pkgPath = path.join(siteDir, 'package.json');
|
|
206
|
+
const pkg = readJsonOrNull(pkgPath);
|
|
207
|
+
if (!pkg) {
|
|
208
|
+
fail('package.json missing');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (pkg.scripts?.['generate:image-variants']) {
|
|
212
|
+
delete pkg.scripts['generate:image-variants'];
|
|
213
|
+
writeJson(pkgPath, pkg);
|
|
214
|
+
ok('removed generate:image-variants script from package.json');
|
|
215
|
+
} else {
|
|
216
|
+
info('package.json scripts already clean');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function dropAmplifyStep(siteDir) {
|
|
221
|
+
const ymlPath = path.join(siteDir, 'amplify.yml');
|
|
222
|
+
if (!fs.existsSync(ymlPath)) {
|
|
223
|
+
info('no amplify.yml present');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const cur = fs.readFileSync(ymlPath, 'utf-8');
|
|
227
|
+
const next = cur
|
|
228
|
+
.split(/\r?\n/)
|
|
229
|
+
.filter((line) => !/^\s*-\s*npm\s+run\s+generate:image-variants\s*$/.test(line))
|
|
230
|
+
.join('\n');
|
|
231
|
+
if (next === cur) {
|
|
232
|
+
info('amplify.yml already clean');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
fs.writeFileSync(ymlPath, next);
|
|
236
|
+
ok('removed generate:image-variants step from amplify.yml');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function bumpCmsVersion(siteDir) {
|
|
240
|
+
const pkgPath = path.join(siteDir, 'package.json');
|
|
241
|
+
const pkg = readJsonOrNull(pkgPath);
|
|
242
|
+
if (!pkg) return;
|
|
243
|
+
let bumped = false;
|
|
244
|
+
if (pkg.dependencies?.['@koehler8/cms'] && pkg.dependencies['@koehler8/cms'] !== TARGET_CMS_VERSION) {
|
|
245
|
+
pkg.dependencies['@koehler8/cms'] = TARGET_CMS_VERSION;
|
|
246
|
+
bumped = true;
|
|
247
|
+
}
|
|
248
|
+
if (pkg.devDependencies?.['@koehler8/cms'] && pkg.devDependencies['@koehler8/cms'] !== TARGET_CMS_VERSION) {
|
|
249
|
+
pkg.devDependencies['@koehler8/cms'] = TARGET_CMS_VERSION;
|
|
250
|
+
bumped = true;
|
|
251
|
+
}
|
|
252
|
+
if (bumped) {
|
|
253
|
+
writeJson(pkgPath, pkg);
|
|
254
|
+
ok(`bumped @koehler8/cms to ${TARGET_CMS_VERSION}`);
|
|
255
|
+
} else {
|
|
256
|
+
info(`@koehler8/cms already at ${TARGET_CMS_VERSION}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function runNpmInstall(siteDir) {
|
|
261
|
+
try {
|
|
262
|
+
execSync('npm install --prefer-offline --no-audit --no-fund', {
|
|
263
|
+
cwd: siteDir,
|
|
264
|
+
stdio: 'inherit',
|
|
265
|
+
});
|
|
266
|
+
ok('npm install completed');
|
|
267
|
+
} catch (err) {
|
|
268
|
+
fail(`npm install failed: ${err.message}`);
|
|
269
|
+
process.exitCode = 1;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function main() {
|
|
274
|
+
const siteDir = path.resolve(process.cwd());
|
|
275
|
+
log('');
|
|
276
|
+
log(`${BOLD}cms image-variant migration${RESET}`);
|
|
277
|
+
log(`${DIM}site:${RESET} ${siteDir}`);
|
|
278
|
+
log('');
|
|
279
|
+
|
|
280
|
+
if (!isCmsSite(siteDir)) {
|
|
281
|
+
fail('Not a @koehler8/cms-consuming site (no @koehler8/cms in package.json deps).');
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
step(1, 'Promote originals from _source/ to flat dir');
|
|
286
|
+
migrateSourceDir(siteDir);
|
|
287
|
+
|
|
288
|
+
step(2, 'Untrack and remove committed variants');
|
|
289
|
+
untrackVariants(siteDir);
|
|
290
|
+
|
|
291
|
+
step(3, 'Append image-variant patterns to .gitignore');
|
|
292
|
+
appendGitignore(siteDir);
|
|
293
|
+
|
|
294
|
+
step(4, 'Remove generate:image-variants script from package.json');
|
|
295
|
+
dropPackageJsonScript(siteDir);
|
|
296
|
+
|
|
297
|
+
step(5, 'Remove generate:image-variants step from amplify.yml');
|
|
298
|
+
dropAmplifyStep(siteDir);
|
|
299
|
+
|
|
300
|
+
step(6, `Bump @koehler8/cms to ${TARGET_CMS_VERSION}`);
|
|
301
|
+
bumpCmsVersion(siteDir);
|
|
302
|
+
|
|
303
|
+
step(7, 'npm install');
|
|
304
|
+
runNpmInstall(siteDir);
|
|
305
|
+
|
|
306
|
+
log('');
|
|
307
|
+
log(`${BOLD}${GREEN}Done.${RESET}`);
|
|
308
|
+
log('');
|
|
309
|
+
log('Next steps:');
|
|
310
|
+
log(` ${DIM}·${RESET} npm run build:ssg # verify variants render in dist/`);
|
|
311
|
+
log(` ${DIM}·${RESET} git diff # review the changes`);
|
|
312
|
+
log(` ${DIM}·${RESET} git add -A && git commit && git push`);
|
|
313
|
+
log('');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
main();
|
package/vite-plugin.js
CHANGED
|
@@ -17,6 +17,12 @@ import { buildRobotsTxt } from './src/utils/robotsGenerator.js';
|
|
|
17
17
|
import { buildSitemap, getSitemapUrl } from './src/utils/sitemapGenerator.js';
|
|
18
18
|
import { buildWebAppManifest } from './src/utils/webAppManifest.js';
|
|
19
19
|
import { sha256Hex } from './src/utils/sha256.js';
|
|
20
|
+
import {
|
|
21
|
+
computeVariantCacheDir,
|
|
22
|
+
resolveImageVariantConfig,
|
|
23
|
+
planVariantJobsFromFlatDir,
|
|
24
|
+
reconcileVariantCache,
|
|
25
|
+
} from './scripts/image-variants/index.js';
|
|
20
26
|
|
|
21
27
|
// ---- Helpers ----
|
|
22
28
|
|
|
@@ -349,6 +355,8 @@ export default function cmsPlugin(options = {}) {
|
|
|
349
355
|
let metadata;
|
|
350
356
|
let tempIndexPath;
|
|
351
357
|
let tempEntryPath;
|
|
358
|
+
let variantCacheDir;
|
|
359
|
+
let variantManifestPath;
|
|
352
360
|
|
|
353
361
|
const writeTempFiles = () => {
|
|
354
362
|
// Write entry file (with optional external theme registrations)
|
|
@@ -453,6 +461,8 @@ export default function cmsPlugin(options = {}) {
|
|
|
453
461
|
siteRoot = path.dirname(siteDir);
|
|
454
462
|
tempIndexPath = path.join(siteRoot, 'index.html');
|
|
455
463
|
tempEntryPath = path.join(siteRoot, ENTRY_FILENAME);
|
|
464
|
+
variantCacheDir = computeVariantCacheDir(siteRoot, siteDir);
|
|
465
|
+
variantManifestPath = path.join(variantCacheDir, '.manifest.json');
|
|
456
466
|
|
|
457
467
|
// Load site config at config-resolution time (needed for SSG routes + HTML injection)
|
|
458
468
|
const contentDir = path.join(siteDir, 'content');
|
|
@@ -498,6 +508,7 @@ export default function cmsPlugin(options = {}) {
|
|
|
498
508
|
alias: {
|
|
499
509
|
'@cms-site': siteDir,
|
|
500
510
|
'@cms-framework': frameworkRoot,
|
|
511
|
+
'@cms-variant-cache': variantCacheDir,
|
|
501
512
|
'@': path.join(frameworkRoot, 'src'),
|
|
502
513
|
'@extensions': extensionsDir,
|
|
503
514
|
},
|
|
@@ -722,6 +733,18 @@ export default modules;
|
|
|
722
733
|
}
|
|
723
734
|
|
|
724
735
|
if (virtualId === VIRTUAL_ASSETS) {
|
|
736
|
+
// Three globs:
|
|
737
|
+
// shared — framework-bundled assets
|
|
738
|
+
// site — site/assets/** flat-dir originals (everything the
|
|
739
|
+
// author put on disk, including any leftover variants
|
|
740
|
+
// from the old pipeline that haven't been cleaned up)
|
|
741
|
+
// variants — generated variants from node_modules/.cache/
|
|
742
|
+
// @koehler8/cms/image-variants/{siteKey}/assets/**.
|
|
743
|
+
// The cache mirrors the site's `assets/` layout so
|
|
744
|
+
// normalizeAssetKey strips the same `assets/` prefix
|
|
745
|
+
// and surfaces variants under canonical `img/...` keys.
|
|
746
|
+
// Spread order: variants first so site flat-dir wins on collision —
|
|
747
|
+
// a hand-placed file in site/assets/img/ overrides the cached variant.
|
|
725
748
|
return `
|
|
726
749
|
import { createAssetResolver } from '@koehler8/cms/utils/assetResolver';
|
|
727
750
|
const shared = import.meta.glob('@cms-framework/src/assets/**/*', {
|
|
@@ -729,15 +752,17 @@ const shared = import.meta.glob('@cms-framework/src/assets/**/*', {
|
|
|
729
752
|
query: '?url',
|
|
730
753
|
import: 'default',
|
|
731
754
|
});
|
|
732
|
-
const site = import.meta.glob(
|
|
733
|
-
'@cms-site/assets/**/*',
|
|
734
|
-
'!@cms-site/assets/_source/**',
|
|
735
|
-
], {
|
|
755
|
+
const site = import.meta.glob('@cms-site/assets/**/*', {
|
|
736
756
|
eager: true,
|
|
737
757
|
query: '?url',
|
|
738
758
|
import: 'default',
|
|
739
759
|
});
|
|
740
|
-
const
|
|
760
|
+
const variants = import.meta.glob('@cms-variant-cache/assets/**/*', {
|
|
761
|
+
eager: true,
|
|
762
|
+
query: '?url',
|
|
763
|
+
import: 'default',
|
|
764
|
+
});
|
|
765
|
+
const resolver = createAssetResolver(shared, { ...variants, ...site });
|
|
741
766
|
export const resolveAssetUrl = resolver.resolveAssetUrl;
|
|
742
767
|
export const resolveAsset = resolver.resolveAsset;
|
|
743
768
|
export const resolveMedia = resolver.resolveMedia;
|
|
@@ -765,6 +790,50 @@ export const assetUrlMap = resolver.assetUrlMap;
|
|
|
765
790
|
return { code: transformed, map: null };
|
|
766
791
|
},
|
|
767
792
|
|
|
793
|
+
async buildStart() {
|
|
794
|
+
// Generate image variants into the cache dir BEFORE the VIRTUAL_ASSETS
|
|
795
|
+
// load() block runs its import.meta.glob — that glob picks up the cache
|
|
796
|
+
// files and surfaces them as Vite-pipelined assets (hashed, emitted to
|
|
797
|
+
// dist/assets/) at build, or served straight from disk during dev.
|
|
798
|
+
//
|
|
799
|
+
// Errors here are warned, not thrown — a broken variant pipeline should
|
|
800
|
+
// not kill the entire build. Sites without source images (no img dir,
|
|
801
|
+
// empty img dir) are no-ops.
|
|
802
|
+
const siteImgDir = path.join(siteDir, 'assets', 'img');
|
|
803
|
+
|
|
804
|
+
// Strict deprecation: warn loudly if the legacy _source/ dir exists.
|
|
805
|
+
// Pipeline only reads the flat dir — sites that haven't migrated will
|
|
806
|
+
// see broken image URLs until they run `npx @koehler8/cms migrate-image-variants`.
|
|
807
|
+
const legacySourceDir = path.join(siteImgDir, '_source');
|
|
808
|
+
if (fs.existsSync(legacySourceDir)) {
|
|
809
|
+
this.warn(
|
|
810
|
+
`[@koehler8/cms] Found legacy site/assets/img/_source/ directory.`
|
|
811
|
+
+ ` As of beta.27 image originals live directly in site/assets/img/`
|
|
812
|
+
+ ` and variants generate at build time into node_modules/.cache/.`
|
|
813
|
+
+ ` Run \`npx @koehler8/cms migrate-image-variants\` from the site`
|
|
814
|
+
+ ` directory to migrate. The pipeline is NOT reading from _source/`
|
|
815
|
+
+ ` — image URLs will 404 until you migrate.`,
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
const variantConfig = resolveImageVariantConfig(siteConfig);
|
|
821
|
+
const jobs = planVariantJobsFromFlatDir({
|
|
822
|
+
siteImgDir,
|
|
823
|
+
config: variantConfig,
|
|
824
|
+
});
|
|
825
|
+
await reconcileVariantCache({
|
|
826
|
+
jobs,
|
|
827
|
+
cacheDir: variantCacheDir,
|
|
828
|
+
manifestPath: variantManifestPath,
|
|
829
|
+
currentConfig: variantConfig,
|
|
830
|
+
log: (msg) => this.warn(`[@koehler8/cms image-variants] ${msg}`),
|
|
831
|
+
});
|
|
832
|
+
} catch (err) {
|
|
833
|
+
this.warn(`[@koehler8/cms image-variants] generation failed: ${err.message}`);
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
|
|
768
837
|
configResolved(resolvedConfig) {
|
|
769
838
|
// Write temp files as soon as config is resolved so vite-ssg's
|
|
770
839
|
// detectEntry (which runs before Vite's build lifecycle) finds them.
|
|
@@ -831,8 +900,55 @@ export const assetUrlMap = resolver.assetUrlMap;
|
|
|
831
900
|
// Watch site content for HMR
|
|
832
901
|
server.watcher.add(siteDir);
|
|
833
902
|
|
|
903
|
+
// Image-variant regen on source-image change. Debounced so bulk
|
|
904
|
+
// drag-and-drop adds don't trigger N pipeline runs. Writes to the
|
|
905
|
+
// cache dir don't re-trigger because we early-return on those events.
|
|
906
|
+
const siteImgDir = path.join(siteDir, 'assets', 'img');
|
|
907
|
+
let pendingVariantRegen = null;
|
|
908
|
+
const scheduleVariantRegen = () => {
|
|
909
|
+
if (pendingVariantRegen) clearTimeout(pendingVariantRegen);
|
|
910
|
+
pendingVariantRegen = setTimeout(async () => {
|
|
911
|
+
pendingVariantRegen = null;
|
|
912
|
+
try {
|
|
913
|
+
const variantConfig = resolveImageVariantConfig(siteConfig);
|
|
914
|
+
const jobs = planVariantJobsFromFlatDir({
|
|
915
|
+
siteImgDir,
|
|
916
|
+
config: variantConfig,
|
|
917
|
+
});
|
|
918
|
+
await reconcileVariantCache({
|
|
919
|
+
jobs,
|
|
920
|
+
cacheDir: variantCacheDir,
|
|
921
|
+
manifestPath: variantManifestPath,
|
|
922
|
+
currentConfig: variantConfig,
|
|
923
|
+
log: (msg) => console.log(`[@koehler8/cms image-variants] ${msg}`),
|
|
924
|
+
});
|
|
925
|
+
// Invalidate the asset-resolver virtual module so its
|
|
926
|
+
// import.meta.glob re-evaluates and the new variant URLs land
|
|
927
|
+
// in the asset map. The full-reload below picks them up.
|
|
928
|
+
const assetMod = server.moduleGraph.getModuleById(resolved(VIRTUAL_ASSETS));
|
|
929
|
+
if (assetMod) server.moduleGraph.invalidateModule(assetMod);
|
|
930
|
+
server.ws.send({ type: 'full-reload' });
|
|
931
|
+
} catch (err) {
|
|
932
|
+
console.warn(`[@koehler8/cms image-variants] regen failed: ${err.message}`);
|
|
933
|
+
}
|
|
934
|
+
}, 250);
|
|
935
|
+
};
|
|
936
|
+
const handleImageEvent = (file) => {
|
|
937
|
+
if (typeof file !== 'string') return;
|
|
938
|
+
// Ignore writes our own pipeline made into the cache dir.
|
|
939
|
+
if (variantCacheDir && file.startsWith(variantCacheDir)) return;
|
|
940
|
+
if (!file.startsWith(siteImgDir)) return;
|
|
941
|
+
scheduleVariantRegen();
|
|
942
|
+
};
|
|
943
|
+
server.watcher.on('add', handleImageEvent);
|
|
944
|
+
server.watcher.on('change', handleImageEvent);
|
|
945
|
+
server.watcher.on('unlink', handleImageEvent);
|
|
946
|
+
|
|
834
947
|
// Invalidate virtual modules when site content changes
|
|
835
948
|
const invalidate = (file) => {
|
|
949
|
+
// Don't double-handle image events — the dedicated handler above
|
|
950
|
+
// already invalidates VIRTUAL_ASSETS and triggers full-reload.
|
|
951
|
+
if (variantCacheDir && file.startsWith(variantCacheDir)) return;
|
|
836
952
|
if (!file.startsWith(siteDir)) return;
|
|
837
953
|
for (const id of VIRTUAL_IDS) {
|
|
838
954
|
const mod = server.moduleGraph.getModuleById(resolved(id));
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Wrapper that runs the framework's generate-image-variants.js from the
|
|
3
|
-
// consuming site repo's CWD. Accepts --site-dir <path> to locate site assets;
|
|
4
|
-
// defaults to ./site.
|
|
5
|
-
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
-
const __dirname = path.dirname(__filename);
|
|
11
|
-
|
|
12
|
-
const args = process.argv.slice(2);
|
|
13
|
-
let siteDir = './site';
|
|
14
|
-
for (let i = 0; i < args.length; i++) {
|
|
15
|
-
if (args[i] === '--site-dir' && args[i + 1]) {
|
|
16
|
-
siteDir = args[i + 1];
|
|
17
|
-
i++;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const resolvedSiteDir = path.resolve(process.cwd(), siteDir);
|
|
22
|
-
process.env.CMS_SITE_DIR = resolvedSiteDir;
|
|
23
|
-
|
|
24
|
-
const { generateVariants } = await import(
|
|
25
|
-
path.resolve(__dirname, '..', 'scripts', 'generate-image-variants.js')
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
await generateVariants({ siteDir: resolvedSiteDir });
|
|
30
|
-
} catch (err) {
|
|
31
|
-
console.error(`❌ ${err.message}`);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|