feat(preview): @formepdf/preview — byte-true in-browser PDF preview + CI parity gate - #89
Conversation
Package skeleton and the byte-identity parity test written before the component (the test is the promise). Not yet runnable — src/render.ts does not exist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…v1, HTML path) Renders HTML through the same engine as the server and shows the actual PDF in an iframe (native viewer), so preview and output agree by construction: - Display: blob → iframe, not a pdf.js-canvas approximation — shows the real bytes. - Options: one shared options object; no previewOptions to drift. - Fonts: bytes-only contract + standardFonts() adapter + fontFingerprint/fontsMatch to turn a silent font fall-back into a visible mismatch banner. renderForPreview is a faithful passthrough (the parity guarantee lives in its absence of transforms). Component: lazy ~7.7MB engine import (out of the initial bundle), debounce, last-good-frame, loading/error, warnings surfaced. Fails-first parity test (red before render.ts existed, now green): the browser path is BYTE-IDENTICAL to server renderHtml on the same (html, options), including embedded fonts. 7/7 tests pass; tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The parity guarantee is silent when broken — the preview would just start disagreeing with output and nothing would say so. So it must be a gate, not a one-off test. - Expand the parity suite to a hermetic inline corpus (invoice table, multipage, borders/backgrounds, and page-counters — the 2-pass sentinel path, the trickiest to keep byte-identical). Inline so a fresh CI checkout needs no external fixtures (@formepdf/html's fixtures aren't git-tracked). - Add a 'Test Preview (parity gate)' step in the packages job, after the HTML WASM + fonts-standard builds it depends on. Divergence fails the build. - State in renderForPreview that the guarantee IS the absence of transforms, so nobody helpfully adds a preview-specific default later. 11/11 tests pass locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
⛔ Changes rejected28 documents changed structurally · 0 errors, 0 warnings, 28 info
|
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 71 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 71 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-statement.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 168 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 168 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-service-agreement.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 325 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 325 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-report-monthly.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 192 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 192 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-remittance-advice.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 135 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 135 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-reference-letter.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 50 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 50 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-receipt.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 105 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 105 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-quote.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 135 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 135 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-purchase-order.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 134 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 134 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-product-catalog.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 237 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 237 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-policy-acknowledgement.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 153 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 153 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-payslip.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 177 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 177 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-packing-slip.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 146 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 146 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-offer-letter.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 86 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 86 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-nda.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 208 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 208 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-memo.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 98 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 98 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-meeting-minutes.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 148 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 148 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-letterhead.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 115 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 115 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-lab-report.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 169 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 169 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-invoice-standard.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 117 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 117 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-invoice-detailed.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 416 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 416 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-inspection-report.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 229 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 229 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-expense-report.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 179 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 179 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-employment-contract.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 316 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 316 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-delivery-note.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 156 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 156 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-credit-note.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 104 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 104 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-cover-letter.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 52 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 52 matched elements moved or resized below the 24pt threshold.
dist/review/northmoor-certificate.pdf — ⛔ Rejected by Dan Molitor · baseline stays at 3ff48be
| Severity | Event | Location | Rule |
|---|---|---|---|
| info | Structure changed but no named event fired (1 of 61 matched elements shifted below the threshold) | document | uncharacterized-change |
Snapshot changed but no nameable event fired: 1 of 61 matched elements moved or resized below the 24pt threshold.
12 documents unchanged: dist/review/template-shipping-label.pdf, dist/review/template-report.pdf, dist/review/template-receipt.pdf, dist/review/template-letter.pdf, dist/review/template-invoice.pdf, dist/review/northmoor-shipping-label.pdf, dist/review/northmoor-report-annual.pdf, dist/review/html-zebra-invoice.pdf, dist/review/html-statement.pdf, dist/review/html-letterhead.pdf, dist/review/html-dashed-borders.pdf, dist/review/facturx-en16931.pdf
No page thumbnails here: GitHub proxies and caches comment images, which would put a rendered customer document in a third-party cache. Open the full diff to see the pages.
Forme Review · 40 runs · powered by pdf-testkit 0.4.1
📄 pdf-testkit —
|
@formepdf/preview— byte-true in-browser PDF preview (v1, HTML path)Renders HTML through the same engine the server uses and shows the actual PDF in an iframe, so preview and output agree by construction — not by approximation. This is the thing a Gotenberg/Puppeteer pipeline gets for free (the browser is both renderer and preview); now Forme gets it because the engine is both, and it paginates properly.
Why it can't silently disagree with output
The audit found three ways a preview can diverge. All three are closed in code:
previewOptions. You pass the sameoptionsobject to<FormePreview>and to your server'srenderHtml.renderForPreviewis a faithful passthrough; the guarantee is the absence of transforms.standardFonts()adapts@formepdf/fonts-standardtorenderHtml's shape, andfontFingerprint/fontsMatchturn a font fall-back mismatch into a visible banner.What's here
render.ts— the parity core (WASM-free,renderHtmlinjected):renderForPreview,standardFonts,fontFingerprint,fontsMatch.FormePreview.tsx— lazy ~7.7MB engine import (out of the initial bundle + a real loading gate), debounce, last-good-frame, loading/error, warnings surfaced, font-mismatch banner.The gate (the point of this PR)
A parity guarantee that's true today and unenforced quietly stops being true — and this one breaks silently. So there's a
Test Preview (parity gate)CI step that assertsrenderForPreview(browser-proxy/workerentry) is byte-identical to the NoderenderHtmlacross a hermetic inline corpus, including the 2-passcounter(page)/counter(pages)path. Divergence fails the build.Fails-first discipline: the byte-identity test was written and made red (before
render.tsexisted), then green. 11/11 pass;tscclean.Scope
v1 is the HTML input path (matches the live prospect's pipeline exactly; simplest parity story). Deferred: JSX input, worker-render mode, a pdf.js-canvas mode (documented approximation), and a jsdom test for the React shell (the parity core — the load-bearing part — is covered; a broken iframe is visible immediately, a broken parity guarantee isn't).
🤖 Generated with Claude Code