waffle

package module
v0.0.0-...-03ad2cf Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 12, 2026 License: MIT Imports: 11 Imported by: 0

README

waffle

Write PDFs in React. Render them from Go. No Node.js, no browser, no sidecar.

Documents are authored in JSX/TSX exactly as react-pdf users write them — components, props, hooks, StyleSheet, Font.register. Everything after that happens inside your Go process: waffle transpiles the source with esbuild, runs real React 18 on the goja JavaScript engine, lays the document out with its own flexbox engine, and writes the PDF with its own writer. Pure Go, CGO_ENABLED=0, one static binary.

JSX ──esbuild──▶ bundle ──goja/React──▶ element tree ──flexbox──▶ pages ──paint──▶ PDF
                 (all in-process: compile once, render in milliseconds)

Highlights

  • React in, PDF out — real React (hooks, context, memo, function components), not a lookalike DSL. Existing react-pdf documents largely work as-is.
  • Zero runtime dependencies — no Node, no Chromium, no LibreOffice, no cgo. go build and ship one binary.
  • Fast — a template compiles once (~4 ms) and renders a one-page report in ~6–7 ms; an 8-page letter with a full-page letterhead and four embedded fonts in ~24 ms. Renders scale across cores with plain goroutines. See Benchmarks.
  • Template asset caching — images are decoded (and their pixels pre-compressed) once per Template, fonts fetched and parsed once, embedded font streams compressed once. Repeat renders skip all of it.
  • Real typography & layout — flexbox (grow/shrink/wrap/percentages/min-max/aspect-ratio), the standard 14 PDF fonts with true AFM metrics, custom TTF embedding measured with real glyph advances, justification, inline styled runs, letterSpacing/wordSpacing, maxLines + ellipsis.
  • The react-pdf surface — pagination (wrap, break, fixed, minPresenceAhead, orphans/widows, {pageNumber}/{totalPages} and function render-props evaluated per page on the live VM), images (PNG/JPEG from data URI, file path, or URL), SVG (paths, shapes, gradients, clipPath, text), Canvas painter, links, bookmarks/outline, AcroForm fields, Note annotations, opacity, CSS transforms, rounded/dashed/dotted borders, and encryption (RC4-128, AES-128, AES-256).
  • Validated output — the test suite strict-validates generated PDFs with pdfcpu.

Quickstart

Author a document in React (invoice.jsx). @waffle/react is a virtual module the engine provides at transpile time — there is nothing to npm install and no node_modules:

import { Document, Page, View, Text } from '@waffle/react';

export default function Invoice({ customer, total }) {
  return (
    <Document title="Invoice">
      <Page size="A4" style={{ padding: 40, fontFamily: 'Helvetica' }}>
        <Text style={{ fontSize: 24 }}>Invoice</Text>
        <Text style={{ fontSize: 12, color: '#666' }}>Billed to: {customer}</Text>
        <Text style={{ marginTop: 20 }}>Total due: ${total}</Text>
      </Page>
    </Document>
  );
}

Render it from Go, supplying data as props:

package main

import (
	"context"
	_ "embed"
	"os"

	"github.com/SwishHQ/waffle"
)

//go:embed invoice.jsx
var invoiceJSX []byte

func main() {
	tmpl, err := waffle.LoadTemplate(invoiceJSX, waffle.TemplateOptions{Filename: "invoice.jsx"})
	if err != nil {
		panic(err)
	}
	out, _ := os.Create("invoice.pdf")
	defer out.Close()
	_, err = tmpl.Render(context.Background(), map[string]any{
		"customer": "Globex Corporation",
		"total":    2400,
	}, out)
	if err != nil {
		panic(err)
	}
}

Or from the CLI:

waffle invoice.jsx invoice.pdf props.json   # JSX + props → PDF
waffle tree.json   out.pdf                  # pre-serialized waffle-tree/v1 → PDF
API at a glance
Call Use
waffle.LoadTemplate(src, opts) → *Template Compile once (esbuild + goja). The Template holds the compiled program and the asset cache; safe for concurrent Render.
Template.Render(ctx, props, w) Execute with props on a fresh VM and write a PDF. Function render-props stay live for per-page evaluation.
Template.Tree(props) Return the intermediate waffle-tree/v1 JSON instead of a PDF.
waffle.RenderReact(ctx, src, props, w) One-shot compile + render.
waffle.RenderTree(ctx, treeJSON, w) Render a pre-serialized element tree from any producer of the contract — no JavaScript runs at all.

Architecture

Two phases: compile once, render many. The full picture is in docs/architecture.excalidraw — open it at excalidraw.com or with the VS Code Excalidraw extension.

1 · LoadTemplate — compile (once, ~4 ms). esbuild transpiles and bundles the JSX in memory, resolving react, react/jsx-runtime, and @waffle/react from assets embedded in the binary (vendored react.production.min.js 18.3 and waffle's runtime: a synchronous hooks dispatcher, Font/StyleSheet shims, and the tree serializer). The bundle is compiled to a *goja.Program, reusable across VMs.

2 · Render — per call (milliseconds, concurrent-safe). Each render runs the program on a fresh goja VM with your props. React renders synchronously in one pass — a document is a pure function of props: useState returns initial values and effects never fire, which is the correct model for producing a file. The VM emits a waffle-tree/v1 JSON document (elements, styles, fonts, inline assets); function-valued render props are registered as $cb callbacks and evaluated back on the live VM per page during pagination.

The Go pipeline then takes over:

Stage Package What happens
contract internal/contract parse + version-gate the tree JSON ($cb callbacks, $inline assets)
tree internal/tree node model, structural validation, prop normalization
styles internal/stylesheet units, colors, shorthands, media queries, inheritance
layout internal/layout flexbox engine (internal/flexbox), text measurement (internal/fontstore: embedded-TTF glyph advances or AFM core-14 metrics), image/SVG sizing
pagination internal/layout block + mid-element splits, forced breaks, fixed elements, orphans/widows, page-number substitution
paint internal/render boxes → PDF content streams: text runs, images, SVG, canvas replay, borders, gradient shadings, links, forms, outline
write internal/pdf object model, Flate streams, font embedding (FontFile2), image XObjects, encryption, deterministic xref/trailer

The asset cache. A Template memoizes everything render-invariant: decoded images (pixels already FlateDecode-compressed), fetched + parsed font stores, and each embedded face's compressed FontFile2 stream. The first render fills the cache; every render after that skips decode, fetch, parse, and recompression entirely — profiling showed those were ~90% of per-render cost on image-heavy documents. Consequence: file/URL assets are read once per Template; load a new Template to pick up changed files.

Benchmarks

Measured with go test -bench on an Apple M4 Pro (12 cores), Go 1.24, warm template, best of 3 runs. Parallel rows run on all cores via b.RunParallel and report aggregate per-op time.

Benchmark Document time/op ≈ throughput
LoadTemplate compile the report template 4.0 ms one-time
RenderReport 1-page report: title + 2 styled tables (17 rows) + footer 6.6 ms ~150/s
RenderReportParallel same, all cores 3.0 ms ~330/s
RenderTreeReport same document from pre-serialized tree JSON (no JavaScript) 1.9 ms ~515/s
AppointmentRender 8-page letter: full-page letterhead ×8, 4 embedded TTFs, inline runs, tables 24 ms ~41/s
AppointmentRenderParallel same, all cores 9.0 ms ~110/s

The RenderTree row isolates the Go pipeline (layout + paint + write); the gap to RenderReport is the cost of executing real React on goja each render.

Under tight container caps (Docker, swap disabled, GOMAXPROCS=1), the 8-page letter holds up on a fraction of a core:

Cap p50 p95 throughput peak memory
0.25 vCPU / 384 MiB 102 ms 322 ms 8.3/s 83 MiB
0.5 vCPU / 512 MiB 29 ms 91 ms 21.6/s 91 MiB

Reproduce:

GOTOOLCHAIN=local go test -bench . -benchmem -run '^$' .                        # engine benchmarks
GOTOOLCHAIN=local go test -bench . -benchmem -run '^$' ./examples/appointment/  # full-letter benchmarks
bash examples/appointment/bench/bench.sh                                        # capped-container harness
# CPU profile of the steady-state render loop:
cd examples/appointment && CPUPROFILE=/tmp/waffle.prof REQUESTS=30 go run ./bench

Examples

Example Shows
examples/react data-driven invoice — the headline "JSX + Go data" flow
examples/appointment 8-page offer letter: letterhead, custom fonts, inline runs, prop-driven tables; go test checks + benchmarks
examples/hello · boxes · text · multipage writer, flexbox, text, and pagination basics

Development

export GOTOOLCHAIN=local   # deps are pinned to Go 1.24
go build ./...
go vet ./...
go test ./...              # PDF validation tests use pdfcpu if installed
go run ./examples/react    # writes invoice.pdf

PDF validation (optional but recommended):

go install github.com/pdfcpu/pdfcpu/cmd/pdfcpu@latest
export PATH="$PATH:$(go env GOPATH)/bin"

Regenerate golden files after an intentional output change: WAFFLE_UPDATE=1 go test ./....

Authors

  • Debarshi Chaudhuri (@Chaudhuri-Debarshi) — creator & maintainer
  • Built pair-programming with Claude (Anthropic); commits carry co-authorship trailers.

License

MIT © 2026 Debarshi Chaudhuri.

Third-party components: the vendored react.production.min.js is MIT-licensed (Meta — see internal/jsruntime/assets/VENDOR.md); the example fonts are the BSD-licensed Go fonts (Bigelow & Holmes — see examples/appointment/assets/fonts/NOTICE.md).

Documentation

Overview

Package waffle renders PDF documents from a React-authored element tree.

Documents are written in ReactJS exactly as react-pdf users write them today. waffle can consume them two ways:

  • In-process (recommended): LoadTemplate/RenderReact transpile a JSX/TSX source with esbuild and execute it with the goja JavaScript engine — real React runs inside the Go process, with no Node.js at render time. goja is waffle's single JS engine; there is no sidecar.
  • Static tree: RenderTree ingests a waffle-tree/v1 JSON document (the serialized element tree), for setups that prefer to produce the tree in a separate step. This is the same contract the in-process path emits.

Both paths feed the same Go pipeline: parse the contract, build and validate the element tree, lay it out (flexbox + text + pagination), and paint the PDF. See the README's Architecture section and docs/architecture.excalidraw.

Index

Constants

View Source
const Version = "0.0.0-dev"

Version is the current waffle library version.

Variables

This section is empty.

Functions

This section is empty.

Types

type RenderInfo

type RenderInfo struct {
	PageCount int
	Warnings  []string
}

RenderInfo reports the outcome of a render.

func RenderReact

func RenderReact(ctx context.Context, source []byte, props any, w io.Writer) (*RenderInfo, error)

RenderReact is a one-shot convenience: compile source and render it with props to a PDF in a single call. Prefer LoadTemplate when rendering the same document repeatedly, so compilation happens once.

func RenderTree

func RenderTree(ctx context.Context, treeJSON []byte, w io.Writer) (*RenderInfo, error)

RenderTree renders a waffle-tree/v1 document (as produced by @waffle/react) to a PDF written to w. It runs the full pipeline: parse the contract, build and validate the element tree, lay it out, and paint it.

This is the static-tree entry point (execution mode 2). It does not evaluate function-valued props (render callbacks, canvas paint, hyphenation); documents that require them need a VM execution mode, added in a later phase.

type Template

type Template struct {
	// contains filtered or unexported fields
}

Template is a compiled React document that renders to PDF with varying props.

The source is a JSX/TSX module whose default export is either a <Document> element or a function of props returning one, exactly as react-pdf documents are authored. It is transpiled (esbuild) and executed (goja) entirely inside the Go process — waffle needs no Node.js at render time. A Template is safe to Render repeatedly, including concurrently: each Render runs on its own VM.

A Template caches its decoded assets: images are decoded (and their pixels compressed) and registered fonts fetched and parsed once, on first use, then reused by every subsequent Render. File and URL sources are therefore read once per Template — a change to the underlying file is picked up by loading a new Template, not by re-rendering an existing one.

func LoadTemplate

func LoadTemplate(source []byte, opts TemplateOptions) (*Template, error)

LoadTemplate compiles a React source into a reusable Template.

func (*Template) Render

func (t *Template) Render(ctx context.Context, props any, w io.Writer) (*RenderInfo, error)

Render executes the template with props and writes a PDF to w. Unlike the static RenderTree path, it keeps the JS VM alive so function render-props (render={fn}) are evaluated per page.

func (*Template) Tree

func (t *Template) Tree(props any) ([]byte, error)

Tree runs the template with props and returns the waffle-tree/v1 JSON, without producing a PDF. Props may be any JSON-encodable value (nil for none).

type TemplateOptions

type TemplateOptions struct {
	// TypeScript parses the source as TSX instead of JSX.
	TypeScript bool
	// Filename labels the source in compile/runtime error messages.
	Filename string
}

TemplateOptions configures how a React source is compiled into a Template.

Directories

Path Synopsis
cmd
waffle command
Command waffle renders a React document to a PDF.
Command waffle renders a React document to a PDF.
examples
appointment command
Command appointment renders the offer/appointment-letter example to a PDF.
Command appointment renders the offer/appointment-letter example to a PDF.
appointment/bench command
Command bench load-tests the in-process JSX->PDF render.
Command bench load-tests the in-process JSX->PDF render.
boxes command
Command boxes renders a small styled View tree (a waffle-tree/v1 document) to boxes.pdf, demonstrating the end-to-end pipeline: contract → tree → layout → PDF.
Command boxes renders a small styled View tree (a waffle-tree/v1 document) to boxes.pdf, demonstrating the end-to-end pipeline: contract → tree → layout → PDF.
hello command
Command hello renders a minimal single-page PDF using the Phase 1 writer core.
Command hello renders a minimal single-page PDF using the Phase 1 writer core.
multipage command
Command multipage builds a data-driven document (24 rows) that overflows a small page, demonstrating automatic pagination.
Command multipage builds a data-driven document (24 rows) that overflows a small page, demonstrating automatic pagination.
react command
This example renders a React-authored invoice to a PDF with data supplied from Go — the whole point of waffle: author in React, drive it with Go data, render in-process on goja with no Node.js.
This example renders a React-authored invoice to a PDF with data supplied from Go — the whole point of waffle: author in React, drive it with Go data, render in-process on goja with no Node.js.
text command
Command text renders a small typographic document to text.pdf, exercising the standard-14 font path (Helvetica/Times/Courier, weights and italics) end to end.
Command text renders a small typographic document to text.pdf, exercising the standard-14 font path (Helvetica/Times/Courier, weights and italics) end to end.
internal
contract
Package contract defines waffle-tree/v1: the versioned JSON boundary between the React authoring layer (@waffle/react) and the Go rendering engine.
Package contract defines waffle-tree/v1: the versioned JSON boundary between the React authoring layer (@waffle/react) and the Go rendering engine.
fetch
Package fetch resolves a resource reference — a data: URI, an http(s) URL, or a local file path — to raw bytes, with a size cap and a scheme allowlist.
Package fetch resolves a resource reference — a data: URI, an http(s) URL, or a local file path — to raw bytes, with a size cap and a scheme allowlist.
flexbox
Package flexbox is waffle's own flexbox layout engine — the replacement for react-pdf's Yoga binding.
Package flexbox is waffle's own flexbox layout engine — the replacement for react-pdf's Yoga binding.
fontstore
Package fontstore registers and resolves fonts and measures text.
Package fontstore registers and resolves fonts and measures text.
imaging
Package imaging decodes images for PDF embedding.
Package imaging decodes images for PDF embedding.
jsruntime
Package jsruntime is waffle's in-process JavaScript engine: it transpiles a user's JSX/TSX React document and runs it entirely inside the Go process to produce a waffle-tree/v1 JSON document — no Node.js at render time.
Package jsruntime is waffle's in-process JavaScript engine: it transpiles a user's JSX/TSX React document and runs it entirely inside the Go process to produce a waffle-tree/v1 JSON document — no Node.js at render time.
layout
Package layout is the layout pipeline: it resolves styles and page sizes, drives the flexbox engine, and produces a tree of positioned boxes ready for painting.
Package layout is the layout pipeline: it resolves styles and page sizes, drives the flexbox engine, and produces a tree of positioned boxes ready for painting.
pdf
Package pdf is a write-only PDF 1.3–1.7 serializer: the low-level object model, document/xref writer, content-stream operator builder, and standard-14 font support.
Package pdf is a write-only PDF 1.3–1.7 serializer: the low-level object model, document/xref writer, content-stream operator builder, and standard-14 font support.
pdf/afm
Package afm parses Adobe Font Metrics for the 14 standard PDF fonts and exposes glyph widths and font metrics used for text measurement.
Package afm parses Adobe Font Metrics for the 14 standard PDF fonts and exposes glyph widths and font metrics used for text measurement.
render
Package render paints a laid-out document (layout.Result) onto the PDF writer, producing the final bytes.
Package render paints a laid-out document (layout.Result) onto the PDF writer, producing the final bytes.
stylesheet
Package stylesheet parses react-pdf-shaped style values into typed, resolved forms for the layout engine: dimensioned values and their unit conversions, colors, font weights, and style flattening.
Package stylesheet parses react-pdf-shaped style values into typed, resolved forms for the layout engine: dimensioned values and their unit conversions, colors, font weights, and style flattening.
svgparse
Package svgparse parses SVG path data and basic shapes into a vector IR of move/line/cubic/close segments, ready to emit as PDF path operators.
Package svgparse parses SVG path data and basic shapes into a vector IR of move/line/cubic/close segments, ready to emit as PDF path operators.
transform
Package transform parses CSS transform strings (translate, scale, rotate, skew, matrix) and composes them into a single 2D affine matrix expressed in PDF operand order [A B C D E F].
Package transform parses CSS transform strings (translate, scale, rotate, skew, matrix) and composes them into a single 2D affine matrix expressed in PDF operand order [A B C D E F].
tree
Package tree is the internal, validated element model the layout pipeline walks.
Package tree is the internal, validated element model the layout pipeline walks.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL