Skip to content

v0.12.1 — LayoutInfo type/runtime drift fix + accessor helpers

Choose a tag to compare

@danmolitor danmolitor released this 26 Aug 14:34
· 118 commits to main since this release

Patch release. No runtime behavior changed — the engine emits the exact same JSON it always did. The declared TypeScript types for renderDocumentWithLayout()'s output had drifted from that JSON in eight places, first flagged internally, then again by an external consumer's dogfood test. This release fixes them at the root and adds enforcement so it doesn't happen a third time.

Fixed

@formepdf/core — declared types now match runtime output.

  • ElementNodeType is now a closed literal union of the 30 nodeType values the engine actually emits (was string). Code like if (element.nodeType === 'Heading') — silently wrong before, since reality is discrete 'H1''H6' — is now a TypeScript compile error. Same treatment for ElementKind (10 values) and 11 style enums (ElementFlexDirection, ElementJustifyContent, ElementAlignItems, ElementAlignContent, ElementFlexWrap, ElementFontStyle, ElementTextAlign, ElementTextDecoration, ElementTextTransform, ElementOverflow, ElementPosition). All exported for narrowing.
  • ElementStyleInfo expanded from 19 to 34 fields. Previously missing: alignContent, breakBefore, breakable, columnGap, rowGap, flexGrow, flexShrink, letterSpacing, minOrphanLines, minWidowLines, overflow, position, top/right/bottom/left, width/height, textDecoration, textTransform.
  • textContent on ElementInfo is now typed as string | null | undefined (was string?). Only populated on TextLine leaves — every non-TextLine node emits null at runtime. The old declaration made consumers reach for the wrong node.
  • Every layout-time transform now documented explicitly on the ElementInfo JSDoc: <Table> unwraps into sibling TableRow nodes, <OrderedList> becomes List + ListItem + Lbl, <Fixed> splits into FixedHeader/FixedFooter, headings are discrete H1H6, <Text> block content is split into TextLine leaves, inline elements don't get their own nodes, <PageBreak> produces no node.

Added

@formepdf/core/layout — new subpath export with stable accessor helpers. Additive; the raw ElementInfo tree is unchanged.

import {
  getNodeText, getTextLines,
  getHeadingLevel, getTableRows, getFixedRegions,
  getListItems, getListItemMarker,
  walkElements, findElements, findFirstElement,
  isNodeType,
} from '@formepdf/core/layout';

The helpers encapsulate each documented layout-time transform in a narrow, deliberately-maintained surface. Consumers get to say getNodeText(paragraph) instead of hand-rolling "walk TextLine children, gather textContent, join lines." When the transforms change in a future release, the helpers absorb the change — consumers ride through transparently.

See the Layout API docs for the full reference and the "prefer helpers unless you need raw" guidance.

⚠️ Arguable-break notes — type-tightening exposes latent bugs

This is technically a patch because the underlying bug was in our declarations, not consumer code. But if your TypeScript code compiled against @formepdf/core@0.12.0 and now fails against 0.12.1, one of these is almost certainly why:

  • style.flexDirection === 'row' — silently wrong before (runtime always emitted 'Row', Rust-side PascalCase); now a compile error. Standard types-tighten territory. Fix: use the PascalCase values, now exported as ElementFlexDirection. Same story for the other 10 style enums.
  • element.textContent changing from string | undefined to string | null | undefined will flag code that assumed non-null. Fix: use the new getNodeText() helper (it handles this correctly by walking TextLine children), or explicitly handle null (which is what the runtime always emitted anyway).

Both cases were bugs before the release; TypeScript is now catching them for you. The CHANGELOG in @formepdf/core has more detail.

Enforcement

Three-directional invariant now enforced in @formepdf/core's own CI:

  1. Every emitted nodeType / kind / style enum → member of its declared union (runtime test)
  2. Every declared ElementNodeType → appears in the rich fixture (coverage tripwire — catches "component shipped without structural coverage")
  3. Every union member → present in the test file's key record (compile-time check — catches "union grew without updating the test")

This closes the drift risk at all three angles. Runtime drift trips one test; declaration-side gaps trip a compile error before the file even runs.

Full changelog

All other @formepdf/* npm packages (shared, react, svelte, preact, renderer, cli, hono, next, resend, mcp, sdk, tailwind, templates) got version-alignment bumps to 0.12.1. No functional changes.