@xlvane/compat 1.0.0 → 1.1.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @xlvane/compat
2
2
 
3
- The [exceljs](https://github.com/exceljs/exceljs) 4.4.0 API, built on [xlvane](https://www.npmjs.com/package/xlvane), with the differences listed in docs/exceljs-compat.md (link below). Code written for exceljs keeps working after one change in `package.json`:
3
+ The [exceljs](https://github.com/exceljs/exceljs) 4.4.0 API, built on [xlvane](https://www.npmjs.com/package/xlvane), with the differences listed below and in [docs/exceljs-compat.md](https://github.com/DevXord/xlvane/blob/main/docs/exceljs-compat.md). Code written for exceljs keeps working after one change in `package.json`:
4
4
 
5
5
  ```json
6
6
  "dependencies": {
@@ -8,8 +8,29 @@ The [exceljs](https://github.com/exceljs/exceljs) 4.4.0 API, built on [xlvane](h
8
8
  }
9
9
  ```
10
10
 
11
+ npm installs the package under the name `exceljs` (an npm alias), so `import ExcelJS from 'exceljs'` and `require('exceljs')` load `@xlvane/compat` and the code stays unchanged.
12
+
13
+ ## Installing
14
+
15
+ In a project that uses exceljs, either install the alias:
16
+
17
+ ```sh
18
+ npm install exceljs@npm:@xlvane/compat@^1
19
+ ```
20
+
21
+ or let the `xlvane` command line prepare the project:
22
+
23
+ ```sh
24
+ npx xlvane migrate # report: the change to package.json and places in the code to check
25
+ npx xlvane migrate --force # change package.json, then run npm install
26
+ ```
27
+
28
+ `xlvane migrate` changes only `package.json`, and only with `--force`. It scans the files that mention exceljs for API outside the supported subset; the matching is textual, so a finding is a place to look at.
29
+
30
+ ## Example
31
+
11
32
  ```js
12
- import ExcelJS from 'exceljs'; // or: const ExcelJS = require('exceljs');
33
+ import ExcelJS from 'exceljs';
13
34
 
14
35
  const workbook = new ExcelJS.Workbook();
15
36
  const sheet = workbook.addWorksheet('Sales');
@@ -21,10 +42,34 @@ sheet.addRow({ region: 'North', revenue: 1200 });
21
42
  await workbook.xlsx.writeFile('sales.xlsx');
22
43
  ```
23
44
 
24
- - Requires Node.js 22.12 or later for `require()` (the package is an ES module; `require()` of ES modules works without a flag from 22.12). ESM `import` works the same way, and named imports such as `import { Workbook } from 'exceljs'` work too.
25
- - The few exceljs features that are not offered (row and column operations on a streamed worksheet, which exceljs itself does not have, and deep imports of exceljs internals such as `exceljs/lib/...`) throw an `XlvError` with code `XLV_E_NOT_SUPPORTED` and the name of the feature, or fail to resolve. Nothing is dropped silently.
26
- - What is supported, partly supported and not supported, with the source of each exceljs behaviour: [docs/exceljs-compat.md](https://github.com/DevXord/xlvane/blob/main/docs/exceljs-compat.md) (also in `node_modules/xlvane/docs/exceljs-compat.md` after installing). Errors are described in `node_modules/xlvane/docs/errors.md`.
45
+ In CommonJS, `const ExcelJS = require('exceljs');` gives the same object; call `writeFile` inside an async function there.
46
+
47
+ ## Requirements
48
+
49
+ - Node.js 22.12 or later (`engines.node`). The package is an ES module; `require()` of ES modules works without a flag from Node.js 22.12. Named imports such as `import { Workbook } from 'exceljs'` work too.
50
+ - The same test bundle runs in Node.js 22, 24 and 26, Bun, Deno, Chromium and the Cloudflare Workers runtime (workerd). Methods that read or write files (`xlsx.readFile`, `xlsx.writeFile`, `csv.readFile`, `csv.writeFile`, the `filename` forms of the streaming classes and of `addImage`) work in Node.js, Bun and Deno; in browsers and Workers they throw `XLV_E_NOT_SUPPORTED`, and a bundler leaves the file helpers out.
51
+ - Dependencies: `xlvane` only, which depends on `fflate` only.
52
+ - TypeScript: the type names of exceljs's `index.d.ts` are exported (`ExcelJS.Worksheet`, `ExcelJS.Fill`, …) and work with `moduleResolution` `bundler` and `nodenext` and with `module: commonjs`. In a CommonJS project, TypeScript refuses `import ExcelJS = require('exceljs')` of an ES module under `module: node16` (TS1471); use `nodenext` or `commonjs`.
53
+
54
+ ## Differences from exceljs
55
+
56
+ The full list, with the exceljs source of each behaviour, is in [docs/exceljs-compat.md](https://github.com/DevXord/xlvane/blob/main/docs/exceljs-compat.md) (also in `node_modules/xlvane/docs/exceljs-compat.md` after installing). The ones most likely to matter:
57
+
58
+ - **Not offered:** row and column operations on a streamed worksheet (`insertRow(s)`, `spliceRows`, `duplicateRow`, `spliceColumns`; exceljs's streaming worksheet does not have them either) throw an `XlvError` with code `XLV_E_NOT_SUPPORTED` and the name of the method. Deep imports of exceljs internals (`exceljs/lib/...`) do not resolve; `exceljs/dist/es5` and the `exceljs/dist/exceljs*.js` files do. Nothing is dropped silently.
59
+ - **CSV:** text that starts with `=`, `+`, `-`, `@`, a tab, CR or LF is written with a leading `'`, so spreadsheet applications do not run it as a formula. `formulaInjection: "off"` writes it unchanged, as exceljs does.
60
+ - **Sheet ids:** `worksheet.id` is written as `sheetId` and kept on loading, as in exceljs; an id outside 1 to 65534 is written as the lowest unused one, and the streaming `WorkbookReader` numbers worksheets 1, 2, 3… in file order.
61
+ - **Views:** only the first view of a worksheet is written; a frozen view always gets the cell right of and below the panes as `topLeftCell`.
62
+ - **Notes:** `protection.locked` and `lockText` read back as `'True'`/`'False'`, the values Excel writes.
63
+ - **Module object:** it is frozen, so adding or replacing its properties (monkey-patching) has no effect.
64
+ - **Streaming writer speed:** `stream.xlsx.WorkbookWriter` compresses in JavaScript (fflate) and took about twice as long as exceljs, which uses Node.js zlib (300 000 rows: 6.1 s against 2.8 s).
65
+ - **Input files** are treated as untrusted, as in xlvane: ZIP and XML limits, no DTDs or external entities. A file over a limit is rejected with an `XlvError` (`XLV_E_*` code, location and hint; `node_modules/xlvane/docs/errors.md`).
66
+
67
+ ## Links
68
+
69
+ - Changes in each version: [CHANGELOG.md](https://github.com/DevXord/xlvane/blob/main/CHANGELOG.md)
70
+ - Bugs and questions: [GitHub issues](https://github.com/DevXord/xlvane/issues)
71
+ - Reporting a vulnerability: [SECURITY.md](https://github.com/DevXord/xlvane/blob/main/SECURITY.md)
27
72
 
28
- @xlvane/compat is an independent project. It is not affiliated with or endorsed by the exceljs project or its authors, and it contains no exceljs code; its API and type declarations follow exceljs 4.4.0, whose MIT license notice is included in LICENSE.
73
+ ## License
29
74
 
30
- License: MIT
75
+ MIT. @xlvane/compat is an independent project. It is not affiliated with or endorsed by the exceljs project or its authors, and it contains no exceljs code; its API and type declarations follow exceljs 4.4.0, whose MIT license notice is included in LICENSE.
package/dist/index.d.ts CHANGED
@@ -295,6 +295,8 @@ export declare class Workbook {
295
295
  get nextId(): number;
296
296
  /** Adds a worksheet. Names follow exceljs rules (31 characters at most; longer names are cut). */
297
297
  addWorksheet(name?: string, options?: WorksheetOptions): Worksheet;
298
+ /** @internal Adds a worksheet with a given id (a loaded file's sheetId, a model's id; D-F03). */
299
+ _addWorksheetWithId(id: number, name?: string, options?: WorksheetOptions): Worksheet;
298
300
  removeWorksheet(id: number | string): void;
299
301
  /** Removes a worksheet by id or name, as `removeWorksheet` (lib/doc/workbook.js L77). */
300
302
  removeWorksheetEx(worksheet: Worksheet): void;
package/dist/index.js CHANGED
@@ -1228,9 +1228,10 @@ function noteBoxOut(box) {
1228
1228
  * The first exceljs view and the tab colour (X4-11, X4-12). xlvane writes one view: frozen panes
1229
1229
  * (`xSplit`/`ySplit` in columns and rows) or a split window (`xSplit`/`ySplit` in 1/20 point, as
1230
1230
  * exceljs), plus `zoomScale`, `showGridLines`, `showRowColHeaders`, `rightToLeft` and `style`.
1231
- * `activeCell` is written for every view; `topLeftCell` only for a normal view (a split view's
1232
- * exceljs `topLeftCell` is `pane@topLeftCell`, which the core does not write); `activePane`
1233
- * follows from the panes.
1231
+ * `activeCell` is written for every view; `topLeftCell` for a normal view as
1232
+ * `sheetView@topLeftCell` and for a split view as `pane@topLeftCell`, as exceljs writes them
1233
+ * (sheet-view-xform.js L72-85; D-F02); a frozen view's `topLeftCell` follows from the panes;
1234
+ * `activePane` follows from the panes.
1234
1235
  */
1235
1236
  function layoutIn(ws) {
1236
1237
  const v = ws.views[0];
@@ -1251,10 +1252,14 @@ function layoutIn(ws) {
1251
1252
  rows: v.ySplit ?? 0,
1252
1253
  cols: v.xSplit ?? 0
1253
1254
  };
1254
- else if (v.state === "split" && ((v.xSplit ?? 0) > 0 || (v.ySplit ?? 0) > 0)) view.split = {
1255
- x: (v.xSplit ?? 0) / 20,
1256
- y: (v.ySplit ?? 0) / 20
1257
- };
1255
+ else if (v.state === "split" && ((v.xSplit ?? 0) > 0 || (v.ySplit ?? 0) > 0)) {
1256
+ const corner = typeof v["topLeftCell"] === "string" && v["topLeftCell"] !== "" ? { topLeftCell: v["topLeftCell"] } : {};
1257
+ view.split = {
1258
+ x: (v.xSplit ?? 0) / 20,
1259
+ y: (v.ySplit ?? 0) / 20,
1260
+ ...corner
1261
+ };
1262
+ }
1258
1263
  }
1259
1264
  let tab = ws.properties["tabColor"];
1260
1265
  if (tab && typeof tab.argb === "string" && /^[0-9A-Fa-f]{7}$/.test(tab.argb)) tab = {
@@ -1356,7 +1361,10 @@ function toNewWorkbook(sheets, date1904, meta = {}) {
1356
1361
  if (meta.views && meta.views.length > 0) book.views(bookViewsIn(meta.views));
1357
1362
  if (meta.calcProperties?.["fullCalcOnLoad"]) book.calculation({ fullCalcOnLoad: true });
1358
1363
  for (const ws of sheets) at(ws.name, void 0, () => {
1359
- const sheet = book.addSheet(ws.name, { state: ws.state });
1364
+ const sheet = book.addSheet(ws.name, {
1365
+ state: ws.state,
1366
+ ...sheetIdOf(ws.id)
1367
+ });
1360
1368
  const useStyles = meta.useStyles !== false;
1361
1369
  const sharedRanges = sharedFormulaRanges(ws);
1362
1370
  const props = sheetPropertiesIn(ws.properties);
@@ -1487,8 +1495,9 @@ function fontOut(f) {
1487
1495
  /**
1488
1496
  * An exceljs view from the view read by xlvane, shaped as exceljs reads one (sheet-view-xform.js
1489
1497
  * L145–192; X4-11): a missing `zoomScaleNormal` reads as 100; a frozen view gets the default
1490
- * `topLeftCell`; a split view gets `xSplit`/`ySplit` in 1/20 point and the active pane xlvane
1491
- * writes; a normal view has no `topLeftCell`, as in exceljs.
1498
+ * `topLeftCell`; a split view gets `xSplit`/`ySplit` in 1/20 point, `pane@topLeftCell` as
1499
+ * `topLeftCell` (sheet-view-xform.js L118-124) and the active pane xlvane writes; a normal view
1500
+ * has no `topLeftCell`, as in exceljs.
1492
1501
  */
1493
1502
  function viewOut(view, freeze) {
1494
1503
  const common = {
@@ -1516,14 +1525,14 @@ function viewOut(view, freeze) {
1516
1525
  }
1517
1526
  const active = view.activeCell !== null ? { activeCell: view.activeCell } : {};
1518
1527
  if (view.split) {
1519
- const { x, y } = view.split;
1528
+ const { x, y, topLeftCell } = view.split;
1520
1529
  const activePane = x > 0 && y > 0 ? "bottomRight" : y > 0 ? "bottomLeft" : "topRight";
1521
1530
  return {
1522
1531
  ...common,
1523
1532
  state: "split",
1524
1533
  xSplit: Math.round(x * 20),
1525
1534
  ySplit: Math.round(y * 20),
1526
- topLeftCell: void 0,
1535
+ topLeftCell,
1527
1536
  activePane,
1528
1537
  ...active,
1529
1538
  ...style
@@ -1665,6 +1674,10 @@ function styleOut(s, numFmt) {
1665
1674
  function readBytes(bytes) {
1666
1675
  return readWorkbook(bytes);
1667
1676
  }
1677
+ /** `{ sheetId }` for a worksheet id that Office accepts (1 to 65534, W2-20), otherwise nothing (D-F03). */
1678
+ function sheetIdOf(id) {
1679
+ return Number.isInteger(id) && id >= 1 && id <= 65534 ? { sheetId: id } : {};
1680
+ }
1668
1681
  // src/sheet-rules.ts
1669
1682
  const MAX_ROW = 1048576;
1670
1683
  const MAX_COL = 16384;
@@ -5844,7 +5857,10 @@ var Workbook = class {
5844
5857
  }
5845
5858
  /** Adds a worksheet. Names follow exceljs rules (31 characters at most; longer names are cut). */
5846
5859
  addWorksheet(name, options) {
5847
- const id = this.nextId;
5860
+ return this._addWorksheetWithId(this.nextId, name, options);
5861
+ }
5862
+ /** @internal Adds a worksheet with a given id (a loaded file's sheetId, a model's id; D-F03). */
5863
+ _addWorksheetWithId(id, name, options) {
5848
5864
  const sheet = new Worksheet(this, id, name, options);
5849
5865
  sheet.orderNo = this._sheets.reduce((acc, ws) => ws?.orderNo !== void 0 && ws.orderNo > acc ? ws.orderNo : acc, 0) + 1;
5850
5866
  this._sheets[id] = sheet;
@@ -5916,8 +5932,10 @@ var Workbook = class {
5916
5932
  this.views = Array.isArray(value["views"]) ? [...value["views"]] : [];
5917
5933
  this._media = Array.isArray(value["media"]) ? [...value["media"]] : [];
5918
5934
  for (const ws of this.worksheets) this.removeWorksheet(ws.id);
5919
- for (const m of Array.isArray(value["worksheets"]) ? value["worksheets"] : []) {
5920
- const ws = this.addWorksheet(String(m["name"] ?? ""), { state: m["state"] ?? "visible" });
5935
+ const models = Array.isArray(value["worksheets"]) ? value["worksheets"] : [];
5936
+ const ids = freeIds(models.map((m) => m["id"]));
5937
+ for (const [i, m] of models.entries()) {
5938
+ const ws = this._addWorksheetWithId(ids[i] ?? this.nextId, String(m["name"] ?? ""), { state: m["state"] ?? "visible" });
5921
5939
  ws.model = m;
5922
5940
  }
5923
5941
  this._definedNames.model = Array.isArray(value["definedNames"]) ? value["definedNames"] : [];
@@ -6030,9 +6048,10 @@ var Workbook = class {
6030
6048
  name: d.name,
6031
6049
  ranges: extractRanges(d.refersTo)
6032
6050
  }));
6051
+ const ids = freeIds(read.sheets.map((s) => s.kind === "worksheet" ? s.sheetId : void 0));
6033
6052
  for (const [position, s] of read.sheets.entries()) {
6034
6053
  if (s.kind !== "worksheet") continue;
6035
- const ws = this.addWorksheet(s.name, { state: s.state });
6054
+ const ws = this._addWorksheetWithId(ids[position] ?? this.nextId, s.name, { state: s.state });
6036
6055
  ws.orderNo = position;
6037
6056
  for (const c of use("cols") ? s.columns : []) {
6038
6057
  const last = Math.min(c.max, 16384);
@@ -6123,6 +6142,30 @@ var Workbook = class {
6123
6142
  }
6124
6143
  }
6125
6144
  };
6145
+ /**
6146
+ * Worksheet ids for a list of wanted ids: each whole number from 1 to 65534 (the sheet ids Office
6147
+ * uses, W2-20) that is not wanted by an earlier entry is kept; other entries (missing, invalid,
6148
+ * larger or repeated ids) get the lowest ids no entry wants, in order. The limit keeps the
6149
+ * `_sheets` array small: an id from an untrusted file such as 4294967295 is not an array index and
6150
+ * 20000000 makes every scan of the array slow (found by review, D-F03). `undefined` entries
6151
+ * (sheets that are not worksheets) get no id.
6152
+ */
6153
+ function freeIds(wanted) {
6154
+ const taken = /* @__PURE__ */ new Set();
6155
+ const kept = wanted.map((w) => {
6156
+ if (typeof w !== "number" || !Number.isInteger(w) || w < 1 || w > 65534 || taken.has(w)) return null;
6157
+ taken.add(w);
6158
+ return w;
6159
+ });
6160
+ let next = 1;
6161
+ return kept.map((k, i) => {
6162
+ if (wanted[i] === void 0) return void 0;
6163
+ if (k !== null) return k;
6164
+ while (taken.has(next)) next++;
6165
+ taken.add(next);
6166
+ return next;
6167
+ });
6168
+ }
6126
6169
  // src/stream.ts
6127
6170
  /**
6128
6171
  * A cell value for the streaming writer. Rows are written as they are committed, before later
@@ -6284,6 +6327,7 @@ var WorksheetWriter = class extends Worksheet {
6284
6327
  return {
6285
6328
  name: this.name,
6286
6329
  state: this.state,
6330
+ ...sheetIdOf(this.id),
6287
6331
  columns: trimTrailing(this.columns.map((c) => columnIn(c, this.writer._useStyles))),
6288
6332
  ...layout.freeze ? { freeze: layout.freeze } : {},
6289
6333
  ...Object.keys(layout.view).length > 0 ? { view: layout.view } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xlvane/compat",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "The commonly used exceljs 4.4.0 API on top of xlvane, for switching with one line in package.json.",
5
5
  "keywords": [
6
6
  "xlsx",
@@ -72,7 +72,7 @@
72
72
  "build": "tsdown"
73
73
  },
74
74
  "dependencies": {
75
- "xlvane": "^1.0.0"
75
+ "xlvane": "^1.1.0"
76
76
  },
77
77
  "publishConfig": {
78
78
  "access": "public",