img-upload-kit 1.1.1 → 2.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/CHANGELOG.md +89 -74
- package/MIGRATION.md +192 -0
- package/README.md +243 -259
- package/dist/bin.js +1186 -0
- package/dist/bin.js.map +1 -0
- package/dist/blur.cjs +40 -412
- package/dist/blur.cjs.map +1 -1
- package/dist/blur.d.cts +5 -5
- package/dist/blur.d.ts +5 -5
- package/dist/blur.js +39 -393
- package/dist/blur.js.map +1 -1
- package/dist/blurDataUtils-DveBRtqE.d.cts +43 -0
- package/dist/blurDataUtils-yx9Q6HlU.d.ts +43 -0
- package/dist/blurService-D5TpMNji.d.ts +83 -0
- package/dist/blurService-DacwylJx.d.cts +83 -0
- package/dist/chunk-25GYKQ6J.js +158 -0
- package/dist/chunk-25GYKQ6J.js.map +1 -0
- package/dist/chunk-34L4TEE3.cjs +226 -0
- package/dist/chunk-34L4TEE3.cjs.map +1 -0
- package/dist/chunk-DFCSN3X7.cjs +158 -0
- package/dist/chunk-DFCSN3X7.cjs.map +1 -0
- package/dist/chunk-EWVIL57J.js +294 -0
- package/dist/chunk-EWVIL57J.js.map +1 -0
- package/dist/chunk-FP5YIWG5.cjs +135 -0
- package/dist/chunk-FP5YIWG5.cjs.map +1 -0
- package/dist/chunk-HILELQGS.js +226 -0
- package/dist/chunk-HILELQGS.js.map +1 -0
- package/dist/chunk-IDLLCVGB.js +2370 -0
- package/dist/chunk-IDLLCVGB.js.map +1 -0
- package/dist/chunk-J6A44Y3W.js +514 -0
- package/dist/chunk-J6A44Y3W.js.map +1 -0
- package/dist/chunk-LOSVFO56.cjs +145 -0
- package/dist/chunk-LOSVFO56.cjs.map +1 -0
- package/dist/chunk-MBQJ6NVJ.cjs +294 -0
- package/dist/chunk-MBQJ6NVJ.cjs.map +1 -0
- package/dist/chunk-MHGQEBPW.js +145 -0
- package/dist/chunk-MHGQEBPW.js.map +1 -0
- package/dist/chunk-QHU7FUO4.cjs +37 -0
- package/dist/chunk-QHU7FUO4.cjs.map +1 -0
- package/dist/chunk-S2IFAWJK.js +37 -0
- package/dist/chunk-S2IFAWJK.js.map +1 -0
- package/dist/chunk-TA5Y64JM.cjs +2370 -0
- package/dist/chunk-TA5Y64JM.cjs.map +1 -0
- package/dist/chunk-VYVZVUNI.js +135 -0
- package/dist/chunk-VYVZVUNI.js.map +1 -0
- package/dist/chunk-XKQMCY5Z.cjs +514 -0
- package/dist/chunk-XKQMCY5Z.cjs.map +1 -0
- package/dist/cli.cjs +273 -76
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +269 -73
- package/dist/cli.js.map +1 -1
- package/dist/headless.cjs +8 -1880
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.d.cts +3 -1
- package/dist/headless.d.ts +3 -1
- package/dist/headless.js +25 -1886
- package/dist/headless.js.map +1 -1
- package/dist/index-BIdnqtW8.d.ts +234 -0
- package/dist/index-Brk_uHs6.d.cts +234 -0
- package/dist/{index-FVXXKf2v.d.ts → index-CPMlaQsC.d.ts} +76 -153
- package/dist/{index-FVXXKf2v.d.cts → index-DHINe-rA.d.cts} +76 -153
- package/dist/index.cjs +64 -532
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.js +63 -504
- package/dist/index.js.map +1 -1
- package/dist/{memoryAdapter-CBu7_3Pp.d.ts → memoryAdapter-Ck0ccZbF.d.cts} +3 -2
- package/dist/{memoryAdapter-BJjyLR-1.d.cts → memoryAdapter-DxEKaG0b.d.ts} +3 -2
- package/dist/placeholders-C4ftwaII.d.ts +5 -0
- package/dist/placeholders-x_3rt65M.d.cts +5 -0
- package/dist/react.cjs +1952 -3257
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +188 -50
- package/dist/react.d.ts +188 -50
- package/dist/react.js +1763 -3013
- package/dist/react.js.map +1 -1
- package/dist/server-handler.cjs +14 -0
- package/dist/server-handler.cjs.map +1 -0
- package/dist/server-handler.d.cts +3 -0
- package/dist/server-handler.d.ts +3 -0
- package/dist/server-handler.js +14 -0
- package/dist/server-handler.js.map +1 -0
- package/dist/server.cjs +1096 -532
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +77 -154
- package/dist/server.d.ts +77 -154
- package/dist/server.js +1050 -448
- package/dist/server.js.map +1 -1
- package/dist/storage.cjs +239 -232
- package/dist/storage.cjs.map +1 -1
- package/dist/storage.d.cts +21 -5
- package/dist/storage.d.ts +21 -5
- package/dist/storage.js +227 -207
- package/dist/storage.js.map +1 -1
- package/dist/styles/animated-image.css +112 -0
- package/dist/styles/dropzone-and-tiles.css +247 -0
- package/dist/styles/modals-and-dock.css +248 -0
- package/dist/styles/primitives.css +197 -0
- package/dist/styles/quality-and-inputs.css +184 -0
- package/dist/styles/tokens.css +123 -0
- package/dist/styles.css +235 -220
- package/dist/types-BrP-o9nd.d.cts +85 -0
- package/dist/types-sZ3KoFQY.d.ts +85 -0
- package/dist/types-yShGNQnW.d.cts +61 -0
- package/dist/types-yShGNQnW.d.ts +61 -0
- package/dist/{types-CiLY0DwB.d.cts → upload-C5Trd6Dh.d.cts} +27 -53
- package/dist/{types-CiLY0DwB.d.ts → upload-C5Trd6Dh.d.ts} +27 -53
- package/package.json +92 -32
- package/dist/blurService-DqsnfPTd.d.cts +0 -172
- 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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
456
|
-
const
|
|
457
|
-
const
|
|
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
|
-
|
|
462
|
-
|
|
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 =
|
|
503
|
+
processBuffer = Buffer.from(normalizedSvg, "utf8");
|
|
478
504
|
}
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
|
489
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
540
|
-
|
|
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
|
-
|
|
543
|
-
}
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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,
|