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.cjs CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  "use strict";
3
2
  var __create = Object.create;
4
3
  var __defProp = Object.defineProperty;
@@ -78,7 +77,8 @@ function normalizeImagePath(imageSrc, options = {}) {
78
77
  for (const pattern of patterns) {
79
78
  const match = cleanUrlNoQuery.match(pattern);
80
79
  if (match) {
81
- const pathPart = match[match.length - 1];
80
+ const groups = match.slice(1).filter(Boolean);
81
+ const pathPart = groups.join("/");
82
82
  normalizedPath = "/" + pathPart.replace(/^\/+/, "");
83
83
  break;
84
84
  }
@@ -124,7 +124,6 @@ function normalizeImagePath(imageSrc, options = {}) {
124
124
  // src/cli/generator.ts
125
125
  var import_promises2 = __toESM(require("fs/promises"), 1);
126
126
  var import_node_path2 = __toESM(require("path"), 1);
127
- var import_sharp4 = __toESM(require("sharp"), 1);
128
127
 
129
128
  // src/server/optimization/types.ts
130
129
  var BLUR_PLACEHOLDER_WIDTH = 128;
@@ -135,13 +134,28 @@ var EDGE_COLOR_FALLBACK = "#6B7280";
135
134
  var EDGE_WHITE_FALLBACK = "#F9FAFB";
136
135
  var WHITE_DOMINANCE_RATIO = 1.35;
137
136
 
137
+ // src/server/optimization/loadSharp.ts
138
+ var SHARP_MISSING_MESSAGE = "img-upload-kit/server requires `sharp` \u2014 run `npm i sharp`";
139
+ var sharpPromise = null;
140
+ function unwrapDefault(mod) {
141
+ const record = mod;
142
+ return record && record.default !== void 0 ? record.default : mod;
143
+ }
144
+ function loadSharp() {
145
+ if (!sharpPromise) {
146
+ sharpPromise = import("sharp").then(
147
+ (mod) => unwrapDefault(mod),
148
+ (error) => {
149
+ sharpPromise = null;
150
+ throw new Error(SHARP_MISSING_MESSAGE, { cause: error });
151
+ }
152
+ );
153
+ }
154
+ return sharpPromise;
155
+ }
156
+
138
157
  // src/server/optimization/blurGenerator.ts
139
158
  var import_blurhash = require("blurhash");
140
- var import_lqip_modern = __toESM(require("lqip-modern"), 1);
141
- var import_sharp3 = __toESM(require("sharp"), 1);
142
-
143
- // src/server/optimization/edgeColorExtractor.ts
144
- var import_sharp2 = __toESM(require("sharp"), 1);
145
159
 
146
160
  // src/utils/color.ts
147
161
  function hexToRgb(hex) {
@@ -165,14 +179,14 @@ function rgbToHex(r, g, b) {
165
179
  }
166
180
 
167
181
  // src/server/optimization/colorHistogram.ts
168
- var import_sharp = __toESM(require("sharp"), 1);
169
182
  async function extractEnhancedColorFromBuffer(buffer, mode = "balanced") {
183
+ const sharp = await loadSharp();
170
184
  try {
171
- const metadata = await (0, import_sharp.default)(buffer).metadata();
185
+ const metadata = await sharp(buffer).metadata();
172
186
  const maxSize = 400;
173
187
  const needsResize = metadata.width && metadata.height && (metadata.width > maxSize || metadata.height > maxSize);
174
188
  if (needsResize) {
175
- const resized = await (0, import_sharp.default)(buffer).resize(maxSize, maxSize, {
189
+ const resized = await sharp(buffer).resize(maxSize, maxSize, {
176
190
  fit: "inside",
177
191
  kernel: "lanczos3",
178
192
  withoutEnlargement: true
@@ -184,7 +198,7 @@ async function extractEnhancedColorFromBuffer(buffer, mode = "balanced") {
184
198
  mode
185
199
  );
186
200
  }
187
- const raw = await (0, import_sharp.default)(buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
201
+ const raw = await sharp(buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
188
202
  return extractDominantColorHistogramAdvanced(
189
203
  raw.data,
190
204
  raw.info.width,
@@ -192,7 +206,7 @@ async function extractEnhancedColorFromBuffer(buffer, mode = "balanced") {
192
206
  mode
193
207
  );
194
208
  } catch {
195
- const stats = await (0, import_sharp.default)(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
209
+ const stats = await sharp(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
196
210
  return extractDominantColorHistogram(stats.data);
197
211
  }
198
212
  }
@@ -311,8 +325,9 @@ function extractDominantColorHistogram(data) {
311
325
 
312
326
  // src/server/optimization/edgeColorExtractor.ts
313
327
  async function extractEdgeColorsFromBuffer(buffer) {
328
+ const sharp = await loadSharp();
314
329
  try {
315
- const sample = await (0, import_sharp2.default)(buffer).resize(400, 400, {
330
+ const sample = await sharp(buffer).resize(400, 400, {
316
331
  fit: "inside",
317
332
  kernel: "lanczos3",
318
333
  withoutEnlargement: true
@@ -452,46 +467,73 @@ function blendEdgeColors(edgeColors) {
452
467
  }
453
468
 
454
469
  // src/server/optimization/blurGenerator.ts
455
- async function encodeBlurHashString(buffer, componentX = 4, componentY = 3) {
456
- const { data, info } = await (0, import_sharp3.default)(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
457
- const clampedX = Math.max(1, Math.min(Math.round(componentX), info.width, 9));
458
- const clampedY = Math.max(1, Math.min(Math.round(componentY), info.height, 9));
470
+ function encodeRawBlurHash(data, width, height, componentX = 4, componentY = 3) {
471
+ const clampedX = Math.max(1, Math.min(Math.round(componentX), width, 9));
472
+ const clampedY = Math.max(1, Math.min(Math.round(componentY), height, 9));
459
473
  return (0, import_blurhash.encode)(
460
474
  new Uint8ClampedArray(data),
461
- info.width,
462
- info.height,
475
+ width,
476
+ height,
463
477
  clampedX,
464
478
  clampedY
465
479
  );
466
480
  }
481
+ async function encodeBlurHashString(buffer, componentX = 4, componentY = 3) {
482
+ const sharp = await loadSharp();
483
+ const { data, info } = await sharp(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
484
+ return encodeRawBlurHash(
485
+ data,
486
+ info.width,
487
+ info.height,
488
+ componentX,
489
+ componentY
490
+ );
491
+ }
467
492
  async function generateBlurPlaceholder(buffer, options = {}) {
468
493
  const targetWidth = options.width ?? BLUR_PLACEHOLDER_WIDTH;
469
494
  const fallbackWidth = options.fallbackWidth ?? BLUR_PLACEHOLDER_FALLBACK_WIDTH;
470
495
  const quality = options.quality ?? BLUR_PLACEHOLDER_QUALITY;
471
496
  const maxChars = options.maxDataUrlChars ?? BLUR_PLACEHOLDER_MAX_CHARS;
497
+ const sharp = await loadSharp();
472
498
  try {
473
499
  let processBuffer = buffer;
474
500
  const isSvg = /<svg[\s>]/i.test(buffer.toString("utf8", 0, 1024));
475
501
  if (isSvg) {
476
502
  const normalizedSvg = buffer.toString("utf8").replace(/<svg\b/gi, "<svg").replace(/<\/svg\s*>/gi, "</svg>");
477
- processBuffer = await (0, import_sharp3.default)(Buffer.from(normalizedSvg, "utf8")).resize(targetWidth, targetWidth, { fit: "inside" }).png().toBuffer();
503
+ processBuffer = Buffer.from(normalizedSvg, "utf8");
478
504
  }
479
- const buildPlaceholder = (width, q) => (0, import_lqip_modern.default)(processBuffer, {
480
- outputFormat: "webp",
481
- resize: width,
482
- outputOptions: { quality: q, effort: 6, smartSubsample: true }
483
- });
484
- let result = await buildPlaceholder(targetWidth, quality);
485
- if (result.metadata.dataURIBase64.length > maxChars) {
486
- result = await buildPlaceholder(fallbackWidth, quality);
505
+ const baseSize = Math.max(targetWidth, 128);
506
+ const rawSample = await sharp(processBuffer).rotate().resize(baseSize, baseSize, { fit: "inside", withoutEnlargement: true }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
507
+ const rawInput = {
508
+ raw: {
509
+ width: rawSample.info.width,
510
+ height: rawSample.info.height,
511
+ channels: 4
512
+ }
513
+ };
514
+ const buildPlaceholderDataUrl = async (width) => {
515
+ const webpBuffer = await sharp(rawSample.data, rawInput).resize(width, width, { fit: "inside", withoutEnlargement: true }).webp({ quality, effort: 6, smartSubsample: true }).toBuffer();
516
+ return `data:image/webp;base64,${webpBuffer.toString("base64")}`;
517
+ };
518
+ let dataURL = await buildPlaceholderDataUrl(targetWidth);
519
+ if (dataURL.length > maxChars) {
520
+ dataURL = await buildPlaceholderDataUrl(fallbackWidth);
487
521
  }
488
- const dataURL = result.metadata.dataURIBase64;
489
- const edgeColors = await extractEdgeColorsFromBuffer(processBuffer);
522
+ const edgeColors = computeEdgeColorSet(
523
+ rawSample.data,
524
+ rawSample.info.width,
525
+ rawSample.info.height
526
+ );
490
527
  const color = blendEdgeColors(edgeColors);
491
528
  let blurHash;
492
529
  if (options.includeBlurHash) {
493
530
  try {
494
- blurHash = await encodeBlurHashString(processBuffer);
531
+ 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 });
532
+ blurHash = encodeRawBlurHash(
533
+ hashSample.data,
534
+ hashSample.info.width,
535
+ hashSample.info.height
536
+ );
495
537
  } catch {
496
538
  blurHash = void 0;
497
539
  }
@@ -506,7 +548,8 @@ async function generateBlurPlaceholder(buffer, options = {}) {
506
548
  }
507
549
  }
508
550
  async function generateFallbackBlur(buffer, includeBlurHash) {
509
- const tinyBuffer = await (0, import_sharp3.default)(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).blur(1).webp({
551
+ const sharp = await loadSharp();
552
+ const tinyBuffer = await sharp(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).blur(1).webp({
510
553
  quality: 75,
511
554
  effort: 6,
512
555
  smartSubsample: false
@@ -526,42 +569,184 @@ async function generateFallbackBlur(buffer, includeBlurHash) {
526
569
  }
527
570
 
528
571
  // src/server/semaphore.ts
572
+ var SemaphoreQueueFullError = class extends Error {
573
+ status = 503;
574
+ constructor(message = "Server upload queue is full. Please try again later.") {
575
+ super(message);
576
+ this.name = "SemaphoreQueueFullError";
577
+ }
578
+ };
579
+ function createAbortError(signal) {
580
+ if (signal?.reason instanceof Error) {
581
+ return signal.reason;
582
+ }
583
+ const err = new Error(
584
+ typeof signal?.reason === "string" ? signal.reason : "Operation aborted"
585
+ );
586
+ err.name = "AbortError";
587
+ return err;
588
+ }
529
589
  var AsyncSemaphore = class {
530
- constructor(concurrency) {
590
+ constructor(concurrency, options) {
531
591
  this.concurrency = concurrency;
532
592
  if (concurrency < 1) {
533
593
  throw new Error("AsyncSemaphore concurrency must be >= 1");
534
594
  }
595
+ if (typeof options === "number") {
596
+ this.maxQueue = options;
597
+ } else if (options && typeof options === "object") {
598
+ this.maxQueue = options.maxQueue;
599
+ }
535
600
  }
536
601
  concurrency;
537
602
  activeCount = 0;
603
+ maxQueue;
538
604
  queue = [];
539
- async run(task) {
540
- await this.acquire();
605
+ get active() {
606
+ return this.activeCount;
607
+ }
608
+ get pending() {
609
+ return this.queue.length;
610
+ }
611
+ async run(task, options = {}) {
612
+ const { signal, timeoutMs, label = "Semaphore task" } = options;
613
+ await this.acquire(signal);
614
+ let rawTaskPromise;
541
615
  try {
542
- return await task();
543
- } finally {
616
+ rawTaskPromise = Promise.resolve().then(task);
617
+ } catch (syncError) {
544
618
  this.release();
619
+ throw syncError;
545
620
  }
621
+ let released = false;
622
+ const releaseOnce = () => {
623
+ if (!released) {
624
+ released = true;
625
+ this.release();
626
+ }
627
+ };
628
+ void rawTaskPromise.then(
629
+ () => releaseOnce(),
630
+ (err) => {
631
+ const pending = err && typeof err === "object" && "pendingTask" in err ? err.pendingTask : void 0;
632
+ if (pending && typeof pending.then === "function") {
633
+ void pending.then(releaseOnce, releaseOnce);
634
+ } else {
635
+ releaseOnce();
636
+ }
637
+ }
638
+ );
639
+ let callerPromise = rawTaskPromise;
640
+ if (timeoutMs !== void 0 && timeoutMs > 0) {
641
+ callerPromise = withTimeout(callerPromise, timeoutMs, label);
642
+ }
643
+ if (signal) {
644
+ callerPromise = raceWithAbort(callerPromise, signal);
645
+ }
646
+ return callerPromise;
546
647
  }
547
- acquire() {
648
+ acquire(signal) {
649
+ if (signal?.aborted) {
650
+ return Promise.reject(createAbortError(signal));
651
+ }
548
652
  if (this.activeCount < this.concurrency) {
549
653
  this.activeCount += 1;
550
654
  return Promise.resolve();
551
655
  }
552
- return new Promise((resolve) => {
553
- this.queue.push(() => {
554
- this.activeCount += 1;
555
- resolve();
556
- });
656
+ if (this.maxQueue !== void 0 && this.queue.length >= this.maxQueue) {
657
+ return Promise.reject(new SemaphoreQueueFullError());
658
+ }
659
+ return new Promise((resolve, reject) => {
660
+ const onAbort = () => {
661
+ const idx = this.queue.indexOf(entry);
662
+ if (idx !== -1) {
663
+ this.queue.splice(idx, 1);
664
+ }
665
+ entry.cancel(createAbortError(signal));
666
+ };
667
+ const entry = {
668
+ grant: () => {
669
+ signal?.removeEventListener("abort", onAbort);
670
+ this.activeCount += 1;
671
+ resolve();
672
+ },
673
+ cancel: (reason) => {
674
+ signal?.removeEventListener("abort", onAbort);
675
+ reject(reason);
676
+ }
677
+ };
678
+ signal?.addEventListener("abort", onAbort, { once: true });
679
+ this.queue.push(entry);
557
680
  });
558
681
  }
559
682
  release() {
560
683
  this.activeCount = Math.max(0, this.activeCount - 1);
561
684
  const next = this.queue.shift();
562
- next?.();
685
+ next?.grant();
563
686
  }
564
687
  };
688
+ function raceWithAbort(task, signal) {
689
+ if (signal.aborted) {
690
+ const err = createAbortError(signal);
691
+ err.pendingTask = task.then(
692
+ () => {
693
+ },
694
+ () => {
695
+ }
696
+ );
697
+ return Promise.reject(err);
698
+ }
699
+ return new Promise((resolve, reject) => {
700
+ const onAbort = () => {
701
+ const err = createAbortError(signal);
702
+ err.pendingTask = task.then(
703
+ () => {
704
+ },
705
+ () => {
706
+ }
707
+ );
708
+ reject(err);
709
+ };
710
+ signal.addEventListener("abort", onAbort, { once: true });
711
+ task.then(
712
+ (val) => {
713
+ signal.removeEventListener("abort", onAbort);
714
+ resolve(val);
715
+ },
716
+ (err) => {
717
+ signal.removeEventListener("abort", onAbort);
718
+ reject(err);
719
+ }
720
+ );
721
+ });
722
+ }
723
+ async function withTimeout(task, timeoutMs, label) {
724
+ let timeoutHandle = null;
725
+ const settledTask = task.then(
726
+ () => {
727
+ },
728
+ () => {
729
+ }
730
+ );
731
+ try {
732
+ return await Promise.race([
733
+ task,
734
+ new Promise((_, reject) => {
735
+ timeoutHandle = setTimeout(() => {
736
+ const err = new Error(
737
+ `${label} timed out after ${timeoutMs}ms`
738
+ );
739
+ err.pendingTask = settledTask;
740
+ reject(err);
741
+ }, timeoutMs);
742
+ })
743
+ ]);
744
+ } finally {
745
+ if (timeoutHandle) {
746
+ clearTimeout(timeoutHandle);
747
+ }
748
+ }
749
+ }
565
750
 
566
751
  // src/cli/scanner.ts
567
752
  var import_promises = __toESM(require("fs/promises"), 1);
@@ -633,22 +818,27 @@ async function loadExistingHashes(manifestPath) {
633
818
  function canReuseCachedEntry(entry, file, includeMedium) {
634
819
  if (!entry || typeof entry !== "object") return false;
635
820
  if (!entry.hash || !entry.color || !entry.variants?.small) return false;
636
- if (entry.metadata) {
637
- if (entry.metadata.size && entry.metadata.size !== String(file.size)) {
638
- return false;
639
- }
640
- if (entry.metadata.mtimeMs && entry.metadata.mtimeMs !== String(Math.round(file.mtimeMs))) {
641
- return false;
642
- }
643
- if (includeMedium && !entry.variants.medium) return false;
821
+ if (!entry.metadata || !entry.metadata.size || !entry.metadata.mtimeMs) {
822
+ return false;
823
+ }
824
+ if (entry.metadata.size !== String(file.size)) {
825
+ return false;
644
826
  }
827
+ if (entry.metadata.mtimeMs !== String(Math.round(file.mtimeMs))) {
828
+ return false;
829
+ }
830
+ if (includeMedium && !entry.variants.medium) return false;
645
831
  return true;
646
832
  }
647
833
  async function resolveImageDimensions(buffer, isSvg) {
648
- const metadata = await (0, import_sharp4.default)(buffer).metadata();
834
+ const sharp = await loadSharp();
835
+ const metadata = await sharp(buffer).metadata();
649
836
  let { width, height } = metadata;
650
837
  if ((!width || !height) && isSvg) {
651
- const match = buffer.toString("utf8").match(/viewBox\s*=\s*["']\s*([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\s*["']/i);
838
+ const svgText = buffer.toString("utf8");
839
+ const match = svgText.match(
840
+ /viewBox\s*=\s*["']\s*([\d.-]+)[\s,]+([\d.-]+)[\s,]+([\d.-]+)[\s,]+([\d.-]+)\s*["']/i
841
+ );
652
842
  if (match) {
653
843
  width = Math.round(Number.parseFloat(match[3]));
654
844
  height = Math.round(Number.parseFloat(match[4]));
@@ -673,8 +863,8 @@ async function generateBlurManifest(imagesDir, manifestFile, options = {}) {
673
863
  await Promise.all(
674
864
  files.map(
675
865
  (file) => semaphore.run(async () => {
866
+ const prior = force ? void 0 : existingHashes[file.relativeKey];
676
867
  try {
677
- const prior = force ? void 0 : existingHashes[file.relativeKey];
678
868
  if (canReuseCachedEntry(prior, file, includeMedium)) {
679
869
  generatedEntries.set(file.relativeKey, prior);
680
870
  cached += 1;
@@ -709,8 +899,16 @@ async function generateBlurManifest(imagesDir, manifestFile, options = {}) {
709
899
  }
710
900
  });
711
901
  processed += 1;
712
- } catch {
902
+ } catch (err) {
713
903
  failed += 1;
904
+ const existing = existingHashes[file.relativeKey];
905
+ if (existing) {
906
+ generatedEntries.set(file.relativeKey, existing);
907
+ }
908
+ const reason = err instanceof Error ? err.message : String(err);
909
+ console.warn(
910
+ `Warning: Failed to generate blur manifest entry for "${file.relativeKey}": ${reason}`
911
+ );
714
912
  } finally {
715
913
  done += 1;
716
914
  options.onProgress?.(done, total, file.relativeKey);
@@ -888,6 +1086,7 @@ async function generateHealthReport(imagesDir, manifestFile) {
888
1086
  }
889
1087
 
890
1088
  // src/cli/commands.ts
1089
+ var CLI_VERSION = "2.0.0".length > 0 ? "2.0.0" : "2.0.0";
891
1090
  var DEFAULT_IMAGES_DIR = "public/images";
892
1091
  var DEFAULT_MANIFEST_FILE = "src/blur-hashes.json";
893
1092
  function findManifestEntry(hashes, imageKey) {
@@ -905,7 +1104,7 @@ function findManifestEntry(hashes, imageKey) {
905
1104
  }
906
1105
  function createCliProgram() {
907
1106
  const program = new import_commander.Command();
908
- program.name("img-upload-kit").description("LQIP, BlurHash, and dominant color manifest CLI tooling").version("1.1.1");
1107
+ program.name("img-upload-kit").description("LQIP, BlurHash, and dominant color manifest CLI tooling").version(CLI_VERSION);
909
1108
  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(
910
1109
  async (opts) => {
911
1110
  const concurrency = Math.max(1, Number.parseInt(opts.concurrency, 10) || 4);
@@ -929,7 +1128,7 @@ function createCliProgram() {
929
1128
  }
930
1129
  console.log(`Manifest is valid (${result.totalEntries} entries).`);
931
1130
  });
932
- 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) => {
1131
+ 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) => {
933
1132
  const sync = await autoSyncCheck(opts.dir, opts.file);
934
1133
  if (sync.inSync) {
935
1134
  console.log(`In sync (${sync.totalEntries} entries).`);
@@ -938,12 +1137,21 @@ function createCliProgram() {
938
1137
  console.log(
939
1138
  `Out of sync: ${sync.missingKeys.length} missing, ${sync.orphanedKeys.length} orphaned.`
940
1139
  );
941
- if (opts.fix && sync.missingKeys.length > 0) {
942
- const result = await generateBlurManifest(opts.dir, opts.file);
943
- console.log(
944
- `Synced manifest (processed: ${result.processed}, cached: ${result.cached}).`
945
- );
946
- return;
1140
+ if (opts.fix) {
1141
+ if (sync.missingKeys.length > 0) {
1142
+ const result = await generateBlurManifest(opts.dir, opts.file);
1143
+ console.log(
1144
+ `Synced manifest (processed: ${result.processed}, cached: ${result.cached}, removed orphans: ${sync.orphanedKeys.length}).`
1145
+ );
1146
+ return;
1147
+ }
1148
+ if (sync.orphanedKeys.length > 0) {
1149
+ const cleaned = await cleanupOrphanedHashes(opts.dir, opts.file);
1150
+ console.log(
1151
+ `Synced manifest (removed ${cleaned.removedCount} orphaned entries).`
1152
+ );
1153
+ return;
1154
+ }
947
1155
  }
948
1156
  if (!opts.fix) process.exitCode = 1;
949
1157
  });
@@ -1016,17 +1224,6 @@ async function runCli(argv) {
1016
1224
  const isUserArgs = argv.length === 0 || CLI_SUBCOMMANDS.has(argv[0]) || argv[0].startsWith("-");
1017
1225
  await program.parseAsync(argv, isUserArgs ? { from: "user" } : void 0);
1018
1226
  }
1019
- function isDirectCliExecution(entryArg) {
1020
- if (!entryArg) return false;
1021
- const normalized = entryArg.replace(/\\/g, "/");
1022
- 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");
1023
- }
1024
- if (typeof process !== "undefined" && isDirectCliExecution(process.argv?.[1])) {
1025
- void runCli().catch((err) => {
1026
- console.error(err instanceof Error ? err.message : String(err));
1027
- process.exitCode = 1;
1028
- });
1029
- }
1030
1227
  // Annotate the CommonJS export names for ESM import in node:
1031
1228
  0 && (module.exports = {
1032
1229
  IGNORED_DIRECTORIES,