npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@quario/xlsx

v0.10.1

Published

Tiny spreadsheet render target for quario. Numbers stay numbers, dates stay dates, and the header row is frozen.

Readme

@quario/xlsx

The spreadsheet render target for quario. Renders a report definition to an .xlsx workbook with cells typed as a number, boolean, or Date, and a frozen header row.

Numbers stay numbers, dates stay dates, and booleans stay booleans. Your users get a workbook they can keep working in, not a grid of strings.

Contents

Install

npm install quario @quario/xlsx

werkmap arrives as the one dependency. It is quario's own spreadsheet writer. It writes a workbook and never reads one. It has no dependencies of its own, and it turns no string into code. The engine is a peer, installed beside it. ESM-only, Node 22+, and browser-ready through any standards-based ESM bundler. The renderer returns bytes, so you decide where they go.

The writer is an implementation detail. This package confines werkmap to a single internal module, so a later version can swap it behind this exact API without a breaking change.

Quick start

import { writeFileSync } from "node:fs";
import { xlsx } from "@quario/xlsx";
import { quario } from "quario";

const report = quario().report(schema, funcs);
const bytes = await report.render(xlsx({ meta: { title: "Sales 2026" } }), data);

writeFileSync("sales.xlsx", bytes);

Any schema the other targets render works here unchanged. A spreadsheet has no pages on screen, so page.header and page.footer become the worksheet's print header and footer: text on every printed page, in the section its align names, with {{ page.number }} and {{ page.total }} as the reader's own live fields, a first page of its own where a band differs there, and nothing in the grid.

One tip that changes the output a lot: interpolate the bare value, not a formatter. A cell written {{ @.amount }} becomes a real numeric cell. {{ currency(@.amount) }} becomes text, because the formatter already turned the number into a string. Let the spreadsheet do the formatting it is good at.

API

xlsx(options?)

The target factory takes this target's host options, validates them at the call, and returns the 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 therefore always writes the same document. report() compiles once and report.render(xlsx(options), data) resolves the workbook bytes. Compile at startup, render per request. Definition problems throw at report(), at compile time.

const report = quario().report(schema, funcs);
const bytes = await report.render(xlsx(), data);

The compiled report carries stream (the raw event generator), names, functions, and paths, like every quario report. Engine-level options (query budgets, the license key) live on the instance, and q.license settles with the verification result.

Rendering is asynchronous and returns the loop between batches, so a large report never blocks the host. Render-time failures reject with located errors.

Options

{
  meta: { title, author, subject },         // optional document properties — strings only, never a date
  page: { size, margin, orientation, fit }, // print setup, written to every worksheet
  filter: true,                             // an autofilter over a worksheet's one table
}

Autofilter

xlsx({ filter: true }) puts an autofilter over a worksheet's one table: its header row, and the data rows under it. It adds the reader's dropdown controls and removes no row. That is what keeps it an affordance rather than a filter. This is normative.

An autofilter sorts as well as filters, so this option is the sort control too. quario writes every value once, in document order, as a literal, and writes no formula, so a reader who reorders the region sees a running total that no longer runs and a row count that no longer counts. Every row is still there. Offer the option over a table whose columns do not depend on the rows before them.

A total row sits outside the range. A total is not data, and a filter that hides one surprises a reader. A filter that sorts it into the middle of the data is worse.

A worksheet that holds more than one table filters nothing. An autofilter needs one header row over one contiguous body, and such a worksheet has neither: covering the first table alone leaves a reader working a control that does not reach the rest, and covering everything puts a second table's header row inside the filtered body as though it were data. The mapping is withdrawn rather than approximated.

A break is how you get one. It opens a worksheet for each group instance, and each of those holds one table. A report without a table filters nothing, and neither does a table with no data row.

The key is off unless you ask. Switching it on for every report would put a dropdown row into every workbook this target writes, and that changes what an existing report produces.

Print setup

page.margin is the document field, and xlsx({ page: { margin } }) applies where the document omits it. Declaring both is a render error. page.size is a host option only, and this target names A4 and letter. A [width, height] pair is withdrawn, because a worksheet prints on named stock. orientation takes portrait or landscape. page.fit takes true or false, and it scales a worksheet to one page wide. This README writes page.fit with its object and never bare, because an image item carries a fit of its own that scales a picture. Print setup reaches every worksheet, which is where the format puts it. A report that declares none of these keys carries no print setup, so a reader applies its own defaults.

Mapping

Worksheets carry the banded walk as rows in render order: report-header items, then per group instance its header items, nested content, and footer items, then the table's header row, data rows, and total rows, then report-footer items. Grouping exists as that order and as row outline levels: an instance's header and footer at its parent's level and its content one deeper, so a reader collapses an instance from the margin and keeps its header and footer in view. Group boundaries add no rows of their own. A report that declares no break produces exactly one worksheet, named Report.

A group boundary is a worksheet boundary. A boundary starts a worksheet and never ends one, so break: "around" yields one worksheet per instance rather than two. A worksheet that a boundary opened takes the group's label as its name, or the instance's key where none is declared, in both cases sanitised, and SCHEMA.md states every step of that.

A group that declares collapsed keeps its rows and hides them, so a reader opens the group again. The control that reopens them goes on the row after the group, which is the instance's footer where it declares one; an instance with no footer puts it on whatever follows, and one whose content ends the worksheet has no row to carry it. reset is withdrawn, because this target writes no page numbers for a sequence to restart. A report header's height is withdrawn, because a grid has no page top to pin from.

Each item takes one row with its cell in the first column, unmerged. A split takes one row with its slots as the cells across it (slot widths withdrawn, a slot rendering nothing an empty cell). A spanning header or total cell is a merged range. Two band roles carry an omakase default that the author's own style always overrides: report-header items render bold at 1.4× the report default's size, group-header items render bold.

Typed cells

A cell whose value is one bare interpolation writes its pre-stringify value as a number, boolean, or Date. Every other cell (mixed literal and interpolation, a lone null, any non-numeric value) joins to display text.

String cells are never formulas. Cell text beginning with =, or anything else, arrives as inert literal text: no formula-injection surface, and no apostrophe mangling either.

A hidden cell keeps its column slot as an empty cell that retains its style.

Number formats

format writes a number format so the grid presents what the other targets do. date builds its pattern from the cell's form (dd mmm yyyy undeclared), with month and weekday names spelled by the reader's application. The number kinds build theirs from the declaration's fraction-digit count: an undeclared number is #,##0.00, percent is 0.00%, and { "kind": "number", "digits": 3 } is #,##0.000. currency is the code the cell wears — its own currency when it declares one, else the instance's — over that currency's minor units unless digits says otherwise: "USD"#,##0.00, "JPY"#,##0, "BHD"#,##0.000. A kind on the wrong type, or a code the engine cannot read, writes no format.

A number kind's negative and zero write the format's other sections, which is how the grid says what the page says: { "kind": "number", "digits": 2, "negative": "parens", "zero": "dash" } becomes #,##0.00;(#,##0.00);"-", and the cell stays a number. A declaration that names neither writes one section, exactly as it did before.

Images

An image item is not a cell: it takes one worksheet row as its anchor and floats over the sheet as a drawing at its natural pixel size. Placement here is best-effort. fit, alt, and style are unused, and all of it may change without a breaking change (see the support matrix). Bytes too short to carry a size never reach this target: the engine reads every image's size and rejects them for every target alike.

Styles

| Declaration | Maps to | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | family | sans → Calibri, serif → Times New Roman, mono → Courier New. Any other name passes through for the host application to resolve | | size, bold, italic, underline, strikethrough | Font (strikethrough → writer's strike) | | color, background | Font colour, solid fill | | align | Horizontal alignment | | valign | Vertical alignment. Undeclared writes nothing, so the spreadsheet application keeps its own default | | border* | Cell borders (solid → thin, dashed, dotted, with width approximated). A row's borders arrive on its cells already | | format, currency | A number format — see Number formats | | uppercase, padding*, spaceBefore, spaceAfter | Not read: a grid has no text-transform, no cell inset of its own, and no flow |

Style blocks layer row under cell, as everywhere in quario, with the report default outermost. This target writes every cell at 10 points unless something declares otherwise, and a cell whose display contains a newline wraps.

Keeping the header in view

The first table's header row anchors the frozen view. Everything from that row up stays visible while the body scrolls, the spreadsheet analogue of the PDF target's repeated headers. A report without a table freezes nothing. This is normative.

This target repeats the same rows on paper. It writes them as a _xlnm.Print_Titles defined name, scoped to that one worksheet. A spreadsheet separates a view from a page, so one intent needs two spellings here. Both spellings cover the same rows, and both are normative.

The percentage is withdrawn, and content sizes the column

Column width percentages size nothing here. A percentage is a share of the table width, and a worksheet's columns are global to the sheet. Column A carries the flattened band item rows and the table's first column alike, so there is no table for a share to be a share of.

The target sizes each worksheet column from the content that column holds instead. A cell votes with a count of the characters it shows, and the widest vote wins. The count is in the format's own unit, which is a count of characters of the workbook's default font.

A table's own cells vote: the header cells, the data cells and the total cells. A band item, a split slot, an image anchor and the marking row do not vote, because none of them is a table column. A cell that covers more than one column does not vote either.

Text votes the length of its longest line. A number and a date hold no text of their own, so each votes the characters the engine presents it as — the same text that declaration produces on a rendered page. A boolean votes nothing, because the reader's application spells TRUE and FALSE in the reader's own language. A vote therefore follows the report's declared locale, and the same report and data still measure the same on every run.

The winning vote gains one character of slack, and a column stops at 60 characters. A column that nothing voted on keeps the application default. All of this is best-effort and may change without a breaking change.

Page columns are withdrawn

A page column count sizes and splits nothing here, and will not change. Page columns are strips of a page, and this target has no page to strip. A break still opens a worksheet here, and the Mapping section above describes that.

Unlicensed marking

An unlicensed render writes the wording from report-start.marking twice: as a styled banner in row 1 (so the whole report, frozen view included, sits one row lower than a licensed render), and as the workbook's description property. Both are normative. Banner styling is best-effort. A licensed render has no banner and no description marking.

Determinism

The document model and the bytes are both deterministic. This target pins the workbook's dates rather than stamping them, and meta is opt-in. werkmap stamps no archive entry with the packing time.

The same schema, data, and options give you the same bytes on one runtime. A runtime here is the JavaScript engine and the zlib build inside it, because werkmap deflates through the platform's own CompressionStream. Compare digests on one runtime. Compare extracted content across two.

The full contract is The XLSX target, with each declaration's fate in the support matrix.

Documentation

The quario documentation is the reference. The report schema is the normative specification of what a report may declare, and @quario/xlsx is this package's own API.

License

Commercial software with readable source. Evaluation is free, unlimited, and watermarked. Per-developer licenses at getquario.com. See the bundled LICENSE.

Pass your license key once, on the instance. quario verifies it offline:

const q = quario({ license: "quario_..." });
await q.license; // { licensed: true, licensee: "Acme BV", id: "1-ACME" }