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.
- package/CHANGELOG.md +56 -41
- package/MIGRATION.md +192 -0
- package/README.md +138 -154
- 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-2OVNXHTN.cjs +514 -0
- package/dist/chunk-2OVNXHTN.cjs.map +1 -0
- package/dist/chunk-3ICG2SLL.cjs +135 -0
- package/dist/chunk-3ICG2SLL.cjs.map +1 -0
- package/dist/chunk-4ASFG3KB.js +514 -0
- package/dist/chunk-4ASFG3KB.js.map +1 -0
- package/dist/chunk-AI3JJLSA.js +2370 -0
- package/dist/chunk-AI3JJLSA.js.map +1 -0
- package/dist/chunk-CV23O4YU.js +145 -0
- package/dist/chunk-CV23O4YU.js.map +1 -0
- package/dist/chunk-G7LLSKBV.js +158 -0
- package/dist/chunk-G7LLSKBV.js.map +1 -0
- package/dist/chunk-IB3TEDLR.js +294 -0
- package/dist/chunk-IB3TEDLR.js.map +1 -0
- package/dist/chunk-KJVYWKIG.cjs +158 -0
- package/dist/chunk-KJVYWKIG.cjs.map +1 -0
- package/dist/chunk-NEDBIIE3.cjs +226 -0
- package/dist/chunk-NEDBIIE3.cjs.map +1 -0
- package/dist/chunk-QHU7FUO4.cjs +37 -0
- package/dist/chunk-QHU7FUO4.cjs.map +1 -0
- package/dist/chunk-QQHZJKNA.js +135 -0
- package/dist/chunk-QQHZJKNA.js.map +1 -0
- package/dist/chunk-S2IFAWJK.js +37 -0
- package/dist/chunk-S2IFAWJK.js.map +1 -0
- package/dist/chunk-TT5C7K3Q.js +226 -0
- package/dist/chunk-TT5C7K3Q.js.map +1 -0
- package/dist/chunk-U2TWTEBY.cjs +2370 -0
- package/dist/chunk-U2TWTEBY.cjs.map +1 -0
- package/dist/chunk-WISG74VL.cjs +294 -0
- package/dist/chunk-WISG74VL.cjs.map +1 -0
- package/dist/chunk-YRXXQUDE.cjs +145 -0
- package/dist/chunk-YRXXQUDE.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 +39 -24
- 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.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
|
|
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
|
|
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
|
-
|
|
411
|
-
const
|
|
412
|
-
const
|
|
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
|
-
|
|
417
|
-
|
|
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 =
|
|
457
|
+
processBuffer = Buffer.from(normalizedSvg, "utf8");
|
|
433
458
|
}
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
|
444
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
495
|
-
|
|
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
|
-
|
|
498
|
-
}
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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,
|