img-upload-kit 1.1.1 → 2.0.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.
Files changed (111) hide show
  1. package/CHANGELOG.md +56 -41
  2. package/MIGRATION.md +192 -0
  3. package/README.md +138 -154
  4. package/dist/bin.js +1186 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/blur.cjs +40 -412
  7. package/dist/blur.cjs.map +1 -1
  8. package/dist/blur.d.cts +5 -5
  9. package/dist/blur.d.ts +5 -5
  10. package/dist/blur.js +39 -393
  11. package/dist/blur.js.map +1 -1
  12. package/dist/blurDataUtils-DveBRtqE.d.cts +43 -0
  13. package/dist/blurDataUtils-yx9Q6HlU.d.ts +43 -0
  14. package/dist/blurService-D5TpMNji.d.ts +83 -0
  15. package/dist/blurService-DacwylJx.d.cts +83 -0
  16. package/dist/chunk-2OVNXHTN.cjs +514 -0
  17. package/dist/chunk-2OVNXHTN.cjs.map +1 -0
  18. package/dist/chunk-3ICG2SLL.cjs +135 -0
  19. package/dist/chunk-3ICG2SLL.cjs.map +1 -0
  20. package/dist/chunk-4ASFG3KB.js +514 -0
  21. package/dist/chunk-4ASFG3KB.js.map +1 -0
  22. package/dist/chunk-AI3JJLSA.js +2370 -0
  23. package/dist/chunk-AI3JJLSA.js.map +1 -0
  24. package/dist/chunk-CV23O4YU.js +145 -0
  25. package/dist/chunk-CV23O4YU.js.map +1 -0
  26. package/dist/chunk-G7LLSKBV.js +158 -0
  27. package/dist/chunk-G7LLSKBV.js.map +1 -0
  28. package/dist/chunk-IB3TEDLR.js +294 -0
  29. package/dist/chunk-IB3TEDLR.js.map +1 -0
  30. package/dist/chunk-KJVYWKIG.cjs +158 -0
  31. package/dist/chunk-KJVYWKIG.cjs.map +1 -0
  32. package/dist/chunk-NEDBIIE3.cjs +226 -0
  33. package/dist/chunk-NEDBIIE3.cjs.map +1 -0
  34. package/dist/chunk-QHU7FUO4.cjs +37 -0
  35. package/dist/chunk-QHU7FUO4.cjs.map +1 -0
  36. package/dist/chunk-QQHZJKNA.js +135 -0
  37. package/dist/chunk-QQHZJKNA.js.map +1 -0
  38. package/dist/chunk-S2IFAWJK.js +37 -0
  39. package/dist/chunk-S2IFAWJK.js.map +1 -0
  40. package/dist/chunk-TT5C7K3Q.js +226 -0
  41. package/dist/chunk-TT5C7K3Q.js.map +1 -0
  42. package/dist/chunk-U2TWTEBY.cjs +2370 -0
  43. package/dist/chunk-U2TWTEBY.cjs.map +1 -0
  44. package/dist/chunk-WISG74VL.cjs +294 -0
  45. package/dist/chunk-WISG74VL.cjs.map +1 -0
  46. package/dist/chunk-YRXXQUDE.cjs +145 -0
  47. package/dist/chunk-YRXXQUDE.cjs.map +1 -0
  48. package/dist/cli.cjs +273 -76
  49. package/dist/cli.cjs.map +1 -1
  50. package/dist/cli.js +269 -73
  51. package/dist/cli.js.map +1 -1
  52. package/dist/headless.cjs +8 -1880
  53. package/dist/headless.cjs.map +1 -1
  54. package/dist/headless.d.cts +3 -1
  55. package/dist/headless.d.ts +3 -1
  56. package/dist/headless.js +25 -1886
  57. package/dist/headless.js.map +1 -1
  58. package/dist/index-BIdnqtW8.d.ts +234 -0
  59. package/dist/index-Brk_uHs6.d.cts +234 -0
  60. package/dist/{index-FVXXKf2v.d.ts → index-CPMlaQsC.d.ts} +76 -153
  61. package/dist/{index-FVXXKf2v.d.cts → index-DHINe-rA.d.cts} +76 -153
  62. package/dist/index.cjs +64 -532
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +7 -4
  65. package/dist/index.d.ts +7 -4
  66. package/dist/index.js +63 -504
  67. package/dist/index.js.map +1 -1
  68. package/dist/{memoryAdapter-CBu7_3Pp.d.ts → memoryAdapter-Ck0ccZbF.d.cts} +3 -2
  69. package/dist/{memoryAdapter-BJjyLR-1.d.cts → memoryAdapter-DxEKaG0b.d.ts} +3 -2
  70. package/dist/placeholders-C4ftwaII.d.ts +5 -0
  71. package/dist/placeholders-x_3rt65M.d.cts +5 -0
  72. package/dist/react.cjs +1952 -3257
  73. package/dist/react.cjs.map +1 -1
  74. package/dist/react.d.cts +188 -50
  75. package/dist/react.d.ts +188 -50
  76. package/dist/react.js +1763 -3013
  77. package/dist/react.js.map +1 -1
  78. package/dist/server-handler.cjs +14 -0
  79. package/dist/server-handler.cjs.map +1 -0
  80. package/dist/server-handler.d.cts +3 -0
  81. package/dist/server-handler.d.ts +3 -0
  82. package/dist/server-handler.js +14 -0
  83. package/dist/server-handler.js.map +1 -0
  84. package/dist/server.cjs +1096 -532
  85. package/dist/server.cjs.map +1 -1
  86. package/dist/server.d.cts +77 -154
  87. package/dist/server.d.ts +77 -154
  88. package/dist/server.js +1050 -448
  89. package/dist/server.js.map +1 -1
  90. package/dist/storage.cjs +239 -232
  91. package/dist/storage.cjs.map +1 -1
  92. package/dist/storage.d.cts +21 -5
  93. package/dist/storage.d.ts +21 -5
  94. package/dist/storage.js +227 -207
  95. package/dist/storage.js.map +1 -1
  96. package/dist/styles/animated-image.css +112 -0
  97. package/dist/styles/dropzone-and-tiles.css +247 -0
  98. package/dist/styles/modals-and-dock.css +248 -0
  99. package/dist/styles/primitives.css +197 -0
  100. package/dist/styles/quality-and-inputs.css +184 -0
  101. package/dist/styles/tokens.css +123 -0
  102. package/dist/styles.css +39 -24
  103. package/dist/types-BrP-o9nd.d.cts +85 -0
  104. package/dist/types-sZ3KoFQY.d.ts +85 -0
  105. package/dist/types-yShGNQnW.d.cts +61 -0
  106. package/dist/types-yShGNQnW.d.ts +61 -0
  107. package/dist/{types-CiLY0DwB.d.cts → upload-C5Trd6Dh.d.cts} +27 -53
  108. package/dist/{types-CiLY0DwB.d.ts → upload-C5Trd6Dh.d.ts} +27 -53
  109. package/package.json +92 -32
  110. package/dist/blurService-DqsnfPTd.d.cts +0 -172
  111. package/dist/blurService-DqsnfPTd.d.ts +0 -172
package/dist/cli.js CHANGED
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
-
3
1
  // src/cli/commands.ts
4
2
  import fs4 from "fs/promises";
5
3
  import path4 from "path";
@@ -33,7 +31,8 @@ function normalizeImagePath(imageSrc, options = {}) {
33
31
  for (const pattern of patterns) {
34
32
  const match = cleanUrlNoQuery.match(pattern);
35
33
  if (match) {
36
- const pathPart = match[match.length - 1];
34
+ const groups = match.slice(1).filter(Boolean);
35
+ const pathPart = groups.join("/");
37
36
  normalizedPath = "/" + pathPart.replace(/^\/+/, "");
38
37
  break;
39
38
  }
@@ -79,7 +78,6 @@ function normalizeImagePath(imageSrc, options = {}) {
79
78
  // src/cli/generator.ts
80
79
  import fs2 from "fs/promises";
81
80
  import path2 from "path";
82
- import sharp4 from "sharp";
83
81
 
84
82
  // src/server/optimization/types.ts
85
83
  var BLUR_PLACEHOLDER_WIDTH = 128;
@@ -90,13 +88,28 @@ var EDGE_COLOR_FALLBACK = "#6B7280";
90
88
  var EDGE_WHITE_FALLBACK = "#F9FAFB";
91
89
  var WHITE_DOMINANCE_RATIO = 1.35;
92
90
 
91
+ // src/server/optimization/loadSharp.ts
92
+ var SHARP_MISSING_MESSAGE = "img-upload-kit/server requires `sharp` \u2014 run `npm i sharp`";
93
+ var sharpPromise = null;
94
+ function unwrapDefault(mod) {
95
+ const record = mod;
96
+ return record && record.default !== void 0 ? record.default : mod;
97
+ }
98
+ function loadSharp() {
99
+ if (!sharpPromise) {
100
+ sharpPromise = import("sharp").then(
101
+ (mod) => unwrapDefault(mod),
102
+ (error) => {
103
+ sharpPromise = null;
104
+ throw new Error(SHARP_MISSING_MESSAGE, { cause: error });
105
+ }
106
+ );
107
+ }
108
+ return sharpPromise;
109
+ }
110
+
93
111
  // src/server/optimization/blurGenerator.ts
94
112
  import { encode as encodeBlurHash } from "blurhash";
95
- import lqip from "lqip-modern";
96
- import sharp3 from "sharp";
97
-
98
- // src/server/optimization/edgeColorExtractor.ts
99
- import sharp2 from "sharp";
100
113
 
101
114
  // src/utils/color.ts
102
115
  function hexToRgb(hex) {
@@ -120,8 +133,8 @@ function rgbToHex(r, g, b) {
120
133
  }
121
134
 
122
135
  // src/server/optimization/colorHistogram.ts
123
- import sharp from "sharp";
124
136
  async function extractEnhancedColorFromBuffer(buffer, mode = "balanced") {
137
+ const sharp = await loadSharp();
125
138
  try {
126
139
  const metadata = await sharp(buffer).metadata();
127
140
  const maxSize = 400;
@@ -266,8 +279,9 @@ function extractDominantColorHistogram(data) {
266
279
 
267
280
  // src/server/optimization/edgeColorExtractor.ts
268
281
  async function extractEdgeColorsFromBuffer(buffer) {
282
+ const sharp = await loadSharp();
269
283
  try {
270
- const sample = await sharp2(buffer).resize(400, 400, {
284
+ const sample = await sharp(buffer).resize(400, 400, {
271
285
  fit: "inside",
272
286
  kernel: "lanczos3",
273
287
  withoutEnlargement: true
@@ -407,46 +421,73 @@ function blendEdgeColors(edgeColors) {
407
421
  }
408
422
 
409
423
  // src/server/optimization/blurGenerator.ts
410
- async function encodeBlurHashString(buffer, componentX = 4, componentY = 3) {
411
- const { data, info } = await sharp3(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
412
- const clampedX = Math.max(1, Math.min(Math.round(componentX), info.width, 9));
413
- const clampedY = Math.max(1, Math.min(Math.round(componentY), info.height, 9));
424
+ function encodeRawBlurHash(data, width, height, componentX = 4, componentY = 3) {
425
+ const clampedX = Math.max(1, Math.min(Math.round(componentX), width, 9));
426
+ const clampedY = Math.max(1, Math.min(Math.round(componentY), height, 9));
414
427
  return encodeBlurHash(
415
428
  new Uint8ClampedArray(data),
416
- info.width,
417
- info.height,
429
+ width,
430
+ height,
418
431
  clampedX,
419
432
  clampedY
420
433
  );
421
434
  }
435
+ async function encodeBlurHashString(buffer, componentX = 4, componentY = 3) {
436
+ const sharp = await loadSharp();
437
+ const { data, info } = await sharp(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
438
+ return encodeRawBlurHash(
439
+ data,
440
+ info.width,
441
+ info.height,
442
+ componentX,
443
+ componentY
444
+ );
445
+ }
422
446
  async function generateBlurPlaceholder(buffer, options = {}) {
423
447
  const targetWidth = options.width ?? BLUR_PLACEHOLDER_WIDTH;
424
448
  const fallbackWidth = options.fallbackWidth ?? BLUR_PLACEHOLDER_FALLBACK_WIDTH;
425
449
  const quality = options.quality ?? BLUR_PLACEHOLDER_QUALITY;
426
450
  const maxChars = options.maxDataUrlChars ?? BLUR_PLACEHOLDER_MAX_CHARS;
451
+ const sharp = await loadSharp();
427
452
  try {
428
453
  let processBuffer = buffer;
429
454
  const isSvg = /<svg[\s>]/i.test(buffer.toString("utf8", 0, 1024));
430
455
  if (isSvg) {
431
456
  const normalizedSvg = buffer.toString("utf8").replace(/<svg\b/gi, "<svg").replace(/<\/svg\s*>/gi, "</svg>");
432
- processBuffer = await sharp3(Buffer.from(normalizedSvg, "utf8")).resize(targetWidth, targetWidth, { fit: "inside" }).png().toBuffer();
457
+ processBuffer = Buffer.from(normalizedSvg, "utf8");
433
458
  }
434
- const buildPlaceholder = (width, q) => lqip(processBuffer, {
435
- outputFormat: "webp",
436
- resize: width,
437
- outputOptions: { quality: q, effort: 6, smartSubsample: true }
438
- });
439
- let result = await buildPlaceholder(targetWidth, quality);
440
- if (result.metadata.dataURIBase64.length > maxChars) {
441
- result = await buildPlaceholder(fallbackWidth, quality);
459
+ const baseSize = Math.max(targetWidth, 128);
460
+ const rawSample = await sharp(processBuffer).rotate().resize(baseSize, baseSize, { fit: "inside", withoutEnlargement: true }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
461
+ const rawInput = {
462
+ raw: {
463
+ width: rawSample.info.width,
464
+ height: rawSample.info.height,
465
+ channels: 4
466
+ }
467
+ };
468
+ const buildPlaceholderDataUrl = async (width) => {
469
+ const webpBuffer = await sharp(rawSample.data, rawInput).resize(width, width, { fit: "inside", withoutEnlargement: true }).webp({ quality, effort: 6, smartSubsample: true }).toBuffer();
470
+ return `data:image/webp;base64,${webpBuffer.toString("base64")}`;
471
+ };
472
+ let dataURL = await buildPlaceholderDataUrl(targetWidth);
473
+ if (dataURL.length > maxChars) {
474
+ dataURL = await buildPlaceholderDataUrl(fallbackWidth);
442
475
  }
443
- const dataURL = result.metadata.dataURIBase64;
444
- const edgeColors = await extractEdgeColorsFromBuffer(processBuffer);
476
+ const edgeColors = computeEdgeColorSet(
477
+ rawSample.data,
478
+ rawSample.info.width,
479
+ rawSample.info.height
480
+ );
445
481
  const color = blendEdgeColors(edgeColors);
446
482
  let blurHash;
447
483
  if (options.includeBlurHash) {
448
484
  try {
449
- blurHash = await encodeBlurHashString(processBuffer);
485
+ const hashSample = rawSample.info.width <= 32 && rawSample.info.height <= 32 ? rawSample : await sharp(rawSample.data, rawInput).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
486
+ blurHash = encodeRawBlurHash(
487
+ hashSample.data,
488
+ hashSample.info.width,
489
+ hashSample.info.height
490
+ );
450
491
  } catch {
451
492
  blurHash = void 0;
452
493
  }
@@ -461,7 +502,8 @@ async function generateBlurPlaceholder(buffer, options = {}) {
461
502
  }
462
503
  }
463
504
  async function generateFallbackBlur(buffer, includeBlurHash) {
464
- const tinyBuffer = await sharp3(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).blur(1).webp({
505
+ const sharp = await loadSharp();
506
+ const tinyBuffer = await sharp(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).blur(1).webp({
465
507
  quality: 75,
466
508
  effort: 6,
467
509
  smartSubsample: false
@@ -481,42 +523,184 @@ async function generateFallbackBlur(buffer, includeBlurHash) {
481
523
  }
482
524
 
483
525
  // src/server/semaphore.ts
526
+ var SemaphoreQueueFullError = class extends Error {
527
+ status = 503;
528
+ constructor(message = "Server upload queue is full. Please try again later.") {
529
+ super(message);
530
+ this.name = "SemaphoreQueueFullError";
531
+ }
532
+ };
533
+ function createAbortError(signal) {
534
+ if (signal?.reason instanceof Error) {
535
+ return signal.reason;
536
+ }
537
+ const err = new Error(
538
+ typeof signal?.reason === "string" ? signal.reason : "Operation aborted"
539
+ );
540
+ err.name = "AbortError";
541
+ return err;
542
+ }
484
543
  var AsyncSemaphore = class {
485
- constructor(concurrency) {
544
+ constructor(concurrency, options) {
486
545
  this.concurrency = concurrency;
487
546
  if (concurrency < 1) {
488
547
  throw new Error("AsyncSemaphore concurrency must be >= 1");
489
548
  }
549
+ if (typeof options === "number") {
550
+ this.maxQueue = options;
551
+ } else if (options && typeof options === "object") {
552
+ this.maxQueue = options.maxQueue;
553
+ }
490
554
  }
491
555
  concurrency;
492
556
  activeCount = 0;
557
+ maxQueue;
493
558
  queue = [];
494
- async run(task) {
495
- await this.acquire();
559
+ get active() {
560
+ return this.activeCount;
561
+ }
562
+ get pending() {
563
+ return this.queue.length;
564
+ }
565
+ async run(task, options = {}) {
566
+ const { signal, timeoutMs, label = "Semaphore task" } = options;
567
+ await this.acquire(signal);
568
+ let rawTaskPromise;
496
569
  try {
497
- return await task();
498
- } finally {
570
+ rawTaskPromise = Promise.resolve().then(task);
571
+ } catch (syncError) {
499
572
  this.release();
573
+ throw syncError;
500
574
  }
575
+ let released = false;
576
+ const releaseOnce = () => {
577
+ if (!released) {
578
+ released = true;
579
+ this.release();
580
+ }
581
+ };
582
+ void rawTaskPromise.then(
583
+ () => releaseOnce(),
584
+ (err) => {
585
+ const pending = err && typeof err === "object" && "pendingTask" in err ? err.pendingTask : void 0;
586
+ if (pending && typeof pending.then === "function") {
587
+ void pending.then(releaseOnce, releaseOnce);
588
+ } else {
589
+ releaseOnce();
590
+ }
591
+ }
592
+ );
593
+ let callerPromise = rawTaskPromise;
594
+ if (timeoutMs !== void 0 && timeoutMs > 0) {
595
+ callerPromise = withTimeout(callerPromise, timeoutMs, label);
596
+ }
597
+ if (signal) {
598
+ callerPromise = raceWithAbort(callerPromise, signal);
599
+ }
600
+ return callerPromise;
501
601
  }
502
- acquire() {
602
+ acquire(signal) {
603
+ if (signal?.aborted) {
604
+ return Promise.reject(createAbortError(signal));
605
+ }
503
606
  if (this.activeCount < this.concurrency) {
504
607
  this.activeCount += 1;
505
608
  return Promise.resolve();
506
609
  }
507
- return new Promise((resolve) => {
508
- this.queue.push(() => {
509
- this.activeCount += 1;
510
- resolve();
511
- });
610
+ if (this.maxQueue !== void 0 && this.queue.length >= this.maxQueue) {
611
+ return Promise.reject(new SemaphoreQueueFullError());
612
+ }
613
+ return new Promise((resolve, reject) => {
614
+ const onAbort = () => {
615
+ const idx = this.queue.indexOf(entry);
616
+ if (idx !== -1) {
617
+ this.queue.splice(idx, 1);
618
+ }
619
+ entry.cancel(createAbortError(signal));
620
+ };
621
+ const entry = {
622
+ grant: () => {
623
+ signal?.removeEventListener("abort", onAbort);
624
+ this.activeCount += 1;
625
+ resolve();
626
+ },
627
+ cancel: (reason) => {
628
+ signal?.removeEventListener("abort", onAbort);
629
+ reject(reason);
630
+ }
631
+ };
632
+ signal?.addEventListener("abort", onAbort, { once: true });
633
+ this.queue.push(entry);
512
634
  });
513
635
  }
514
636
  release() {
515
637
  this.activeCount = Math.max(0, this.activeCount - 1);
516
638
  const next = this.queue.shift();
517
- next?.();
639
+ next?.grant();
518
640
  }
519
641
  };
642
+ function raceWithAbort(task, signal) {
643
+ if (signal.aborted) {
644
+ const err = createAbortError(signal);
645
+ err.pendingTask = task.then(
646
+ () => {
647
+ },
648
+ () => {
649
+ }
650
+ );
651
+ return Promise.reject(err);
652
+ }
653
+ return new Promise((resolve, reject) => {
654
+ const onAbort = () => {
655
+ const err = createAbortError(signal);
656
+ err.pendingTask = task.then(
657
+ () => {
658
+ },
659
+ () => {
660
+ }
661
+ );
662
+ reject(err);
663
+ };
664
+ signal.addEventListener("abort", onAbort, { once: true });
665
+ task.then(
666
+ (val) => {
667
+ signal.removeEventListener("abort", onAbort);
668
+ resolve(val);
669
+ },
670
+ (err) => {
671
+ signal.removeEventListener("abort", onAbort);
672
+ reject(err);
673
+ }
674
+ );
675
+ });
676
+ }
677
+ async function withTimeout(task, timeoutMs, label) {
678
+ let timeoutHandle = null;
679
+ const settledTask = task.then(
680
+ () => {
681
+ },
682
+ () => {
683
+ }
684
+ );
685
+ try {
686
+ return await Promise.race([
687
+ task,
688
+ new Promise((_, reject) => {
689
+ timeoutHandle = setTimeout(() => {
690
+ const err = new Error(
691
+ `${label} timed out after ${timeoutMs}ms`
692
+ );
693
+ err.pendingTask = settledTask;
694
+ reject(err);
695
+ }, timeoutMs);
696
+ })
697
+ ]);
698
+ } finally {
699
+ if (timeoutHandle) {
700
+ clearTimeout(timeoutHandle);
701
+ }
702
+ }
703
+ }
520
704
 
521
705
  // src/cli/scanner.ts
522
706
  import fs from "fs/promises";
@@ -588,22 +772,27 @@ async function loadExistingHashes(manifestPath) {
588
772
  function canReuseCachedEntry(entry, file, includeMedium) {
589
773
  if (!entry || typeof entry !== "object") return false;
590
774
  if (!entry.hash || !entry.color || !entry.variants?.small) return false;
591
- if (entry.metadata) {
592
- if (entry.metadata.size && entry.metadata.size !== String(file.size)) {
593
- return false;
594
- }
595
- if (entry.metadata.mtimeMs && entry.metadata.mtimeMs !== String(Math.round(file.mtimeMs))) {
596
- return false;
597
- }
598
- if (includeMedium && !entry.variants.medium) return false;
775
+ if (!entry.metadata || !entry.metadata.size || !entry.metadata.mtimeMs) {
776
+ return false;
777
+ }
778
+ if (entry.metadata.size !== String(file.size)) {
779
+ return false;
599
780
  }
781
+ if (entry.metadata.mtimeMs !== String(Math.round(file.mtimeMs))) {
782
+ return false;
783
+ }
784
+ if (includeMedium && !entry.variants.medium) return false;
600
785
  return true;
601
786
  }
602
787
  async function resolveImageDimensions(buffer, isSvg) {
603
- const metadata = await sharp4(buffer).metadata();
788
+ const sharp = await loadSharp();
789
+ const metadata = await sharp(buffer).metadata();
604
790
  let { width, height } = metadata;
605
791
  if ((!width || !height) && isSvg) {
606
- const match = buffer.toString("utf8").match(/viewBox\s*=\s*["']\s*([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\s*["']/i);
792
+ const svgText = buffer.toString("utf8");
793
+ const match = svgText.match(
794
+ /viewBox\s*=\s*["']\s*([\d.-]+)[\s,]+([\d.-]+)[\s,]+([\d.-]+)[\s,]+([\d.-]+)\s*["']/i
795
+ );
607
796
  if (match) {
608
797
  width = Math.round(Number.parseFloat(match[3]));
609
798
  height = Math.round(Number.parseFloat(match[4]));
@@ -628,8 +817,8 @@ async function generateBlurManifest(imagesDir, manifestFile, options = {}) {
628
817
  await Promise.all(
629
818
  files.map(
630
819
  (file) => semaphore.run(async () => {
820
+ const prior = force ? void 0 : existingHashes[file.relativeKey];
631
821
  try {
632
- const prior = force ? void 0 : existingHashes[file.relativeKey];
633
822
  if (canReuseCachedEntry(prior, file, includeMedium)) {
634
823
  generatedEntries.set(file.relativeKey, prior);
635
824
  cached += 1;
@@ -664,8 +853,16 @@ async function generateBlurManifest(imagesDir, manifestFile, options = {}) {
664
853
  }
665
854
  });
666
855
  processed += 1;
667
- } catch {
856
+ } catch (err) {
668
857
  failed += 1;
858
+ const existing = existingHashes[file.relativeKey];
859
+ if (existing) {
860
+ generatedEntries.set(file.relativeKey, existing);
861
+ }
862
+ const reason = err instanceof Error ? err.message : String(err);
863
+ console.warn(
864
+ `Warning: Failed to generate blur manifest entry for "${file.relativeKey}": ${reason}`
865
+ );
669
866
  } finally {
670
867
  done += 1;
671
868
  options.onProgress?.(done, total, file.relativeKey);
@@ -843,6 +1040,7 @@ async function generateHealthReport(imagesDir, manifestFile) {
843
1040
  }
844
1041
 
845
1042
  // src/cli/commands.ts
1043
+ var CLI_VERSION = "2.0.0".length > 0 ? "2.0.0" : "2.0.0";
846
1044
  var DEFAULT_IMAGES_DIR = "public/images";
847
1045
  var DEFAULT_MANIFEST_FILE = "src/blur-hashes.json";
848
1046
  function findManifestEntry(hashes, imageKey) {
@@ -860,7 +1058,7 @@ function findManifestEntry(hashes, imageKey) {
860
1058
  }
861
1059
  function createCliProgram() {
862
1060
  const program = new Command();
863
- program.name("img-upload-kit").description("LQIP, BlurHash, and dominant color manifest CLI tooling").version("1.1.1");
1061
+ program.name("img-upload-kit").description("LQIP, BlurHash, and dominant color manifest CLI tooling").version(CLI_VERSION);
864
1062
  program.command("generate").description("Scan an image directory and generate a blur-hashes JSON manifest").option("-d, --dir <dir>", "Source images directory", DEFAULT_IMAGES_DIR).option("-o, --out <file>", "Output manifest JSON path", DEFAULT_MANIFEST_FILE).option("-f, --force", "Force regenerate all entries ignoring cache", false).option("-c, --concurrency <n>", "Max parallel processing workers", "4").option("--no-medium", "Skip generating medium (128px) LQIP variants").action(
865
1063
  async (opts) => {
866
1064
  const concurrency = Math.max(1, Number.parseInt(opts.concurrency, 10) || 4);
@@ -884,7 +1082,7 @@ function createCliProgram() {
884
1082
  }
885
1083
  console.log(`Manifest is valid (${result.totalEntries} entries).`);
886
1084
  });
887
- program.command("sync").description("Check whether disk images and manifest entries are in sync").option("-d, --dir <dir>", "Source images directory", DEFAULT_IMAGES_DIR).option("-f, --file <file>", "Manifest JSON path", DEFAULT_MANIFEST_FILE).option("--fix", "Generate missing blur manifest entries automatically", false).action(async (opts) => {
1085
+ program.command("sync").description("Check whether disk images and manifest entries are in sync").option("-d, --dir <dir>", "Source images directory", DEFAULT_IMAGES_DIR).option("-f, --file <file>", "Manifest JSON path", DEFAULT_MANIFEST_FILE).option("--fix", "Generate missing and remove orphaned blur manifest entries automatically", false).action(async (opts) => {
888
1086
  const sync = await autoSyncCheck(opts.dir, opts.file);
889
1087
  if (sync.inSync) {
890
1088
  console.log(`In sync (${sync.totalEntries} entries).`);
@@ -893,12 +1091,21 @@ function createCliProgram() {
893
1091
  console.log(
894
1092
  `Out of sync: ${sync.missingKeys.length} missing, ${sync.orphanedKeys.length} orphaned.`
895
1093
  );
896
- if (opts.fix && sync.missingKeys.length > 0) {
897
- const result = await generateBlurManifest(opts.dir, opts.file);
898
- console.log(
899
- `Synced manifest (processed: ${result.processed}, cached: ${result.cached}).`
900
- );
901
- return;
1094
+ if (opts.fix) {
1095
+ if (sync.missingKeys.length > 0) {
1096
+ const result = await generateBlurManifest(opts.dir, opts.file);
1097
+ console.log(
1098
+ `Synced manifest (processed: ${result.processed}, cached: ${result.cached}, removed orphans: ${sync.orphanedKeys.length}).`
1099
+ );
1100
+ return;
1101
+ }
1102
+ if (sync.orphanedKeys.length > 0) {
1103
+ const cleaned = await cleanupOrphanedHashes(opts.dir, opts.file);
1104
+ console.log(
1105
+ `Synced manifest (removed ${cleaned.removedCount} orphaned entries).`
1106
+ );
1107
+ return;
1108
+ }
902
1109
  }
903
1110
  if (!opts.fix) process.exitCode = 1;
904
1111
  });
@@ -971,17 +1178,6 @@ async function runCli(argv) {
971
1178
  const isUserArgs = argv.length === 0 || CLI_SUBCOMMANDS.has(argv[0]) || argv[0].startsWith("-");
972
1179
  await program.parseAsync(argv, isUserArgs ? { from: "user" } : void 0);
973
1180
  }
974
- function isDirectCliExecution(entryArg) {
975
- if (!entryArg) return false;
976
- const normalized = entryArg.replace(/\\/g, "/");
977
- return normalized === "cli.js" || normalized === "cli.cjs" || normalized.endsWith("/cli.js") || normalized.endsWith("/cli.cjs") || normalized.endsWith("/cli/index.ts") || normalized.endsWith("/bin/img-upload-kit") || normalized.endsWith("/.bin/img-upload-kit") || normalized.endsWith("/bin/image-upload-kit") || normalized.endsWith("/.bin/image-upload-kit") || normalized.endsWith("/bin/blur-cli") || normalized.endsWith("/.bin/blur-cli");
978
- }
979
- if (typeof process !== "undefined" && isDirectCliExecution(process.argv?.[1])) {
980
- void runCli().catch((err) => {
981
- console.error(err instanceof Error ? err.message : String(err));
982
- process.exitCode = 1;
983
- });
984
- }
985
1181
  export {
986
1182
  IGNORED_DIRECTORIES,
987
1183
  SUPPORTED_IMAGE_EXTENSIONS,