@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.
@@ -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 resolver = createAssetResolver(shared, site);
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
- }