@cj-tech-master/excelts 1.4.2 → 1.4.4

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 (49) hide show
  1. package/README.md +3 -3
  2. package/README_zh.md +3 -3
  3. package/dist/browser/excelts.iife.js +8135 -2722
  4. package/dist/browser/excelts.iife.js.map +1 -1
  5. package/dist/browser/excelts.iife.min.js +86 -23
  6. package/dist/cjs/stream/xlsx/workbook-writer.js +3 -2
  7. package/dist/cjs/utils/cell-format.js +13 -9
  8. package/dist/cjs/utils/sheet-utils.js +125 -15
  9. package/dist/cjs/utils/unzip/extract.js +166 -0
  10. package/dist/cjs/utils/unzip/index.js +7 -1
  11. package/dist/cjs/utils/xml-stream.js +25 -3
  12. package/dist/cjs/utils/zip/compress.js +261 -0
  13. package/dist/cjs/utils/zip/crc32.js +154 -0
  14. package/dist/cjs/utils/zip/index.js +70 -0
  15. package/dist/cjs/utils/zip/zip-builder.js +378 -0
  16. package/dist/cjs/utils/zip-stream.js +30 -34
  17. package/dist/cjs/xlsx/xform/book/defined-name-xform.js +36 -2
  18. package/dist/cjs/xlsx/xform/list-xform.js +6 -0
  19. package/dist/cjs/xlsx/xform/sheet/cell-xform.js +6 -1
  20. package/dist/cjs/xlsx/xform/sheet/row-xform.js +24 -2
  21. package/dist/cjs/xlsx/xform/table/filter-column-xform.js +4 -0
  22. package/dist/esm/stream/xlsx/workbook-writer.js +3 -2
  23. package/dist/esm/utils/cell-format.js +13 -9
  24. package/dist/esm/utils/sheet-utils.js +125 -15
  25. package/dist/esm/utils/unzip/extract.js +160 -0
  26. package/dist/esm/utils/unzip/index.js +2 -0
  27. package/dist/esm/utils/xml-stream.js +25 -3
  28. package/dist/esm/utils/zip/compress.js +220 -0
  29. package/dist/esm/utils/zip/crc32.js +116 -0
  30. package/dist/esm/utils/zip/index.js +55 -0
  31. package/dist/esm/utils/zip/zip-builder.js +372 -0
  32. package/dist/esm/utils/zip-stream.js +30 -34
  33. package/dist/esm/xlsx/xform/book/defined-name-xform.js +36 -2
  34. package/dist/esm/xlsx/xform/list-xform.js +6 -0
  35. package/dist/esm/xlsx/xform/sheet/cell-xform.js +6 -1
  36. package/dist/esm/xlsx/xform/sheet/row-xform.js +24 -2
  37. package/dist/esm/xlsx/xform/table/filter-column-xform.js +4 -0
  38. package/dist/types/utils/sheet-utils.d.ts +8 -2
  39. package/dist/types/utils/unzip/extract.d.ts +92 -0
  40. package/dist/types/utils/unzip/index.d.ts +1 -0
  41. package/dist/types/utils/xml-stream.d.ts +2 -0
  42. package/dist/types/utils/zip/compress.d.ts +83 -0
  43. package/dist/types/utils/zip/crc32.d.ts +55 -0
  44. package/dist/types/utils/zip/index.d.ts +52 -0
  45. package/dist/types/utils/zip/zip-builder.d.ts +110 -0
  46. package/dist/types/utils/zip-stream.d.ts +6 -12
  47. package/dist/types/xlsx/xform/list-xform.d.ts +1 -0
  48. package/dist/types/xlsx/xform/sheet/row-xform.d.ts +2 -0
  49. package/package.json +1 -1
@@ -4,30 +4,140 @@
4
4
  */
5
5
  import { Workbook } from "../doc/workbook.js";
6
6
  import { colCache } from "./col-cache.js";
7
- import { dateToExcel } from "./utils.js";
8
7
  import { format as cellFormat } from "./cell-format.js";
8
+ /**
9
+ * Convert a Date object back to Excel serial number without timezone issues.
10
+ * This reverses the excelToDate conversion exactly.
11
+ * excelToDate uses: new Date(Math.round((v - 25569) * 24 * 3600 * 1000))
12
+ * So we reverse it: (date.getTime() / (24 * 3600 * 1000)) + 25569
13
+ */
14
+ function dateToExcelSerial(d) {
15
+ return d.getTime() / (24 * 3600 * 1000) + 25569;
16
+ }
17
+ /**
18
+ * Check if format is a pure time format (no date components like y, m for month, d)
19
+ * Time formats only contain: h, m (minutes in time context), s, AM/PM
20
+ * Excludes elapsed time formats like [h]:mm:ss which should keep full serial number
21
+ */
22
+ function isTimeOnlyFormat(fmt) {
23
+ // Remove quoted strings first
24
+ const cleaned = fmt.replace(/"[^"]*"/g, "");
25
+ // Elapsed time formats [h], [m], [s] should NOT be treated as time-only
26
+ // They need the full serial number to calculate total hours/minutes/seconds
27
+ if (/\[[hms]\]/i.test(cleaned)) {
28
+ return false;
29
+ }
30
+ // Remove color codes and conditions (but we already checked for [h], [m], [s])
31
+ const withoutBrackets = cleaned.replace(/\[[^\]]*\]/g, "");
32
+ // Check if it has time components (h, s, or AM/PM)
33
+ const hasTimeComponents = /[hs]/i.test(withoutBrackets) || /AM\/PM|A\/P/i.test(withoutBrackets);
34
+ // Check if it has date components (y, d, or m not adjacent to h/s which would make it minutes)
35
+ // In Excel: "m" after "h" or before "s" is minutes, otherwise it's month
36
+ const hasDateComponents = /[yd]/i.test(withoutBrackets);
37
+ // If it has time but no date components, it's a time-only format
38
+ // Also check for standalone 'm' that's not minutes (not near h or s)
39
+ if (hasDateComponents) {
40
+ return false;
41
+ }
42
+ // Check for month 'm' - if 'm' exists but not in h:m or m:s context, it's a date format
43
+ if (/m/i.test(withoutBrackets) && !hasTimeComponents) {
44
+ return false;
45
+ }
46
+ return hasTimeComponents;
47
+ }
48
+ /**
49
+ * Check if format is a date format (contains y, d, or month-m)
50
+ * Used to determine if dateFormat override should be applied
51
+ */
52
+ function isDateFormat(fmt) {
53
+ // Remove quoted strings first
54
+ const cleaned = fmt.replace(/"[^"]*"/g, "");
55
+ // Elapsed time formats [h], [m], [s] are NOT date formats
56
+ if (/\[[hms]\]/i.test(cleaned)) {
57
+ return false;
58
+ }
59
+ // Remove color codes and conditions
60
+ const withoutBrackets = cleaned.replace(/\[[^\]]*\]/g, "");
61
+ // Check for year or day components
62
+ if (/[yd]/i.test(withoutBrackets)) {
63
+ return true;
64
+ }
65
+ // Check for month 'm' - only if it's NOT in time context (not near h or s)
66
+ // In Excel: "m" after "h" or before "s" is minutes, otherwise it's month
67
+ if (/m/i.test(withoutBrackets)) {
68
+ const hasTimeComponents = /[hs]/i.test(withoutBrackets) || /AM\/PM|A\/P/i.test(withoutBrackets);
69
+ // If no time components, 'm' is month
70
+ if (!hasTimeComponents) {
71
+ return true;
72
+ }
73
+ // If has time components, need to check if 'm' is month or minutes
74
+ // Simplified: if format has both date-like and time-like patterns, consider it a date format
75
+ // e.g., "m/d/yy h:mm" - has 'm' as month and 'mm' as minutes
76
+ }
77
+ return false;
78
+ }
79
+ /**
80
+ * Format a value (Date, number, boolean, string) according to the given format
81
+ * Handles timezone-independent conversion for Date objects
82
+ * @param value - The value to format
83
+ * @param fmt - The format string to use
84
+ * @param dateFormat - Optional override format for date values (not applied to time or elapsed time formats)
85
+ */
86
+ function formatValue(value, fmt, dateFormat) {
87
+ // Date object - convert back to Excel serial number
88
+ if (value instanceof Date) {
89
+ let serial = dateToExcelSerial(value);
90
+ // For time-only formats, use only the fractional part (time portion)
91
+ if (isTimeOnlyFormat(fmt)) {
92
+ serial = serial % 1;
93
+ if (serial < 0) {
94
+ serial += 1;
95
+ }
96
+ return cellFormat(fmt, serial);
97
+ }
98
+ // Only apply dateFormat override to actual date formats
99
+ // (not elapsed time formats like [h]:mm:ss)
100
+ const actualFmt = dateFormat && isDateFormat(fmt) ? dateFormat : fmt;
101
+ return cellFormat(actualFmt, serial);
102
+ }
103
+ // Number/Boolean/String - let cellFormat handle it
104
+ return cellFormat(fmt, value);
105
+ }
9
106
  /**
10
107
  * Get formatted display text for a cell value
11
108
  * Returns the value formatted according to the cell's numFmt
12
- * This matches Excel's display exactly
109
+ * This matches Excel's display exactly (timezone-independent)
110
+ * @param cell - The cell to get display text for
111
+ * @param dateFormat - Optional override format for date values
13
112
  */
14
- function getCellDisplayText(cell) {
113
+ function getCellDisplayText(cell, dateFormat) {
15
114
  const value = cell.value;
16
115
  const fmt = cell.numFmt || "General";
17
116
  // Null/undefined
18
117
  if (value == null) {
19
118
  return "";
20
119
  }
21
- // Date object - convert to Excel serial number
22
- if (value instanceof Date) {
23
- const serial = dateToExcel(value, false);
24
- return cellFormat(fmt, serial);
120
+ // Date/Number/Boolean/String - format directly
121
+ if (value instanceof Date ||
122
+ typeof value === "number" ||
123
+ typeof value === "boolean" ||
124
+ typeof value === "string") {
125
+ return formatValue(value, fmt, dateFormat);
25
126
  }
26
- // Number/Boolean/String - let cellFormat handle it
27
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
28
- return cellFormat(fmt, value);
127
+ // Formula type - use the result value
128
+ if (typeof value === "object" && "formula" in value) {
129
+ const result = value.result;
130
+ if (result == null) {
131
+ return "";
132
+ }
133
+ if (result instanceof Date ||
134
+ typeof result === "number" ||
135
+ typeof result === "boolean" ||
136
+ typeof result === "string") {
137
+ return formatValue(result, fmt, dateFormat);
138
+ }
29
139
  }
30
- // Fallback to cell.text for other types (rich text, hyperlink, error, formula, etc.)
140
+ // Fallback to cell.text for other types (rich text, hyperlink, error, etc.)
31
141
  return cell.text;
32
142
  }
33
143
  // =============================================================================
@@ -263,7 +373,7 @@ export function sheetToJson(worksheet, opts) {
263
373
  let isEmpty = true;
264
374
  for (let col = startCol; col <= endCol; col++) {
265
375
  const cell = worksheet.getCell(row, col);
266
- const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
376
+ const val = o.raw === false ? getCellDisplayText(cell, o.dateFormat).trim() : cell.value;
267
377
  if (val != null && val !== "") {
268
378
  rowData[col - startCol] = val;
269
379
  isEmpty = false;
@@ -291,7 +401,7 @@ export function sheetToJson(worksheet, opts) {
291
401
  let isEmpty = true;
292
402
  for (let col = startCol; col <= endCol; col++) {
293
403
  const cell = worksheet.getCell(row, col);
294
- const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
404
+ const val = o.raw === false ? getCellDisplayText(cell, o.dateFormat).trim() : cell.value;
295
405
  const key = encodeCol(col - 1); // 0-indexed for encodeCol
296
406
  if (val != null && val !== "") {
297
407
  rowData[key] = val;
@@ -318,7 +428,7 @@ export function sheetToJson(worksheet, opts) {
318
428
  const colIdx = col - startCol;
319
429
  const key = headerOpt[colIdx] ?? `__EMPTY_${colIdx}`;
320
430
  const cell = worksheet.getCell(row, col);
321
- const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
431
+ const val = o.raw === false ? getCellDisplayText(cell, o.dateFormat).trim() : cell.value;
322
432
  if (val != null && val !== "") {
323
433
  rowData[key] = val;
324
434
  isEmpty = false;
@@ -360,7 +470,7 @@ export function sheetToJson(worksheet, opts) {
360
470
  let isEmpty = true;
361
471
  for (let col = startCol; col <= endCol; col++) {
362
472
  const cell = worksheet.getCell(row, col);
363
- const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
473
+ const val = o.raw === false ? getCellDisplayText(cell, o.dateFormat).trim() : cell.value;
364
474
  const key = headers[col - startCol];
365
475
  if (val != null && val !== "") {
366
476
  rowData[key] = val;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Simple ZIP extraction utilities
3
+ * Provides easy-to-use Promise-based API for extracting ZIP files
4
+ */
5
+ import { Readable } from "stream";
6
+ import { createParse } from "./parse.js";
7
+ /**
8
+ * Extract all files from a ZIP buffer
9
+ *
10
+ * @param zipData - ZIP file data as Buffer or Uint8Array
11
+ * @returns Map of file paths to their content
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { extractAll } from "./utils/unzip/extract.js";
16
+ *
17
+ * const zipData = fs.readFileSync("archive.zip");
18
+ * const files = await extractAll(zipData);
19
+ *
20
+ * for (const [path, file] of files) {
21
+ * console.log(`${path}: ${file.data.length} bytes`);
22
+ * }
23
+ * ```
24
+ */
25
+ export async function extractAll(zipData) {
26
+ const files = new Map();
27
+ const buffer = Buffer.isBuffer(zipData) ? zipData : Buffer.from(zipData);
28
+ const parse = createParse({ forceStream: true });
29
+ const stream = Readable.from([buffer]);
30
+ stream.pipe(parse);
31
+ for await (const entry of parse) {
32
+ const zipEntry = entry;
33
+ const isDirectory = zipEntry.type === "Directory";
34
+ if (isDirectory) {
35
+ files.set(zipEntry.path, {
36
+ path: zipEntry.path,
37
+ data: Buffer.alloc(0),
38
+ isDirectory: true,
39
+ size: 0
40
+ });
41
+ zipEntry.autodrain();
42
+ }
43
+ else {
44
+ const data = await zipEntry.buffer();
45
+ files.set(zipEntry.path, {
46
+ path: zipEntry.path,
47
+ data,
48
+ isDirectory: false,
49
+ size: data.length
50
+ });
51
+ }
52
+ }
53
+ return files;
54
+ }
55
+ /**
56
+ * Extract a single file from a ZIP buffer
57
+ *
58
+ * @param zipData - ZIP file data as Buffer or Uint8Array
59
+ * @param filePath - Path of the file to extract
60
+ * @returns File content as Buffer, or null if not found
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * import { extractFile } from "./utils/unzip/extract.js";
65
+ *
66
+ * const zipData = fs.readFileSync("archive.zip");
67
+ * const content = await extractFile(zipData, "readme.txt");
68
+ * if (content) {
69
+ * console.log(content.toString("utf-8"));
70
+ * }
71
+ * ```
72
+ */
73
+ export async function extractFile(zipData, filePath) {
74
+ const buffer = Buffer.isBuffer(zipData) ? zipData : Buffer.from(zipData);
75
+ const parse = createParse({ forceStream: true });
76
+ const stream = Readable.from([buffer]);
77
+ stream.pipe(parse);
78
+ for await (const entry of parse) {
79
+ const zipEntry = entry;
80
+ if (zipEntry.path === filePath) {
81
+ if (zipEntry.type === "Directory") {
82
+ return Buffer.alloc(0);
83
+ }
84
+ return zipEntry.buffer();
85
+ }
86
+ zipEntry.autodrain();
87
+ }
88
+ return null;
89
+ }
90
+ /**
91
+ * List all file paths in a ZIP buffer (without extracting content)
92
+ *
93
+ * @param zipData - ZIP file data as Buffer or Uint8Array
94
+ * @returns Array of file paths
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * import { listFiles } from "./utils/unzip/extract.js";
99
+ *
100
+ * const zipData = fs.readFileSync("archive.zip");
101
+ * const paths = await listFiles(zipData);
102
+ * console.log(paths); // ["file1.txt", "folder/file2.txt", ...]
103
+ * ```
104
+ */
105
+ export async function listFiles(zipData) {
106
+ const paths = [];
107
+ const buffer = Buffer.isBuffer(zipData) ? zipData : Buffer.from(zipData);
108
+ const parse = createParse({ forceStream: true });
109
+ const stream = Readable.from([buffer]);
110
+ stream.pipe(parse);
111
+ for await (const entry of parse) {
112
+ const zipEntry = entry;
113
+ paths.push(zipEntry.path);
114
+ zipEntry.autodrain();
115
+ }
116
+ return paths;
117
+ }
118
+ /**
119
+ * Iterate over ZIP entries with a callback (memory efficient for large ZIPs)
120
+ *
121
+ * @param zipData - ZIP file data as Buffer or Uint8Array
122
+ * @param callback - Async callback for each entry, return false to stop iteration
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * import { forEachEntry } from "./utils/unzip/extract.js";
127
+ *
128
+ * await forEachEntry(zipData, async (path, getData) => {
129
+ * if (path.endsWith(".xml")) {
130
+ * const content = await getData();
131
+ * console.log(content.toString("utf-8"));
132
+ * }
133
+ * return true; // continue iteration
134
+ * });
135
+ * ```
136
+ */
137
+ export async function forEachEntry(zipData, callback) {
138
+ const buffer = Buffer.isBuffer(zipData) ? zipData : Buffer.from(zipData);
139
+ const parse = createParse({ forceStream: true });
140
+ const stream = Readable.from([buffer]);
141
+ stream.pipe(parse);
142
+ for await (const entry of parse) {
143
+ const zipEntry = entry;
144
+ let dataPromise = null;
145
+ const getData = () => {
146
+ if (!dataPromise) {
147
+ dataPromise = zipEntry.buffer();
148
+ }
149
+ return dataPromise;
150
+ };
151
+ const shouldContinue = await callback(zipEntry.path, getData, zipEntry);
152
+ // If callback didn't read data, drain it
153
+ if (!dataPromise) {
154
+ zipEntry.autodrain();
155
+ }
156
+ if (shouldContinue === false) {
157
+ break;
158
+ }
159
+ }
160
+ }
@@ -10,3 +10,5 @@ export { bufferStream } from "./buffer-stream.js";
10
10
  export { parse as parseBuffer } from "./parse-buffer.js";
11
11
  export { parseDateTime } from "./parse-datetime.js";
12
12
  export { parseExtraField } from "./parse-extra-field.js";
13
+ // Simple extraction API
14
+ export { extractAll, extractFile, listFiles, forEachEntry } from "./extract.js";
@@ -4,6 +4,8 @@ const OPEN_ANGLE = "<";
4
4
  const CLOSE_ANGLE = ">";
5
5
  const OPEN_ANGLE_SLASH = "</";
6
6
  const CLOSE_SLASH_ANGLE = "/>";
7
+ // Chunk size for periodic consolidation (reduces final join overhead)
8
+ const CHUNK_SIZE = 10000;
7
9
  function pushAttribute(xml, name, value) {
8
10
  xml.push(` ${name}="${xmlEncode(value.toString())}"`);
9
11
  }
@@ -21,15 +23,23 @@ function pushAttributes(xml, attributes) {
21
23
  class XmlStream {
22
24
  constructor() {
23
25
  this._xml = [];
26
+ this._chunks = [];
24
27
  this._stack = [];
25
28
  this._rollbacks = [];
26
29
  }
30
+ _consolidate() {
31
+ // Periodically join small strings into larger chunks to reduce final join overhead
32
+ if (this._xml.length >= CHUNK_SIZE) {
33
+ this._chunks.push(this._xml.join(""));
34
+ this._xml = [];
35
+ }
36
+ }
27
37
  get tos() {
28
38
  return this._stack.length ? this._stack[this._stack.length - 1] : undefined;
29
39
  }
30
40
  get cursor() {
31
41
  // handy way to track whether anything has been added
32
- return this._xml.length;
42
+ return this._chunks.length * CHUNK_SIZE + this._xml.length;
33
43
  }
34
44
  openXml(docAttributes) {
35
45
  const xml = this._xml;
@@ -96,6 +106,7 @@ class XmlStream {
96
106
  }
97
107
  this.open = false;
98
108
  this.leaf = false;
109
+ this._consolidate();
99
110
  }
100
111
  leafNode(name, attributes, text) {
101
112
  this.openNode(name, attributes);
@@ -115,7 +126,8 @@ class XmlStream {
115
126
  xml: this._xml.length,
116
127
  stack: this._stack.length,
117
128
  leaf: this.leaf,
118
- open: this.open
129
+ open: this.open,
130
+ chunksLength: this._chunks.length
119
131
  });
120
132
  return this.cursor;
121
133
  }
@@ -130,12 +142,22 @@ class XmlStream {
130
142
  if (this._stack.length > r.stack) {
131
143
  this._stack.splice(r.stack, this._stack.length - r.stack);
132
144
  }
145
+ if (this._chunks.length > r.chunksLength) {
146
+ this._chunks.splice(r.chunksLength, this._chunks.length - r.chunksLength);
147
+ }
133
148
  this.leaf = r.leaf;
134
149
  this.open = r.open;
135
150
  }
136
151
  get xml() {
137
152
  this.closeAll();
138
- return this._xml.join("");
153
+ // Join chunks first, then remaining xml array
154
+ if (this._chunks.length === 0) {
155
+ return this._xml.join("");
156
+ }
157
+ if (this._xml.length > 0) {
158
+ this._chunks.push(this._xml.join(""));
159
+ }
160
+ return this._chunks.join("");
139
161
  }
140
162
  }
141
163
  XmlStream.StdDocAttributes = {
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Native compression utilities using platform APIs
3
+ *
4
+ * - Node.js: Uses native zlib module (C++ implementation, fastest)
5
+ * - Browser: Uses CompressionStream API (Chrome 80+, Firefox 113+, Safari 16.4+)
6
+ *
7
+ * Both use "deflate-raw" format which is required for ZIP files
8
+ * (raw DEFLATE without zlib header/trailer)
9
+ */
10
+ // Detect environment
11
+ const isNode = typeof process !== "undefined" && process.versions?.node;
12
+ // Lazy-loaded zlib module for Node.js
13
+ let _zlib = null;
14
+ let _zlibLoading = null;
15
+ // Auto-initialize zlib in Node.js environment
16
+ if (isNode) {
17
+ _zlibLoading = import("zlib")
18
+ .then(module => {
19
+ _zlib = module.default ?? module;
20
+ return _zlib;
21
+ })
22
+ .catch(() => {
23
+ _zlib = null;
24
+ return null;
25
+ });
26
+ }
27
+ /**
28
+ * Get zlib module (Node.js only)
29
+ * Returns null if not yet loaded or not in Node.js
30
+ */
31
+ function getZlib() {
32
+ return _zlib;
33
+ }
34
+ /**
35
+ * Ensure zlib is loaded (Node.js only)
36
+ * Call this before using sync methods if you need to guarantee availability
37
+ */
38
+ export async function ensureZlib() {
39
+ if (_zlibLoading) {
40
+ return _zlibLoading;
41
+ }
42
+ return _zlib;
43
+ }
44
+ /**
45
+ * Check if native zlib is available (Node.js)
46
+ */
47
+ export function hasNativeZlib() {
48
+ const zlib = getZlib();
49
+ return zlib !== null && typeof zlib.deflateRawSync === "function";
50
+ }
51
+ /**
52
+ * Check if CompressionStream is available (Browser/Node.js 17+)
53
+ */
54
+ export function hasCompressionStream() {
55
+ return typeof CompressionStream !== "undefined";
56
+ }
57
+ /**
58
+ * Compress data using the best available native method
59
+ *
60
+ * Priority:
61
+ * 1. Node.js zlib (if available) - fastest, supports compression levels
62
+ * 2. CompressionStream (browser/Node.js 17+) - no level support
63
+ * 3. Return uncompressed data (fallback)
64
+ *
65
+ * @param data - Data to compress
66
+ * @param options - Compression options
67
+ * @returns Compressed data
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * const data = new TextEncoder().encode("Hello, World!");
72
+ * const compressed = await compress(data, { level: 6 });
73
+ * ```
74
+ */
75
+ export async function compress(data, options = {}) {
76
+ const level = options.level ?? 6;
77
+ // Level 0 means no compression
78
+ if (level === 0) {
79
+ return data;
80
+ }
81
+ // Ensure zlib is loaded first
82
+ const zlib = await ensureZlib();
83
+ // Try Node.js zlib first (fastest, supports levels)
84
+ if (zlib && typeof zlib.deflateRawSync === "function") {
85
+ const result = zlib.deflateRawSync(Buffer.from(data), { level });
86
+ return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
87
+ }
88
+ // Fall back to CompressionStream (browser/Node.js 17+)
89
+ if (typeof CompressionStream !== "undefined") {
90
+ return compressWithCompressionStream(data);
91
+ }
92
+ // No compression available - return original data
93
+ console.warn("No native compression available, returning uncompressed data");
94
+ return data;
95
+ }
96
+ /**
97
+ * Compress data synchronously using Node.js zlib
98
+ * Only available in Node.js environment
99
+ *
100
+ * @param data - Data to compress
101
+ * @param options - Compression options
102
+ * @returns Compressed data
103
+ * @throws Error if not in Node.js environment
104
+ */
105
+ export function compressSync(data, options = {}) {
106
+ const level = options.level ?? 6;
107
+ if (level === 0) {
108
+ return data;
109
+ }
110
+ const zlib = getZlib();
111
+ if (!zlib || typeof zlib.deflateRawSync !== "function") {
112
+ throw new Error("Synchronous compression is only available in Node.js environment");
113
+ }
114
+ const result = zlib.deflateRawSync(Buffer.from(data), { level });
115
+ return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
116
+ }
117
+ /**
118
+ * Compress using browser's native CompressionStream
119
+ * Uses "deflate-raw" format (required for ZIP files)
120
+ *
121
+ * Note: CompressionStream does not support compression level configuration
122
+ *
123
+ * @param data - Data to compress
124
+ * @returns Compressed data
125
+ */
126
+ async function compressWithCompressionStream(data) {
127
+ const cs = new CompressionStream("deflate-raw");
128
+ const writer = cs.writable.getWriter();
129
+ const reader = cs.readable.getReader();
130
+ // Write data and close
131
+ writer.write(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
132
+ writer.close();
133
+ // Read all compressed chunks
134
+ const chunks = [];
135
+ let totalLength = 0;
136
+ while (true) {
137
+ const { done, value } = await reader.read();
138
+ if (done) {
139
+ break;
140
+ }
141
+ chunks.push(value);
142
+ totalLength += value.length;
143
+ }
144
+ // Combine chunks into single array
145
+ const result = new Uint8Array(totalLength);
146
+ let offset = 0;
147
+ for (const chunk of chunks) {
148
+ result.set(chunk, offset);
149
+ offset += chunk.length;
150
+ }
151
+ return result;
152
+ }
153
+ /**
154
+ * Decompress data using the best available native method
155
+ *
156
+ * @param data - Compressed data (deflate-raw format)
157
+ * @returns Decompressed data
158
+ */
159
+ export async function decompress(data) {
160
+ // Ensure zlib is loaded first
161
+ const zlib = await ensureZlib();
162
+ // Try Node.js zlib first
163
+ if (zlib && typeof zlib.inflateRawSync === "function") {
164
+ const result = zlib.inflateRawSync(Buffer.from(data));
165
+ return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
166
+ }
167
+ // Fall back to DecompressionStream
168
+ if (typeof DecompressionStream !== "undefined") {
169
+ return decompressWithDecompressionStream(data);
170
+ }
171
+ throw new Error("No native decompression available");
172
+ }
173
+ /**
174
+ * Decompress data synchronously using Node.js zlib
175
+ *
176
+ * @param data - Compressed data (deflate-raw format)
177
+ * @returns Decompressed data
178
+ * @throws Error if not in Node.js environment
179
+ */
180
+ export function decompressSync(data) {
181
+ const zlib = getZlib();
182
+ if (!zlib || typeof zlib.inflateRawSync !== "function") {
183
+ throw new Error("Synchronous decompression is only available in Node.js environment");
184
+ }
185
+ const result = zlib.inflateRawSync(Buffer.from(data));
186
+ return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
187
+ }
188
+ /**
189
+ * Decompress using browser's native DecompressionStream
190
+ *
191
+ * @param data - Compressed data (deflate-raw format)
192
+ * @returns Decompressed data
193
+ */
194
+ async function decompressWithDecompressionStream(data) {
195
+ const ds = new DecompressionStream("deflate-raw");
196
+ const writer = ds.writable.getWriter();
197
+ const reader = ds.readable.getReader();
198
+ // Write data and close
199
+ writer.write(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
200
+ writer.close();
201
+ // Read all decompressed chunks
202
+ const chunks = [];
203
+ let totalLength = 0;
204
+ while (true) {
205
+ const { done, value } = await reader.read();
206
+ if (done) {
207
+ break;
208
+ }
209
+ chunks.push(value);
210
+ totalLength += value.length;
211
+ }
212
+ // Combine chunks into single array
213
+ const result = new Uint8Array(totalLength);
214
+ let offset = 0;
215
+ for (const chunk of chunks) {
216
+ result.set(chunk, offset);
217
+ offset += chunk.length;
218
+ }
219
+ return result;
220
+ }