@quario/xlsx 0.7.1 → 0.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # @quario/xlsx
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **Every target factory now refuses an option it does not understand.** An unknown key, a key with
8
+ a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the
9
+ factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx`
10
+ already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read
11
+ the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no
12
+ signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding
13
+ one.
14
+
15
+ The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and
16
+ `hostMeta` validates the `{ title, author, subject }` contract the document targets share. A
17
+ fourth document property is one edit rather than three.
18
+
19
+ **What this changes for you.** One options object spread across several targets stops working if
20
+ any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`,
21
+ `html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers
22
+ share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works.
23
+ TypeScript does not warn about this: excess-property checking fires on an object literal and not
24
+ on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.
25
+
26
+ Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and
27
+ `{ meta: { title: config.title } }` over a config that carries neither are both fine.
28
+ `html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws
29
+ instead of turning path stamping on. A `fonts` mapping given as an array is refused by
30
+ `@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.
31
+
32
+ Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests
33
+ `instanceof Error` is unaffected. One that compares the constructor is not.
34
+
35
+ `@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null`
36
+ and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }`
37
+ is a property you did not write rather than one of the wrong type, and an inherited enumerable key
38
+ is no longer reported as an option you wrote.
39
+
40
+ `@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the
41
+ object you passed no longer changes what a configured target writes.
42
+
43
+ - **`meta` is now read once, at the factory call.** It was read inside the render, so a host that
44
+ mutated its options object between renders got a different workbook from the same configured
45
+ target. The documented behaviour was always the factory call. If you were mutating an options
46
+ object to change the document properties, build a new target instead.
47
+
48
+ ### Patch Changes
49
+
50
+ - Updated dependencies
51
+ - quario@0.9.0
52
+
3
53
  ## 0.7.1
4
54
 
5
55
  ### Patch Changes
package/README.md CHANGED
@@ -13,12 +13,12 @@ can keep working in, not a grid of strings.
13
13
  npm install quario @quario/xlsx
14
14
  ```
15
15
 
16
- [exceljs](https://github.com/exceljs/exceljs) comes along as a dependency; the engine is a peer, installed beside it. ESM-only,
16
+ [exceljs](https://github.com/exceljs/exceljs) arrives as a dependency. The engine is a peer, installed beside it. ESM-only,
17
17
  Node 22+, and browser-ready through any standards-based ESM bundler. The renderer returns bytes,
18
18
  so you decide where they go.
19
19
 
20
- > The writer is an implementation detail: exceljs is confined to a single internal module, so it
21
- > can be swapped behind this exact API without a breaking change.
20
+ > The writer is an implementation detail. This package confines exceljs to a single internal
21
+ > module, so a later version can swap it behind this exact API without a breaking change.
22
22
 
23
23
  ## Quick start
24
24
 
@@ -33,11 +33,11 @@ const bytes = await report.render(xlsx({ meta: { title: "Sales 2026" } }), data)
33
33
  writeFileSync("sales.xlsx", bytes);
34
34
  ```
35
35
 
36
- Any schema the other targets render works here unchanged. A spreadsheet has no pages, so page bands
37
- are ignored.
36
+ Any schema the other targets render works here unchanged. A spreadsheet has no pages, so this
37
+ target ignores page bands.
38
38
 
39
39
  One tip that changes the output a lot: **interpolate the bare value, not a formatter.** A cell
40
- written `{{ @.amount }}` becomes a real numeric cell; `{{ currency(@.amount) }}` becomes text,
40
+ written `{{ @.amount }}` becomes a real numeric cell. `{{ currency(@.amount) }}` becomes text,
41
41
  because the formatter already turned the number into a string. Let the spreadsheet do the
42
42
  formatting it is good at.
43
43
 
@@ -45,8 +45,9 @@ formatting it is good at.
45
45
 
46
46
  ### `xlsx(options?)`
47
47
 
48
- The target factory: it takes this target's host options and returns the target you pass to
49
- `render`. `report()` compiles once and `report.render(xlsx(options), data)` resolves the
48
+ The target factory takes this target's host options, validates them at the call, and returns the
49
+ target you pass to `render`. The factory refuses an option it does not know. It throws a `TypeError` at the call for an unknown key. It throws one also for a key with a value of the wrong type. It also reads `meta` at the call. One configured target
50
+ therefore always writes the same document. `report()` compiles once and `report.render(xlsx(options), data)` resolves the
50
51
  workbook bytes. Compile at startup, render per request. Definition problems throw at
51
52
  `report()`, at compile time.
52
53
 
@@ -59,7 +60,7 @@ The compiled report carries `stream` (the raw event generator), `names`, `functi
59
60
  `paths`, like every quario report. Engine-level options (`query` budgets, the license key)
60
61
  live on the instance, and `q.license` settles with the verification result.
61
62
 
62
- Rendering is asynchronous and hands the loop back between batches, so a large report never blocks the
63
+ Rendering is asynchronous and returns the loop between batches, so a large report never blocks the
63
64
  host. Render-time failures reject with located errors.
64
65
 
65
66
  ### Options
@@ -74,12 +75,12 @@ host. Render-time failures reject with located errors.
74
75
 
75
76
  One worksheet, named `Report`, with the banded walk flattened onto it as rows in render order:
76
77
  report-header items, then per group instance its header items, nested content, and footer items,
77
- then the table's header row, data rows, and total rows, then report-footer items. Grouping shows up
78
- as that order alone. Group boundaries add no rows of their own, and `break: "page"` and
79
- `reset: "page"` are ignored, as are `page.margin` and a report header's `height`.
78
+ then the table's header row, data rows, and total rows, then report-footer items. Grouping exists
79
+ as that order alone. Group boundaries add no rows of their own. This target ignores `break: "page"`,
80
+ `reset: "page"`, `page.margin`, and a report header's `height`.
80
81
 
81
- Each item takes one row with its cell in the first column, unmerged; a split takes one row with its
82
- slots as the cells across it (slot widths withdrawn, a slot rendering nothing an empty cell), and a
82
+ Each item takes one row with its cell in the first column, unmerged. A split takes one row with its
83
+ slots as the cells across it (slot widths withdrawn, a slot rendering nothing an empty cell). A
83
84
  spanning header or total cell is a merged range. Two band roles carry an
84
85
  omakase default that the author's own style always overrides: report-header items render bold at
85
86
  size 14, group-header items render bold.
@@ -99,9 +100,9 @@ A hidden cell keeps its column slot as an empty cell that retains its style.
99
100
 
100
101
  `format` writes a number format so the grid presents what the other targets do. `date` builds
101
102
  its pattern from the cell's `form` (`dd mmm yyyy` undeclared), with month and weekday names
102
- spelled by the reader's application; the number kinds build theirs from the declaration's
103
- fraction-digit count an undeclared `number` is `#,##0.00`, `percent` is `0.00%`,
104
- `{ "kind": "number", "digits": 3 }` is `#,##0.000` — and `currency` is the code the cell wears
103
+ spelled by the reader's application. The number kinds build theirs from the declaration's
104
+ fraction-digit count: an undeclared `number` is `#,##0.00`, `percent` is `0.00%`, and
105
+ `{ "kind": "number", "digits": 3 }` is `#,##0.000`. `currency` is the code the cell wears
105
106
  — its own `currency` when it declares one, else the instance's — over that currency's minor
106
107
  units unless `digits` says otherwise: `"USD"#,##0.00`, `"JPY"#,##0`, `"BHD"#,##0.000`. A kind
107
108
  on the wrong type, or a code the engine cannot read, writes no format.
@@ -119,18 +120,18 @@ every target alike.
119
120
 
120
121
  | Declaration | Maps to |
121
122
  | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
122
- | `family` | `sans` → Calibri, `serif` → Times New Roman, `mono` → Courier New; any other name passes through for the host application to resolve |
123
+ | `family` | `sans` → Calibri, `serif` → Times New Roman, `mono` → Courier New. Any other name passes through for the host application to resolve |
123
124
  | `size`, `bold`, `italic`, `underline`, `strikethrough` | Font (`strikethrough` → writer's `strike`) |
124
125
  | `color`, `background` | Font colour, solid fill |
125
126
  | `align` | Horizontal alignment |
126
- | `valign` | Vertical alignment; undeclared writes nothing, so the spreadsheet application keeps its own default |
127
- | `border*` | Cell borders (`solid` → `thin`, `dashed`, `dotted`; width approximated); a row's borders arrive on its cells already |
127
+ | `valign` | Vertical alignment. Undeclared writes nothing, so the spreadsheet application keeps its own default |
128
+ | `border*` | Cell borders (`solid` → `thin`, `dashed`, `dotted`, with width approximated). A row's borders arrive on its cells already |
128
129
  | `format`, `currency` | A number format — see [Number formats](#number-formats) |
129
130
  | `uppercase`, `padding*`, `spaceBefore`, `spaceAfter` | Not read: a grid has no text-transform, no cell inset of its own, and no flow |
130
131
 
131
132
  Style blocks layer row under cell, as everywhere in quario, with the report default outermost.
132
- Every cell is written at 10 points unless something declares otherwise, and a cell whose display
133
- contains a newline wraps.
133
+ This target writes every cell at 10 points unless something declares otherwise, and a cell whose
134
+ display contains a newline wraps.
134
135
 
135
136
  ### Freeze
136
137
 
@@ -148,20 +149,20 @@ table for a share to be a share of.
148
149
  ### Page columns are withdrawn
149
150
 
150
151
  A [page column](https://getquario.com/docs/reference/support-matrix/#page-and-pagination) count
151
- sizes and splits nothing here, and will not change, as `break: "page"` is ignored. Page columns
152
+ sizes and splits nothing here, and will not change, as this target ignores `break: "page"`. Page columns
152
153
  are strips of a page, and this target has no page to strip.
153
154
 
154
155
  ## Unlicensed marking
155
156
 
156
157
  An unlicensed render writes the wording from `report-start.marking` twice: as a styled banner in
157
158
  row 1 (so the whole report, frozen view included, sits one row lower than a licensed render), and
158
- as the workbook's description property. Both are normative; banner styling is best-effort. A
159
+ as the workbook's description property. Both are normative. Banner styling is best-effort. A
159
160
  licensed render has no banner and no description marking.
160
161
 
161
162
  ## Determinism
162
163
 
163
- The document model is deterministic: the workbook's dates are pinned rather than stamped, and
164
- `meta` is opt-in. The **bytes** are not. The writer stamps archive entries with the packing time.
164
+ The document model is deterministic. This target pins the workbook's dates rather than stamping
165
+ them, and `meta` is opt-in. The **bytes** are not. The writer stamps archive entries with the packing time.
165
166
  Compare extracted content, not digests.
166
167
 
167
168
  The full contract is [The XLSX target](https://getquario.com/docs/diving-deeper/xlsx-target/),
@@ -177,10 +178,10 @@ specification of what a report may declare, and
177
178
 
178
179
  ## License
179
180
 
180
- Commercial software with readable source. Free, unlimited, watermarked evaluation; per-developer
181
- licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
181
+ Commercial software with readable source. Evaluation is free, unlimited, and watermarked.
182
+ Per-developer licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
182
183
 
183
- Pass your license key once, on the instance; it is verified offline:
184
+ Pass your license key once, on the instance. quario verifies it offline:
184
185
 
185
186
  ```js
186
187
  const q = quario({ license: "quario_..." });
package/lib/index.d.ts CHANGED
@@ -7,7 +7,12 @@ export interface XlsxMeta {
7
7
  subject?: string;
8
8
  }
9
9
 
10
- /** Host controls, taken at the factory call. */
10
+ /**
11
+ * Host controls, taken and validated at the factory call: an option this
12
+ * target does not understand, or one of the wrong type, throws a `TypeError`
13
+ * there rather than costing the host the option in silence. `meta` is read
14
+ * there too, so one configured target always writes the same document.
15
+ */
11
16
  export interface XlsxOptions {
12
17
  meta?: XlsxMeta;
13
18
  }
package/lib/index.js CHANGED
@@ -15,7 +15,7 @@
15
15
  * the packing time. Byte-for-byte output is a goal for the writer that will
16
16
  * replace `workbook.js`.
17
17
  */
18
- import { walk } from "quario";
18
+ import { hostMeta, hostOptions, walk } from "quario";
19
19
  import { field } from "./cell.js";
20
20
  import { merge as under } from "./style.js";
21
21
  import { append, create, embed, freeze, mark, place, save, sheet, span } from "./workbook.js";
@@ -66,14 +66,22 @@ let BANNER = { bold: true, background: "#fff2cc" };
66
66
  * runs the engine's `walk` driver over the handlers below, so large reports
67
67
  * stay cooperative.
68
68
  *
69
+ * Host controls are taken and validated here, at the factory call: an option
70
+ * this target does not understand, or one of the wrong type, throws where the
71
+ * host wrote it rather than costing them the option in silence
72
+ * (`docs/adr/0072`). `meta` is also *read* here, so a render is reproducible
73
+ * from the call that configured it.
74
+ *
69
75
  * @param {XlsxOptions} [options] Host controls (see SCHEMA.md, "The XLSX target").
70
76
  * @returns {{name: "xlsx", compile: (stream: any) => (data?: any) => Promise<Uint8Array>}}
71
77
  * The target (see SCHEMA.md, "Instances and targets").
72
78
  */
73
79
  export function xlsx(options) {
80
+ hostOptions(options, ["meta"], "options");
81
+ let meta = hostMeta(options?.meta, "options.meta");
74
82
  /** @type {(stream: any) => (data?: any) => Promise<Uint8Array>} */
75
83
  let compile = (stream) => async (data) => {
76
- let workbook = create(options?.meta);
84
+ let workbook = create(meta);
77
85
  let worksheet = sheet(workbook);
78
86
  // The first table anchors the frozen header row. It is global to the
79
87
  // sheet, so later tables never move it.
package/lib/workbook.js CHANGED
@@ -10,12 +10,16 @@
10
10
  import ExcelJS from "exceljs";
11
11
 
12
12
  // Host metadata keys as the workbook properties they become -- `author` is the
13
- // only one exceljs names differently.
13
+ // only one exceljs names differently, and this mapping is all this target owns
14
+ // of `meta`. The contract itself (which properties, each a string, never a
15
+ // date) is the engine's `hostMeta`, checked at the factory call, so nothing
16
+ // here restates it: a present property is a string, and a nullish one is a
17
+ // property the host did not write.
14
18
  /** @type {(workbook: any, meta: any) => void} */
15
19
  let describe = (workbook, meta) => {
16
- if (typeof meta.title === "string") workbook.title = meta.title;
17
- if (typeof meta.author === "string") workbook.creator = meta.author;
18
- if (typeof meta.subject === "string") workbook.subject = meta.subject;
20
+ if (meta.title != null) workbook.title = meta.title;
21
+ if (meta.author != null) workbook.creator = meta.author;
22
+ if (meta.subject != null) workbook.subject = meta.subject;
19
23
  };
20
24
 
21
25
  // A fresh workbook. Never the current time: a timestamp would make the same
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/xlsx",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "The spreadsheet render target for quario — in the makings, not yet released",
5
5
  "homepage": "https://getquario.com",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -42,12 +42,12 @@
42
42
  "@arethetypeswrong/cli": "^0.18.3",
43
43
  "@size-limit/preset-small-lib": "^13.0.3",
44
44
  "@types/node": "^22.20.1",
45
- "quario": "^0.8.0",
45
+ "quario": "^0.9.0",
46
46
  "size-limit": "^13.0.3",
47
47
  "typescript": "^7.0.2"
48
48
  },
49
49
  "peerDependencies": {
50
- "quario": "^0.8.0"
50
+ "quario": "^0.9.0"
51
51
  },
52
52
  "size-limit": [
53
53
  {