@reportwright/cli 0.14.0-rc.1 → 0.14.0-rc.2

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/CHANGELOG.md CHANGED
@@ -2,10 +2,20 @@
2
2
 
3
3
  All `@reportwright/*` packages (formerly `@pagewrightjs/*`) share one version. The versioning policy is in [CONTRIBUTING.md](CONTRIBUTING.md#versioning-semver).
4
4
 
5
+ ## 0.14.0-rc.2 — 2026-10-10 (release candidate)
6
+
7
+ ### Fixed (from the rc.1 release gate)
8
+ - **Memory of a zebra table in `exportPdfStream`.** A detail row whose style reads `RowNumber()` (`backgroundColor: '=IIf(RowNumber() Mod 2 = 0, "#f3f4f6", "#ffffff")'`) was refused by the streaming planner ("detail expressions", in `r.why`) and rendered whole, so every page stayed in memory: ~3.5–8 KB a row, 558 MB–1 GB at 100,000 rows. It now streams (row styles may read `RowNumber()`, as row values could), and its pages are the whole render's (pixels, text). Peak RSS at 100,000 rows, probe `scripts/eng-probe.mjs`: plain 270 MB, zebra 1,056 → 287 MB, zebra with expression borders and formats 1,261 → 300 MB.
9
+ - **Expression styles that give the same values share one style.** Each row's evaluated style was a new object, and the cell styles built on it a new cache entry per row (garbage V8 collected late: about 2× the peak RSS of a plain table and 2.5× its time). Rows whose expressions give the same plain values now share one frozen style (at most 64 per style), so a zebra table renders at about the plain table's speed (100,000 rows: 5.8 s → 2.1 s; plain 1.6 s). Reports without expression styles are byte-identical; the zebra reports' bytes change only because they now stream (same pages, pixels and text).
10
+ - The zebra PDF stays larger than the plain one (100,000 rows: 7.4 MB against 3.5 MB): each cell's fill is drawn as a rectangle with its colour (`rg … re f`), as the whole render always drew it; there is no resource per row.
11
+
12
+ ### Docs
13
+ - The engine README says when a streamed PDF's memory stays flat, and what "Page N of M" costs (a counting pass: about 1.5–1.8× the time, +10–60 MB at 50,000 rows).
14
+
5
15
  ## 0.14.0-rc.1 — 2026-10-10 (release candidate)
6
16
 
7
17
  ### Changed
8
- - **PDFs are written by `@reportwright/pdf`** (pinned `0.1.0-beta.5`, no dependencies of its own), the streaming PDF writer extracted from the engine. `exportPdfStream` (plain, PDF/A, PDF/UA-tagged, `parallel` workers) and `exportPdf` for reports of text (any font, shaped scripts), boxes, ellipses, lines and links go through it. Images, SVG paths, rounded boxes, form fields, encryption, signing (`prepare`), `save` options, and whole-model PDF/A and tagged files stay on pdf-lib for now.
18
+ - **PDFs are written by `@reportwright/pdf`** (pinned `0.1.0-beta.9`, no dependencies of its own), the streaming PDF writer extracted from the engine. `exportPdfStream` (plain, PDF/A, PDF/UA-tagged, `parallel` workers) and `exportPdf` for reports of text (any font, shaped scripts), boxes, ellipses, lines and links go through it. Images, SVG paths, rounded boxes, form fields, encryption, signing (`prepare`), `save` options, and whole-model PDF/A and tagged files stay on pdf-lib for now.
9
19
  - **The bytes change; the content does not.** `scripts/writer-parity.mjs` exports every report of the gate corpus (the repository's reports, the streaming ledgers, the 54-report tester corpus) with both writers and requires pixel-identical pages (pdftoppm, 72 dpi, zero differing pixels), the same text per page, `qpdf --check` and veraPDF (PDF/A-2b, 2a, PDF/UA-1) passing, the same fonts (embedded, subset), links, outline, structure elements, form fields, attachments, metadata and page labels, and the same bytes on two runs. One accepted difference: right-to-left locale formats (ar-SA, he-IL) no longer carry their invisible bidi marks (U+200F…) in the extracted text, so a viewer copies "Page 1 of 1" instead of "1 of 1 Page"; the words and pixels are the same.
10
20
  - **`exportPdf` of a plain report no longer loads pdf-lib.** A report in embedded fonts (Inter, Noto) exports about twice as fast (one 300-row grouped table: ~110 ms → ~50 ms cold) and with 5–10 MB less memory; its file is about 5% larger (the font subset keeps glyph IDs, as the streaming writer always did).
11
21
  - The file ID of a streamed PDF without `id` is now a hash of its content (it was random): the same report and `creationDate` give the same bytes.
package/README.md CHANGED
@@ -8,6 +8,12 @@
8
8
 
9
9
  The CLI carries [`@reportwright/engine`](https://github.com/MrArun005/reportwright/tree/main/packages/engine); fonts come from [`@reportwright/fonts`](https://github.com/MrArun005/reportwright/tree/main/packages/fonts).
10
10
 
11
+ ## Screenshots
12
+
13
+ `pw render` output, page 1 (reports from the repository's `reports/`):
14
+
15
+ <img src="https://raw.githubusercontent.com/MrArun005/reportwright/main/docs/screenshots/sales-by-region.png" alt="Grouped table" width="32%"> <img src="https://raw.githubusercontent.com/MrArun005/reportwright/main/docs/screenshots/loan-statement.png" alt="Loan statement" width="32%"> <img src="https://raw.githubusercontent.com/MrArun005/reportwright/main/docs/screenshots/tablix-showcase.png" alt="Pivot tables" width="32%">
16
+
11
17
  ## Install
12
18
 
13
19
  ```bash
package/dist/cli.mjs CHANGED
@@ -18831,31 +18831,63 @@ function resolveStyle(layers, ctx) {
18831
18831
  }
18832
18832
  }
18833
18833
  if (allStatic) {
18834
- let map = CACHE;
18834
+ let map2 = CACHE;
18835
18835
  for (const l of layers) {
18836
18836
  if (!l || typeof l !== "object") continue;
18837
- let next = map.get(l);
18837
+ let next = map2.get(l);
18838
18838
  if (!next) {
18839
18839
  next = { map: /* @__PURE__ */ new WeakMap(), value: null };
18840
- map.set(l, next);
18840
+ map2.set(l, next);
18841
18841
  }
18842
18842
  node3 = next;
18843
- map = next.map;
18843
+ map2 = next.map;
18844
18844
  }
18845
18845
  if (node3?.value) return node3.value;
18846
18846
  }
18847
18847
  const out = {};
18848
+ let key = allStatic ? null : "";
18848
18849
  for (const l of layers) {
18849
18850
  if (!l || typeof l !== "object") continue;
18850
18851
  for (const k in l) {
18851
18852
  const v = l[k];
18852
18853
  if (v === void 0) continue;
18853
- out[k] = isDynamic(v) ? safe(v, ctx) : v;
18854
+ if (!isDynamic(v)) {
18855
+ out[k] = v;
18856
+ continue;
18857
+ }
18858
+ const x = out[k] = safe(v, ctx);
18859
+ if (key === null) continue;
18860
+ const t = typeof x;
18861
+ if (t === "string") key += `${k}${JSON.stringify(x)},`;
18862
+ else if (x == null || t === "boolean" || t === "number" && !Object.is(x, -0)) key += `${k}:${x},`;
18863
+ else key = null;
18864
+ }
18865
+ }
18866
+ if (allStatic) {
18867
+ if (node3) node3.value = Object.freeze(out);
18868
+ return out;
18869
+ }
18870
+ if (key === null) return out;
18871
+ let map = CACHE;
18872
+ for (const l of layers) {
18873
+ if (!l || typeof l !== "object") continue;
18874
+ let next = map.get(l);
18875
+ if (!next) {
18876
+ next = { map: /* @__PURE__ */ new WeakMap(), value: null };
18877
+ map.set(l, next);
18854
18878
  }
18879
+ node3 = next;
18880
+ map = next.map;
18855
18881
  }
18856
- if (allStatic && node3) node3.value = Object.freeze(out);
18882
+ if (!node3) return out;
18883
+ const kept = node3.dyn ||= /* @__PURE__ */ new Map();
18884
+ const hit = kept.get(key);
18885
+ if (hit) return hit;
18886
+ if (kept.size >= DYN_KEEP) return out;
18887
+ kept.set(key, Object.freeze(out));
18857
18888
  return out;
18858
18889
  }
18890
+ var DYN_KEEP = 64;
18859
18891
  var CACHE = /* @__PURE__ */ new WeakMap();
18860
18892
  var DYN = /* @__PURE__ */ new WeakMap();
18861
18893
  function hasDynamic(l) {
@@ -31253,7 +31285,7 @@ function memorySorter(cmp) {
31253
31285
  };
31254
31286
  }
31255
31287
  var okValue = (v, o) => typeof v !== "string" || !isDynamic(v) || streamable(v, o);
31256
- var okStyle = (st, d = 0) => !st || typeof st !== "object" || d > 8 || Object.values(st).every((v) => v && typeof v === "object" ? okStyle(v, d + 1) : okValue(v));
31288
+ var okStyle = (st, o, d = 0) => !st || typeof st !== "object" || d > 8 || Object.values(st).every((v) => v && typeof v === "object" ? okStyle(v, o, d + 1) : okValue(v, o));
31257
31289
  var isHidden = (h) => !(h == null || h === false || h === "");
31258
31290
  function foldOf(v) {
31259
31291
  if (typeof v !== "string" || !v.startsWith("=")) return null;
@@ -31314,8 +31346,9 @@ async function planStream(def0, opts) {
31314
31346
  for (const g of groups) if (g.parent || g.sortBy || g.collapsible && g.initiallyCollapsed || g.expandLevels != null || !okValue(g.expr)) return no2("group shape");
31315
31347
  const okRow = (row, { rowNumber = false, folds: folds2 = false } = {}) => {
31316
31348
  if (!row || isHidden(row.hidden) && row.hidden !== true) return false;
31317
- if (!okStyle(row.style)) return false;
31318
- return (Array.isArray(row.cells) ? row.cells : []).every((c) => !c || !c.item && !(Number(c.rowSpan) > 1) && !("raw" in c) && okStyle(c.style) && (okValue(c.value, { rowNumber }) || folds2 && foldOf(c.value)));
31349
+ const so = opts.paged ? { rowNumber } : void 0;
31350
+ if (!okStyle(row.style, so)) return false;
31351
+ return (Array.isArray(row.cells) ? row.cells : []).every((c) => !c || !c.item && !(Number(c.rowSpan) > 1) && !("raw" in c) && okStyle(c.style, so) && (okValue(c.value, { rowNumber }) || folds2 && foldOf(c.value)));
31319
31352
  };
31320
31353
  if (!(item.detail || []).every((r) => okRow(r, { rowNumber: true }))) return no2("detail expressions");
31321
31354
  if (!(item.header || []).every((r) => okRow(r))) return no2("header expressions");
@@ -31829,7 +31862,7 @@ async function planPaged(def0, opts = {}) {
31829
31862
  const { def, parts } = await resolveReuse(def0, makeLoader(opts.loadReport, /* @__PURE__ */ new Map()), () => {
31830
31863
  }, opts.reportId ? [opts.reportId] : [], { items: 0 });
31831
31864
  if (parts) return no2("report parts");
31832
- const plan2 = await planStream(def, { ...opts, format: "xlsx" });
31865
+ const plan2 = await planStream(def, { ...opts, format: "xlsx", paged: true });
31833
31866
  if (plan2.why !== void 0) return no2(plan2.why);
31834
31867
  if ((def.dataSets || []).length !== 1) return no2("more than one data set");
31835
31868
  let ds = def.dataSets[0];
@@ -33297,7 +33330,10 @@ stream
33297
33330
  literal(key, text2) {
33298
33331
  const F = fontOf(key);
33299
33332
  let out = "";
33300
- const L = F.lat ||= new Array(256);
33333
+ const L = (
33334
+ /** @type {any} */
33335
+ F.lat ||= new Array(256)
33336
+ );
33301
33337
  let i = 0;
33302
33338
  for (; i < text2.length; i++) {
33303
33339
  const c = text2.charCodeAt(i);
@@ -34241,7 +34277,8 @@ async function pdfOf(model, fontStore2, opt) {
34241
34277
  if (opt.subset && !fieldFonts.has(key) && !fontStore2.get(key)["OS/2"]?.fsType?.noSubsetting) {
34242
34278
  const cps = used.get(key) || /* @__PURE__ */ new Set([32]);
34243
34279
  try {
34244
- const sub = withPadding(await opt.subset(bytes, cps, glyphs ? { glyphs: glyphs.keys() } : void 0));
34280
+ const sub = withPadding(await /** @type {any} */
34281
+ opt.subset(bytes, cps, glyphs ? { glyphs: glyphs.keys() } : void 0));
34245
34282
  const f4 = fontkit.create(sub);
34246
34283
  for (const cp of cps) void f4.glyphForCodePoint(cp).advanceWidth;
34247
34284
  for (const id of glyphs?.keys() || []) void f4.getGlyph(id).advanceWidth;
@@ -34293,7 +34330,9 @@ async function pdfOf(model, fontStore2, opt) {
34293
34330
  if (why) return { img: null, why: `the PNG is not usable (${why})` };
34294
34331
  if (!local) return { img: await doc.embedPng(bytes) };
34295
34332
  const e2 = await PngEmbedder.for(bytes);
34296
- const size = src.length + 2 * ((e2.image.rgbChannel?.length || 0) + (e2.image.alphaChannel?.length || 0));
34333
+ const size = src.length + 2 * /** @type {any} */
34334
+ ((e2.image.rgbChannel?.length || 0) + /** @type {any} */
34335
+ (e2.image.alphaChannel?.length || 0));
34297
34336
  if (size > PNG_CACHE_ENTRY) return { img: await doc.embedPng(bytes) };
34298
34337
  const entry = { e: e2, z: (
34299
34338
  /** @type {any[]} */
@@ -34364,6 +34403,7 @@ async function pdfOf(model, fontStore2, opt) {
34364
34403
  c.lineWidth(1);
34365
34404
  draw();
34366
34405
  const ops = (
34406
+ /** @type {any} */
34367
34407
  /** @type {any} */
34368
34408
  p.getContentStream().operators
34369
34409
  );
@@ -36828,7 +36868,7 @@ var THEME = `${XML2}<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingm
36828
36868
  // package.json
36829
36869
  var package_default = {
36830
36870
  name: "reportwright",
36831
- version: "0.14.0-rc.1",
36871
+ version: "0.14.0-rc.2",
36832
36872
  license: "MIT",
36833
36873
  private: true,
36834
36874
  engines: { node: "22.x" },
@@ -36856,7 +36896,8 @@ var package_default = {
36856
36896
  "build:packages": "node scripts/build-embed.mjs && node scripts/build-packages.mjs",
36857
36897
  "scan:packages": "node scripts/scan-packages.mjs",
36858
36898
  perf: "node scripts/perf.mjs",
36859
- "build:site": "node scripts/build-site.mjs"
36899
+ "build:site": "node scripts/build-site.mjs",
36900
+ "format:check": "node scripts/format-check.mjs"
36860
36901
  },
36861
36902
  dependencies: {
36862
36903
  "@anthropic-ai/sdk": "^0.131.0",
@@ -36869,7 +36910,7 @@ var package_default = {
36869
36910
  "@node-saml/node-saml": "^5.1.0",
36870
36911
  "@pdf-lib/fontkit": "^1.1.1",
36871
36912
  "@pdf-lib/standard-fonts": "^1.0.0",
36872
- "@reportwright/pdf": "0.1.0-beta.8",
36913
+ "@reportwright/pdf": "0.1.0-beta.9",
36873
36914
  "@signpdf/placeholder-pdf-lib": "^3.3.0",
36874
36915
  "@signpdf/signer-p12": "^3.3.0",
36875
36916
  "@signpdf/signpdf": "^3.3.0",