/** * XLSX - Abstract base class for XLSX operations * * Contains all platform-agnostic logic shared between Node.js and Browser versions: * - reconcile: Reconcile model after parsing * - _process*Entry: Process individual ZIP entries * - add*: Add content to ZIP during writing * - prepareModel: Prepare model for writing * - loadFromFiles: Load from pre-extracted ZIP data */ import type { ZipTimestampMode } from "../../archive/zip-spec/timestamps.js"; import type { PivotTableSubtotal } from "../pivot-table.js"; import type { Workbook } from "../workbook.browser.js"; import { type IEventEmitter } from "../../stream/index.js"; type StreamListener = Parameters[1]; interface EmitterLike { on(event: string, listener: StreamListener): this; once(event: string, listener: StreamListener): this; off(event: string, listener: StreamListener): this; } export interface IParseStream extends EmitterLike { pipe(dest: any): any; [Symbol.asyncIterator]?: () => AsyncIterator; } /** * Minimal write-side shape required to receive XLSX bytes. Anything that * behaves like a Node `WritableStream` (a `write()` method plus `end()` and * event emitter basics) satisfies this — including Node's `fs.WriteStream`, * `PassThrough`, and our `@stream` Writable class. */ export interface IWritableStream extends EmitterLike { write(data: string | Uint8Array): boolean | void | Promise; end(): void; pipe?(dest: any): any; } /** * An in-memory buffered stream. Extends the write-side shape with `read()` * and optional `toBuffer()` for callers that use the stream as a sink and * then harvest the accumulated bytes (e.g. `writeBuffer()`'s internal buffer). */ export interface IStreamBuf extends IWritableStream { read(): Uint8Array | null; toBuffer?(): Uint8Array | null; } export interface IZipWriter extends EmitterLike { append(data: string | Uint8Array, options: { name: string; base64?: boolean; }): void; /** Create a streaming entry: write chunks incrementally, then call end(). */ createEntry(name: string): { write(chunk: string): void; end(): void; }; pipe(stream: IWritableStream): void; finalize(): void; /** Wait for downstream backpressure to clear. Resolves immediately if no backpressure. */ waitForDrain(): Promise; } /** * Options for reading (loading) an XLSX workbook. * * All officially supported options are declared below with proper types so * callers get IDE completion and type-checking. Additional fields are * permitted via the index signature for forward compatibility and for * callers who subclass `XLSX` to pass through private flags. */ export interface XlsxReadOptions { /** * When the input to `load()` is a string, interpret it as a base64-encoded * zip archive instead of a binary buffer. Defaults to `false`. */ base64?: boolean; /** * Maximum number of rows to parse from each worksheet. Rows beyond this * limit are silently skipped. Useful for previewing very large sheets * without loading everything into memory. */ maxRows?: number; /** * Maximum number of columns to parse from each worksheet. Columns beyond * this limit are silently skipped. Useful for previewing very wide sheets. */ maxCols?: number; /** * List of worksheet XML node names to skip while parsing (e.g. * `"dataValidations"`, `"conditionalFormatting"`). Use for workbooks that * contain corrupted or unsupported elements you want to ignore. */ ignoreNodes?: string[]; /** * Forward-compatibility / subclass extension escape hatch. Unknown keys are * passed through to internal loaders; unrecognised keys are ignored. */ [key: string]: unknown; } export interface ZipWriterOptions { level?: number; /** ZIP entry modification time (optional). If omitted, defaults to current time. */ modTime?: Date; /** Timestamp writing strategy for ZIP entry metadata (optional). */ timestamps?: ZipTimestampMode; } export type XlsxTemplateMode = "preserve" | "strict"; /** * Options for writing an XLSX workbook. * * All officially supported options are declared below with proper types so * callers get IDE completion and type-checking. Additional fields are * permitted via the index signature for forward compatibility and for * callers who subclass `XLSX` to pass through private flags. */ export interface XlsxWriteOptions { /** ZIP archive options (compression level, timestamps, ...). */ zip?: ZipWriterOptions; /** * Use a shared-string table for cell text values. Defaults to `true`. * Set to `false` to write string values inline (larger file, but streams * better for very large sheets). */ useSharedStrings?: boolean; /** * Emit style definitions (fonts, fills, borders, number formats, …). * Defaults to `true`. Set to `false` to skip style blocks for maximum * compatibility with minimal readers. */ useStyles?: boolean; /** * Template fidelity strategy for loaded workbooks. The default `"preserve"` * byte-preserves clean chart parts and may structurally re-render edited parts * when no safe raw XML patch is available. `"strict"` fails the write instead * of re-rendering any edited loaded chart/chartEx part that cannot be patched * in-place, preventing silent loss of unknown template XML. * * When a strict write fails, the thrown error enumerates the unrecognised * `c15:`/`cx14:` extension paths the parser observed, so authors can decide * between relaxing the mode or reshaping the mutation into a patch-friendly * path. To inspect those paths *before* writing (for example to decide * whether to opt into strict mode at all), use {@link Chart.unknownElements} * on each chart of interest. */ templateMode?: XlsxTemplateMode; /** * Convenience alias for `templateMode: "strict"`. Strict mode refuses to * silently drop vendor-extension XML (`c15:…`, `cx14:…`) that the * structured parser does not understand. Use this when round-tripping * Excel-authored template files where preserving exotic extension * elements matters more than the ability to re-render modified charts. */ strictTemplateMode?: boolean; /** * Run the OOXML self-check on the produced bytes and `console.warn` * every detected problem. Only {@link XLSX.writeBuffer} honours this * flag — `write(stream)` / `writeFile` cannot post-validate because * their output is streamed to the caller. * * Default resolution: * - In Node.js with `NODE_ENV !== "production"` and NOT running * under vitest (`process.env.VITEST !== "true"`): `true`. * - In production, in the browser, or under vitest: `false`. * * The vitest carve-out exists because running validation on every * `writeBuffer` call inflates fixture `beforeAll` hooks that build * hundreds of workbooks by ~50 seconds on typical hardware. Tests * that want validation use {@link expectValidXlsx} directly. * * Pass `true` to force validation (even in production or under * vitest), or `false` to suppress it (even in development). The * self-check never throws: a failed validation becomes a warning so * writers that intentionally produce non-conformant xlsx for * testing keep working. * * @see OoxmlValidationReport */ validate?: boolean; /** * Forward-compatibility / subclass extension escape hatch. Unknown keys are * passed through to internal writers; unrecognised keys are ignored. */ [key: string]: unknown; } export type XlsxOptions = XlsxReadOptions & XlsxWriteOptions; export interface WorkbookMediaLike { type: string; extension: string; name?: string; filename?: string; buffer?: Uint8Array; base64?: string; /** External link target — when set, the image is referenced, not embedded. */ link?: string; } export interface MediaModel { media: WorkbookMediaLike[]; } interface ZipEntryLike { name: string; type: "Directory" | "File" | "Symlink"; stream: IParseStream; drain: () => Promise; } /** * XLSX class - handles Excel file operations * Works in both Node.js and Browser environments * * Generic over the concrete `Workbook` type so a Node subclass that extends * `XLSX` automatically narrows `load()` / `read()` / * `loadFromFiles()` / etc. to return the Node `Workbook` (which exposes * `xlsx.readFile` / `xlsx.writeFile`). Without this, those methods are * inherited unchanged and surface the browser `Workbook` type to Node * consumers — see issue #160. * * The default type parameter keeps the public XLSX surface unchanged for * external callers (`new XLSX(workbook)` is still typed as `XLSX`). */ declare class XLSX { workbook: TWorkbook; static RelType: { OfficeDocument: string; Worksheet: string; CalcChain: string; SharedStrings: string; Styles: string; Theme: string; Hyperlink: string; Image: string; CoreProperties: string; ExtenderProperties: string; Comments: string; VmlDrawing: string; Table: string; PivotCacheDefinition: string; PivotCacheRecords: string; PivotTable: string; FeaturePropertyBag: string; CtrlProp: string; SheetMetadata: string; ExternalLink: string; ExternalLinkPath: string; Chart: string; ChartEx: string; ChartStyle: string; ChartColors: string; Drawing: string; ChartUserShapes: string; Chartsheet: string; ThreadedComments: string; Person: string; Slicer: string; SlicerCache: string; Timeline: string; TimelineCache: string; }; constructor(workbook: TWorkbook); /** * Create a stream from binary data (for media/themes) */ protected createBinaryStream(data: Uint8Array): IParseStream; /** * Create a stream from string content (for XML parsing) */ protected createTextStream(content: string): IParseStream; /** * Create a StreamBuf instance for buffering data */ protected createStreamBuf(): IStreamBuf; /** * Convert buffer/Uint8Array to string */ protected bufferToString(data: string | ArrayBuffer | Uint8Array): string; /** * Create a ZIP writer adapter. * Can be overridden by subclasses for platform-specific implementations. */ protected createZipWriter(options?: XlsxWriteOptions["zip"]): IZipWriter; /** * Write all workbook content to a ZIP writer * Shared by both Node.js write() and browser writeBuffer() */ protected writeToZip(zip: IZipWriter, options?: XlsxWriteOptions): Promise; /** * Emit the raw slicer/timeline parts captured on load. Pure * byte-copy — excelts does not modify these parts. The partner * Content-Types and rels are covered separately (content types in * `addContentTypes`, sheet/workbook rels by the corresponding * xforms consuming the existing `xl/_rels/*.rels` captured on * load). */ addSlicerAndTimelineParts(zip: IZipWriter, model: any): Promise; /** * Write the workbook-level `xl/persons/person.xml` part when the * model carries Office 365 threaded-comment authors. No-op when the * persons list is empty so legacy files without threaded comments * stay byte-identical. */ addPersons(zip: IZipWriter, model: any): Promise; /** * Read workbook from a stream */ read(stream: IParseStream, options?: XlsxReadOptions): Promise; /** * Write workbook to a stream */ write(stream: IWritableStream, options?: XlsxWriteOptions): Promise; /** * Load a workbook from binary data. * * Accepted inputs: * - `Uint8Array` (and `Buffer`, which is a Uint8Array at runtime) * - `ArrayBuffer` / `SharedArrayBuffer` * - Any `ArrayBufferView` (DataView, Int8Array, Float32Array, …) — the * underlying bytes are reinterpreted as a zip archive * - `string` — treated as base64-encoded data when `options.base64 === true`; * raw binary cannot be round-tripped through a JS string and is rejected * to prevent silent corruption. */ load(data: Uint8Array | ArrayBuffer | ArrayBufferView | string, options?: XlsxReadOptions): Promise; /** * Internal: Load from Uint8Array buffer */ protected loadBuffer(buffer: Uint8Array, options?: XlsxReadOptions): Promise; /** * Internal: Load workbook from an async stream of ZIP entries. * * This is the foundation for TRUE streaming reads on platforms that have a * streaming ZIP parser (e.g. Node.js `modules/archive` Parse). */ /** * Create an empty model for parsing XLSX files. * Shared by loadFromZipEntries and loadFromFiles. */ private createEmptyModel; /** * Collect all data from a stream into a single Uint8Array. * Reusable helper for passthrough and drawing processing. */ protected collectStreamData(stream: IParseStream): Promise; /** * Process a known OOXML entry (workbook, styles, shared strings, etc.) * Returns true if handled, false if should be passed to _processDefaultEntry */ protected _processKnownEntry(stream: IParseStream, model: any, entryName: string, options?: XlsxOptions): Promise; protected loadFromZipEntries(entries: AsyncIterable, options?: XlsxOptions): Promise; /** * Write workbook to buffer */ writeBuffer(options?: XlsxWriteOptions): Promise; /** * Add media files to ZIP * Supports buffer, base64, and filename (if readFileAsync is provided) */ addMedia(zip: IZipWriter, model: MediaModel): Promise; /** * Optional file reader - can be overridden by subclasses (e.g., Node.js version) */ protected readFileAsync?: (filename: string) => Promise; parseRels(stream: IParseStream): Promise; parseWorkbook(stream: IParseStream): Promise; parseSharedStrings(stream: IParseStream): Promise; reconcile(model: any, options?: XlsxOptions): void; /** * Copy the raw bytes of each chart's user-shapes drawing part onto * the owning `ChartEntry.userShapesXml` so the writer can emit them * back verbatim (and so {@link Chart.userShapesXml} can surface them * to user code). Runs after all ZIP entries have been processed * because chart rels and drawing bytes stream in independent order. * * Skips charts that have no `ChartUserShapes` rel. The bytes stay * keyed by drawing name (e.g. `drawing3`) inside `model.drawingRaw` * since a workbook may have many user-shape drawings across * different charts; we look up each chart's target through its * rels file. */ protected _reconcileChartUserShapes(model: any): void; /** * Join the three on-disk sources that together describe external workbook * references into a single dense `model.externalLinks: ExternalLinkModel[]`. * * Sources: * - `` list in workbook.xml (declaration order) * - `xl/_rels/workbook.xml.rels` (rId → internal path) * - `xl/externalLinks/externalLink{N}.xml` (sheet names, cached values) * - `xl/externalLinks/_rels/externalLink{N}.xml.rels` (target, TargetMode) * * The 1-based index of each resulting ExternalLinkModel matches the `[N]` * used in formula strings — this is the single source of truth formula * code should rely on. */ protected _reconcileExternalLinks(model: any): void; /** * Write-time pass that brings the workbook model into a shape the writer * can serialise cleanly. Two concerns: * * 1. Build the final external-link list for this write, combining * user-declared links (`wb.externalLinks`) with auto-discovered * ones from previous writes (cached on the Workbook). The result * is assigned to `model.externalLinks` and consumed by the writer; * `wb.externalLinks` is **not** modified. * * 2. Scan every formula cell for `[Book]Sheet!` prefixes. Filename-form * references that don't match an existing link trigger * `_recordAutoExternalLink()` on the Workbook, which adds the target * to the private writer cache (so subsequent writes are fixed-point * stable) but leaves `wb.externalLinks` untouched. * * 3. Rewrite every external-ref formula so it uses the numeric `[N]` * form, the canonical OOXML storage form. This mutation lands on * the cell's model object — matching the library's existing * write-time pattern for `ssId`, `styleId`, `si`, and `cm`. * Subsequent writes see the `[N]` form directly and resolve it * against `model.externalLinks`, giving idempotent output. */ protected _normaliseExternalLinks(model: any): void; /** * Rewrite a single formula so every external-ref prefix uses the numeric * `[N]` form. When an unknown filename-form reference is found we record * it on the workbook's private writer cache (so the next write can still * resolve it) and append a local link to `scratch.links` so subsequent * refs in the same formula see the freshly-assigned index. */ private _normaliseFormulaExternalRefs; /** * Reconcile pivot tables by linking them to worksheets and their cache data. */ protected _reconcilePivotTables(model: any): void; protected _extractTableNumber(name: string): number; protected _buildCacheIdMap(model: any): Map; protected _buildDefinitionToCacheIdMap(model: any): Map; protected _determineMetric(dataFields: Array<{ subtotal?: string; }>): PivotTableSubtotal; protected _determineValueMetrics(dataFields: Array<{ subtotal?: string; }>, defaultMetric: PivotTableSubtotal): PivotTableSubtotal[]; _processWorksheetEntry(stream: IParseStream, model: any, sheetNo: number, options: XlsxOptions | undefined, path: string): Promise; _processChartsheetEntry(stream: IParseStream, model: any, sheetNo: number): Promise; _processCommentEntry(stream: IParseStream, model: any, zipPath: string): Promise; _processTableEntry(stream: IParseStream, model: any, zipPath: string): Promise; _processWorksheetRelsEntry(stream: IParseStream, model: any, sheetNo: number): Promise; _processMediaEntry(stream: IParseStream, model: any, filename: string): Promise; /** * Process a drawing XML entry. * * @param stream - Stream to read from (used in loadFromZipEntries path) * @param model - Model to populate * @param name - Drawing name (e.g., "drawing1") * @param rawData - Pre-read raw data (used in loadFromFiles path to avoid re-reading stream) */ _processDrawingEntry(stream: IParseStream, model: any, name: string, rawData?: Uint8Array): Promise; /** * Stash raw bytes of a chart-overlay drawing part. `c:userShapes` * parts live under `xl/drawings/chartUserShape{N}.xml` in files we * write ourselves and can use arbitrary names in foreign files (the * rel target is the only authoritative reference). The bytes are * keyed by the stem so `_reconcileChartUserShapes` can match them * against each chart's `ChartUserShapes` rel Target. */ _processChartUserShapesEntry(_stream: IParseStream, model: any, name: string, rawData?: Uint8Array): Promise; _processDrawingRelsEntry(entry: any, model: any, name: string): Promise; _processVmlDrawingEntry(entry: any, model: any, zipPath: string): Promise; _processVmlDrawingHFEntry(entry: any, model: any, _name: string): Promise; _processThemeEntry(stream: IParseStream, model: any, name: string): Promise; _processPivotTableEntry(stream: IParseStream, model: any, name: string): Promise; _processPivotTableRelsEntry(stream: IParseStream, model: any, name: string): Promise; _processPivotCacheDefinitionEntry(stream: IParseStream, model: any, name: string): Promise; _processPivotCacheRecordsEntry(stream: IParseStream, model: any, name: string): Promise; /** * Parse `xl/externalLinks/externalLink{N}.xml` into the intermediate * ParsedExternalLink shape. Reconciliation (joining with the rels file * and the workbook's `` list) happens later in * {@link reconcile}. */ _processExternalLinkEntry(stream: IParseStream, model: any, index: number): Promise; /** * Parse `xl/externalLinks/_rels/externalLink{N}.xml.rels`. The Target / * TargetMode carried here is what Excel uses to locate the actual external * file at open time, so we must preserve it verbatim (including relative * paths like `"测试.xlsx"`). */ _processExternalLinkRelsEntry(stream: IParseStream, model: any, index: number): Promise; _processChartEntry(stream: IParseStream, model: any, chartNumber: number, rawData?: Uint8Array): Promise; _processChartRelsEntry(stream: IParseStream, model: any, chartNumber: number): Promise; _processChartStyleEntry(stream: IParseStream, model: any, styleNumber: number): Promise; _processChartColorsEntry(stream: IParseStream, model: any, colorsNumber: number): Promise; loadFromFiles(zipData: Record, options?: XlsxReadOptions): Promise; /** * Process default entries (drawings, comments, tables, etc.) * @param rawData Optional raw entry data for passthrough preservation (used by loadFromFiles) */ protected _processDefaultEntry(stream: IParseStream, model: any, entryName: string, rawData?: Uint8Array): Promise; /** * Helper: render an xform directly to a streaming zip entry. * Avoids buffering the entire XML string in memory. * Awaits backpressure drain after each entry to respect downstream flow control. */ private _renderToZip; addContentTypes(zip: IZipWriter, model: any): Promise; addApp(zip: IZipWriter, model: any): Promise; addCore(zip: IZipWriter, model: any): Promise; addThemes(zip: IZipWriter, model: any): Promise; addOfficeRels(zip: IZipWriter, _model: any): Promise; addWorkbookRels(zip: IZipWriter, model: any): Promise; addFeaturePropertyBag(zip: IZipWriter, model: any): Promise; addMetadata(zip: IZipWriter, model: any): Promise; addSharedStrings(zip: IZipWriter, model: any): Promise; addStyles(zip: IZipWriter, model: any): Promise; addWorkbook(zip: IZipWriter, model: any): Promise; addWorksheets(zip: IZipWriter, model: any): Promise; addChartsheets(zip: IZipWriter, model: any): Promise; addDrawings(zip: IZipWriter, model: any): Promise; addCharts(zip: IZipWriter, model: any, strictTemplateMode?: boolean): Promise; addChartExEntries(zip: IZipWriter, model: any, strictTemplateMode?: boolean): Promise; private _appendChartExSidecars; private _buildChartExRels; addTables(zip: IZipWriter, model: any): Promise; /** * Write every external workbook reference into the archive. For each * {@link ExternalLinkModel} in `model.externalLinks` we emit two files: * * xl/externalLinks/externalLink{index}.xml — sheet names + cache * xl/externalLinks/_rels/externalLink{index}.xml.rels — target path * * The target-path rel carries `TargetMode="External"` with a **bare * relative** `Target` whenever the user supplied one. This is the single * line that makes Office / WPS resolve the referenced workbook relative * to the current file's directory (not the `%USERPROFILE%\Documents` * fallback) — the root of the relative-path external-link behaviour. */ addExternalLinks(zip: IZipWriter, model: any): Promise; addPivotTables(zip: IZipWriter, model: any): Promise; _finalize(zip: IZipWriter): Promise; prepareModel(model: any, options: any): void; prepareChartExSidecars(model: any): void; prepareChartsheets(model: any): void; } export { XLSX };