v1.1.0 — Charts, server rendering, and an autonomous agent surface
pdfnative-react v1.1.0
Released 2026-07-25
Charts, server rendering, and an agent surface complete enough to drive the
package without a human.
Tracks the pdfnative 1.6.0
engine release. Everything in the public API is additive — but two
install-time floors moved, so read the next section first.
Compatibility — read this first
npm install pdfnative-react@^1.1.0 pdfnative@^1.6.0 react@^19| Requirement | 1.0.0 | 1.1.0 |
|---|---|---|
pdfnative peer |
^1.5.0 |
^1.6.0 |
| Node.js | >=20 |
>=22 |
| React | ^19.0.0 |
^19.0.0 (unchanged) |
Why the engine floor moved. <Chart> compiles to a chart block, which
does not exist before pdfnative 1.6.0. A 1.5 engine would receive an unknown
block type and silently drop or mis-render it. A loud install-time requirement
is better than a quiet wrong PDF.
Why the Node floor moved. It is inherited, not invented:
pdfnative@1.6.0 itself requires Node ≥ 22, so any compliant install is already
there. We now say so.
No API was removed, renamed, or changed in a backward-incompatible way.
docSpecSchema() and docSpecSchemaId() still work. PdfStructureError is
still importable from every path it was, and is still the same class object, so
instanceof is unaffected.
Security — re-render anything you encrypted
Two engine fixes arrive with the ^1.6.0 floor, and both affect documents
this package produced. If you have ever shipped a document with
layout.encryption, re-render it.
- Encrypted documents leaked their outline, link URIs and metadata. Before
engine 1.6.0 only streams were encrypted; strings were not. Because
<Document outline="auto">derives bookmark titles from every<Heading>, a
password-protected document produced here disclosed its section headings, its
<Link url>targets and itsmetadatato anyone who opened the file without
the password. - AES-256 output was not spec-compliant. The engine's R6 hash used SHA-256
for every round instead of the SHA-256/384/512 rotation ISO 32000-2
Algorithm 2.B requires, soalgorithm: 'aes256'files written on engine
≤ 1.5.0 were unreadable by strictly compliant readers. Output changes
bit-for-bit; the engine keeps a legacy fallback so old files still open.
Neither is a defect in pdfnative-react's own code, and nothing you do at the
wrapper level worked around them — the fix is the engine upgrade this release
requires. See the Security section of the CHANGELOG.
Highlights
Charts
<Chart
chartType="bar"
series={[{ label: '2026', values: [15400, 21200, 29800, 38600] }]}
categories={['Q1', 'Q2', 'Q3', 'Q4']}
title="Revenue by quarter"
altText="Revenue rises each quarter from 15.4k to 38.6k."
/>Five types — bar, barH, line, pie, donut — drawn as pure PDF path
operators. No rasterisation, no chart library, no new runtime dependency, and
the output is real vector art that stays sharp at any zoom and passes PDF/A.
Multi-series, legends, "nice" axis ticks, gridlines, markers, palette overrides,
negative values, and a tagged-PDF /Figure + /Alt.
The matching DocSpec tuple is ['chart', { chartType, series, … }].
Serving a PDF
// app/invoice/[id]/route.tsx
export async function GET() {
return renderToResponse(<Invoice />, { fileName: 'invoice.pdf' });
}renderToResponse returns a web-standard Response. Because Response is a
platform primitive rather than a framework type, the same code runs unchanged on
Node, the Edge runtime, Deno, Bun and Cloudflare Workers.
Streams by default — the body is a ReadableStream fed by the engine's
page-by-page generator, so peak memory stays flat and the client receives bytes
immediately. buffered: true switches to one buffer and adds Content-Length.
Content-Disposition follows RFC 6266, including filename* for non-ASCII
names.
A client subpath, so RSC apps need no wrapper
import { PDFViewer, usePdf } from 'pdfnative-react/client';pdfnative-react/client ships with the 'use client' directive already
applied — usePdf, usePdfStream, PDFViewer, PDFDownloadLink and
BlobProvider. The root barrel still exports them for apps with no RSC
boundary, and stays unmarked on purpose, because renderToResponse has to
remain server-safe.
One boundary this does not move: importing the package from a Server Component
or a 'use server' file still fails at module load, because the reconciler
needs createContext and React's react-server condition does not provide it.
Use a Route Handler — which is what the example above is.
Two packaging fixes ship alongside it. The bundle now keeps the node: prefix
on its dynamic node:fs/promises import, without which Deno and Cloudflare
nodejs_compat could not resolve it — so the edge runtimes listed above now
genuinely build. And importing pure data no longer pulls in the React
reconciler: import { version } went from 10 137 bytes to 3 216, as did
validateSpec, schema() and capabilityManifest(). The build fails if either
regresses.
Document-level page furniture
<Document
watermark="DRAFT"
header={{ left: 'Acme Inc', right: '{date}' }}
footer={{ center: '{title}', right: 'Page {page} of {pages}' }}
tagged="pdfa3b"
attachments={[{ filename: 'data.xml', data, mimeType: 'application/xml' }]}
/>These PdfLayoutOptions fields already worked, as an opaque and entirely
undocumented layout pass-through. They are now first-class props, with types,
schema coverage, samples and tests. {page}, {pages}, {date} and {title}
resolve at render time.
They are props rather than components on purpose: they are page furniture, not
blocks in the flow, and a component would mean a host tag with no corresponding
pdfnative block. An explicit layout prop still wins over all of them.
Linting
const report = lintDocument(<Invoice />);
// { ok, findings: [{ code, severity, message, blockIndex?, hint? }], counts }Eighteen deterministic rules with stable L_* codes — 10 error, 7 warning,
1 info — covering accessibility (missing alt text, tables without headers,
skipped heading levels, unlabelled form fields) and, more valuably, eight
constraints the engine would otherwise enforce by throwing mid-render:
| Rule | Would otherwise |
|---|---|
L_CHART_EMPTY |
Throw — no series, or a series with no values |
L_CHART_SERIES |
Throw — pie/donut need exactly one series |
L_CHART_CATEGORIES |
Throw — series length must match categories |
L_CHART_VALUES |
Throw — non-finite, or negative in a pie/donut |
L_CHART_POINTS |
Throw — 10 000-point ceiling |
L_ATTACHMENTS_NEED_PDFA3 |
Throw — attachments require tagged="pdfa3b" |
L_TAGGED_ENCRYPTED |
Throw — PDF/A and encryption are mutually exclusive |
L_MAX_BLOCKS_EXCEEDED |
Throw — past maxBlocks, default 100 000 |
Two more catch output that renders successfully but is wrong:
L_EMPTY_DOCUMENT (a blank page) and L_TAGGED_NO_FONTS (a PDF/A file veraPDF
rejects for a non-embedded font).
It runs on the compiled document model, so JSX and DocSpec share one
implementation, and it is pure — no console output, no throwing.
An agent surface that can actually run alone
Until now an agent could author cheaply, via DocSpec, but could not check
the environment, discover the API, or verify its own output. That is closed:
doctor(); // will this environment work? never throws
capabilityManifest(); // every component, block, entry point, error code
schema('list'); // seven subjects, each with a versioned $id
validateSpec(json); // path-anchored findings, no JSON-Schema engine needed
lintSpec(spec); // accessibility + engine legalityPlus a stable E_* error taxonomy: every error carries a code and serializes
to { ok: false, error: { code, message } }. Branch on the code — messages are
reworded between releases, codes are not.
The human-in-the-loop governance contract now ships as runtime capability too
(aiGovernancePolicy, agentRulesText, validateIssueDraft), so an agent
working from an installed package — with no repository checkout — can read the
rules it must follow. llms.txt is now in the published tarball for the same
reason.
Four dry-run tiers, cheapest first:
| Tier | Call | Catches |
|---|---|---|
| 1 | validateSpec |
Malformed shape |
| 2 | compileSpec |
Structure that cannot map onto the model |
| 3 | lintSpec |
Accessibility, and engine constraints that would throw |
| 4 | inspectSpec |
Pagination and geometry |
Under the hood: one table, no drift
The hard part of shipping a machine-readable API description is that it rots.
src/registry.ts now holds the block grammar, the component list and the lint
rules as single-source tables; the JSON Schema, validateSpec and the capability
manifest all derive from them.
Two independent locks make omission a failure rather than a silent gap:
- Compile-time —
Assert<Equals<…>>types mean adding a member to
BlockSpecorHostTagwithout registering it failsnpm run typecheck. - Test-time —
tests/registry.test.tspins the exact ordered contents, and
tests/agent.test.tsxasserts every name the manifest advertises resolves to
a real export of the barrel.
The mechanism was verified by deleting a registry entry and confirming both
halves fail.
What is deliberately not here
pdfnative 1.6.0 also shipped text extraction, form fill/flatten, an encrypted-PDF
reader, streaming page-tree manipulation, and output re-encryption. None of them
are re-exported here, because they operate on existing bytes and this package
authors documents — golden rule 7.
docs/RECIPES.md is the new counterpart: working code for
each of those, calling pdfnative directly on the bytes this library produces.
No wrapper, no indirection, no API we would owe you forever.
Also considered and dropped: <Outline> / <Bookmark> sugar (outline="auto"
already covers the common case), and automatic dev-mode lint warnings (they
would make render behaviour depend on NODE_ENV and put unrequested output in
your logs).
Validation
npm run typecheck:all— clean (src + tests + samples)npm run lint— clean, zero warnings- 226 tests across 16 files, all green (was 79 across 8)
- Coverage 94.8% statements · 86.0% branches · 97.8% functions · 95.8% lines
(thresholds 85/80/85/85, unchanged) npm run build— ESM + CJS +.d.ts+.d.cts- CJS and ESM import smoke tests on the built artifacts
npm pack --dry-run—llms.txtpresent in the tarball- Every new sample executed end to end and verified to produce a valid PDF
This release was additionally put through five independent adversarial
reviews across three rounds — architecture, documentation accuracy, an
engine-1.6.0 gap analysis, an ecosystem state-of-the-art pass, and a final
go/no-go verification. Every confirmed finding is fixed.
Among them: a stack-overflow path in validateSpec on hostile input,
prototype-chain resolution in schema(), a mutable reference to the lint
registry leaking through a returned schema, five lint-rule defects, an
incomplete capability manifest, an RFC 8187 encoding gap, a bundle that emitted
an unresolvable fs/promises specifier, a 'use client' directive that never
reached dist/, and a broken annotation example. Three new lint rules
(L_CHART_EMPTY, L_MAX_BLOCKS_EXCEEDED and L_ATTACHMENTS_NEED_PDFA3) came
directly out of that process, each from a real engine exception the linter
could not previously pre-empt.
Two review findings were rejected after verification rather than acted on —
a disputed coverage figure that turned out correct, and a workflow
"inconsistency" that is in fact shared with the sibling packages. The full
record, including what was deliberately not fixed and why, is in the
PR draft.