zip-writer 1.0.0 → 1.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.
Files changed (45) hide show
  1. package/dist/constants.d.ts +21 -0
  2. package/dist/constants.d.ts.map +1 -0
  3. package/{src/constants.ts → dist/constants.js} +1 -5
  4. package/dist/constants.js.map +1 -0
  5. package/dist/crc-lookup.d.ts +6 -0
  6. package/dist/crc-lookup.d.ts.map +1 -0
  7. package/dist/crc-lookup.js +22 -0
  8. package/dist/crc-lookup.js.map +1 -0
  9. package/{src/crc.ts → dist/crc.d.ts} +2 -39
  10. package/dist/crc.d.ts.map +1 -0
  11. package/dist/crc.js +62 -0
  12. package/dist/crc.js.map +1 -0
  13. package/dist/index.d.ts +104 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +245 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/utils.d.ts +4 -0
  18. package/dist/utils.d.ts.map +1 -0
  19. package/dist/utils.js +12 -0
  20. package/dist/utils.js.map +1 -0
  21. package/dist/write-central-directory.d.ts +14 -0
  22. package/dist/write-central-directory.d.ts.map +1 -0
  23. package/dist/write-central-directory.js +279 -0
  24. package/dist/write-central-directory.js.map +1 -0
  25. package/dist/write-entry.d.ts +24 -0
  26. package/dist/write-entry.d.ts.map +1 -0
  27. package/dist/write-entry.js +173 -0
  28. package/dist/write-entry.js.map +1 -0
  29. package/package.json +12 -3
  30. package/.claude/settings.local.json +0 -11
  31. package/.github/workflows/test.yml +0 -36
  32. package/.husky/pre-commit +0 -1
  33. package/eslint.config.mjs +0 -9
  34. package/src/crc-lookup.ts +0 -27
  35. package/src/index.ts +0 -342
  36. package/src/utils.ts +0 -17
  37. package/src/write-central-directory.ts +0 -405
  38. package/src/write-entry.ts +0 -246
  39. package/test/commands.ts +0 -47
  40. package/test/crc.test.ts +0 -45
  41. package/test/integration.test.ts +0 -483
  42. package/test/utils.ts +0 -40
  43. package/tsconfig.json +0 -36
  44. package/tsconfig.node.json +0 -10
  45. package/vitest.config.ts +0 -46
package/src/index.ts DELETED
@@ -1,342 +0,0 @@
1
- import { writeZipEntry } from "./write-entry.js";
2
- import {
3
- getCDFH,
4
- getEOCDZip64,
5
- getEOCDStandard,
6
- } from "./write-central-directory.js";
7
- import { ZIP64_LIMIT } from "./constants.js";
8
- import { noop } from "./utils.js";
9
-
10
- const BUFFER_SIZE = 16 * 1024; // 16 KB
11
- const bufferedQueue = new ByteLengthQueuingStrategy({
12
- highWaterMark: BUFFER_SIZE,
13
- });
14
- const unbufferedQueue = { highWaterMark: 0 };
15
-
16
- export interface EntryOptions {
17
- /** Entry name including internal path */
18
- name: string;
19
- /** Entry comment */
20
- comment?: string;
21
- /** Entry date */
22
- date?: Date;
23
- /** Entry permissions */
24
- mode?: number;
25
- /** Compression method to `STORE` (defaults to `DEFLATE`) */
26
- store?: boolean;
27
- }
28
-
29
- export interface EntryInfoZip64 extends EntryOptions {
30
- /** Byte offset of the local file header within the ZIP archive */
31
- startOffset: bigint;
32
- /** CRC32 checksum of the uncompressed data */
33
- crc32: number;
34
- /** Uncompressed size in bytes */
35
- uncompressedSize: bigint;
36
- /** Compressed size in bytes */
37
- compressedSize: bigint;
38
- /** Entry information is in ZIP64 format */
39
- zip64: true;
40
- }
41
-
42
- export type EntryInfoStandard = {
43
- [K in keyof EntryInfoZip64]: EntryInfoZip64[K] extends bigint
44
- ? number
45
- : K extends "zip64"
46
- ? false
47
- : EntryInfoZip64[K];
48
- };
49
-
50
- export type EntryInfo = EntryInfoZip64 | EntryInfoStandard;
51
-
52
- export class ZipWriter<TIsZip64 extends boolean = false> {
53
- #offset = BigInt(0);
54
- #zipStream = new TransformStream(
55
- {
56
- transform: (chunk, controller) => {
57
- controller.enqueue(chunk);
58
- this.#offset += BigInt(chunk.byteLength);
59
- },
60
- },
61
- // Unbuffered on the writable side, we already queue entries
62
- unbufferedQueue,
63
- // Readable buffer, so that temporary delays on writable consumers don't
64
- // block the zip writing.
65
- bufferedQueue
66
- );
67
- #entries: EntryInfo[] = [];
68
- #queued: Promise<unknown> = Promise.resolve();
69
- #entryCount = 0;
70
- #finalized = false;
71
-
72
- get readable() {
73
- return this.#zipStream.readable;
74
- }
75
-
76
- get entries(): ReadonlyArray<Readonly<EntryInfo>> {
77
- return this.#entries;
78
- }
79
-
80
- /**
81
- * Wait until all currently queued entries have been written.
82
- */
83
- async onceQueueEmpty(): Promise<void> {
84
- while (this.#entries.length < this.#entryCount) {
85
- await this.#queued;
86
- }
87
- }
88
-
89
- /**
90
- * Create a new entry in the Zip archive. Use the returned
91
- * entryWriter.writable to write data to the entry. You can await
92
- * entryWriter.entry() to get the entry info once all data has been written.
93
- *
94
- * @example
95
- * ```ts
96
- * const response = await fetch(imageUrl);
97
- * response.body!.pipeTo(
98
- * zipWriter.entry({ name: "path/to/file.png" }).writable
99
- * );
100
- * ```
101
- *
102
- * @example
103
- * ```ts
104
- * const entryWriter = zipWriter.entry({ name: "hello.txt" });
105
- * const writer = entryWriter.writable.getWriter();
106
- * await writer.write(
107
- * new TextEncoder().encode("Hello, World!")
108
- * );
109
- * await writer.close();
110
- * console.log(await entryWriter.entry()); // Entry info
111
- * ```
112
- *
113
- * @param options Entry options
114
- */
115
- entry(options: EntryOptions) {
116
- if (this.#finalized) {
117
- throw new TypeError("Cannot add entry after finalize() has been called");
118
- }
119
- this.#entryCount++;
120
-
121
- // Capture the previous queue position
122
- const prevQueued = this.#queued;
123
-
124
- // Create a promise that will be resolved when this entry completes
125
- let entryDone: () => void;
126
- let entryError: (reason: unknown) => void;
127
- this.#queued = new Promise<void>((resolve, reject) => {
128
- entryDone = resolve;
129
- entryError = reject;
130
- });
131
- // prevent unhandled rejection, but we still want this.#queued to reject
132
- // when awaited if there's an error
133
- this.#queued.catch(noop);
134
-
135
- return new EntryWriter(async (readable) => {
136
- // Wait for previous entry to complete
137
- await prevQueued;
138
-
139
- const writer = this.#zipStream.writable.getWriter();
140
- const reader = readable.getReader();
141
- try {
142
- const entryInfo = await writeZipEntry({
143
- writer,
144
- reader,
145
- entryOptions: options,
146
- startOffset: this.#offset,
147
- });
148
- this.#entries.push(entryInfo);
149
- entryDone();
150
- return entryInfo;
151
- } catch (err) {
152
- entryError(err);
153
- await reader.cancel(err);
154
- // Abort the zip stream - it's corrupted after a partial entry write
155
- this.#zipStream.writable.abort(err);
156
- // Don't call entryDone() - leave subsequent entries waiting forever
157
- // since the zip stream is corrupted
158
- // This error is handled in the EntryWriter constructor, and used to
159
- // abort the entry writable stream
160
- throw err;
161
- } finally {
162
- reader.releaseLock();
163
- writer.releaseLock();
164
- }
165
- });
166
- }
167
-
168
- /**
169
- * Set the entries of the Zip archive. This can be used to reorder,
170
- * remove, or rename entries before finalizing the archive.
171
- *
172
- * @param entries New entries array
173
- */
174
- setEntries(entries: ReadonlyArray<Readonly<EntryInfo>>) {
175
- const currentEntriesByOffset = new Map<bigint, EntryInfo>();
176
- for (const entry of this.#entries) {
177
- currentEntriesByOffset.set(BigInt(entry.startOffset), entry);
178
- }
179
- // Entries can't have their offset, crc32, compressed size, or uncompressed
180
- // size changed, but they can be removed, reordered, or have the name and
181
- // comment changed.
182
- for (const entry of entries) {
183
- const existingEntry = currentEntriesByOffset.get(
184
- BigInt(entry.startOffset)
185
- );
186
- if (!existingEntry) {
187
- throw new Error(
188
- `Cannot set entries: entry at offset ${entry.startOffset} does not exist`
189
- );
190
- }
191
- if (existingEntry.crc32 !== entry.crc32) {
192
- throw new Error(
193
- `Cannot set entries: entry at offset ${entry.startOffset} has different CRC32`
194
- );
195
- }
196
- if (existingEntry.uncompressedSize !== entry.uncompressedSize) {
197
- throw new Error(
198
- `Cannot set entries: entry at offset ${entry.startOffset} has different uncompressed size`
199
- );
200
- }
201
- if (existingEntry.compressedSize !== entry.compressedSize) {
202
- throw new Error(
203
- `Cannot set entries: entry at offset ${entry.startOffset} has different compressed size`
204
- );
205
- }
206
- }
207
- this.#entries = entries.map((entry) => ({ ...entry }));
208
- }
209
-
210
- async finalize(): Promise<{
211
- /** Does the archive use ZIP64 format? */
212
- zip64: boolean;
213
- /** Total uncompressed size of all entries */
214
- uncompressedEntriesSize: bigint;
215
- /** Total compressed size of all entries */
216
- compressedEntriesSize: bigint;
217
- /** Total size of the ZIP file */
218
- fileSize: bigint;
219
- }> {
220
- if (this.#finalized) {
221
- throw new TypeError("finalize() has already been called");
222
- }
223
- this.#finalized = true;
224
-
225
- // We do this as an IIFE to avoid an unhandled error, so that a user can
226
- // call finalize() without needing to await and catch an error. Any error
227
- // will be propagated to the stream output, but the user can optionally
228
- // await finalize() to also see the error.
229
- const finalizePromise = (async () => {
230
- await this.onceQueueEmpty();
231
-
232
- // Write central directory headers for all entries
233
- const centralDirectoryStart = this.#offset;
234
- const writer = this.#zipStream.writable.getWriter();
235
-
236
- try {
237
- let entryUsesZip64 = false;
238
- let uncompressedEntriesSize = BigInt(0);
239
- let compressedEntriesSize = BigInt(0);
240
- for (const entry of this.#entries) {
241
- if (entry.zip64) {
242
- entryUsesZip64 = true;
243
- }
244
- uncompressedEntriesSize += BigInt(entry.uncompressedSize);
245
- compressedEntriesSize += BigInt(entry.compressedSize);
246
- await writer.write(getCDFH(entry));
247
- }
248
-
249
- const centralDirectorySize = this.#offset - centralDirectoryStart;
250
-
251
- // Determine if the archive needs ZIP64 based on entries or central directory
252
- const eocdNeedsZip64 =
253
- this.#entries.length > 0xffff ||
254
- centralDirectoryStart > ZIP64_LIMIT ||
255
- centralDirectorySize > ZIP64_LIMIT;
256
-
257
- let eocd: Uint8Array<ArrayBuffer>;
258
- if (eocdNeedsZip64) {
259
- eocd = getEOCDZip64(
260
- BigInt(this.#entries.length),
261
- centralDirectoryStart,
262
- centralDirectorySize
263
- );
264
- } else {
265
- eocd = getEOCDStandard(
266
- this.#entries.length,
267
- Number(centralDirectoryStart),
268
- Number(centralDirectorySize)
269
- );
270
- }
271
-
272
- await writer.write(eocd);
273
-
274
- // Close the zip stream
275
- await writer.close();
276
-
277
- return {
278
- zip64: eocdNeedsZip64 || entryUsesZip64,
279
- uncompressedEntriesSize,
280
- compressedEntriesSize,
281
- fileSize: this.#offset,
282
- };
283
- } catch (err) {
284
- await writer.abort(err);
285
- throw err;
286
- } finally {
287
- writer.releaseLock();
288
- }
289
- })();
290
- finalizePromise.catch(noop); // prevent unhandled rejection
291
- return finalizePromise;
292
- }
293
- }
294
-
295
- class EntryWriter {
296
- // Unbuffered stream to directly pipe to the zip writer
297
- #entryStream = new TransformStream(
298
- undefined,
299
- unbufferedQueue,
300
- unbufferedQueue
301
- );
302
- #written: Promise<EntryInfo>;
303
-
304
- /**
305
- * @param enqueueAndPipe Function that enqueues the entry data readable, pipes it to the Zip stream, and returns a promise that resolves to the entry info once done.
306
- */
307
- constructor(
308
- enqueueAndPipe: (
309
- readable: ReadableStream<Uint8Array<ArrayBuffer>>
310
- ) => Promise<EntryInfo>
311
- ) {
312
- // We could try to pass the zipstream writable to EntryWriter, but then
313
- // EntryWriter would not know when the entry has finished writing, and would
314
- // not have a way to get the entry info.
315
- this.#written = enqueueAndPipe(this.#entryStream.readable);
316
- // abort the writable quickly if there is an error, and ensure we don't
317
- // throw an uncaught error.
318
- this.#written.catch((reason) => {
319
- this.#entryStream.writable.abort(reason);
320
- });
321
- }
322
-
323
- /**
324
- * Writable stream to write entry data to.
325
- *
326
- * @example
327
- * ```ts
328
- * ReadableStream.from([new TextEncoder().encode("Hello, World!")])
329
- * .pipeTo(entryWriter.writable);
330
- * ```
331
- */
332
- get writable() {
333
- return this.#entryStream.writable;
334
- }
335
-
336
- /**
337
- * Get entry info. Resolves once all data has been written to the Zip stream.
338
- */
339
- async entry(): Promise<EntryInfo> {
340
- return this.#written;
341
- }
342
- }
package/src/utils.ts DELETED
@@ -1,17 +0,0 @@
1
- export function getDosTime(date: Date): number {
2
- return (
3
- (date.getHours() << 11) |
4
- (date.getMinutes() << 5) |
5
- (date.getSeconds() >> 1)
6
- );
7
- }
8
-
9
- export function getDosDate(date: Date): number {
10
- return (
11
- ((date.getFullYear() - 1980) << 9) |
12
- ((date.getMonth() + 1) << 5) |
13
- date.getDate()
14
- );
15
- }
16
-
17
- export function noop() {}