Skip to content

feat: a cell object may be written where a value goes — closes #433 - #562

Merged
productdevbook merged 1 commit into
mainfrom
feat/inline-cells-in-rows
Aug 13, 2026
Merged

feat: a cell object may be written where a value goes — closes #433#562
productdevbook merged 1 commit into
mainfrom
feat/inline-cells-in-rows

Conversation

@productdevbook

@productdevbook productdevbook commented Aug 13, 2026

Copy link
Copy Markdown
Owner

feat: a cell object may be written where a value goes — closes #433

rows was the grid and cells the per-cell detail, keyed "row,col",
so styling one cell meant naming its position twice and keeping the two
spellings in step by hand. #433 asked for the ExcelJS shape instead:

rows: [[{ value: "Region", style: { font: { bold: true } } }, "Revenue"]]

That shape already half-existed, and the other half lost data

XlsxStreamWriter.addRow has taken { value, style, formula } inline
since it existed (StreamStyledCell). writeOdsStream takes the shape
too, though only the value and formula of it — per-cell styles are
the buffered ODS writer's alone. So the two XLSX writers disagreed about
what a row entry may be.

The buffered one did not refuse the shape it did not accept.
resolveRows read the object as a value, and the cell came out empty
— value, style and formula all gone, no error. The ODS writer did the
same. A caller who guessed the natural spelling got a file with holes in
it and nothing to explain them.

One rule, applied before anything reads the grid

src/_inline-cells.ts lifts an inline cell into cells at the writer
entry, so the two writers, the auto-width measurer, the pivot source
collector and the table extent all still see a grid of values. cells
keeps its precedence: where both describe a position, the explicit map
wins.

A Date is the only object a CellValue can be and a HyperlinkValue
is the object the data[] path already accepts in a value position; both
stay values. Everything else is read as a cell — not by inspecting its
shape, but because nothing else was ever a legal entry, so the
alternative to reading it is dropping it.

The formats that carry only values — CSV, TSV, JSON, NDJSON, XML, HTML,
Markdown — reduce an inline cell to its value through toCellValue,
which is a total function rather than a cast: each site stays correct
whether or not the sheet went through the split.

Cost

A typeof per cell, and no copy when the grid is all values — which is
every grid written before this. writeXlsx at 100,000 × 12, three runs
each, one process per measurement:

before   2200 / 2386 / 2358 ms   peakRSS 751 / 751 / 752 MB
after    2565 / 2329 / 2285 ms   peakRSS 749 / 750 / 754 MB

Medians 2358 → 2329 ms. Inside the run-to-run spread; peak RSS unchanged.

Checked

12 tests, 7 of which fail on the commit before this one — the five that
pass there are the guards (a Date and a HyperlinkValue are not cells,
cells wins, a clean grid is not copied, the caller's sheet is not
mutated). Two of the new tests assert the inline spelling and the map
spelling produce the same bytes, in both formats.

pnpm test green — 10,595 tests, 235 files. Size within budget.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

🤖 Generated with Claude Code

`rows` was the grid and `cells` the per-cell detail, keyed `"row,col"`,
so styling one cell meant naming its position twice and keeping the two
spellings in step by hand. #433 asked for the ExcelJS shape instead:

    rows: [[{ value: "Region", style: { font: { bold: true } } }, "Revenue"]]

## That shape already half-existed, and the other half lost data

`XlsxStreamWriter.addRow` has taken `{ value, style, formula }` inline
since it existed (`StreamStyledCell`). `writeOdsStream` takes the shape
too, though only the `value` and `formula` of it — per-cell styles are
the buffered ODS writer's alone. So the two XLSX writers disagreed about
what a row entry may be.

The buffered one did not refuse the shape it did not accept.
`resolveRows` read the object as a value, and the cell came out **empty**
— value, style and formula all gone, no error. The ODS writer did the
same. A caller who guessed the natural spelling got a file with holes in
it and nothing to explain them.

## One rule, applied before anything reads the grid

`src/_inline-cells.ts` lifts an inline cell into `cells` at the writer
entry, so the two writers, the auto-width measurer, the pivot source
collector and the table extent all still see a grid of values. `cells`
keeps its precedence: where both describe a position, the explicit map
wins.

A `Date` is the only object a `CellValue` can be and a `HyperlinkValue`
is the object the `data[]` path already accepts in a value position; both
stay values. Everything else is read as a cell — not by inspecting its
shape, but because nothing else was ever a legal entry, so the
alternative to reading it is dropping it.

The formats that carry only values — CSV, TSV, JSON, NDJSON, XML, HTML,
Markdown — reduce an inline cell to its value through `toCellValue`,
which is a total function rather than a cast: each site stays correct
whether or not the sheet went through the split.

## Cost

A `typeof` per cell, and no copy when the grid is all values — which is
every grid written before this. `writeXlsx` at 100,000 × 12, three runs
each, one process per measurement:

    before   2200 / 2386 / 2358 ms   peakRSS 751 / 751 / 752 MB
    after    2565 / 2329 / 2285 ms   peakRSS 749 / 750 / 754 MB

Medians 2358 → 2329 ms. Inside the run-to-run spread; peak RSS unchanged.

## Checked

12 tests, 7 of which fail on the commit before this one — the five that
pass there are the guards (a `Date` and a `HyperlinkValue` are not cells,
`cells` wins, a clean grid is not copied, the caller's sheet is not
mutated). Two of the new tests assert the inline spelling and the map
spelling produce the **same bytes**, in both formats.

`pnpm test` green — 10,595 tests, 235 files. Size within budget.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@productdevbook
productdevbook force-pushed the feat/inline-cells-in-rows branch from 43e4e57 to 3a508be Compare August 13, 2026 16:46
@productdevbook
productdevbook merged commit 6de6d85 into main Aug 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

write custom table

1 participant