@cj-tech-master/excelts 6.2.0 → 7.0.0-canary.20260329132155.5bf7cee

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 (65) hide show
  1. package/README.md +45 -17
  2. package/README_zh.md +43 -15
  3. package/dist/browser/index.browser.d.ts +1 -1
  4. package/dist/browser/index.browser.js +1 -1
  5. package/dist/browser/index.d.ts +2 -2
  6. package/dist/browser/index.js +1 -1
  7. package/dist/browser/modules/archive/unzip/stream.base.js +124 -8
  8. package/dist/browser/modules/archive/unzip/stream.browser.js +126 -11
  9. package/dist/browser/modules/archive/unzip/stream.js +149 -57
  10. package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +0 -2
  11. package/dist/browser/modules/excel/stream/workbook-writer.d.ts +2 -2
  12. package/dist/browser/modules/excel/types.d.ts +0 -2
  13. package/dist/browser/modules/pdf/excel-bridge.d.ts +29 -0
  14. package/dist/browser/modules/pdf/excel-bridge.js +423 -0
  15. package/dist/browser/modules/pdf/index.d.ts +22 -24
  16. package/dist/browser/modules/pdf/index.js +22 -25
  17. package/dist/browser/modules/pdf/pdf.d.ts +121 -0
  18. package/dist/browser/modules/pdf/pdf.js +255 -0
  19. package/dist/browser/modules/pdf/render/layout-engine.d.ts +10 -8
  20. package/dist/browser/modules/pdf/render/layout-engine.js +115 -209
  21. package/dist/browser/modules/pdf/render/pdf-exporter.d.ts +9 -62
  22. package/dist/browser/modules/pdf/render/pdf-exporter.js +38 -78
  23. package/dist/browser/modules/pdf/render/style-converter.d.ts +20 -18
  24. package/dist/browser/modules/pdf/render/style-converter.js +24 -23
  25. package/dist/browser/modules/pdf/types.d.ts +193 -11
  26. package/dist/browser/modules/pdf/types.js +22 -1
  27. package/dist/cjs/index.js +3 -3
  28. package/dist/cjs/modules/archive/unzip/stream.base.js +124 -8
  29. package/dist/cjs/modules/archive/unzip/stream.browser.js +126 -11
  30. package/dist/cjs/modules/archive/unzip/stream.js +149 -57
  31. package/dist/cjs/modules/pdf/excel-bridge.js +426 -0
  32. package/dist/cjs/modules/pdf/index.js +25 -28
  33. package/dist/cjs/modules/pdf/pdf.js +258 -0
  34. package/dist/cjs/modules/pdf/render/layout-engine.js +116 -210
  35. package/dist/cjs/modules/pdf/render/pdf-exporter.js +37 -79
  36. package/dist/cjs/modules/pdf/render/style-converter.js +24 -23
  37. package/dist/cjs/modules/pdf/types.js +23 -2
  38. package/dist/esm/index.browser.js +1 -1
  39. package/dist/esm/index.js +1 -1
  40. package/dist/esm/modules/archive/unzip/stream.base.js +124 -8
  41. package/dist/esm/modules/archive/unzip/stream.browser.js +126 -11
  42. package/dist/esm/modules/archive/unzip/stream.js +149 -57
  43. package/dist/esm/modules/pdf/excel-bridge.js +423 -0
  44. package/dist/esm/modules/pdf/index.js +22 -25
  45. package/dist/esm/modules/pdf/pdf.js +255 -0
  46. package/dist/esm/modules/pdf/render/layout-engine.js +115 -209
  47. package/dist/esm/modules/pdf/render/pdf-exporter.js +38 -78
  48. package/dist/esm/modules/pdf/render/style-converter.js +24 -23
  49. package/dist/esm/modules/pdf/types.js +22 -1
  50. package/dist/iife/excelts.iife.js +918 -267
  51. package/dist/iife/excelts.iife.js.map +1 -1
  52. package/dist/iife/excelts.iife.min.js +34 -34
  53. package/dist/types/index.browser.d.ts +1 -1
  54. package/dist/types/index.d.ts +2 -2
  55. package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +0 -2
  56. package/dist/types/modules/excel/stream/workbook-writer.d.ts +2 -2
  57. package/dist/types/modules/excel/types.d.ts +0 -2
  58. package/dist/types/modules/pdf/excel-bridge.d.ts +29 -0
  59. package/dist/types/modules/pdf/index.d.ts +22 -24
  60. package/dist/types/modules/pdf/pdf.d.ts +121 -0
  61. package/dist/types/modules/pdf/render/layout-engine.d.ts +10 -8
  62. package/dist/types/modules/pdf/render/pdf-exporter.d.ts +9 -62
  63. package/dist/types/modules/pdf/render/style-converter.d.ts +20 -18
  64. package/dist/types/modules/pdf/types.d.ts +193 -11
  65. package/package.json +1 -1
package/README.md CHANGED
@@ -6,15 +6,12 @@ Modern TypeScript Excel Workbook Manager - Read, manipulate and write spreadshee
6
6
 
7
7
  ## About This Project
8
8
 
9
- ExcelTS is a modern TypeScript Excel workbook manager with:
9
+ ExcelTS is a zero-dependency TypeScript toolkit for spreadsheets and documents:
10
10
 
11
- - 🚀 **Zero Runtime Dependencies** - Pure TypeScript implementation with no external packages
12
- - **Broad Runtime Support** - LTS Node.js, Bun, and modern browsers (Chrome, Firefox, Safari, Edge)
13
- - ✅ **Full TypeScript Support** - Complete type definitions and modern TypeScript patterns
14
- - ✅ **Modern Build System** - Using Rolldown for faster builds
15
- - ✅ **Enhanced Testing** - Migrated to Vitest with browser testing support
16
- - ✅ **ESM First** - Native ES Module support with CommonJS compatibility
17
- - ✅ **Named Exports** - All exports are named for better tree-shaking
11
+ - 🚀 **Zero Runtime Dependencies** Pure TypeScript, no external packages
12
+ - 📦 **Five Modules** Excel (XLSX/JSON), PDF (standalone engine + Excel bridge), CSV (RFC 4180), Archive (ZIP/TAR), Stream (cross-platform)
13
+ - ✅ **Cross-Platform** Node.js 22+, Bun, Chrome 89+, Firefox 102+, Safari 14.1+
14
+ - ✅ **ESM First** Native ES Modules with CommonJS compatibility and full tree-shaking
18
15
 
19
16
  ## Translations
20
17
 
@@ -104,7 +101,7 @@ cell.fill = {
104
101
  - Pivot tables
105
102
 
106
103
  - **PDF Export**
107
- - Zero-dependency Excel-to-PDF conversion
104
+ - Full-featured, zero-dependency PDF engine (standalone or with Excel)
108
105
  - Full cell styling (fonts, colors, borders, fills, alignment)
109
106
  - Automatic pagination with repeat header rows
110
107
  - TrueType font embedding for Unicode/CJK text
@@ -141,7 +138,7 @@ import { Readable, pipeline, createTransform } from "@cj-tech-master/excelts/str
141
138
 
142
139
  Each subpath supports `browser`, `import` (ESM), and `require` (CJS) conditions. See the module READMEs for details:
143
140
 
144
- - [PDF Module](src/modules/pdf/README.md) - Zero-dependency Excel-to-PDF export with encryption and font embedding
141
+ - [PDF Module](src/modules/pdf/README.md) - Full-featured zero-dependency PDF engine with encryption and font embedding
145
142
  - [CSV Module](src/modules/csv/README.md) - RFC 4180 parser/formatter, streaming, data generation
146
143
  - [Archive Module](src/modules/archive/README.md) - ZIP/TAR create/read/edit, compression, encryption
147
144
  - [Stream Module](src/modules/stream/README.md) - Cross-platform Readable/Writable/Transform/Duplex
@@ -151,7 +148,7 @@ Each subpath supports `browser`, `import` (ESM), and `require` (CJS) conditions.
151
148
  Export any workbook to PDF with zero external dependencies:
152
149
 
153
150
  ```javascript
154
- import { Workbook, exportPdf } from "@cj-tech-master/excelts";
151
+ import { Workbook, excelToPdf } from "@cj-tech-master/excelts";
155
152
 
156
153
  const workbook = new Workbook();
157
154
  const sheet = workbook.addWorksheet("Report");
@@ -163,7 +160,7 @@ sheet.addRow({ product: "Widget", revenue: 1000 });
163
160
  sheet.getColumn("revenue").numFmt = "$#,##0.00";
164
161
 
165
162
  // One-line export
166
- const pdf = exportPdf(workbook, {
163
+ const pdf = excelToPdf(workbook, {
167
164
  showGridLines: true,
168
165
  showPageNumbers: true,
169
166
  title: "Sales Report"
@@ -184,13 +181,13 @@ window.open(url);
184
181
  ```javascript
185
182
  const workbook = new Workbook();
186
183
  await workbook.xlsx.readFile("input.xlsx");
187
- const pdf = exportPdf(workbook);
184
+ const pdf = excelToPdf(workbook);
188
185
  ```
189
186
 
190
187
  ### Encryption
191
188
 
192
189
  ```javascript
193
- const pdf = exportPdf(workbook, {
190
+ const pdf = excelToPdf(workbook, {
194
191
  encryption: {
195
192
  ownerPassword: "admin",
196
193
  userPassword: "reader",
@@ -204,11 +201,42 @@ const pdf = exportPdf(workbook, {
204
201
  ```javascript
205
202
  import { readFileSync } from "fs";
206
203
 
207
- const pdf = exportPdf(workbook, {
204
+ const pdf = excelToPdf(workbook, {
208
205
  font: readFileSync("NotoSansSC-Regular.ttf") // TrueType font for CJK text
209
206
  });
210
207
  ```
211
208
 
209
+ ### Standalone PDF (No Excel)
210
+
211
+ Generate PDFs from plain data — no workbook, no Map objects, no boilerplate:
212
+
213
+ ```javascript
214
+ import { pdf } from "@cj-tech-master/excelts/pdf";
215
+
216
+ // Simplest — pass a 2D array
217
+ const bytes = pdf([
218
+ ["Product", "Revenue"],
219
+ ["Widget", 1000],
220
+ ["Gadget", 2500]
221
+ ]);
222
+
223
+ // With column widths and styled cells
224
+ const bytes = pdf(
225
+ {
226
+ name: "Report",
227
+ columns: [
228
+ { width: 25, header: "Product" },
229
+ { width: 15, header: "Revenue" }
230
+ ],
231
+ data: [
232
+ ["Widget", 1000],
233
+ ["Gadget", 2500]
234
+ ]
235
+ },
236
+ { showGridLines: true }
237
+ );
238
+ ```
239
+
212
240
  For the full API reference and all options, see the [PDF Module documentation](src/modules/pdf/README.md).
213
241
 
214
242
  ## Archive Utilities (ZIP/TAR)
@@ -506,8 +534,8 @@ import {
506
534
  xmlDecode,
507
535
 
508
536
  // PDF export
509
- exportPdf, // Workbook -> Uint8Array (PDF)
510
- PdfExporter, // Class-based PDF export
537
+ pdf, // Simplest: pdf([["A", 1], ["B", 2]]) → Uint8Array
538
+ excelToPdf, // Workbook -> Uint8Array (Excel-to-PDF)
511
539
  PageSizes, // Built-in page size definitions
512
540
  PdfError, // Base PDF error
513
541
  PdfRenderError, // Layout/rendering failures
package/README_zh.md CHANGED
@@ -6,15 +6,12 @@
6
6
 
7
7
  ## 关于本项目
8
8
 
9
- ExcelTS 是现代化的 TypeScript Excel 工作簿管理器,具有以下特性:
9
+ ExcelTS 是零依赖的 TypeScript 电子表格与文档工具包:
10
10
 
11
- - 🚀 **零运行时依赖** - 纯 TypeScript 实现,无任何外部包依赖
12
- - **广泛运行时支持** - 支持 LTS Node.js、Bun 及主流最新浏览器(Chrome、Firefox、Safari、Edge)
13
- - ✅ **完整的 TypeScript 支持** - 完整的类型定义和现代 TypeScript 模式
14
- - ✅ **现代构建系统** - 使用 Rolldown 进行更快的构建
15
- - ✅ **增强的测试** - 迁移到 Vitest 并支持浏览器测试
16
- - ✅ **ESM 优先** - 原生 ES Module 支持,兼容 CommonJS
17
- - ✅ **命名导出** - 所有导出都是命名导出,更好的 tree-shaking
11
+ - 🚀 **零运行时依赖** 纯 TypeScript 实现,无任何外部包依赖
12
+ - 📦 **五大模块** Excel(XLSX/JSON)、PDF(独立引擎 + Excel 桥接)、CSV(RFC 4180)、Archive(ZIP/TAR)、Stream(跨平台)
13
+ - ✅ **跨平台** Node.js 22+、Bun、Chrome 89+、Firefox 102+、Safari 14.1+
14
+ - ✅ **ESM 优先** 原生 ES Modules,兼容 CommonJS,完整 tree-shaking 支持
18
15
 
19
16
  ## 翻译
20
17
 
@@ -151,7 +148,7 @@ import { Readable, pipeline, createTransform } from "@cj-tech-master/excelts/str
151
148
  零依赖将任意工作簿导出为 PDF:
152
149
 
153
150
  ```javascript
154
- import { Workbook, exportPdf } from "@cj-tech-master/excelts";
151
+ import { Workbook, excelToPdf } from "@cj-tech-master/excelts";
155
152
 
156
153
  const workbook = new Workbook();
157
154
  const sheet = workbook.addWorksheet("报告");
@@ -163,7 +160,7 @@ sheet.addRow({ product: "组件A", revenue: 1000 });
163
160
  sheet.getColumn("revenue").numFmt = "¥#,##0.00";
164
161
 
165
162
  // 一行导出
166
- const pdf = exportPdf(workbook, {
163
+ const pdf = excelToPdf(workbook, {
167
164
  showGridLines: true,
168
165
  showPageNumbers: true,
169
166
  title: "销售报告"
@@ -184,13 +181,13 @@ window.open(url);
184
181
  ```javascript
185
182
  const workbook = new Workbook();
186
183
  await workbook.xlsx.readFile("input.xlsx");
187
- const pdf = exportPdf(workbook);
184
+ const pdf = excelToPdf(workbook);
188
185
  ```
189
186
 
190
187
  ### 加密
191
188
 
192
189
  ```javascript
193
- const pdf = exportPdf(workbook, {
190
+ const pdf = excelToPdf(workbook, {
194
191
  encryption: {
195
192
  ownerPassword: "admin",
196
193
  userPassword: "reader",
@@ -204,11 +201,42 @@ const pdf = exportPdf(workbook, {
204
201
  ```javascript
205
202
  import { readFileSync } from "fs";
206
203
 
207
- const pdf = exportPdf(workbook, {
204
+ const pdf = excelToPdf(workbook, {
208
205
  font: readFileSync("NotoSansSC-Regular.ttf") // 嵌入 TrueType 字体以支持中文
209
206
  });
210
207
  ```
211
208
 
209
+ ### 独立 PDF 生成(无需 Excel)
210
+
211
+ 无需工作簿,直接从数组数据生成 PDF:
212
+
213
+ ```javascript
214
+ import { pdf } from "@cj-tech-master/excelts/pdf";
215
+
216
+ // 最简方式 — 传入二维数组
217
+ const bytes = pdf([
218
+ ["产品", "收入"],
219
+ ["组件A", 1000],
220
+ ["组件B", 2500]
221
+ ]);
222
+
223
+ // 带列宽和样式
224
+ const bytes = pdf(
225
+ {
226
+ name: "报告",
227
+ columns: [
228
+ { width: 25, header: "产品" },
229
+ { width: 15, header: "收入" }
230
+ ],
231
+ data: [
232
+ ["组件A", 1000],
233
+ ["组件B", 2500]
234
+ ]
235
+ },
236
+ { showGridLines: true }
237
+ );
238
+ ```
239
+
212
240
  完整 API 参考和所有选项请查看 [PDF 模块文档](src/modules/pdf/README.md)。
213
241
 
214
242
  ## 归档工具(ZIP/TAR)
@@ -504,8 +532,8 @@ import {
504
532
  xmlDecode,
505
533
 
506
534
  // PDF 导出
507
- exportPdf, // Workbook -> Uint8Array (PDF)
508
- PdfExporter, // 基于类的 PDF 导出
535
+ pdf, // 最简单:pdf([["A", 1], ["B", 2]]) → Uint8Array
536
+ excelToPdf, // Workbook -> Uint8Array(Excel 转 PDF
509
537
  PageSizes, // 内置页面尺寸定义
510
538
  PdfError, // PDF 基础错误
511
539
  PdfRenderError, // 布局/渲染错误
@@ -37,6 +37,6 @@ export { xmlEncode, xmlDecode } from "./utils/utils.base.js";
37
37
  export { DateParser, DateFormatter, getSupportedFormats, type DateFormat } from "./utils/datetime.js";
38
38
  export { BaseError, type BaseErrorOptions, toError, errorToJSON, getErrorChain, getRootCause } from "./utils/errors.js";
39
39
  export { concatUint8Arrays, toUint8Array, stringToUint8Array, uint8ArrayToString } from "./utils/binary.js";
40
- export { PdfExporter, exportPdf, PageSizes, PdfError, PdfRenderError, PdfFontError, PdfStructureError, isPdfError } from "./modules/pdf/index.js";
40
+ export { pdf, excelToPdf, PageSizes, PdfError, PdfRenderError, PdfFontError, PdfStructureError, isPdfError } from "./modules/pdf/index.js";
41
41
  export type { PdfExportOptions, PdfPageSize, PdfOrientation, PdfMargins, PageSizeName, PdfColor } from "./modules/pdf/index.js";
42
42
  export { ExcelError, isExcelError, ExcelFileError, ExcelDownloadError, ExcelNotSupportedError, ExcelStreamStateError, InvalidAddressError, ColumnOutOfBoundsError, RowOutOfBoundsError, MergeConflictError, InvalidValueTypeError, XmlParseError, WorksheetNameError, PivotTableError, TableError, ImageError, MaxItemsExceededError } from "./modules/excel/errors.js";
@@ -58,7 +58,7 @@ export { concatUint8Arrays, toUint8Array, stringToUint8Array, uint8ArrayToString
58
58
  // =============================================================================
59
59
  // PDF Export (Browser-compatible, zero external dependencies)
60
60
  // =============================================================================
61
- export { PdfExporter, exportPdf, PageSizes, PdfError, PdfRenderError, PdfFontError, PdfStructureError, isPdfError } from "./modules/pdf/index.js";
61
+ export { pdf, excelToPdf, PageSizes, PdfError, PdfRenderError, PdfFontError, PdfStructureError, isPdfError } from "./modules/pdf/index.js";
62
62
  // =============================================================================
63
63
  // Errors
64
64
  // =============================================================================
@@ -19,7 +19,7 @@ export type { PivotTable, PivotTableModel, PivotTableValue, PivotTableSource, Ca
19
19
  export type { FormCheckboxModel, FormCheckboxOptions, FormControlRange, FormControlAnchor } from "./modules/excel/form-control.js";
20
20
  export type { WorkbookReaderOptions, ParseEvent, SharedStringEvent, WorksheetReadyEvent, HyperlinksEvent } from "./modules/excel/stream/workbook-reader.js";
21
21
  export type { WorksheetReaderOptions, WorksheetEvent, RowEvent, HyperlinkEvent, WorksheetHyperlink } from "./modules/excel/stream/worksheet-reader.js";
22
- export type { WorkbookWriterOptions, WorkbookZipOptions, ZipOptions, ZlibOptions } from "./modules/excel/stream/workbook-writer.js";
22
+ export type { WorkbookWriterOptions, WorkbookZipOptions, ZlibOptions } from "./modules/excel/stream/workbook-writer.js";
23
23
  export type { CsvOptions, CsvInput } from "./modules/excel/workbook.js";
24
24
  export { CsvParserStream, CsvFormatterStream, createCsvParserStream, createCsvFormatterStream } from "./modules/csv/stream/index.js";
25
25
  export { DefinedNames, type DefinedNameModel } from "./modules/excel/defined-names.js";
@@ -37,6 +37,6 @@ export { xmlEncode, xmlDecode } from "./utils/utils.base.js";
37
37
  export { DateParser, DateFormatter, getSupportedFormats, type DateFormat } from "./utils/datetime.js";
38
38
  export { BaseError, type BaseErrorOptions, toError, errorToJSON, getErrorChain, getRootCause } from "./utils/errors.js";
39
39
  export { concatUint8Arrays, toUint8Array, stringToUint8Array, uint8ArrayToString } from "./utils/binary.js";
40
- export { PdfExporter, exportPdf, PageSizes, PdfError, PdfRenderError, PdfFontError, PdfStructureError, isPdfError } from "./modules/pdf/index.js";
40
+ export { pdf, excelToPdf, PageSizes, PdfError, PdfRenderError, PdfFontError, PdfStructureError, isPdfError } from "./modules/pdf/index.js";
41
41
  export type { PdfExportOptions, PdfPageSize, PdfOrientation, PdfMargins, PageSizeName, PdfColor } from "./modules/pdf/index.js";
42
42
  export { ExcelError, isExcelError, ExcelFileError, ExcelDownloadError, ExcelNotSupportedError, ExcelStreamStateError, InvalidAddressError, ColumnOutOfBoundsError, RowOutOfBoundsError, MergeConflictError, InvalidValueTypeError, XmlParseError, WorksheetNameError, PivotTableError, TableError, ImageError, MaxItemsExceededError } from "./modules/excel/errors.js";
@@ -54,7 +54,7 @@ export { concatUint8Arrays, toUint8Array, stringToUint8Array, uint8ArrayToString
54
54
  // =============================================================================
55
55
  // PDF Export
56
56
  // =============================================================================
57
- export { PdfExporter, exportPdf, PageSizes, PdfError, PdfRenderError, PdfFontError, PdfStructureError, isPdfError } from "./modules/pdf/index.js";
57
+ export { pdf, excelToPdf, PageSizes, PdfError, PdfRenderError, PdfFontError, PdfStructureError, isPdfError } from "./modules/pdf/index.js";
58
58
  // =============================================================================
59
59
  // Errors
60
60
  // =============================================================================
@@ -4,6 +4,46 @@ import { ByteQueue } from "../shared/byte-queue.js";
4
4
  import { EMPTY_UINT8ARRAY } from "../shared/bytes.js";
5
5
  import { decodeZipPath, resolveZipStringCodec } from "../shared/text.js";
6
6
  import { PatternScanner } from "./pattern-scanner.js";
7
+ /**
8
+ * Returns true when `err` is the Node.js ERR_STREAM_PREMATURE_CLOSE error.
9
+ *
10
+ * This error is emitted by `finished()` / `pipeline()` when a stream is
11
+ * destroyed before it has properly ended (e.g. a consumer breaks out of a
12
+ * `for await` loop, or the entry PassThrough is destroyed by an external
13
+ * consumer). In the context of ZIP parsing, a premature close on an *entry*
14
+ * stream is not a fatal error — the parse loop only needs to advance the ZIP
15
+ * cursor past the entry's compressed data.
16
+ */
17
+ function isPrematureCloseError(err) {
18
+ if (!(err instanceof Error)) {
19
+ return false;
20
+ }
21
+ return err.code === "ERR_STREAM_PREMATURE_CLOSE" || err.message === "Premature close";
22
+ }
23
+ /**
24
+ * Wait for an entry's writable side to finish, tolerating premature close.
25
+ *
26
+ * The parse loop calls this after pumping all compressed data into an entry.
27
+ * It ensures the decompressed data has been flushed through the inflater →
28
+ * entry pipeline before advancing the ZIP cursor.
29
+ *
30
+ * If the consumer has already destroyed / autodraining the entry (e.g. early
31
+ * break, external destroy, Readable.from() wrapper), `finished()` rejects
32
+ * with ERR_STREAM_PREMATURE_CLOSE. This is not an error for the parse loop
33
+ * — the compressed data has been fully read from the ZIP cursor, so we
34
+ * can safely continue.
35
+ */
36
+ async function awaitEntryCompletion(entry) {
37
+ try {
38
+ await finished(entry, { readable: false });
39
+ }
40
+ catch (err) {
41
+ if (!isPrematureCloseError(err)) {
42
+ throw err;
43
+ }
44
+ // Entry was destroyed/autodraining — treat as normal completion.
45
+ }
46
+ }
7
47
  import { DEFAULT_PARSE_THRESHOLD_BYTES, buildZipEntryProps, getZipEntryType, hasDataDescriptorFlag, isFileSizeKnown, parseExtraField, readDataDescriptor, readLocalFileHeader, resolveZipEntryLastModifiedDateTime, runParseLoopCore, isValidZipRecordSignature } from "./parser-core.js";
8
48
  export const DEFAULT_UNZIP_STREAM_HIGH_WATER_MARK = 256 * 1024;
9
49
  export function autodrain(stream) {
@@ -415,6 +455,7 @@ export function streamUntilValidatedDataDescriptor(options) {
415
455
  }
416
456
  while (available > 0) {
417
457
  // Try to find and validate a descriptor candidate.
458
+ let pendingCandidate = false;
418
459
  while (true) {
419
460
  const idx = scanner.find(source);
420
461
  if (idx === -1) {
@@ -472,15 +513,67 @@ export function streamUntilValidatedDataDescriptor(options) {
472
513
  scanner.searchFrom = idx + 1;
473
514
  continue;
474
515
  }
475
- // Not enough bytes to validate yet. Re-check this candidate once more bytes arrive.
516
+ // Not enough bytes to validate yet. Re-check this candidate once
517
+ // more bytes arrive. Mark as pending so we don't accidentally
518
+ // advance searchFrom past it via onNoMatch().
476
519
  scanner.searchFrom = idx;
520
+ pendingCandidate = true;
521
+ // If the source is finished (no more bytes will arrive), attempt a
522
+ // relaxed validation: accept the descriptor without checking the
523
+ // next-record signature. This handles the case where the descriptor
524
+ // is at the very end of the available data (e.g. the last entry in
525
+ // the ZIP, or the next-record header hasn't been fully buffered yet
526
+ // due to extreme input fragmentation).
527
+ if (source.isFinished() && idx + 16 <= available) {
528
+ const descriptorCompressedSize = source.peekUint32LE(idx + 8);
529
+ const expectedCompressedSize = (bytesEmitted + idx) >>> 0;
530
+ if (descriptorCompressedSize !== null &&
531
+ descriptorCompressedSize === expectedCompressedSize) {
532
+ // Descriptor compressed size matches — accept it.
533
+ if (idx > 0) {
534
+ if (source.peekChunks && source.discard) {
535
+ const parts = source.peekChunks(idx);
536
+ let written = 0;
537
+ for (const part of parts) {
538
+ output.write(part);
539
+ written += part.length;
540
+ }
541
+ if (written > 0) {
542
+ source.discard(written);
543
+ bytesEmitted += written;
544
+ scanner.onConsume(written);
545
+ }
546
+ }
547
+ else {
548
+ output.write(source.read(idx));
549
+ bytesEmitted += idx;
550
+ scanner.onConsume(idx);
551
+ }
552
+ }
553
+ done = true;
554
+ source.maybeReleaseWriteCallback?.();
555
+ cleanup();
556
+ output.end();
557
+ return;
558
+ }
559
+ }
477
560
  break;
478
561
  }
479
- // No validated match yet.
480
- scanner.onNoMatch(available);
562
+ // Only advance the scanner's search cursor when there is no pending
563
+ // candidate waiting for more bytes. Without this guard, onNoMatch()
564
+ // would move searchFrom past the candidate, causing it to be skipped
565
+ // and the entry data to be flushed — leading to FILE_ENDED.
566
+ if (!pendingCandidate) {
567
+ scanner.onNoMatch(available);
568
+ }
481
569
  // Flush most of the buffered data but keep a tail so a potential signature
482
570
  // split across chunks can still be detected/validated.
483
- const flushLen = Math.max(0, available - keepTailBytes);
571
+ // When a pending candidate exists, do NOT flush past it.
572
+ let maxFlush = available - keepTailBytes;
573
+ if (pendingCandidate) {
574
+ maxFlush = Math.min(maxFlush, scanner.searchFrom);
575
+ }
576
+ const flushLen = Math.max(0, maxFlush);
484
577
  if (flushLen > 0) {
485
578
  if (source.peekChunks && source.discard) {
486
579
  const parts = source.peekChunks(flushLen);
@@ -673,7 +766,7 @@ async function pumpKnownCompressedSizeToEntry(io, inflater, entry, compressedSiz
673
766
  inflater.end();
674
767
  }
675
768
  // Wait for all writes to complete (not for consumption).
676
- await finished(entry, { readable: false });
769
+ await awaitEntryCompletion(entry);
677
770
  }
678
771
  finally {
679
772
  inflater.removeListener("error", onError);
@@ -787,8 +880,8 @@ async function readFileRecord(opts, io, emitter, inflateFactory, state, threshol
787
880
  const compressedData = await io.pull(compressedSize);
788
881
  const decompressedData = inflateRawSync(compressedData);
789
882
  entry.end(decompressedData);
790
- // Wait for entry stream write to complete (not for read/consume)
791
- await finished(entry, { readable: false });
883
+ // Wait for entry stream write to complete (not for read/consume).
884
+ await awaitEntryCompletion(entry);
792
885
  return;
793
886
  }
794
887
  const inflater = needsInflate
@@ -807,7 +900,30 @@ async function readFileRecord(opts, io, emitter, inflateFactory, state, threshol
807
900
  }
808
901
  return;
809
902
  }
810
- await pipeline(io.streamUntilDataDescriptor(), inflater, entry);
903
+ // pipeline() destroys all streams if any stream errors or closes early.
904
+ // If the entry was destroyed by the consumer, pipeline rejects with
905
+ // ERR_STREAM_PREMATURE_CLOSE. This typically happens when the entry's
906
+ // writable side is force-destroyed and the entire parse operation is
907
+ // being torn down (abort/error).
908
+ try {
909
+ await pipeline(io.streamUntilDataDescriptor(), inflater, entry);
910
+ }
911
+ catch (pipelineErr) {
912
+ if (!isPrematureCloseError(pipelineErr)) {
913
+ throw pipelineErr;
914
+ }
915
+ // Entry was destroyed — attempt to read the data descriptor; if it
916
+ // fails (cursor misaligned), swallow the error since the entry was
917
+ // abandoned and the operation is ending.
918
+ try {
919
+ const dd = await readDataDescriptor(async (l) => io.pull(l));
920
+ entry.size = dd.uncompressedSize ?? 0;
921
+ }
922
+ catch {
923
+ // Cursor misaligned — not recoverable but not worth surfacing.
924
+ }
925
+ return;
926
+ }
811
927
  const dd = await readDataDescriptor(async (l) => io.pull(l));
812
928
  entry.size = dd.uncompressedSize ?? 0;
813
929
  }
@@ -488,7 +488,26 @@ export function createParseClass(createInflateRawFn) {
488
488
  this.finished = false;
489
489
  this._driverState = {};
490
490
  this._parsingDone = Promise.resolve();
491
+ // ---------------------------------------------------------------
492
+ // Parser completion — explicit deferred, independent of stream
493
+ // lifecycle events. Mirrors the Node.js Parse implementation.
494
+ // ---------------------------------------------------------------
495
+ this._parserDoneFlag = false;
496
+ this._parserError = null;
497
+ this._parserDeferred = null;
498
+ this._parserDonePromise = null;
499
+ // ---------------------------------------------------------------
500
+ // Entry queue — custom [Symbol.asyncIterator] reads from here.
501
+ // ---------------------------------------------------------------
502
+ this._entryQueue = [];
503
+ this._entryWaiter = null;
504
+ this._entriesDone = false;
491
505
  this._opts = opts;
506
+ // Route error events to the parser deferred.
507
+ this.on("error", (err) => {
508
+ this._rejectParserDeferred(err);
509
+ this._closeEntryQueue(err);
510
+ });
492
511
  // Default values are intentionally conservative to avoid memory spikes
493
512
  // when parsing large archives under slow consumers.
494
513
  const hi = Math.max(64 * 1024, opts.inputHighWaterMarkBytes ?? 2 * 1024 * 1024);
@@ -510,10 +529,13 @@ export function createParseClass(createInflateRawFn) {
510
529
  },
511
530
  pushEntry: (entry) => {
512
531
  this.push(entry);
532
+ this._enqueueEntry(entry);
513
533
  },
514
534
  // Browser version historically only pushed entries when forceStream=true.
515
535
  // Keep this behavior to avoid changing stream piping semantics.
516
536
  pushEntryIfPiped: (_entry) => {
537
+ // Always feed the entry queue regardless of pipe state.
538
+ this._enqueueEntry(_entry);
517
539
  return;
518
540
  },
519
541
  emitCrxHeader: (header) => {
@@ -522,12 +544,6 @@ export function createParseClass(createInflateRawFn) {
522
544
  },
523
545
  emitError: (err) => {
524
546
  this.__emittedError = err;
525
- // Ensure upstream writers don't hang waiting for a deferred write callback.
526
- if (this._writeCb) {
527
- const cb = this._writeCb;
528
- this._writeCb = undefined;
529
- cb(err);
530
- }
531
547
  this.emit("error", err);
532
548
  },
533
549
  emitClose: () => {
@@ -556,11 +572,22 @@ export function createParseClass(createInflateRawFn) {
556
572
  this._parsingDone = runParseLoop(this._opts, io, emitter, inflateFactory, this._driverState
557
573
  // No inflateRawSync - always use streaming DecompressionStream in browser
558
574
  );
559
- this._parsingDone.catch((e) => {
575
+ this._parsingDone.then(() => {
576
+ if (this.__emittedError) {
577
+ this._rejectParserDeferred(this.__emittedError);
578
+ this._closeEntryQueue(this.__emittedError);
579
+ }
580
+ else {
581
+ this._resolveParserDeferred();
582
+ this._closeEntryQueue();
583
+ }
584
+ }, (e) => {
560
585
  if (!this.__emittedError || this.__emittedError !== e) {
561
586
  this.__emittedError = e;
562
587
  this.emit("error", e);
563
588
  }
589
+ this._rejectParserDeferred(e);
590
+ this._closeEntryQueue(e);
564
591
  this.emit("close");
565
592
  });
566
593
  });
@@ -864,11 +891,99 @@ export function createParseClass(createInflateRawFn) {
864
891
  });
865
892
  }
866
893
  promise() {
867
- return new Promise((resolve, reject) => {
868
- this.on("finish", resolve);
869
- this.on("end", resolve);
870
- this.on("error", reject);
894
+ if (this._parserDoneFlag) {
895
+ return this._parserError ? Promise.reject(this._parserError) : Promise.resolve();
896
+ }
897
+ if (this._parserDonePromise) {
898
+ return this._parserDonePromise;
899
+ }
900
+ this._parserDonePromise = new Promise((resolve, reject) => {
901
+ this._parserDeferred = { resolve, reject };
871
902
  });
903
+ return this._parserDonePromise;
904
+ }
905
+ // ---------------------------------------------------------------
906
+ // Parser completion deferred
907
+ // ---------------------------------------------------------------
908
+ _resolveParserDeferred() {
909
+ if (this._parserDoneFlag) {
910
+ return;
911
+ }
912
+ this._parserDoneFlag = true;
913
+ if (this._parserDeferred) {
914
+ const { resolve } = this._parserDeferred;
915
+ this._parserDeferred = null;
916
+ resolve();
917
+ }
918
+ }
919
+ _rejectParserDeferred(err) {
920
+ if (this._parserDoneFlag) {
921
+ return;
922
+ }
923
+ this._parserDoneFlag = true;
924
+ this._parserError = err;
925
+ if (this._parserDeferred) {
926
+ const { reject } = this._parserDeferred;
927
+ this._parserDeferred = null;
928
+ reject(err);
929
+ }
930
+ }
931
+ // ---------------------------------------------------------------
932
+ // Entry queue management
933
+ // ---------------------------------------------------------------
934
+ _enqueueEntry(entry) {
935
+ if (this._entryWaiter) {
936
+ const { resolve } = this._entryWaiter;
937
+ this._entryWaiter = null;
938
+ resolve({ value: entry, done: false });
939
+ }
940
+ else {
941
+ this._entryQueue.push(entry);
942
+ }
943
+ }
944
+ _closeEntryQueue(err) {
945
+ this._entriesDone = true;
946
+ if (this._entryWaiter) {
947
+ const waiter = this._entryWaiter;
948
+ this._entryWaiter = null;
949
+ if (err) {
950
+ waiter.reject(err);
951
+ }
952
+ else {
953
+ waiter.resolve({ value: undefined, done: true });
954
+ }
955
+ }
956
+ }
957
+ // ---------------------------------------------------------------
958
+ // Custom async iterator
959
+ // ---------------------------------------------------------------
960
+ [Symbol.asyncIterator]() {
961
+ const iterator = {
962
+ next: () => {
963
+ if (this._entryQueue.length > 0) {
964
+ return Promise.resolve({ value: this._entryQueue.shift(), done: false });
965
+ }
966
+ if (this._entriesDone) {
967
+ if (this._parserError) {
968
+ return Promise.reject(this._parserError);
969
+ }
970
+ return Promise.resolve({ value: undefined, done: true });
971
+ }
972
+ return new Promise((resolve, reject) => {
973
+ this._entryWaiter = { resolve, reject };
974
+ });
975
+ },
976
+ return: () => {
977
+ this._entriesDone = true;
978
+ this._entryQueue.length = 0;
979
+ this._entryWaiter = null;
980
+ return Promise.resolve({ value: undefined, done: true });
981
+ },
982
+ [Symbol.asyncIterator]() {
983
+ return iterator;
984
+ }
985
+ };
986
+ return iterator;
872
987
  }
873
988
  };
874
989
  }