feat: a cell object may be written where a value goes — closes #433 - #562
Merged
Conversation
`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
force-pushed
the
feat/inline-cells-in-rows
branch
from
August 13, 2026 16:46
43e4e57 to
3a508be
Compare
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: a cell object may be written where a value goes — closes #433
rowswas the grid andcellsthe 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:
That shape already half-existed, and the other half lost data
XlsxStreamWriter.addRowhas taken{ value, style, formula }inlinesince it existed (
StreamStyledCell).writeOdsStreamtakes the shapetoo, though only the
valueandformulaof it — per-cell styles arethe 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.
resolveRowsread 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.tslifts an inline cell intocellsat the writerentry, so the two writers, the auto-width measurer, the pivot source
collector and the table extent all still see a grid of values.
cellskeeps its precedence: where both describe a position, the explicit map
wins.
A
Dateis the only object aCellValuecan be and aHyperlinkValueis the object the
data[]path already accepts in a value position; bothstay 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
typeofper cell, and no copy when the grid is all values — which isevery grid written before this.
writeXlsxat 100,000 × 12, three runseach, one process per measurement:
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
Dateand aHyperlinkValueare not cells,cellswins, a clean grid is not copied, the caller's sheet is notmutated). Two of the new tests assert the inline spelling and the map
spelling produce the same bytes, in both formats.
pnpm testgreen — 10,595 tests, 235 files. Size within budget.Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
🤖 Generated with Claude Code