@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.
- package/README.md +45 -17
- package/README_zh.md +43 -15
- package/dist/browser/index.browser.d.ts +1 -1
- package/dist/browser/index.browser.js +1 -1
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/browser/modules/archive/unzip/stream.base.js +124 -8
- package/dist/browser/modules/archive/unzip/stream.browser.js +126 -11
- package/dist/browser/modules/archive/unzip/stream.js +149 -57
- package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +0 -2
- package/dist/browser/modules/excel/stream/workbook-writer.d.ts +2 -2
- package/dist/browser/modules/excel/types.d.ts +0 -2
- package/dist/browser/modules/pdf/excel-bridge.d.ts +29 -0
- package/dist/browser/modules/pdf/excel-bridge.js +423 -0
- package/dist/browser/modules/pdf/index.d.ts +22 -24
- package/dist/browser/modules/pdf/index.js +22 -25
- package/dist/browser/modules/pdf/pdf.d.ts +121 -0
- package/dist/browser/modules/pdf/pdf.js +255 -0
- package/dist/browser/modules/pdf/render/layout-engine.d.ts +10 -8
- package/dist/browser/modules/pdf/render/layout-engine.js +115 -209
- package/dist/browser/modules/pdf/render/pdf-exporter.d.ts +9 -62
- package/dist/browser/modules/pdf/render/pdf-exporter.js +38 -78
- package/dist/browser/modules/pdf/render/style-converter.d.ts +20 -18
- package/dist/browser/modules/pdf/render/style-converter.js +24 -23
- package/dist/browser/modules/pdf/types.d.ts +193 -11
- package/dist/browser/modules/pdf/types.js +22 -1
- package/dist/cjs/index.js +3 -3
- package/dist/cjs/modules/archive/unzip/stream.base.js +124 -8
- package/dist/cjs/modules/archive/unzip/stream.browser.js +126 -11
- package/dist/cjs/modules/archive/unzip/stream.js +149 -57
- package/dist/cjs/modules/pdf/excel-bridge.js +426 -0
- package/dist/cjs/modules/pdf/index.js +25 -28
- package/dist/cjs/modules/pdf/pdf.js +258 -0
- package/dist/cjs/modules/pdf/render/layout-engine.js +116 -210
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +37 -79
- package/dist/cjs/modules/pdf/render/style-converter.js +24 -23
- package/dist/cjs/modules/pdf/types.js +23 -2
- package/dist/esm/index.browser.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/modules/archive/unzip/stream.base.js +124 -8
- package/dist/esm/modules/archive/unzip/stream.browser.js +126 -11
- package/dist/esm/modules/archive/unzip/stream.js +149 -57
- package/dist/esm/modules/pdf/excel-bridge.js +423 -0
- package/dist/esm/modules/pdf/index.js +22 -25
- package/dist/esm/modules/pdf/pdf.js +255 -0
- package/dist/esm/modules/pdf/render/layout-engine.js +115 -209
- package/dist/esm/modules/pdf/render/pdf-exporter.js +38 -78
- package/dist/esm/modules/pdf/render/style-converter.js +24 -23
- package/dist/esm/modules/pdf/types.js +22 -1
- package/dist/iife/excelts.iife.js +918 -267
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +34 -34
- package/dist/types/index.browser.d.ts +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +0 -2
- package/dist/types/modules/excel/stream/workbook-writer.d.ts +2 -2
- package/dist/types/modules/excel/types.d.ts +0 -2
- package/dist/types/modules/pdf/excel-bridge.d.ts +29 -0
- package/dist/types/modules/pdf/index.d.ts +22 -24
- package/dist/types/modules/pdf/pdf.d.ts +121 -0
- package/dist/types/modules/pdf/render/layout-engine.d.ts +10 -8
- package/dist/types/modules/pdf/render/pdf-exporter.d.ts +9 -62
- package/dist/types/modules/pdf/render/style-converter.d.ts +20 -18
- package/dist/types/modules/pdf/types.d.ts +193 -11
- 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
|
|
9
|
+
ExcelTS is a zero-dependency TypeScript toolkit for spreadsheets and documents:
|
|
10
10
|
|
|
11
|
-
- 🚀 **Zero Runtime Dependencies**
|
|
12
|
-
-
|
|
13
|
-
- ✅ **
|
|
14
|
-
- ✅ **
|
|
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
|
-
-
|
|
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) -
|
|
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,
|
|
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 =
|
|
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 =
|
|
184
|
+
const pdf = excelToPdf(workbook);
|
|
188
185
|
```
|
|
189
186
|
|
|
190
187
|
### Encryption
|
|
191
188
|
|
|
192
189
|
```javascript
|
|
193
|
-
const pdf =
|
|
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 =
|
|
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
|
-
|
|
510
|
-
|
|
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
|
|
9
|
+
ExcelTS 是零依赖的 TypeScript 电子表格与文档工具包:
|
|
10
10
|
|
|
11
|
-
- 🚀 **零运行时依赖**
|
|
12
|
-
-
|
|
13
|
-
- ✅
|
|
14
|
-
- ✅
|
|
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,
|
|
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 =
|
|
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 =
|
|
184
|
+
const pdf = excelToPdf(workbook);
|
|
188
185
|
```
|
|
189
186
|
|
|
190
187
|
### 加密
|
|
191
188
|
|
|
192
189
|
```javascript
|
|
193
|
-
const pdf =
|
|
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 =
|
|
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
|
-
|
|
508
|
-
|
|
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 {
|
|
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 {
|
|
61
|
+
export { pdf, excelToPdf, PageSizes, PdfError, PdfRenderError, PdfFontError, PdfStructureError, isPdfError } from "./modules/pdf/index.js";
|
|
62
62
|
// =============================================================================
|
|
63
63
|
// Errors
|
|
64
64
|
// =============================================================================
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -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,
|
|
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 {
|
|
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";
|
package/dist/browser/index.js
CHANGED
|
@@ -54,7 +54,7 @@ export { concatUint8Arrays, toUint8Array, stringToUint8Array, uint8ArrayToString
|
|
|
54
54
|
// =============================================================================
|
|
55
55
|
// PDF Export
|
|
56
56
|
// =============================================================================
|
|
57
|
-
export {
|
|
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
|
|
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
|
-
//
|
|
480
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
868
|
-
this.
|
|
869
|
-
|
|
870
|
-
|
|
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
|
}
|