@minhvuong/pirate-storage-preview-browser 0.1.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/README.md +9 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +109 -0
- package/dist/types.d.ts +36 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +212 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @minhvuong/pirate-storage-preview-browser
|
|
2
|
+
|
|
3
|
+
Optional browser preview generator for `@minhvuong/pirate-storage`.
|
|
4
|
+
|
|
5
|
+
This is the low-memory Web Worker implementation extracted from the Discord demo. It reads JPEG
|
|
6
|
+
or PNG dimensions without decoding the complete source at full resolution, requests a reduced
|
|
7
|
+
decode when supported, and encodes a JPEG below the configured attachment target.
|
|
8
|
+
|
|
9
|
+
It is deliberately separate from core so server-only and non-image consumers do not install it.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { GeneratedPreview, PreviewGenerator } from "@minhvuong/pirate-storage";
|
|
2
|
+
import type { ImagePreviewMetrics, PreviewProgress } from "./types";
|
|
3
|
+
export * from "./types";
|
|
4
|
+
export declare const DISCORD_PREVIEW_LIMIT_BYTES: number;
|
|
5
|
+
export declare const DISCORD_PREVIEW_TARGET_BYTES: number;
|
|
6
|
+
export interface PreparedImagePreview extends GeneratedPreview {
|
|
7
|
+
metrics: ImagePreviewMetrics;
|
|
8
|
+
metadata: {
|
|
9
|
+
metrics: ImagePreviewMetrics;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface BrowserImagePreviewOptions {
|
|
13
|
+
id?: string;
|
|
14
|
+
/** Generate only for files larger than this. Defaults to Discord's recommended 8 MiB part. */
|
|
15
|
+
thresholdBytes?: number;
|
|
16
|
+
targetBytes?: number;
|
|
17
|
+
workerFactory?: () => Worker;
|
|
18
|
+
}
|
|
19
|
+
export declare function createBrowserImagePreview(options?: BrowserImagePreviewOptions): PreviewGenerator;
|
|
20
|
+
export declare function prepareImagePreview(file: Blob, filename: string, options?: {
|
|
21
|
+
targetBytes?: number;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
onProgress?: (progress: PreviewProgress) => void;
|
|
24
|
+
workerFactory?: () => Worker;
|
|
25
|
+
}): Promise<PreparedImagePreview>;
|
|
26
|
+
/** Compatibility name used by the original Discord demo. */
|
|
27
|
+
export declare function prepareDiscordImagePreview(file: File, options?: {
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
onProgress?: (progress: PreviewProgress) => void;
|
|
30
|
+
workerFactory?: () => Worker;
|
|
31
|
+
}): Promise<PreparedImagePreview>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var DISCORD_PREVIEW_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
3
|
+
var DISCORD_PREVIEW_TARGET_BYTES = DISCORD_PREVIEW_LIMIT_BYTES - 128 * 1024;
|
|
4
|
+
function createBrowserImagePreview(options = {}) {
|
|
5
|
+
const thresholdBytes = positiveInteger(options.thresholdBytes ?? DISCORD_PREVIEW_LIMIT_BYTES, "thresholdBytes");
|
|
6
|
+
const targetBytes = positiveInteger(options.targetBytes ?? DISCORD_PREVIEW_TARGET_BYTES, "targetBytes");
|
|
7
|
+
if (targetBytes >= thresholdBytes) {
|
|
8
|
+
throw new Error("targetBytes must be smaller than thresholdBytes");
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
id: options.id ?? "browser-image",
|
|
12
|
+
supports(source) {
|
|
13
|
+
return Boolean(source.blob) && source.size > thresholdBytes && isSupportedImage(source.contentType, source.name);
|
|
14
|
+
},
|
|
15
|
+
async generate(source, context) {
|
|
16
|
+
if (!source.blob || !isSupportedImage(source.contentType, source.name))
|
|
17
|
+
return;
|
|
18
|
+
return prepareImagePreview(source.blob, source.name, {
|
|
19
|
+
targetBytes,
|
|
20
|
+
signal: context.signal,
|
|
21
|
+
onProgress: context.onProgress,
|
|
22
|
+
workerFactory: options.workerFactory
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function prepareImagePreview(file, filename, options = {}) {
|
|
28
|
+
const targetBytes = positiveInteger(options.targetBytes ?? DISCORD_PREVIEW_TARGET_BYTES, "targetBytes");
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
if (options.signal?.aborted) {
|
|
31
|
+
reject(abortError());
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const worker = options.workerFactory?.() ?? new Worker(new URL("./worker.js", import.meta.url), { type: "module" });
|
|
35
|
+
let settled = false;
|
|
36
|
+
const finish = () => {
|
|
37
|
+
worker.terminate();
|
|
38
|
+
options.signal?.removeEventListener("abort", handleAbort);
|
|
39
|
+
};
|
|
40
|
+
const handleAbort = () => {
|
|
41
|
+
if (settled)
|
|
42
|
+
return;
|
|
43
|
+
settled = true;
|
|
44
|
+
finish();
|
|
45
|
+
reject(abortError());
|
|
46
|
+
};
|
|
47
|
+
options.signal?.addEventListener("abort", handleAbort, { once: true });
|
|
48
|
+
worker.addEventListener("message", (event) => {
|
|
49
|
+
if (settled)
|
|
50
|
+
return;
|
|
51
|
+
const message = event.data;
|
|
52
|
+
if (message.type === "progress") {
|
|
53
|
+
options.onProgress?.(message.progress);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
settled = true;
|
|
57
|
+
finish();
|
|
58
|
+
if (message.type === "error") {
|
|
59
|
+
reject(new Error(message.message));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const previewName = `${stripExtension(filename)}.preview.jpg`;
|
|
63
|
+
resolve({
|
|
64
|
+
file: new File([message.blob], previewName, {
|
|
65
|
+
type: "image/jpeg",
|
|
66
|
+
lastModified: Date.now()
|
|
67
|
+
}),
|
|
68
|
+
width: message.metrics.outputWidth,
|
|
69
|
+
height: message.metrics.outputHeight,
|
|
70
|
+
metrics: message.metrics,
|
|
71
|
+
metadata: { metrics: message.metrics }
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
worker.addEventListener("error", (event) => {
|
|
75
|
+
if (settled)
|
|
76
|
+
return;
|
|
77
|
+
settled = true;
|
|
78
|
+
finish();
|
|
79
|
+
reject(new Error(event.message || "The image preview worker crashed"));
|
|
80
|
+
});
|
|
81
|
+
worker.postMessage({ file, targetBytes });
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function prepareDiscordImagePreview(file, options = {}) {
|
|
85
|
+
return prepareImagePreview(file, file.name, options);
|
|
86
|
+
}
|
|
87
|
+
function isSupportedImage(contentType, filename) {
|
|
88
|
+
return contentType === "image/jpeg" || contentType === "image/png" || /\.(?:jpe?g|png)$/i.test(filename);
|
|
89
|
+
}
|
|
90
|
+
function stripExtension(filename) {
|
|
91
|
+
const lastDot = filename.lastIndexOf(".");
|
|
92
|
+
return lastDot > 0 ? filename.slice(0, lastDot) : filename;
|
|
93
|
+
}
|
|
94
|
+
function positiveInteger(value, name) {
|
|
95
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
96
|
+
throw new Error(`${name} must be a positive integer`);
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
function abortError() {
|
|
101
|
+
return new DOMException("Preview generation was cancelled", "AbortError");
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
prepareImagePreview,
|
|
105
|
+
prepareDiscordImagePreview,
|
|
106
|
+
createBrowserImagePreview,
|
|
107
|
+
DISCORD_PREVIEW_TARGET_BYTES,
|
|
108
|
+
DISCORD_PREVIEW_LIMIT_BYTES
|
|
109
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type PreviewStage = "inspect" | "decode" | "draw" | "encode";
|
|
2
|
+
export interface PreviewProgress {
|
|
3
|
+
percentage: number;
|
|
4
|
+
stage: PreviewStage;
|
|
5
|
+
}
|
|
6
|
+
export interface ImagePreviewMetrics {
|
|
7
|
+
decoder: "image-bitmap" | "webcodecs";
|
|
8
|
+
sourceBytes: number;
|
|
9
|
+
sourceWidth: number;
|
|
10
|
+
sourceHeight: number;
|
|
11
|
+
outputBytes: number;
|
|
12
|
+
outputWidth: number;
|
|
13
|
+
outputHeight: number;
|
|
14
|
+
quality: number;
|
|
15
|
+
elapsedMs: number;
|
|
16
|
+
encodeAttempts: number;
|
|
17
|
+
decodedSurfaceBytes: number;
|
|
18
|
+
fullDecodeBytes: number;
|
|
19
|
+
estimatedPeakWorkingBytes: number;
|
|
20
|
+
targetBytes: number;
|
|
21
|
+
}
|
|
22
|
+
export type PreviewWorkerRequest = {
|
|
23
|
+
file: Blob;
|
|
24
|
+
targetBytes: number;
|
|
25
|
+
};
|
|
26
|
+
export type PreviewWorkerResponse = {
|
|
27
|
+
type: "progress";
|
|
28
|
+
progress: PreviewProgress;
|
|
29
|
+
} | {
|
|
30
|
+
type: "result";
|
|
31
|
+
blob: Blob;
|
|
32
|
+
metrics: ImagePreviewMetrics;
|
|
33
|
+
} | {
|
|
34
|
+
type: "error";
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/worker.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// src/worker.ts
|
|
2
|
+
var worker = globalThis;
|
|
3
|
+
var JPEG_HEADER_READ_BYTES = 2 * 1024 * 1024;
|
|
4
|
+
var MIN_QUALITY = 0.72;
|
|
5
|
+
var INITIAL_QUALITY = 0.9;
|
|
6
|
+
var MAX_QUALITY = 0.98;
|
|
7
|
+
var TARGET_TOLERANCE = 0.04;
|
|
8
|
+
var MAX_ENCODE_ATTEMPTS = 4;
|
|
9
|
+
var JPEG_DCT_SCALES = [1 / 8, 1 / 4, 3 / 8, 1 / 2, 5 / 8, 3 / 4, 7 / 8];
|
|
10
|
+
worker.addEventListener("message", (event) => {
|
|
11
|
+
createPreview(event.data).catch((error) => {
|
|
12
|
+
post({
|
|
13
|
+
type: "error",
|
|
14
|
+
message: error instanceof Error ? error.message : "Could not create the image preview"
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
async function createPreview({ file, targetBytes }) {
|
|
19
|
+
const startedAt = performance.now();
|
|
20
|
+
postProgress("inspect", 4);
|
|
21
|
+
assertWorkerSupport();
|
|
22
|
+
const source = await readImageDimensions(file);
|
|
23
|
+
const idealScale = chooseInitialScale(file.size, targetBytes);
|
|
24
|
+
const isJpeg = file.type === "image/jpeg";
|
|
25
|
+
const scale = isJpeg ? snapToJpegDctScale(idealScale) : idealScale;
|
|
26
|
+
const outputWidth = outputDimension(source.width * scale, isJpeg);
|
|
27
|
+
const outputHeight = outputDimension(source.height * scale, isJpeg);
|
|
28
|
+
postProgress("decode", 14);
|
|
29
|
+
const decoded = await decodeReduced(file, outputWidth, outputHeight);
|
|
30
|
+
postProgress("draw", 48);
|
|
31
|
+
const canvas = new OffscreenCanvas(outputWidth, outputHeight);
|
|
32
|
+
const context = canvas.getContext("2d", { alpha: false });
|
|
33
|
+
if (!context) {
|
|
34
|
+
decoded.close();
|
|
35
|
+
throw new Error("This browser could not create a 2D preview canvas");
|
|
36
|
+
}
|
|
37
|
+
context.fillStyle = "#ffffff";
|
|
38
|
+
context.fillRect(0, 0, outputWidth, outputHeight);
|
|
39
|
+
context.drawImage(decoded.image, 0, 0, outputWidth, outputHeight);
|
|
40
|
+
decoded.close();
|
|
41
|
+
const { candidate, attempts } = await encodeNearTarget(canvas, targetBytes);
|
|
42
|
+
const decodedSurfaceBytes = outputWidth * outputHeight * 4;
|
|
43
|
+
const metrics = {
|
|
44
|
+
decoder: decoded.decoder,
|
|
45
|
+
sourceBytes: file.size,
|
|
46
|
+
sourceWidth: source.width,
|
|
47
|
+
sourceHeight: source.height,
|
|
48
|
+
outputBytes: candidate.blob.size,
|
|
49
|
+
outputWidth,
|
|
50
|
+
outputHeight,
|
|
51
|
+
quality: candidate.quality,
|
|
52
|
+
elapsedMs: performance.now() - startedAt,
|
|
53
|
+
encodeAttempts: attempts,
|
|
54
|
+
decodedSurfaceBytes,
|
|
55
|
+
fullDecodeBytes: source.width * source.height * 4,
|
|
56
|
+
estimatedPeakWorkingBytes: file.size + decodedSurfaceBytes * 2 + candidate.blob.size,
|
|
57
|
+
targetBytes
|
|
58
|
+
};
|
|
59
|
+
canvas.width = 1;
|
|
60
|
+
canvas.height = 1;
|
|
61
|
+
postProgress("encode", 100);
|
|
62
|
+
post({ type: "result", blob: candidate.blob, metrics });
|
|
63
|
+
}
|
|
64
|
+
async function encodeNearTarget(canvas, targetBytes) {
|
|
65
|
+
let low = MIN_QUALITY;
|
|
66
|
+
let high = MAX_QUALITY;
|
|
67
|
+
let quality = INITIAL_QUALITY;
|
|
68
|
+
let best = null;
|
|
69
|
+
let smallest = null;
|
|
70
|
+
for (let attempt = 1;attempt <= MAX_ENCODE_ATTEMPTS; attempt += 1) {
|
|
71
|
+
postProgress("encode", 55 + Math.round(attempt / MAX_ENCODE_ATTEMPTS * 40));
|
|
72
|
+
const blob = await canvas.convertToBlob({ type: "image/jpeg", quality });
|
|
73
|
+
const candidate = { blob, quality };
|
|
74
|
+
if (!smallest || blob.size < smallest.blob.size)
|
|
75
|
+
smallest = candidate;
|
|
76
|
+
if (blob.size <= targetBytes && (!best || blob.size > best.blob.size))
|
|
77
|
+
best = candidate;
|
|
78
|
+
const utilization = blob.size / targetBytes;
|
|
79
|
+
if (utilization <= 1 && utilization >= 1 - TARGET_TOLERANCE) {
|
|
80
|
+
return { candidate, attempts: attempt };
|
|
81
|
+
}
|
|
82
|
+
if (blob.size > targetBytes)
|
|
83
|
+
high = quality;
|
|
84
|
+
else
|
|
85
|
+
low = quality;
|
|
86
|
+
quality = Math.round((low + high) / 2 * 1000) / 1000;
|
|
87
|
+
}
|
|
88
|
+
if (best)
|
|
89
|
+
return { candidate: best, attempts: MAX_ENCODE_ATTEMPTS };
|
|
90
|
+
if (smallest) {
|
|
91
|
+
throw new Error(`The reduced image is still ${(smallest.blob.size / 1024 / 1024).toFixed(2)} MiB at minimum quality`);
|
|
92
|
+
}
|
|
93
|
+
throw new Error("The browser JPEG encoder returned no preview data");
|
|
94
|
+
}
|
|
95
|
+
function chooseInitialScale(sourceBytes, targetBytes) {
|
|
96
|
+
return Math.min(0.98, Math.max(0.05, Math.sqrt(targetBytes / sourceBytes) * 0.92));
|
|
97
|
+
}
|
|
98
|
+
function snapToJpegDctScale(idealScale) {
|
|
99
|
+
let selected = JPEG_DCT_SCALES[0];
|
|
100
|
+
for (const scale of JPEG_DCT_SCALES) {
|
|
101
|
+
if (scale > idealScale)
|
|
102
|
+
break;
|
|
103
|
+
selected = scale;
|
|
104
|
+
}
|
|
105
|
+
return selected;
|
|
106
|
+
}
|
|
107
|
+
function outputDimension(value, jpegDctScale) {
|
|
108
|
+
return jpegDctScale ? Math.max(1, Math.ceil(value)) : Math.max(2, Math.floor(value / 2) * 2);
|
|
109
|
+
}
|
|
110
|
+
function postProgress(stage, percentage) {
|
|
111
|
+
post({ type: "progress", progress: { stage, percentage } });
|
|
112
|
+
}
|
|
113
|
+
function post(message) {
|
|
114
|
+
worker.postMessage(message);
|
|
115
|
+
}
|
|
116
|
+
function assertWorkerSupport() {
|
|
117
|
+
if (typeof OffscreenCanvas !== "function") {
|
|
118
|
+
throw new Error("This browser needs OffscreenCanvas support");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function decodeReduced(file, width, height) {
|
|
122
|
+
if (typeof ImageDecoder === "function" && await ImageDecoder.isTypeSupported(file.type)) {
|
|
123
|
+
const decoder = new ImageDecoder({
|
|
124
|
+
type: file.type,
|
|
125
|
+
data: file.stream(),
|
|
126
|
+
desiredWidth: width,
|
|
127
|
+
desiredHeight: height,
|
|
128
|
+
preferAnimation: false
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
const result = await decoder.decode({ frameIndex: 0 });
|
|
132
|
+
return {
|
|
133
|
+
decoder: "webcodecs",
|
|
134
|
+
image: result.image,
|
|
135
|
+
close: () => {
|
|
136
|
+
result.image.close();
|
|
137
|
+
decoder.close();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
decoder.close();
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (typeof createImageBitmap !== "function") {
|
|
146
|
+
throw new Error("This browser needs WebCodecs ImageDecoder or createImageBitmap support");
|
|
147
|
+
}
|
|
148
|
+
const bitmap = await createImageBitmap(file, {
|
|
149
|
+
imageOrientation: "from-image",
|
|
150
|
+
resizeWidth: width,
|
|
151
|
+
resizeHeight: height,
|
|
152
|
+
resizeQuality: "high"
|
|
153
|
+
});
|
|
154
|
+
return { decoder: "image-bitmap", image: bitmap, close: () => bitmap.close() };
|
|
155
|
+
}
|
|
156
|
+
async function readImageDimensions(file) {
|
|
157
|
+
if (file.type === "image/jpeg")
|
|
158
|
+
return readJpegDimensions(file);
|
|
159
|
+
if (file.type === "image/png")
|
|
160
|
+
return readPngDimensions(file);
|
|
161
|
+
throw new Error("The low-memory preview path currently supports JPEG and PNG images");
|
|
162
|
+
}
|
|
163
|
+
async function readJpegDimensions(file) {
|
|
164
|
+
const bytes = new Uint8Array(await file.slice(0, Math.min(file.size, JPEG_HEADER_READ_BYTES)).arrayBuffer());
|
|
165
|
+
if (bytes[0] !== 255 || bytes[1] !== 216)
|
|
166
|
+
throw new Error("The selected file is not a JPEG");
|
|
167
|
+
let offset = 2;
|
|
168
|
+
while (offset + 8 < bytes.length) {
|
|
169
|
+
if (bytes[offset] !== 255) {
|
|
170
|
+
offset += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
while (bytes[offset] === 255)
|
|
174
|
+
offset += 1;
|
|
175
|
+
const marker = bytes[offset];
|
|
176
|
+
offset += 1;
|
|
177
|
+
if (marker === undefined || marker === 217 || marker === 218)
|
|
178
|
+
break;
|
|
179
|
+
if (marker === 1 || marker >= 208 && marker <= 215)
|
|
180
|
+
continue;
|
|
181
|
+
const segmentLength = readUint16(bytes, offset);
|
|
182
|
+
if (segmentLength < 2 || offset + segmentLength > bytes.length)
|
|
183
|
+
break;
|
|
184
|
+
if (isStartOfFrame(marker)) {
|
|
185
|
+
const height = readUint16(bytes, offset + 3);
|
|
186
|
+
const width = readUint16(bytes, offset + 5);
|
|
187
|
+
if (width > 0 && height > 0)
|
|
188
|
+
return { width, height };
|
|
189
|
+
}
|
|
190
|
+
offset += segmentLength;
|
|
191
|
+
}
|
|
192
|
+
throw new Error("Could not find JPEG dimensions in the first 2 MiB of metadata");
|
|
193
|
+
}
|
|
194
|
+
async function readPngDimensions(file) {
|
|
195
|
+
const bytes = new Uint8Array(await file.slice(0, 24).arrayBuffer());
|
|
196
|
+
const signature = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
197
|
+
if (signature.some((value, index) => bytes[index] !== value)) {
|
|
198
|
+
throw new Error("The selected file is not a PNG");
|
|
199
|
+
}
|
|
200
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
201
|
+
const width = view.getUint32(16);
|
|
202
|
+
const height = view.getUint32(20);
|
|
203
|
+
if (width <= 0 || height <= 0)
|
|
204
|
+
throw new Error("The PNG has invalid dimensions");
|
|
205
|
+
return { width, height };
|
|
206
|
+
}
|
|
207
|
+
function readUint16(bytes, offset) {
|
|
208
|
+
return (bytes[offset] ?? 0) << 8 | (bytes[offset + 1] ?? 0);
|
|
209
|
+
}
|
|
210
|
+
function isStartOfFrame(marker) {
|
|
211
|
+
return marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204;
|
|
212
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@minhvuong/pirate-storage-preview-browser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "bun build src/index.ts src/worker.ts --outdir dist --target browser --packages external && tsc -p tsconfig.build.json",
|
|
21
|
+
"check-types": "tsc --noEmit",
|
|
22
|
+
"test": "bun test"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@minhvuong/pirate-storage": "workspace:*"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"typescript": "catalog:"
|
|
29
|
+
}
|
|
30
|
+
}
|