@joycostudio/iris 0.0.0-stage → 0.0.1

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 ADDED
@@ -0,0 +1,658 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/cli/index.ts
4
+ import { access, writeFile as writeFile2 } from "fs/promises";
5
+ import path4 from "path";
6
+ import { createJiti } from "jiti";
7
+
8
+ // packages/node/generate.ts
9
+ import { mkdir, readFile as readFile3, realpath as realpath2, rename, writeFile } from "fs/promises";
10
+ import path3 from "path";
11
+ import sharp2 from "sharp";
12
+
13
+ // packages/node/discover.ts
14
+ import { readFile } from "fs/promises";
15
+ import path from "path";
16
+ import { parse } from "@babel/parser";
17
+ import fg from "fast-glob";
18
+ import { minimatch } from "minimatch";
19
+ var ignored = [
20
+ "**/node_modules/**",
21
+ "**/.next/**",
22
+ "**/.git/**",
23
+ "**/dist/**",
24
+ "**/coverage/**",
25
+ "**/generated/**",
26
+ "**/*.generated.*"
27
+ ];
28
+ var codeFile = /\.(?:[cm]?[jt]s|[jt]sx)$/i;
29
+ var imageFile = /\.(?:png|jpe?g|webp|avif|gif)$/i;
30
+ async function discoverIrisSources(options, rootDir) {
31
+ const publicDir = path.resolve(rootDir, options.publicDir ?? "public");
32
+ const exclude = options.exclude ?? [];
33
+ const outputDir = path.resolve(rootDir, options.output);
34
+ const explicit = [];
35
+ const patterns = [];
36
+ const found = /* @__PURE__ */ new Set();
37
+ for (const source of options.sources ?? []) {
38
+ if (typeof source === "string" && !/^(?:https?:\/\/|\/)/i.test(source) && fg.isDynamicPattern(source))
39
+ patterns.push(source);
40
+ else explicit.push(source);
41
+ }
42
+ const globOptions = {
43
+ cwd: rootDir,
44
+ onlyFiles: true,
45
+ followSymbolicLinks: false,
46
+ ignore: [...ignored, ...exclude]
47
+ };
48
+ for (const file of await fg(patterns, globOptions)) {
49
+ if (!imageFile.test(file)) continue;
50
+ const relative = path.relative(publicDir, path.resolve(rootDir, file));
51
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
52
+ throw new Error(
53
+ `Iris source pattern matched ${file} outside publicDir. Image globs must stay inside ${publicDir}.`
54
+ );
55
+ }
56
+ found.add(`/${relative.split(path.sep).map(encodeURIComponent).join("/")}`);
57
+ }
58
+ for (const file of (await fg(options.scan ?? [], globOptions)).sort()) {
59
+ if (!codeFile.test(file) || /\.d\.[cm]?ts$/i.test(file) || path.resolve(rootDir, file).startsWith(outputDir + path.sep))
60
+ continue;
61
+ const code = await readFile(path.resolve(rootDir, file), "utf8");
62
+ try {
63
+ for (const source of scanImageLiterals(code, file)) found.add(source);
64
+ } catch (error) {
65
+ throw new Error(
66
+ `Iris could not scan ${file}: ${error instanceof Error ? error.message : String(error)}`
67
+ );
68
+ }
69
+ }
70
+ const explicitUrls = new Set(
71
+ explicit.map((source) => typeof source === "string" ? source : source.src)
72
+ );
73
+ const excluded = (src) => exclude.some(
74
+ (pattern) => src === pattern || minimatch(src, pattern, { dot: true, nonegate: true, nocomment: true })
75
+ );
76
+ return [
77
+ ...[...found].sort().filter((src) => !explicitUrls.has(src) && !excluded(src)),
78
+ ...explicit.filter(
79
+ (source) => !excluded(typeof source === "string" ? source : source.src)
80
+ )
81
+ ];
82
+ }
83
+ function scanImageLiterals(code, filename) {
84
+ const ast = parse(code, {
85
+ sourceType: "unambiguous",
86
+ sourceFilename: filename,
87
+ attachComment: false,
88
+ plugins: [
89
+ .../\.[cm]?tsx?$/i.test(filename) ? ["typescript"] : [],
90
+ .../\.[jt]sx$|\.[cm]?js$/i.test(filename) ? ["jsx"] : []
91
+ ]
92
+ });
93
+ const literals = [];
94
+ function walk(value) {
95
+ if (!value || typeof value !== "object") return;
96
+ if (Array.isArray(value)) {
97
+ value.forEach(walk);
98
+ return;
99
+ }
100
+ const node = value;
101
+ if (typeof node.type !== "string") return;
102
+ if (node.type.startsWith("Comment") || node.type === "BinaryExpression" || node.type === "TaggedTemplateExpression" || node.type === "ImportDeclaration" || node.type === "ExportAllDeclaration" || node.type === "TSLiteralType")
103
+ return;
104
+ if (node.type === "StringLiteral") {
105
+ if (typeof node.value === "string") literals.push(node.value);
106
+ return;
107
+ }
108
+ if (node.type === "TemplateLiteral") {
109
+ const template = node;
110
+ if (template.expressions.length === 0 && template.quasis[0]?.value.cooked)
111
+ literals.push(template.quasis[0].value.cooked);
112
+ return;
113
+ }
114
+ for (const [key, child] of Object.entries(node)) {
115
+ if (![
116
+ "comments",
117
+ "leadingComments",
118
+ "trailingComments",
119
+ "innerComments",
120
+ "loc",
121
+ "extra"
122
+ ].includes(key))
123
+ walk(child);
124
+ }
125
+ }
126
+ walk(ast.program);
127
+ return [
128
+ ...new Set(
129
+ literals.filter(
130
+ (value) => /^(?:https?:\/\/|\/).+\.(?:png|jpe?g|webp|avif|gif)(?:[?#].*)?$/i.test(
131
+ value
132
+ )
133
+ )
134
+ )
135
+ ].sort();
136
+ }
137
+
138
+ // packages/node/prepare.ts
139
+ import { readFile as readFile2, realpath } from "fs/promises";
140
+ import path2 from "path";
141
+ import fg2 from "fast-glob";
142
+ import sharp from "sharp";
143
+
144
+ // packages/core/policy.ts
145
+ import { minimatch as minimatch2 } from "minimatch";
146
+ function irisFailure(code, message) {
147
+ return { ok: false, error: { code, message } };
148
+ }
149
+ function positive(value) {
150
+ return Number.isFinite(value) && value > 0;
151
+ }
152
+ function validVariant(value) {
153
+ return value && typeof value.url === "string" && !!value.url && positive(value.width) && positive(value.height) && Number.isFinite(value.bytes) && value.bytes >= 0;
154
+ }
155
+
156
+ // packages/node/prepare.ts
157
+ async function prepareIrisAssets({
158
+ iris,
159
+ rootDir = process.cwd(),
160
+ publicDir = "public",
161
+ fetch: fetcher = fetch
162
+ }) {
163
+ const publicRoot = path2.resolve(rootDir, publicDir);
164
+ try {
165
+ let inspect2 = function(src) {
166
+ let pending = inspected.get(src);
167
+ if (!pending) {
168
+ pending = (async () => {
169
+ let bytes;
170
+ if (/^https?:\/\//i.test(src)) {
171
+ const response = await fetcher(src, {
172
+ signal: AbortSignal.timeout(3e4)
173
+ });
174
+ if (!response.ok)
175
+ throw new Error(`Cannot inspect ${src}: HTTP ${response.status}`);
176
+ bytes = Buffer.from(await response.arrayBuffer());
177
+ } else {
178
+ const pathname = decodeURIComponent(
179
+ src.split(/[?#]/, 1)[0]
180
+ ).replace(/^\/+/, "");
181
+ const file = path2.resolve(publicRoot, pathname);
182
+ within(publicRoot, file);
183
+ within(await realpath(publicRoot), await realpath(file));
184
+ bytes = await readFile2(file);
185
+ }
186
+ const meta = await sharp(bytes, { animated: true }).metadata();
187
+ const swapped = (meta.orientation ?? 1) >= 5;
188
+ const width = swapped ? meta.height : meta.width;
189
+ const height = swapped ? meta.width : meta.pageHeight ?? meta.height;
190
+ if (!width || !height)
191
+ throw new Error(`Cannot inspect dimensions of ${src}`);
192
+ return {
193
+ url: src,
194
+ width,
195
+ height,
196
+ bytes: bytes.length,
197
+ format: meta.format,
198
+ progressive: meta.isProgressive ?? false,
199
+ alpha: Boolean(
200
+ meta.hasAlpha && ((meta.pages ?? 1) > 1 || !(await sharp(bytes).stats()).isOpaque)
201
+ ),
202
+ animated: (meta.pages ?? 1) > 1
203
+ };
204
+ })();
205
+ inspected.set(src, pending);
206
+ }
207
+ return pending;
208
+ };
209
+ var inspect = inspect2;
210
+ const entries = /* @__PURE__ */ new Map();
211
+ const explicit = [];
212
+ for (const input of iris.assets) {
213
+ const src = typeof input === "string" ? input : input.src;
214
+ if (/^https?:\/\//i.test(src)) {
215
+ if (fg2.isDynamicPattern(new URL(src).pathname))
216
+ return irisFailure(
217
+ "remote_pattern",
218
+ "Remote URLs must be explicit; a glob cannot enumerate a bucket"
219
+ );
220
+ explicit.push(input);
221
+ } else if (typeof input === "string" && fg2.isDynamicPattern(src.startsWith("/") ? src.split(/[?#]/, 1)[0] : src)) {
222
+ const files = await fg2(
223
+ src.startsWith("/") ? src.split(/[?#]/, 1)[0].slice(1) : src,
224
+ {
225
+ cwd: src.startsWith("/") ? publicRoot : rootDir,
226
+ onlyFiles: true,
227
+ followSymbolicLinks: false,
228
+ ignore: ["**/node_modules/**", "**/.git/**", "**/.next/**"]
229
+ }
230
+ );
231
+ for (const file of files.sort()) {
232
+ const full = path2.resolve(
233
+ src.startsWith("/") ? publicRoot : rootDir,
234
+ file
235
+ );
236
+ const relative = within(publicRoot, full);
237
+ const url = `/${relative.split(path2.sep).map(encodeURIComponent).join("/")}`;
238
+ entries.set(url, url);
239
+ }
240
+ } else explicit.push(input);
241
+ }
242
+ for (const input of explicit) {
243
+ const src = typeof input === "string" ? input : input.src;
244
+ entries.set(src, input);
245
+ }
246
+ const inspected = /* @__PURE__ */ new Map();
247
+ const catalog = [];
248
+ const identities = /* @__PURE__ */ new Set();
249
+ for (const input of entries.values()) {
250
+ const entry = typeof input === "string" ? { src: input } : input;
251
+ const metadata = "width" in entry ? entry : await inspect2(entry.src);
252
+ const { width, height, bytes, format, progressive, alpha, animated } = metadata;
253
+ const variants = await Promise.all(
254
+ (entry.variants ?? []).map(
255
+ (variant) => typeof variant === "string" ? inspect2(variant) : variant
256
+ )
257
+ );
258
+ if (!validVariant({ url: entry.src, width, height, bytes }) || !variants.every(validVariant))
259
+ return irisFailure("invalid_asset", `Invalid metadata for ${entry.src}`);
260
+ for (const identity of /* @__PURE__ */ new Set([entry.src, entry.id ?? entry.src])) {
261
+ if (identities.has(identity))
262
+ return irisFailure(
263
+ "duplicate_asset",
264
+ `Ambiguous asset identity: ${identity}`
265
+ );
266
+ identities.add(identity);
267
+ }
268
+ catalog.push({
269
+ src: entry.src,
270
+ id: entry.id,
271
+ width,
272
+ height,
273
+ bytes,
274
+ format,
275
+ progressive,
276
+ alpha,
277
+ animated,
278
+ variants,
279
+ placeholder: entry.placeholder,
280
+ placeholders: entry.placeholders
281
+ });
282
+ }
283
+ return { ok: true, data: catalog };
284
+ } catch (error) {
285
+ return irisFailure(
286
+ "prepare_failed",
287
+ error instanceof Error ? error.message : String(error)
288
+ );
289
+ }
290
+ }
291
+ function within(root, file) {
292
+ const relative = path2.relative(root, file);
293
+ if (relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
294
+ throw new Error(`Image source escapes publicDir: ${file}`);
295
+ }
296
+ return relative;
297
+ }
298
+
299
+ // packages/node/generate.ts
300
+ async function generateIris({
301
+ iris,
302
+ generation,
303
+ rootDir = process.cwd(),
304
+ configFile = "iris.config.ts",
305
+ allowStale = false,
306
+ fetch: fetcher = fetch
307
+ }) {
308
+ if (!generation.output?.trim())
309
+ throw new Error("Iris requires an output directory");
310
+ const directory = path3.resolve(rootDir, generation.output);
311
+ const publicDir = path3.resolve(rootDir, generation.publicDir ?? "public");
312
+ const adapter = generation.adapter !== false;
313
+ try {
314
+ const sources = await discoverIrisSources(
315
+ {
316
+ ...generation,
317
+ sources: [...iris.assets, ...generation.sources ?? []]
318
+ },
319
+ rootDir
320
+ );
321
+ const entries = /* @__PURE__ */ new Map();
322
+ for (const value of sources) {
323
+ const source = typeof value === "string" ? { src: value } : value;
324
+ const previous = entries.get(source.src);
325
+ entries.set(source.src, {
326
+ ...previous,
327
+ ...source,
328
+ optional: Boolean(
329
+ previous ? "optional" in previous && previous.optional && "optional" in source && source.optional : "optional" in source && source.optional
330
+ )
331
+ });
332
+ }
333
+ const catalog = {};
334
+ const placeholders = {};
335
+ const queue = [...entries.values()];
336
+ let skipped = 0;
337
+ let cursor = 0;
338
+ async function worker() {
339
+ while (cursor < queue.length) {
340
+ const source = queue[cursor++];
341
+ if ("width" in source) {
342
+ const prepared = await prepareIrisAssets({
343
+ iris: { assets: [source] },
344
+ rootDir,
345
+ publicDir,
346
+ fetch: fetcher
347
+ });
348
+ if (!prepared.ok) throw new Error(prepared.error.message);
349
+ catalog[source.src] = prepared.data[0];
350
+ if (source.placeholder) placeholders[source.src] = source.placeholder;
351
+ continue;
352
+ }
353
+ let bytes;
354
+ if (/^https?:\/\//i.test(source.src)) {
355
+ const response = await fetcher(source.src, {
356
+ signal: AbortSignal.timeout(3e4)
357
+ });
358
+ if (source.optional && response.status === 404) {
359
+ skipped++;
360
+ continue;
361
+ }
362
+ if (!response.ok)
363
+ throw new Error(`${response.status} fetching ${source.src}`);
364
+ bytes = Buffer.from(await response.arrayBuffer());
365
+ } else {
366
+ const pathname = decodeURIComponent(
367
+ source.src.split(/[?#]/, 1)[0]
368
+ ).replace(/^\/+/, "");
369
+ const file = path3.resolve(publicDir, pathname);
370
+ for (const relative of [
371
+ path3.relative(publicDir, file),
372
+ path3.relative(await realpath2(publicDir), await realpath2(file))
373
+ ]) {
374
+ if (relative === ".." || relative.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative))
375
+ throw new Error(
376
+ `Iris local source escapes publicDir: ${source.src}`
377
+ );
378
+ }
379
+ bytes = await readFile3(file);
380
+ }
381
+ const meta = await sharp2(bytes, { animated: true }).metadata();
382
+ const swapped = (meta.orientation ?? 1) >= 5;
383
+ const width = swapped ? meta.height : meta.width;
384
+ const height = swapped ? meta.width : meta.pageHeight ?? meta.height;
385
+ if (!width || !height)
386
+ throw new Error(`Cannot inspect dimensions of ${source.src}`);
387
+ const placeholderOptions = source.placeholderOptions ?? {};
388
+ const format = placeholderOptions.format ?? "webp";
389
+ const size = placeholderOptions.width ?? 10;
390
+ const quality = placeholderOptions.quality ?? 40;
391
+ async function encodePlaceholder({
392
+ width: width2,
393
+ quality: quality2
394
+ }) {
395
+ const buffer = await sharp2(bytes, { page: 0, pages: 1 }).rotate().resize({
396
+ width: width2,
397
+ height: width2,
398
+ fit: "inside",
399
+ withoutEnlargement: true
400
+ }).toFormat(format, { quality: quality2 }).toBuffer();
401
+ return `data:image/${format};base64,${buffer.toString("base64")}`;
402
+ }
403
+ const placeholder = source.placeholder ?? await encodePlaceholder({ width: size, quality });
404
+ placeholders[source.src] = placeholder;
405
+ if (source.placeholderOnly) continue;
406
+ const preparedPlaceholders = [...source.placeholders ?? []];
407
+ for (const request of iris.placeholderRequests) {
408
+ if (preparedPlaceholders.some(
409
+ (item) => item.width === request.width && item.quality === request.quality
410
+ ))
411
+ continue;
412
+ const url = request.width === size && request.quality === quality ? placeholder : await encodePlaceholder(request);
413
+ preparedPlaceholders.push({ ...request, url });
414
+ }
415
+ const animated = (meta.pages ?? 1) > 1;
416
+ const alpha = Boolean(
417
+ meta.hasAlpha && (animated || !(await sharp2(bytes).stats()).isOpaque)
418
+ );
419
+ const metadata = {
420
+ src: source.src,
421
+ id: source.id,
422
+ width,
423
+ height,
424
+ bytes: bytes.length,
425
+ format: meta.format,
426
+ alpha,
427
+ animated,
428
+ progressive: meta.isProgressive ?? false,
429
+ placeholder,
430
+ placeholders: preparedPlaceholders,
431
+ variants: []
432
+ };
433
+ if (source.variants?.length) {
434
+ const prepared = await prepareIrisAssets({
435
+ iris: {
436
+ assets: [
437
+ { ...metadata, variants: [] },
438
+ ...source.variants.filter(
439
+ (item) => typeof item === "string"
440
+ )
441
+ ]
442
+ },
443
+ rootDir,
444
+ publicDir,
445
+ fetch: fetcher
446
+ });
447
+ if (!prepared.ok) throw new Error(prepared.error.message);
448
+ metadata.variants = [
449
+ ...metadata.variants,
450
+ ...source.variants.map((variant) => {
451
+ if (typeof variant !== "string") return variant;
452
+ const asset = prepared.data.find((item) => item.src === variant);
453
+ return { ...asset, url: asset.src };
454
+ })
455
+ ];
456
+ }
457
+ catalog[source.src] = metadata;
458
+ }
459
+ }
460
+ const concurrency = generation.concurrency ?? 4;
461
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1)
462
+ throw new Error("concurrency must be a positive integer");
463
+ await Promise.all(Array.from({ length: concurrency }, worker));
464
+ const sorted = (data) => Object.fromEntries(
465
+ Object.entries(data).sort(([a], [b]) => a.localeCompare(b))
466
+ );
467
+ const files = [
468
+ ["catalog.json", sorted(catalog)],
469
+ ["placeholders.json", sorted(placeholders)]
470
+ ];
471
+ if (generation.report)
472
+ files.push([
473
+ "report.json",
474
+ {
475
+ inspected: Object.keys(catalog).length,
476
+ skipped,
477
+ assets: Object.values(catalog).map((asset) => ({
478
+ src: asset.src,
479
+ result: iris.resolve({ asset })
480
+ }))
481
+ }
482
+ ]);
483
+ await mkdir(directory, { recursive: true });
484
+ for (const [name, data] of files)
485
+ await atomicWrite(
486
+ path3.join(directory, name),
487
+ JSON.stringify(data, null, 2) + "\n"
488
+ );
489
+ if (adapter) {
490
+ let configImport = path3.relative(directory, path3.resolve(rootDir, configFile)).split(path3.sep).join("/").replace(/\.[cm]?[jt]s$/, "");
491
+ if (!configImport.startsWith(".")) configImport = `./${configImport}`;
492
+ await atomicWrite(
493
+ path3.join(directory, "image.tsx"),
494
+ `"use client"
495
+
496
+ // Generated by Iris. Do not edit.
497
+ import { createIrisNextImage } from "@joycostudio/iris/next"
498
+ import iris from ${JSON.stringify(configImport)}
499
+ import catalog from "./catalog.json"
500
+
501
+ export const IrisImage = createIrisNextImage({ iris, catalog })
502
+ `
503
+ );
504
+ }
505
+ return {
506
+ stale: false,
507
+ placeholders: Object.keys(placeholders).length,
508
+ inspected: Object.keys(catalog).length,
509
+ skipped
510
+ };
511
+ } catch (error) {
512
+ if (allowStale && await usableOutput({ directory, adapter })) {
513
+ console.warn("Iris generation failed; keeping existing files:", error);
514
+ return { stale: true, placeholders: 0, inspected: 0, skipped: 0 };
515
+ }
516
+ throw error;
517
+ }
518
+ }
519
+ async function atomicWrite(file, content) {
520
+ await writeFile(`${file}.tmp`, content);
521
+ await rename(`${file}.tmp`, file);
522
+ }
523
+ async function usableOutput({
524
+ directory,
525
+ adapter
526
+ }) {
527
+ try {
528
+ const catalog = JSON.parse(
529
+ await readFile3(path3.join(directory, "catalog.json"), "utf8")
530
+ );
531
+ const placeholders = JSON.parse(
532
+ await readFile3(path3.join(directory, "placeholders.json"), "utf8")
533
+ );
534
+ if (!catalog || Array.isArray(catalog) || !Object.keys(catalog).length || !placeholders || Array.isArray(placeholders))
535
+ return false;
536
+ if (adapter && !(await readFile3(path3.join(directory, "image.tsx"), "utf8")).includes(
537
+ "createIrisNextImage"
538
+ ))
539
+ return false;
540
+ return true;
541
+ } catch {
542
+ return false;
543
+ }
544
+ }
545
+
546
+ // packages/cli/index.ts
547
+ var args = process.argv.slice(2);
548
+ void main().catch((error) => {
549
+ console.error(error);
550
+ process.exitCode = 1;
551
+ });
552
+ async function main() {
553
+ const command = args.find((argument) => !argument.startsWith("-"));
554
+ if (!command || command === "help" || args.includes("--help") || args.includes("-h")) {
555
+ printHelp();
556
+ } else if (command === "init") {
557
+ await runInit();
558
+ } else if (command === "generate") {
559
+ await runGenerate();
560
+ } else {
561
+ throw new Error(`Unknown Iris command: ${command}`);
562
+ }
563
+ }
564
+ async function runInit() {
565
+ const rootDir = process.cwd();
566
+ const existing = await findExistingConfig(rootDir);
567
+ if (existing) {
568
+ console.log(`${path4.basename(existing)} already exists; left unchanged.`);
569
+ return;
570
+ }
571
+ await writeFile2(path4.join(rootDir, "iris.config.ts"), initialConfig, {
572
+ flag: "wx"
573
+ });
574
+ console.log("Created iris.config.ts.");
575
+ console.log(
576
+ "Review sources and scan in iris.config.ts, then run iris generate."
577
+ );
578
+ }
579
+ var initialConfig = `import { createIris } from "@joycostudio/iris"
580
+
581
+ export default createIris({
582
+ assets: ["public/**/*.{png,jpg,jpeg,webp,avif,gif}"],
583
+ recipes: {
584
+ direct: { preview: "none" },
585
+ blur: { placeholder: "blur" },
586
+ },
587
+ rules: [{ when: { priority: true }, recipe: "direct" }],
588
+ fallback: "blur",
589
+ })
590
+
591
+ export const generate = { output: "src/generated/iris" }
592
+ `;
593
+ async function runGenerate() {
594
+ const rootDir = process.cwd();
595
+ const configArgument = optionValue("--config");
596
+ const configPath = configArgument ? path4.resolve(rootDir, configArgument) : await findConfig(rootDir);
597
+ const jiti = createJiti(process.cwd(), { interopDefault: false });
598
+ const imported = await jiti.import(configPath);
599
+ const iris = imported.default;
600
+ if (!iris || typeof iris.select !== "function" || typeof iris.resolve !== "function")
601
+ throw new Error(
602
+ `${path4.relative(rootDir, configPath)} must default-export createIris(...)`
603
+ );
604
+ const generation = typeof imported.generate === "function" ? await imported.generate() : imported.generate ?? { output: "src/generated/iris" };
605
+ const result = await generateIris({
606
+ iris,
607
+ generation,
608
+ rootDir,
609
+ configFile: configPath,
610
+ allowStale: args.includes("--allow-stale")
611
+ });
612
+ if (result.stale) console.log("Iris kept existing generated files.");
613
+ else
614
+ console.log(
615
+ `Iris prepared ${result.inspected} assets and ${result.placeholders} placeholders; skipped ${result.skipped} optional sources.`
616
+ );
617
+ }
618
+ async function findConfig(rootDir) {
619
+ const existing = await findExistingConfig(rootDir);
620
+ if (existing) return existing;
621
+ throw new Error(
622
+ "Iris could not find iris.config.ts (or .mts/.js/.mjs). Run iris init first."
623
+ );
624
+ }
625
+ async function findExistingConfig(rootDir) {
626
+ for (const name of [
627
+ "iris.config.ts",
628
+ "iris.config.mts",
629
+ "iris.config.js",
630
+ "iris.config.mjs"
631
+ ]) {
632
+ const candidate = path4.join(rootDir, name);
633
+ try {
634
+ await access(candidate);
635
+ return candidate;
636
+ } catch (error) {
637
+ if (error.code !== "ENOENT") throw error;
638
+ }
639
+ }
640
+ return null;
641
+ }
642
+ function optionValue(name) {
643
+ const index = args.indexOf(name);
644
+ return index < 0 ? void 0 : args[index + 1];
645
+ }
646
+ function printHelp() {
647
+ console.log(`Iris responsive-image tooling
648
+
649
+ Usage:
650
+ iris init
651
+ iris generate [--config path] [--allow-stale]
652
+
653
+ Commands:
654
+ init Create a default iris.config.ts without replacing existing config
655
+ generate Discover sources from iris.config and write image manifests
656
+ `);
657
+ }
658
+ //# sourceMappingURL=cli.js.map