@shbernal/ts-xlsx 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +230 -0
  3. package/dist/core/address.d.ts +49 -0
  4. package/dist/core/address.js +125 -0
  5. package/dist/core/autofilter.d.ts +55 -0
  6. package/dist/core/autofilter.js +48 -0
  7. package/dist/core/cell.d.ts +132 -0
  8. package/dist/core/cell.js +249 -0
  9. package/dist/core/color-resolution.d.ts +58 -0
  10. package/dist/core/color-resolution.js +170 -0
  11. package/dist/core/column.d.ts +87 -0
  12. package/dist/core/column.js +167 -0
  13. package/dist/core/comment-thread.d.ts +116 -0
  14. package/dist/core/comment-thread.js +62 -0
  15. package/dist/core/conditional-formatting-overlay.d.ts +15 -0
  16. package/dist/core/conditional-formatting-overlay.js +24 -0
  17. package/dist/core/conditional-formatting.d.ts +70 -0
  18. package/dist/core/conditional-formatting.js +35 -0
  19. package/dist/core/containers.d.ts +2 -0
  20. package/dist/core/containers.js +25 -0
  21. package/dist/core/data-validation-overlay.d.ts +28 -0
  22. package/dist/core/data-validation-overlay.js +56 -0
  23. package/dist/core/data-validation.d.ts +38 -0
  24. package/dist/core/data-validation.js +13 -0
  25. package/dist/core/date.d.ts +25 -0
  26. package/dist/core/date.js +58 -0
  27. package/dist/core/formula.d.ts +49 -0
  28. package/dist/core/formula.js +319 -0
  29. package/dist/core/grid-edits.d.ts +22 -0
  30. package/dist/core/grid-edits.js +206 -0
  31. package/dist/core/image.d.ts +76 -0
  32. package/dist/core/image.js +65 -0
  33. package/dist/core/internal.d.ts +11 -0
  34. package/dist/core/internal.js +29 -0
  35. package/dist/core/merge.d.ts +13 -0
  36. package/dist/core/merge.js +26 -0
  37. package/dist/core/modern-functions.d.ts +1 -0
  38. package/dist/core/modern-functions.js +165 -0
  39. package/dist/core/page-setup.d.ts +94 -0
  40. package/dist/core/page-setup.js +6 -0
  41. package/dist/core/pivot-table.d.ts +113 -0
  42. package/dist/core/pivot-table.js +249 -0
  43. package/dist/core/preserved.d.ts +58 -0
  44. package/dist/core/preserved.js +5 -0
  45. package/dist/core/protection.d.ts +67 -0
  46. package/dist/core/protection.js +68 -0
  47. package/dist/core/range.d.ts +112 -0
  48. package/dist/core/range.js +283 -0
  49. package/dist/core/row.d.ts +73 -0
  50. package/dist/core/row.js +135 -0
  51. package/dist/core/style.d.ts +246 -0
  52. package/dist/core/style.js +125 -0
  53. package/dist/core/table-style.d.ts +67 -0
  54. package/dist/core/table-style.js +90 -0
  55. package/dist/core/table.d.ts +194 -0
  56. package/dist/core/table.js +291 -0
  57. package/dist/core/theme.d.ts +90 -0
  58. package/dist/core/theme.js +272 -0
  59. package/dist/core/value.d.ts +115 -0
  60. package/dist/core/value.js +144 -0
  61. package/dist/core/workbook-protection.d.ts +27 -0
  62. package/dist/core/workbook-protection.js +25 -0
  63. package/dist/core/workbook.d.ts +528 -0
  64. package/dist/core/workbook.js +732 -0
  65. package/dist/core/worksheet-model.d.ts +34 -0
  66. package/dist/core/worksheet-model.js +100 -0
  67. package/dist/core/worksheet.d.ts +647 -0
  68. package/dist/core/worksheet.js +1025 -0
  69. package/dist/customui/errors.d.ts +15 -0
  70. package/dist/customui/errors.js +15 -0
  71. package/dist/customui/index.d.ts +2 -0
  72. package/dist/customui/index.js +7 -0
  73. package/dist/customui/ribbon.d.ts +86 -0
  74. package/dist/customui/ribbon.js +180 -0
  75. package/dist/entries/core.d.ts +23 -0
  76. package/dist/entries/core.js +20 -0
  77. package/dist/entries/csv.d.ts +2 -0
  78. package/dist/entries/csv.js +6 -0
  79. package/dist/entries/customui.d.ts +1 -0
  80. package/dist/entries/customui.js +3 -0
  81. package/dist/entries/errors.d.ts +7 -0
  82. package/dist/entries/errors.js +20 -0
  83. package/dist/entries/vba.d.ts +2 -0
  84. package/dist/entries/vba.js +7 -0
  85. package/dist/entries/xlsb.d.ts +1 -0
  86. package/dist/entries/xlsb.js +7 -0
  87. package/dist/entries/xlsx.d.ts +5 -0
  88. package/dist/entries/xlsx.js +18 -0
  89. package/dist/errors.d.ts +65 -0
  90. package/dist/errors.js +66 -0
  91. package/dist/index.d.ts +7 -0
  92. package/dist/index.js +19 -0
  93. package/dist/io/csv/read.d.ts +15 -0
  94. package/dist/io/csv/read.js +113 -0
  95. package/dist/io/csv/write.d.ts +28 -0
  96. package/dist/io/csv/write.js +124 -0
  97. package/dist/io/opc/errors.d.ts +46 -0
  98. package/dist/io/opc/errors.js +49 -0
  99. package/dist/io/opc/inflate.d.ts +15 -0
  100. package/dist/io/opc/inflate.js +91 -0
  101. package/dist/io/opc/namespaces.d.ts +18 -0
  102. package/dist/io/opc/namespaces.js +18 -0
  103. package/dist/io/opc/part-paths.d.ts +4 -0
  104. package/dist/io/opc/part-paths.js +35 -0
  105. package/dist/io/opc/read-opc.d.ts +24 -0
  106. package/dist/io/opc/read-opc.js +153 -0
  107. package/dist/io/opc/read-options.d.ts +10 -0
  108. package/dist/io/opc/read-options.js +7 -0
  109. package/dist/io/opc/rels.d.ts +15 -0
  110. package/dist/io/opc/rels.js +28 -0
  111. package/dist/io/opc/sniff-format.d.ts +27 -0
  112. package/dist/io/opc/sniff-format.js +92 -0
  113. package/dist/io/style/xf-style.d.ts +47 -0
  114. package/dist/io/style/xf-style.js +95 -0
  115. package/dist/io/xlsb/errors.d.ts +15 -0
  116. package/dist/io/xlsb/errors.js +15 -0
  117. package/dist/io/xlsb/formula.d.ts +48 -0
  118. package/dist/io/xlsb/formula.js +398 -0
  119. package/dist/io/xlsb/primitives.d.ts +86 -0
  120. package/dist/io/xlsb/primitives.js +230 -0
  121. package/dist/io/xlsb/ptg-functions.d.ts +16 -0
  122. package/dist/io/xlsb/ptg-functions.js +659 -0
  123. package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
  124. package/dist/io/xlsb/read-shared-strings.js +22 -0
  125. package/dist/io/xlsb/read-styles.d.ts +4 -0
  126. package/dist/io/xlsb/read-styles.js +386 -0
  127. package/dist/io/xlsb/read-worksheet.d.ts +9 -0
  128. package/dist/io/xlsb/read-worksheet.js +301 -0
  129. package/dist/io/xlsb/read.d.ts +20 -0
  130. package/dist/io/xlsb/read.js +199 -0
  131. package/dist/io/xlsb/record-stream.d.ts +14 -0
  132. package/dist/io/xlsb/record-stream.js +56 -0
  133. package/dist/io/xlsb/record-types.d.ts +55 -0
  134. package/dist/io/xlsb/record-types.js +73 -0
  135. package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
  136. package/dist/io/xlsx/cell-accumulator.js +172 -0
  137. package/dist/io/xlsx/cell-value.d.ts +32 -0
  138. package/dist/io/xlsx/cell-value.js +85 -0
  139. package/dist/io/xlsx/comments.d.ts +74 -0
  140. package/dist/io/xlsx/comments.js +243 -0
  141. package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
  142. package/dist/io/xlsx/conditional-formatting.js +464 -0
  143. package/dist/io/xlsx/data-validation.d.ts +20 -0
  144. package/dist/io/xlsx/data-validation.js +258 -0
  145. package/dist/io/xlsx/edit-vba.d.ts +23 -0
  146. package/dist/io/xlsx/edit-vba.js +139 -0
  147. package/dist/io/xlsx/errors.d.ts +17 -0
  148. package/dist/io/xlsx/errors.js +17 -0
  149. package/dist/io/xlsx/hyperlinks.d.ts +42 -0
  150. package/dist/io/xlsx/hyperlinks.js +139 -0
  151. package/dist/io/xlsx/images.d.ts +34 -0
  152. package/dist/io/xlsx/images.js +218 -0
  153. package/dist/io/xlsx/namespaces.d.ts +55 -0
  154. package/dist/io/xlsx/namespaces.js +55 -0
  155. package/dist/io/xlsx/package-plan.d.ts +103 -0
  156. package/dist/io/xlsx/package-plan.js +168 -0
  157. package/dist/io/xlsx/pivot-read.d.ts +5 -0
  158. package/dist/io/xlsx/pivot-read.js +134 -0
  159. package/dist/io/xlsx/pivot.d.ts +12 -0
  160. package/dist/io/xlsx/pivot.js +141 -0
  161. package/dist/io/xlsx/read-rows.d.ts +85 -0
  162. package/dist/io/xlsx/read-rows.js +286 -0
  163. package/dist/io/xlsx/read-styles.d.ts +8 -0
  164. package/dist/io/xlsx/read-styles.js +551 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +4 -0
  166. package/dist/io/xlsx/read-worksheet.js +528 -0
  167. package/dist/io/xlsx/read.d.ts +32 -0
  168. package/dist/io/xlsx/read.js +680 -0
  169. package/dist/io/xlsx/relationships.d.ts +33 -0
  170. package/dist/io/xlsx/relationships.js +47 -0
  171. package/dist/io/xlsx/rich-runs.d.ts +13 -0
  172. package/dist/io/xlsx/rich-runs.js +59 -0
  173. package/dist/io/xlsx/rich-text.d.ts +7 -0
  174. package/dist/io/xlsx/rich-text.js +23 -0
  175. package/dist/io/xlsx/shared-formulas.d.ts +6 -0
  176. package/dist/io/xlsx/shared-formulas.js +52 -0
  177. package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
  178. package/dist/io/xlsx/shared-strings-read.js +70 -0
  179. package/dist/io/xlsx/shared-strings.d.ts +17 -0
  180. package/dist/io/xlsx/shared-strings.js +50 -0
  181. package/dist/io/xlsx/sheet-properties.d.ts +13 -0
  182. package/dist/io/xlsx/sheet-properties.js +211 -0
  183. package/dist/io/xlsx/styles.d.ts +138 -0
  184. package/dist/io/xlsx/styles.js +821 -0
  185. package/dist/io/xlsx/tables.d.ts +9 -0
  186. package/dist/io/xlsx/tables.js +208 -0
  187. package/dist/io/xlsx/threaded-comments.d.ts +102 -0
  188. package/dist/io/xlsx/threaded-comments.js +308 -0
  189. package/dist/io/xlsx/workbook-xml.d.ts +12 -0
  190. package/dist/io/xlsx/workbook-xml.js +353 -0
  191. package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
  192. package/dist/io/xlsx/worksheet-xml.js +643 -0
  193. package/dist/io/xlsx/write-stream.d.ts +171 -0
  194. package/dist/io/xlsx/write-stream.js +399 -0
  195. package/dist/io/xlsx/write.d.ts +81 -0
  196. package/dist/io/xlsx/write.js +396 -0
  197. package/dist/io/xlsx/x14-ext.d.ts +9 -0
  198. package/dist/io/xlsx/x14-ext.js +12 -0
  199. package/dist/vba/cfb-writer.d.ts +19 -0
  200. package/dist/vba/cfb-writer.js +277 -0
  201. package/dist/vba/cfb.d.ts +16 -0
  202. package/dist/vba/cfb.js +324 -0
  203. package/dist/vba/codepage.d.ts +18 -0
  204. package/dist/vba/codepage.js +87 -0
  205. package/dist/vba/errors.d.ts +24 -0
  206. package/dist/vba/errors.js +24 -0
  207. package/dist/vba/index.d.ts +5 -0
  208. package/dist/vba/index.js +15 -0
  209. package/dist/vba/ms-ovba.d.ts +12 -0
  210. package/dist/vba/ms-ovba.js +176 -0
  211. package/dist/vba/project-editor.d.ts +62 -0
  212. package/dist/vba/project-editor.js +377 -0
  213. package/dist/vba/project.d.ts +45 -0
  214. package/dist/vba/project.js +160 -0
  215. package/dist/vba/vba-encoding.d.ts +14 -0
  216. package/dist/vba/vba-encoding.js +39 -0
  217. package/dist/xml/errors.d.ts +15 -0
  218. package/dist/xml/errors.js +15 -0
  219. package/dist/xml/xml-read.d.ts +107 -0
  220. package/dist/xml/xml-read.js +283 -0
  221. package/dist/xml/xml.d.ts +41 -0
  222. package/dist/xml/xml.js +86 -0
  223. package/package.json +117 -0
@@ -0,0 +1,171 @@
1
+ import { type Readable, type Writable } from 'node:stream';
2
+ import type { AutoFilter } from '../../core/autofilter.ts';
3
+ import type { Cell } from '../../core/cell.ts';
4
+ import type { ConditionalFormatting } from '../../core/conditional-formatting.ts';
5
+ import type { DataValidation } from '../../core/data-validation.ts';
6
+ import type { AnchorPoint } from '../../core/image.ts';
7
+ import type { SheetProtectionOptions } from '../../core/protection.ts';
8
+ import { type CellValue } from '../../core/value.ts';
9
+ import { type AddImageOptions, type AddWorksheetOptions, Workbook } from '../../core/workbook.ts';
10
+ import type { Worksheet } from '../../core/worksheet.ts';
11
+ import type { StyleRegistry } from './styles.ts';
12
+ import { type FlushedSheet } from './write.ts';
13
+ /** Calculation settings applied to the streamed workbook. Mirrors the {@link Workbook} flags. */
14
+ export interface CalcProperties {
15
+ /** Ask the consumer to recalculate every formula on open — the OOXML `fullCalcOnLoad` flag. */
16
+ fullCalcOnLoad?: boolean;
17
+ }
18
+ type SinkOptions = {
19
+ /**
20
+ * Write the package to a caller-owned {@link Writable} sink — an outbound upload, a cloud-SDK
21
+ * stream, any destination the caller controls. {@link WorkbookStreamWriter.commit} pushes every
22
+ * chunk into it and settles only once the sink has finished (or rejects if it errors), so a
23
+ * caller can deterministically sequence work after the upload completes. Mutually exclusive
24
+ * with {@link filename}.
25
+ */
26
+ readonly stream: Writable;
27
+ readonly filename?: never;
28
+ } | {
29
+ readonly stream?: never;
30
+ /**
31
+ * Write the package to a file at this path. The writer opens a `fs.createWriteStream` for it; if
32
+ * the destination cannot be opened (bad path, name too long) the stream errors and
33
+ * {@link WorkbookStreamWriter.commit} rejects with that I/O error rather than hanging. Mutually
34
+ * exclusive with {@link stream}.
35
+ */
36
+ readonly filename: string;
37
+ } | {
38
+ readonly stream?: never;
39
+ readonly filename?: never;
40
+ };
41
+ /** Options fixed at construction that shape the whole streamed package. */
42
+ export type WorkbookStreamWriterOptions = SinkOptions & {
43
+ /**
44
+ * Pool plain string cell values into a shared-strings table rather than storing each inline — the
45
+ * same {@link WriteOptions.useSharedStrings} the buffered writer exposes. Off by default.
46
+ */
47
+ readonly useSharedStrings?: boolean;
48
+ };
49
+ /**
50
+ * A row appended to a {@link WorksheetStreamWriter}. Style its cells through {@link cells}, then call
51
+ * {@link commit} to mark it finished. In an eager (inline-strings) writer, committing serialises the
52
+ * row and frees its cells from the model, bounding peak memory; with `useSharedStrings` on it is a
53
+ * no-op and the row stays live until the workbook commits.
54
+ */
55
+ export declare class StreamedRow {
56
+ #private;
57
+ constructor(cells: readonly Cell[], sheet: WorksheetStreamWriter | null, number: number);
58
+ /** The cells this row materialised, for styling before it is committed. */
59
+ get cells(): readonly Cell[];
60
+ /** Finalise the row: an eager writer serialises it now and releases its cells; otherwise a no-op.
61
+ * Committing twice is harmless — the second call does nothing rather than re-emitting the row. */
62
+ commit(): void;
63
+ }
64
+ /**
65
+ * A worksheet being written incrementally. Append rows with {@link addRow}/{@link addRows}, style
66
+ * cells through {@link getCell}, then {@link commit} to freeze it — after which any further mutation
67
+ * is rejected with a legible error rather than silently accepted or crashing.
68
+ */
69
+ export declare class WorksheetStreamWriter {
70
+ #private;
71
+ constructor(sheet: Worksheet, eager: boolean, styles: StyleRegistry);
72
+ /** The sheet's name. */
73
+ get name(): string;
74
+ /** The number of rows written so far — spans gaps and formatted-only rows, like the model, and
75
+ * survives the eviction of eagerly-flushed rows. */
76
+ get rowCount(): number;
77
+ /** Append one row of values after the last used row; the cells are returned for styling. */
78
+ addRow(values: CellValue[]): StreamedRow;
79
+ /** Append a batch of rows in one call, each landing directly below the previous. */
80
+ addRows(rows: CellValue[][]): StreamedRow[];
81
+ /**
82
+ * Serialise an eagerly-committed row and release its cells from the model. Called by
83
+ * {@link StreamedRow.commit}; the row's `<row>` XML is retained (interned into the workbook's live
84
+ * style registry so its ids stay valid) and the cell graph is dropped, bounding peak memory.
85
+ *
86
+ * @throws {@link AuthoringError} if the row carries a shared-formula cell — a finished row cannot join the
87
+ * whole-sheet formula planning, so shared formulas must be authored through {@link getCell}.
88
+ */
89
+ flushRow(number: number, cells: readonly Cell[]): void;
90
+ flushedSheet(): FlushedSheet | undefined;
91
+ /** Address a cell by its A1 reference to read or style it before the sheet is committed. */
92
+ getCell(reference: string): Cell;
93
+ /**
94
+ * Attach a data validation to a range before the sheet is committed. Delegates to the model, so the
95
+ * streamed package emits the `<dataValidations>` block in its CT_Worksheet position — before
96
+ * `<hyperlinks>` — because both writers share one worksheet serializer.
97
+ */
98
+ addDataValidation(sqref: string, rule: DataValidation, options?: {
99
+ extended?: boolean;
100
+ }): void;
101
+ /**
102
+ * Attach a conditional formatting to a range before the sheet is committed. Like every other block,
103
+ * it lands in its schema-mandated slot — after `<mergeCells>`, before `<dataValidations>` and
104
+ * `<hyperlinks>` — since the streamed sheet is serialized through the same path as a buffered write.
105
+ */
106
+ addConditionalFormatting(formatting: ConditionalFormatting): void;
107
+ /**
108
+ * Anchor a workbook image (the id from {@link WorkbookStreamWriter.addImage}) to this sheet,
109
+ * spanning the rectangle from the top-left grid point `tl` to the bottom-right `br`. The streamed
110
+ * package emits the drawing part, its media relationship, and the sheet's `<drawing>` reference
111
+ * exactly as a buffered write does — both writers share `buildPackageParts`.
112
+ */
113
+ addImage(imageId: number, anchor: {
114
+ readonly tl: AnchorPoint;
115
+ readonly br: AnchorPoint;
116
+ }): void;
117
+ /**
118
+ * Apply the sheet's autofilter before it is committed; mirrors {@link Worksheet.autoFilter}. The
119
+ * streamed package emits `<autoFilter>` in its CT_Worksheet slot — after `<sheetProtection>` — and
120
+ * contributes the hidden `_FilterDatabase` defined name, exactly as a buffered write does.
121
+ */
122
+ set autoFilter(filter: string | AutoFilter | undefined);
123
+ get autoFilter(): AutoFilter | undefined;
124
+ /**
125
+ * Apply sheet-level protection before the sheet is committed; mirrors {@link Worksheet.protect}. The
126
+ * shared serializer places `<sheetProtection>` ahead of `<autoFilter>` per CT_Worksheet, so a
127
+ * streamed sheet carrying both stays valid rather than corrupt.
128
+ */
129
+ protect(password?: string, options?: SheetProtectionOptions): void;
130
+ /** Freeze the sheet: no more rows or edits may be added after this. */
131
+ commit(): void;
132
+ /** Whether the sheet has been committed. */
133
+ get committed(): boolean;
134
+ get model(): Worksheet;
135
+ }
136
+ /**
137
+ * A workbook written incrementally to a Node stream. Add worksheets, append their rows, commit each
138
+ * sheet, then {@link commit} the workbook to assemble and stream the package. The produced bytes are
139
+ * available both as the resolved value of `commit()` and through {@link stream} (a Node `Readable`
140
+ * that a caller can `pipe`).
141
+ */
142
+ export declare class WorkbookStreamWriter {
143
+ #private;
144
+ /** Calculation settings for the workbook; set `fullCalcOnLoad` before committing to emit it. */
145
+ readonly calcProperties: CalcProperties;
146
+ constructor(options?: WorkbookStreamWriterOptions);
147
+ /** Document-level metadata written to the package's core properties. */
148
+ get properties(): Workbook['properties'];
149
+ /**
150
+ * The output stream carrying the package bytes. A caller drives it with Node's standard idiom —
151
+ * `writer.stream.pipe(out)` — which composes because `pipe` returns its destination. The stream is
152
+ * created lazily on first access so a caller handing the writer its own sink is still free to
153
+ * ignore this one.
154
+ */
155
+ get stream(): Readable;
156
+ /**
157
+ * Register a picture's bytes on the workbook's shared media registry and return its id, to anchor
158
+ * on any sheet with {@link WorksheetStreamWriter.addImage}. Mirrors {@link Workbook.addImage}: one
159
+ * media part backs an image anchored on several sheets. Rejected once the workbook is committed.
160
+ */
161
+ addImage(options: AddImageOptions): number;
162
+ /** Create a worksheet and append it to the workbook. */
163
+ addWorksheet(name: string, options?: AddWorksheetOptions): WorksheetStreamWriter;
164
+ /**
165
+ * Assemble the workbook into its package, stream the bytes through {@link stream}, and resolve with
166
+ * the same bytes. Every sheet is frozen first, so a row added after this rejects legibly. Idempotent
167
+ * only in that a second call throws rather than re-emitting.
168
+ */
169
+ commit(): Promise<Uint8Array>;
170
+ }
171
+ export {};
@@ -0,0 +1,399 @@
1
+ // The streaming `.xlsx` writer: author a workbook incrementally and emit its package through a Node
2
+ // stream, rather than holding the finished bytes in one buffer as {@link writeXlsx} does.
3
+ //
4
+ // A producer adds worksheets, appends rows as it generates them, commits each sheet, then commits the
5
+ // workbook — at which point the package is assembled and streamed out. The output goes to whichever
6
+ // destination the caller chose at construction: its own pull-based `stream` to pipe, a caller-owned
7
+ // `Writable` sink (an outbound upload), or a `filename` the writer opens. In every case `commit()`
8
+ // settles — it resolves once a supplied sink has flushed the whole package and rejects if that sink
9
+ // errors (an unopenable file), never hanging on a finish signal that will not come. The output rides a
10
+ // genuinely
11
+ // streamed zip container (fflate's `Zip`/`ZipDeflate`), which computes each entry's CRC-32
12
+ // incrementally, so the archive is well-formed by construction — the defect the upstream "streaming
13
+ // writer emits a corrupt zip" reports describe is structurally absent here. The bytes reload
14
+ // identically to a whole-file write because both writers share `buildPackageParts` for every part.
15
+ //
16
+ // Peak memory: a row committed with `row.commit()` is serialised to its `<row>` XML immediately and
17
+ // its cells evicted from the model, so an append-driven producer holds only the rows still in flight
18
+ // rather than the whole sheet's cell graph. The eagerly-rendered rows intern into the workbook's live
19
+ // style registry — the same one that emits `xl/styles.xml` — so their style ids stay correct; their
20
+ // XML is handed to `buildPackageParts` and emitted at the head of `<sheetData>`. This eager path runs
21
+ // with strings inline (a shared-strings pool is inherently whole-workbook, so it defeats bounding);
22
+ // turning `useSharedStrings` on falls back to holding every row live until commit. A flushed row is a
23
+ // finished row: it cannot join whole-sheet derivations, so a shared-formula clone in a committed row
24
+ // is rejected, and rows reached only through `getCell` (never `row.commit()`) stay live and serialise
25
+ // the ordinary way. The package bytes themselves are still assembled once at commit — a later slice
26
+ // can flush each sheet's `<sheetData>` straight into its streamed zip entry to bound that half too.
27
+ import { createWriteStream } from 'node:fs';
28
+ import { PassThrough } from 'node:stream';
29
+ import { Zip, ZipDeflate } from 'fflate';
30
+ import { encodeAddress } from "../../core/address.js";
31
+ import { INTERNAL } from "../../core/internal.js";
32
+ import { isSharedFormulaValue } from "../../core/value.js";
33
+ import { Workbook } from "../../core/workbook.js";
34
+ import { AuthoringError } from "../../errors.js";
35
+ import { buildColumnDefaults, buildPackageParts, createStyleRegistry, Extent, renderRow, } from "./write.js";
36
+ /**
37
+ * A row appended to a {@link WorksheetStreamWriter}. Style its cells through {@link cells}, then call
38
+ * {@link commit} to mark it finished. In an eager (inline-strings) writer, committing serialises the
39
+ * row and frees its cells from the model, bounding peak memory; with `useSharedStrings` on it is a
40
+ * no-op and the row stays live until the workbook commits.
41
+ */
42
+ export class StreamedRow {
43
+ #cells;
44
+ #sheet;
45
+ #number;
46
+ #committed = false;
47
+ constructor(cells, sheet, number) {
48
+ this.#cells = cells;
49
+ this.#sheet = sheet;
50
+ this.#number = number;
51
+ }
52
+ /** The cells this row materialised, for styling before it is committed. */
53
+ get cells() {
54
+ return this.#cells;
55
+ }
56
+ /** Finalise the row: an eager writer serialises it now and releases its cells; otherwise a no-op.
57
+ * Committing twice is harmless — the second call does nothing rather than re-emitting the row. */
58
+ commit() {
59
+ if (this.#committed)
60
+ return;
61
+ this.#committed = true;
62
+ this.#sheet?.flushRow(this.#number, this.#cells);
63
+ }
64
+ }
65
+ /**
66
+ * A worksheet being written incrementally. Append rows with {@link addRow}/{@link addRows}, style
67
+ * cells through {@link getCell}, then {@link commit} to freeze it — after which any further mutation
68
+ * is rejected with a legible error rather than silently accepted or crashing.
69
+ */
70
+ export class WorksheetStreamWriter {
71
+ #sheet;
72
+ #eager;
73
+ #styles;
74
+ #committed = false;
75
+ // The last row number this writer appended, tracked independently of the model: an eager writer
76
+ // evicts flushed rows, which lowers the model's used range, so leaning on it would reuse numbers.
77
+ #lastRow = 0;
78
+ // The column defaults an eagerly-rendered row inherits, frozen at the first flush so every flushed
79
+ // row composes against the same columns even as later ones are defined.
80
+ #columnDefaults;
81
+ #flushedRows = [];
82
+ #extent = new Extent();
83
+ #maxRowOutlineLevel = 0;
84
+ constructor(sheet, eager, styles) {
85
+ this.#sheet = sheet;
86
+ this.#eager = eager;
87
+ this.#styles = styles;
88
+ }
89
+ /** The sheet's name. */
90
+ get name() {
91
+ return this.#sheet.name;
92
+ }
93
+ /** The number of rows written so far — spans gaps and formatted-only rows, like the model, and
94
+ * survives the eviction of eagerly-flushed rows. */
95
+ get rowCount() {
96
+ return Math.max(this.#lastRow, this.#sheet.rowCount);
97
+ }
98
+ /** Append one row of values after the last used row; the cells are returned for styling. */
99
+ addRow(values) {
100
+ this.#assertOpen();
101
+ if (!this.#eager)
102
+ return new StreamedRow(this.#sheet.addRow(values), null, 0);
103
+ const number = this.#nextRowNumber();
104
+ return new StreamedRow(this.#placeRow(number, values), this, number);
105
+ }
106
+ /** Append a batch of rows in one call, each landing directly below the previous. */
107
+ addRows(rows) {
108
+ this.#assertOpen();
109
+ if (!this.#eager)
110
+ return this.#sheet.addRows(rows).map((cells) => new StreamedRow(cells, null, 0));
111
+ return rows.map((values) => this.addRow(values));
112
+ }
113
+ // The next append position: past both this writer's own high-water mark and any rows a `getCell`
114
+ // materialised, so appends never collide with random-access edits or with already-evicted rows.
115
+ #nextRowNumber() {
116
+ this.#lastRow = Math.max(this.#lastRow, this.#sheet.rowCount) + 1;
117
+ return this.#lastRow;
118
+ }
119
+ // Materialise a positional row at an explicit number (the model's own append would reuse numbers once
120
+ // eviction shrinks its used range), returning the cells for styling. A hole leaves its cell absent.
121
+ #placeRow(number, values) {
122
+ const cells = [];
123
+ values.forEach((value, index) => {
124
+ const cell = this.#sheet.getCell(encodeAddress(index + 1, number));
125
+ cell.value = value;
126
+ cells.push(cell);
127
+ });
128
+ return cells;
129
+ }
130
+ /**
131
+ * Serialise an eagerly-committed row and release its cells from the model. Called by
132
+ * {@link StreamedRow.commit}; the row's `<row>` XML is retained (interned into the workbook's live
133
+ * style registry so its ids stay valid) and the cell graph is dropped, bounding peak memory.
134
+ *
135
+ * @throws {@link AuthoringError} if the row carries a shared-formula cell — a finished row cannot join the
136
+ * whole-sheet formula planning, so shared formulas must be authored through {@link getCell}.
137
+ */
138
+ flushRow(number, cells) {
139
+ for (const cell of cells) {
140
+ if (isSharedFormulaValue(cell.value)) {
141
+ throw new AuthoringError(`row ${number} of streamed sheet "${this.#sheet.name}" carries a shared-formula cell; a ` +
142
+ 'committed row is finalised before the sheet is planned, so author shared formulas through ' +
143
+ 'getCell (leaving the row uncommitted) instead');
144
+ }
145
+ }
146
+ this.#columnDefaults ??= buildColumnDefaults(this.#sheet);
147
+ const properties = this.#sheet.getRow(number).properties;
148
+ // The row's outline depth is read off here because eviction is about to take its properties with
149
+ // it, and `<sheetFormatPr outlineLevelRow>` — written long after, once every row is flushed —
150
+ // reports the deepest level on the whole sheet.
151
+ this.#maxRowOutlineLevel = Math.max(this.#maxRowOutlineLevel, properties?.outlineLevel ?? 0);
152
+ const { xml, minCol, maxCol } = renderRow({ number, cells, properties }, {
153
+ columnDefaults: this.#columnDefaults,
154
+ styles: this.#styles,
155
+ sharedStrings: null,
156
+ sharedRoles: new Map(),
157
+ collapsedSummaries: new Set(),
158
+ });
159
+ if (xml !== '') {
160
+ this.#flushedRows.push({ number, xml });
161
+ this.#extent.add(number, minCol, maxCol);
162
+ }
163
+ this.#sheet[INTERNAL].evictRow(number);
164
+ }
165
+ // The rows this writer flushed, or undefined if none — handed to buildPackageParts at commit.
166
+ flushedSheet() {
167
+ if (this.#flushedRows.length === 0)
168
+ return undefined;
169
+ return {
170
+ rows: this.#flushedRows,
171
+ extent: this.#extent,
172
+ maxRowOutlineLevel: this.#maxRowOutlineLevel,
173
+ };
174
+ }
175
+ /** Address a cell by its A1 reference to read or style it before the sheet is committed. */
176
+ getCell(reference) {
177
+ this.#assertOpen();
178
+ return this.#sheet.getCell(reference);
179
+ }
180
+ /**
181
+ * Attach a data validation to a range before the sheet is committed. Delegates to the model, so the
182
+ * streamed package emits the `<dataValidations>` block in its CT_Worksheet position — before
183
+ * `<hyperlinks>` — because both writers share one worksheet serializer.
184
+ */
185
+ addDataValidation(sqref, rule, options = {}) {
186
+ this.#assertOpen();
187
+ this.#sheet.addDataValidation(sqref, rule, options);
188
+ }
189
+ /**
190
+ * Attach a conditional formatting to a range before the sheet is committed. Like every other block,
191
+ * it lands in its schema-mandated slot — after `<mergeCells>`, before `<dataValidations>` and
192
+ * `<hyperlinks>` — since the streamed sheet is serialized through the same path as a buffered write.
193
+ */
194
+ addConditionalFormatting(formatting) {
195
+ this.#assertOpen();
196
+ this.#sheet.addConditionalFormatting(formatting);
197
+ }
198
+ /**
199
+ * Anchor a workbook image (the id from {@link WorkbookStreamWriter.addImage}) to this sheet,
200
+ * spanning the rectangle from the top-left grid point `tl` to the bottom-right `br`. The streamed
201
+ * package emits the drawing part, its media relationship, and the sheet's `<drawing>` reference
202
+ * exactly as a buffered write does — both writers share `buildPackageParts`.
203
+ */
204
+ addImage(imageId, anchor) {
205
+ this.#assertOpen();
206
+ this.#sheet.addImage(imageId, anchor);
207
+ }
208
+ /**
209
+ * Apply the sheet's autofilter before it is committed; mirrors {@link Worksheet.autoFilter}. The
210
+ * streamed package emits `<autoFilter>` in its CT_Worksheet slot — after `<sheetProtection>` — and
211
+ * contributes the hidden `_FilterDatabase` defined name, exactly as a buffered write does.
212
+ */
213
+ set autoFilter(filter) {
214
+ this.#assertOpen();
215
+ this.#sheet.autoFilter = filter;
216
+ }
217
+ get autoFilter() {
218
+ return this.#sheet.autoFilter;
219
+ }
220
+ /**
221
+ * Apply sheet-level protection before the sheet is committed; mirrors {@link Worksheet.protect}. The
222
+ * shared serializer places `<sheetProtection>` ahead of `<autoFilter>` per CT_Worksheet, so a
223
+ * streamed sheet carrying both stays valid rather than corrupt.
224
+ */
225
+ protect(password, options = {}) {
226
+ this.#assertOpen();
227
+ this.#sheet.protect(password, options);
228
+ }
229
+ /** Freeze the sheet: no more rows or edits may be added after this. */
230
+ commit() {
231
+ this.#committed = true;
232
+ }
233
+ /** Whether the sheet has been committed. */
234
+ get committed() {
235
+ return this.#committed;
236
+ }
237
+ // Internal: the underlying model sheet, so the workbook writer can serialise it at commit time.
238
+ get model() {
239
+ return this.#sheet;
240
+ }
241
+ #assertOpen() {
242
+ if (this.#committed) {
243
+ throw new AuthoringError(`worksheet "${this.#sheet.name}" is already committed — its rows are finalised and no more can be added`);
244
+ }
245
+ }
246
+ }
247
+ /**
248
+ * A workbook written incrementally to a Node stream. Add worksheets, append their rows, commit each
249
+ * sheet, then {@link commit} the workbook to assemble and stream the package. The produced bytes are
250
+ * available both as the resolved value of `commit()` and through {@link stream} (a Node `Readable`
251
+ * that a caller can `pipe`).
252
+ */
253
+ export class WorkbookStreamWriter {
254
+ #workbook = new Workbook();
255
+ #sheets = [];
256
+ #writeOptions;
257
+ #sink;
258
+ // The single style registry shared by the eager per-row flush and the commit-time serialisation, so a
259
+ // flushed row's style ids match the styles.xml built from the same table.
260
+ #styles;
261
+ // Eager per-row flushing runs with strings inline; a shared-strings pool is inherently whole-workbook,
262
+ // so it cannot bound memory — turning it on keeps every row live until commit.
263
+ #eager;
264
+ #stream;
265
+ #committed = false;
266
+ /** Calculation settings for the workbook; set `fullCalcOnLoad` before committing to emit it. */
267
+ calcProperties = {};
268
+ constructor(options = {}) {
269
+ this.#writeOptions = { useSharedStrings: options.useSharedStrings ?? false };
270
+ this.#eager = !this.#writeOptions.useSharedStrings;
271
+ this.#styles = createStyleRegistry(this.#workbook);
272
+ if (options.stream && options.filename) {
273
+ throw new AuthoringError('provide either a stream or a filename to the streaming writer, not both');
274
+ }
275
+ this.#sink =
276
+ options.stream ?? (options.filename ? createWriteStream(options.filename) : undefined);
277
+ }
278
+ /** Document-level metadata written to the package's core properties. */
279
+ get properties() {
280
+ return this.#workbook.properties;
281
+ }
282
+ /**
283
+ * The output stream carrying the package bytes. A caller drives it with Node's standard idiom —
284
+ * `writer.stream.pipe(out)` — which composes because `pipe` returns its destination. The stream is
285
+ * created lazily on first access so a caller handing the writer its own sink is still free to
286
+ * ignore this one.
287
+ */
288
+ get stream() {
289
+ this.#stream ??= new PassThrough();
290
+ return this.#stream;
291
+ }
292
+ /**
293
+ * Register a picture's bytes on the workbook's shared media registry and return its id, to anchor
294
+ * on any sheet with {@link WorksheetStreamWriter.addImage}. Mirrors {@link Workbook.addImage}: one
295
+ * media part backs an image anchored on several sheets. Rejected once the workbook is committed.
296
+ */
297
+ addImage(options) {
298
+ if (this.#committed) {
299
+ throw new AuthoringError('the workbook is already committed — no more images can be registered');
300
+ }
301
+ return this.#workbook.addImage(options);
302
+ }
303
+ /** Create a worksheet and append it to the workbook. */
304
+ addWorksheet(name, options = {}) {
305
+ if (this.#committed) {
306
+ throw new AuthoringError('the workbook is already committed — no more worksheets can be added');
307
+ }
308
+ const sheet = new WorksheetStreamWriter(this.#workbook.addWorksheet(name, options), this.#eager, this.#styles);
309
+ this.#sheets.push(sheet);
310
+ return sheet;
311
+ }
312
+ /**
313
+ * Assemble the workbook into its package, stream the bytes through {@link stream}, and resolve with
314
+ * the same bytes. Every sheet is frozen first, so a row added after this rejects legibly. Idempotent
315
+ * only in that a second call throws rather than re-emitting.
316
+ */
317
+ async commit() {
318
+ if (this.#committed) {
319
+ throw new AuthoringError('the workbook is already committed');
320
+ }
321
+ this.#committed = true;
322
+ for (const sheet of this.#sheets)
323
+ sheet.commit();
324
+ if (this.calcProperties.fullCalcOnLoad)
325
+ this.#workbook.fullCalcOnLoad = true;
326
+ // Hand every sheet's eagerly-flushed rows to the shared serialiser, which emits them alongside the
327
+ // rows still live in the model. The style registry is the same one the flushed rows interned into,
328
+ // so styles.xml stays consistent with the ids already baked into their XML.
329
+ const flushed = new Map();
330
+ for (const sheet of this.#sheets) {
331
+ const sheetFlushed = sheet.flushedSheet();
332
+ if (sheetFlushed)
333
+ flushed.set(sheet.model, sheetFlushed);
334
+ }
335
+ const parts = buildPackageParts(this.#workbook, {
336
+ ...this.#writeOptions,
337
+ styles: this.#styles,
338
+ flushed,
339
+ });
340
+ const owned = this.#stream;
341
+ const sink = this.#sink;
342
+ // Track the caller sink's terminal state before writing a byte, so an open failure that errors on a
343
+ // later tick (a bad filename) is caught rather than lost — the whole point of the reject-not-hang
344
+ // contract.
345
+ const sinkSettled = sink ? settleOnFinish(sink) : undefined;
346
+ const bytes = await streamZipPackage(parts, (chunk) => {
347
+ owned?.write(chunk);
348
+ sink?.write(chunk);
349
+ });
350
+ owned?.end();
351
+ sink?.end();
352
+ await sinkSettled;
353
+ return bytes;
354
+ }
355
+ }
356
+ // Resolve when a caller-supplied sink has flushed the whole package (`finish`), or reject if it errors
357
+ // (`error`) — the commit promise must settle either way, never hang. Whichever fires first wins; the
358
+ // other is ignored.
359
+ function settleOnFinish(sink) {
360
+ return new Promise((resolve, reject) => {
361
+ sink.once('finish', resolve);
362
+ sink.once('error', reject);
363
+ });
364
+ }
365
+ // Zip the package parts through fflate's streaming container, forwarding each output chunk to `onChunk`
366
+ // as it is produced and resolving with the whole archive once the final chunk arrives. `ZipDeflate`
367
+ // deflates synchronously, so the callback fires inline as each part is pushed — the CRC-32 fflate
368
+ // stamps into every entry's header therefore always matches the bytes it just compressed.
369
+ function streamZipPackage(parts, onChunk) {
370
+ return new Promise((resolve, reject) => {
371
+ const collected = [];
372
+ const zip = new Zip((err, chunk, final) => {
373
+ if (err) {
374
+ reject(err);
375
+ return;
376
+ }
377
+ collected.push(chunk);
378
+ onChunk(chunk);
379
+ if (final)
380
+ resolve(concat(collected));
381
+ });
382
+ for (const [name, data] of Object.entries(parts)) {
383
+ const entry = new ZipDeflate(name, { level: 6 });
384
+ zip.add(entry);
385
+ entry.push(data, true);
386
+ }
387
+ zip.end();
388
+ });
389
+ }
390
+ function concat(chunks) {
391
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
392
+ const out = new Uint8Array(total);
393
+ let offset = 0;
394
+ for (const chunk of chunks) {
395
+ out.set(chunk, offset);
396
+ offset += chunk.length;
397
+ }
398
+ return out;
399
+ }
@@ -0,0 +1,81 @@
1
+ import type { Workbook } from '../../core/workbook.ts';
2
+ import type { Worksheet } from '../../core/worksheet.ts';
3
+ import { StyleRegistry } from './styles.ts';
4
+ import { type FlushedSheet } from './worksheet-xml.ts';
5
+ export { buildColumnDefaults, Extent, type FlushedSheet, type RowRenderContext, renderRow, } from './worksheet-xml.ts';
6
+ /** Options controlling how {@link writeXlsx} serialises a workbook. */
7
+ export interface WriteOptions {
8
+ /**
9
+ * Pool plain string cell values into a shared-strings table (`xl/sharedStrings.xml`) that cells
10
+ * reference by index, rather than storing each string inline in its cell. Deduplicates repeated
11
+ * text and matches Excel's own storage; off by default, which keeps strings inline and omits the
12
+ * part. Rich-text values stay inline regardless, so their run formatting is unaffected.
13
+ */
14
+ readonly useSharedStrings?: boolean;
15
+ }
16
+ /**
17
+ * {@link WriteOptions} plus the streaming writer's internal wiring, so a buffered caller's options
18
+ * object can never carry fields meant only for {@link WorkbookStreamWriter}'s own use. Not exported
19
+ * from the public barrel — {@link buildPackageParts} is the only consumer outside this module.
20
+ */
21
+ export interface InternalWriteOptions extends WriteOptions {
22
+ /**
23
+ * The style registry to intern into, in place of a freshly-seeded one. The streaming writer
24
+ * serialises each committed row eagerly (freeing its cells), so those rows' style ids must be
25
+ * assigned by the very same registry that later emits `xl/styles.xml` — otherwise the ids in the
26
+ * pre-rendered rows would not match the styles part. When omitted the buffered path seeds its own,
27
+ * so its output is unchanged.
28
+ */
29
+ readonly styles?: StyleRegistry;
30
+ /**
31
+ * Per-sheet rows already serialised and evicted from the model by the streaming writer, keyed by
32
+ * the model worksheet. Their XML is emitted ahead of the sheet's remaining live rows and their
33
+ * extent folds into `<dimension>`. Absent for the buffered path, which holds every row live.
34
+ */
35
+ readonly flushed?: ReadonlyMap<Worksheet, FlushedSheet>;
36
+ }
37
+ /**
38
+ * Serialise a workbook into an `.xlsx` package.
39
+ *
40
+ * @throws {@link AuthoringError} if the workbook has no worksheets (a zero-sheet package is corrupt),
41
+ * or holds a value the writer cannot yet represent.
42
+ */
43
+ export declare function writeXlsx(workbook: Workbook, options?: WriteOptions): Uint8Array;
44
+ /**
45
+ * Serialise a workbook into an `.xlsx` package, deflating off the calling thread.
46
+ *
47
+ * Produces the same package {@link writeXlsx} does — every part compresses to identical bytes — and
48
+ * exists for one reason: DEFLATE dominates the cost of writing a large workbook, and {@link writeXlsx}
49
+ * spends all of it on the caller's thread. Here `fflate` deflates each part in a worker, so the event
50
+ * loop keeps turning (stalls drop from the whole write to tens of milliseconds) and parts compress in
51
+ * parallel, which on a multi-sheet workbook also finishes sooner. On a single-sheet workbook there is
52
+ * only one part to deflate, so expect responsiveness rather than speed.
53
+ *
54
+ * Building the parts still happens on the calling thread — only compression moves. That is why there
55
+ * is no `readXlsxAsync` mirroring this: reading is dominated by XML parsing and model building, which
56
+ * no worker can take, and the reader's zip-bomb ceiling is enforced by counting output between
57
+ * synchronous input slices. See ADR-0024.
58
+ *
59
+ * @throws {@link AuthoringError} — as a rejection — under the same conditions as {@link writeXlsx};
60
+ * the part-building it shares happens before any worker is involved. A failure raised by the zip
61
+ * layer itself (including an environment that cannot spawn a worker) propagates unwrapped, exactly
62
+ * as it does from {@link writeXlsx}.
63
+ */
64
+ export declare function writeXlsxAsync(workbook: Workbook, options?: WriteOptions): Promise<Uint8Array>;
65
+ /**
66
+ * A style registry seeded from a workbook's read-in style layers (differential styles, named cell
67
+ * styles, custom indexed palette), ready to intern authored styles after them. Both the buffered
68
+ * pass and the streaming writer build their registry through here so a cell's style id means the
69
+ * same thing whichever writer emits it.
70
+ */
71
+ export declare function createStyleRegistry(workbook: Workbook): StyleRegistry;
72
+ /**
73
+ * Assemble a workbook into the map of OPC package parts (part name → bytes) that make up an `.xlsx`,
74
+ * short of zipping them. This is the whole serialisation — content types, relationships, workbook,
75
+ * per-sheet XML, styles, theme, media, tables, and props — factored out of {@link writeXlsx} so the
76
+ * streaming writer can drive the identical parts through a streamed zip container rather than
77
+ * `zipSync`. Neither writer duplicates a byte of serialisation.
78
+ *
79
+ * @throws {@link AuthoringError} if the workbook has no worksheets, or holds a value the writer cannot represent.
80
+ */
81
+ export declare function buildPackageParts(workbook: Workbook, options?: InternalWriteOptions): Record<string, Uint8Array>;