# Knowledge Base — pdfnative-react This document explains how pdfnative-react is built and why. It is the reference for contributors and for AI agents working in this repository. ## 1. What this package is pdfnative-react is a **custom React renderer** for the [`pdfnative`](https://www.npmjs.com/package/pdfnative) PDF engine. You compose a document with JSX components; a React reconciler compiles that tree into a `pdfnative` `DocumentParams` object; the engine renders it to PDF bytes. It is **not** a CSS/flexbox layout engine. pdfnative models a document as an ordered *flow of blocks*, and pdfnative-react exposes those blocks 1:1. ## 2. The compile pipeline ``` JSX tree │ React.createElement (via component factories in components.tsx) ▼ Host tree of ElementNode / TextNode (reconciler/nodes.ts) │ react-reconciler (mutation mode) (reconciler/host-config.ts) ▼ RootContainer { children: HostNode[] } │ serialize() (reconciler/serialize.ts) ▼ DocumentParams { title, blocks, … } (pdfnative model) │ buildDocumentPDFBytes / …Stream (core-bridge/index.ts → pdfnative) ▼ Uint8Array (a valid PDF: %PDF-… …%%EOF) ``` Key properties: - **Synchronous & DOM-free.** `compile()` reconciles into an in-memory tree and serializes immediately. There is no DOM, no async scheduling, no Suspense. - **Single source of truth.** Every pdfnative import funnels through `src/core-bridge/index.ts`, keeping the engine surface small and auditable. ## 3. Module map | Module | Responsibility | |---|---| | `src/components.tsx` | Public component factories; each emits a lowercase host tag. `Section` is the one *composite* (no host tag). | | `src/reconciler/nodes.ts` | Host tree node types (`ElementNode`, `TextNode`, `RootContainer`). | | `src/reconciler/host-config.ts` | The react-reconciler `HostConfig` (mutation mode). | | `src/reconciler/serialize.ts` | Pure transform: host tree → `DocumentParams`. | | `src/reconciler/render.ts` | `compile(node)` — drives the reconciler and serializes. | | `src/render.ts` | `renderToBytes/Blob/Stream/File/FileStream`, `compileDocument`, `inspectDocument`. | | `src/fonts.ts` | `resolveFonts` (loader map → `FontEntry[]`) + internal `optionsWithFonts`. `validateFontData` is re-exported from `core-bridge`. | | `src/assets.ts` | `fromUrl` / `fromBase64` image-byte helpers (pure, no engine import). | | `src/hooks.ts` | `usePdf`, `usePdfStream` (client). | | `src/viewer.tsx` | `PDFViewer`, `PDFDownloadLink`, `BlobProvider` (client). | | `src/core-bridge/index.ts` | The only place that imports `pdfnative` at runtime. | | `src/types.ts` | Public types + type-only re-exports of the pdfnative model. | | `src/index.ts` | Public barrel. | The golden rule has one sanctioned exception: `src/types.ts` may import *type-only* from `pdfnative` directly. All *runtime* imports go through `core-bridge`. `src/types.ts` also defines the ergonomic `FontLoader` (`() => Promise`) rather than re-exporting the engine's stricter one, because the auto-generated font-data modules do not structurally satisfy it under `strict`; `resolveFonts` widens to the engine's loader type in one spot. ## 4. react-reconciler version contract This is the single most fragile dependency relationship. Three versions must move together: | React | `react-reconciler` runtime | `@types/react-reconciler` | |---|---|---| | 19 | `^0.31` | `^0.32` | Notes learned the hard way: - The **types and runtime are skewed by one minor**: `@types@0.32` describes the `0.31` runtime. Do not assume the type version equals the runtime version. - The `0.32` `HostConfig` has **14 generic parameters** (the last is `TransitionStatus`) and requires the transition/priority and suspense-on-commit members (`setCurrentUpdatePriority`, `maySuspendCommit`, `waitForCommitToBeReady`, …). `prepareUpdate` was removed; `commitUpdate` is `(instance, type, prevProps, nextProps, handle)`. - `getRootHostContext` / `getChildHostContext` **must return a non-null value.** React uses `null` as its internal "no context" sentinel and will throw *"Expected host context to exist"* (then spin into an OOM) if you return `null`. We return a frozen empty object. - `createContainer` takes **11 positional args** in `0.32` (it added `onDefaultTransitionIndicator` before `transitionCallbacks`). - The synchronous flush API was renamed: the `0.31` runtime exposes `updateContainerSync` + `flushSyncWork`, not `flushSync`. `render.ts` prefers the new pair and falls back to `flushSync(fn)` for older runtimes. ## 5. Serialization rules (`serialize.ts`) - Text for a block comes from its `text` prop, otherwise from the concatenated text of its children. - `` → `style: 'numbered'`; otherwise `'bullet'`. - **Nested lists** (`toListItem`): an `` serializes to a plain `string` when it has no sub-items (byte-identical to the flat case), or to a `{ text, items }` `ListItem` otherwise. Its own text collects **only** non-`item`/`list` children — reusing `elementText` here would wrongly swallow the sub-items' text into the parent label. Sub-items come from the `items` prop, directly nested `` children, or a nested child ``. - `` headers come from the `headers` prop or the first ``; data rows come from the `rows` prop or ``/`` children; `cellBorders`/`cellVAlign` pass straight through. - **Document-level** `outline` and `pageLabels` are `` props (not content blocks) — they reference post-layout page indexes, like `metadata`. - `
` is a **composite** component: React resolves it to a `` (optionally preceded by ``) plus its children *before* the reconciler runs, so the serializer never sees a `section` host tag. - `` siblings are joined with an inserted `pageBreak` block. - The root must be ``; otherwise a `PdfStructureError` is thrown. - `undefined` props are stripped so emitted JSON is deterministic. ## 6. Testing - `tests/compile.test.tsx` — asserts the `DocumentParams` shape for every block, including outline/pageLabels, nested lists (all forms + the flat-list regression), `
`, and table `cellBorders`/`cellVAlign`. - `tests/render.test.tsx` — asserts real PDF output (`%PDF-` … `%%EOF`) for bytes/blob/stream/file, `renderToFileStream` (and that `/Outlines` survives the streaming path), `inspectDocument` geometry, `fromUrl`/`fromBase64`, and `resolveFonts`. - `tests/options.test.tsx` — layout/font merge behavior and that `viewerPreferences`/`debug` survive it. - `tests/hooks.test.tsx` — exercises `usePdf`/`usePdfStream` under jsdom, including the async `options.fonts` path. - `tests/spec.test.tsx` — asserts `compileSpec` parity with the equivalent JSX, nested list/outline/pageLabels/cellBorders forwarding, `inspectSpec`, real `renderSpec*` PDF output, and the JSON Schema `$id`/version/recursive `$defs`. - `tests/version.test.ts` — pins `version` to `package.json` and `CITATION.cff` (reads them via `process.cwd()`; `import.meta.url` file URLs break under jsdom). - jsdom lacks `URL.createObjectURL`; `tests/setup.ts` stubs it. ## 7. Agent authoring contract (`src/spec/`) pdfnative-react is a *library*, so the token cost LLM agents pay is **authoring** a document, not invoking a CLI. The `src/spec/` layer gives agents a compact, JSON-serializable grammar — `DocSpec` — that compiles to the **same** `DocumentParams` as the equivalent JSX. ``` DocSpec (tuples) spec/types.ts — the grammar │ specToElement() spec/compile.ts — projects tuples onto components ▼ element ──────► the normal compile pipeline (§2) ``` Design rules: - **Parity by construction.** `compile.ts` builds the JSX tree from the existing components via `createElement`, so a spec and its JSX twin can never drift. Tests assert `compileSpec(spec)` `toEqual` the JSX `compileDocument`. - **Block tuples** are `[kind, …payload, opts?]`; per-block opts reuse the component prop types (via `Pick`/`Omit`) so the spec inherits the components' type safety. `TableRowSpec` accepts either a `string[]` (widened to `{ cells, type:'default', pointed:false }`) or a full `PdfRow`. - **Versioned schema.** `docSpecSchema()` returns a Draft 2020-12 JSON Schema whose `$id` is `https://pdfnative.dev/schema/react//doc-spec.schema.json` (`version` comes from `src/version.ts`, the single source of truth that `tests/version.test.ts` pins to `package.json`). Agents can self-validate a spec before rendering. - **Isomorphic, no `'use client'`.** The spec module is pure/render-agnostic; `renderSpec*` reuse the existing isomorphic `render*` entry points. - **No `['sec']` tuple.** `
` is JSX sugar with no capability beyond a heading followed by its blocks, so DocSpec stays frugal and omits it — agents emit `['h2', title]` + the blocks directly. Nested lists, `outline`, `pageLabels`, and table `cellBorders`/`cellVAlign` *are* in the grammar, because they express capability the tuples otherwise couldn't. - **GOTCHA.** `createElement` for default-param components (`Spacer`, `TableOfContents`) needs an explicit generic (`createElement`), otherwise TS infers `Attributes` and rejects the extra props (TS2769). ## 8. Design boundaries - **No ``/flexbox.** Honest mapping to the engine's block flow. - **React 19 only.** The reconciler is bound to a single, pinned `react-reconciler` contract; dual React 18/19 support is a non-goal. - **Browser & Node.** `renderToFile` / `renderToFileStream` are Node-only (dynamic `node:fs`); `fromUrl`/`fromBase64` and everything else are isomorphic. - **Authoring only.** pdfnative-react builds documents. Byte-level post-processing — merge/split, annotations, digital signatures, crypto providers, font compilation — is done with the `pdfnative` engine directly on the bytes this library emits. The wrapper deliberately does not re-export those APIs.