lexical-pdf-wasm
v0.1.0
Published
Generate PDFs from [Lexical](https://lexical.dev) editor state entirely in the browser. Powered by a Rust PDF engine compiled to WebAssembly.
Readme
lexical-pdf-wasm
Generate PDFs from Lexical editor state entirely in the browser. Powered by a Rust PDF engine compiled to WebAssembly.
Install
npm install lexical-pdf-wasmESM-only. Works with any bundler that supports import.meta.url (Vite, Webpack 5, Rollup, esbuild, etc.).
Quick start
import { generatePdf } from "lexical-pdf-wasm";
const pdfBytes = await generatePdf(editor.getEditorState().toJSON());That's it. WASM loads automatically on first call.
Download the PDF
import { generatePdf } from "lexical-pdf-wasm";
async function downloadPdf(editor) {
const pdfBytes = await generatePdf(editor.getEditorState().toJSON());
const blob = new Blob([pdfBytes], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "document.pdf";
a.click();
URL.revokeObjectURL(url);
}Open in a new tab
const pdfBytes = await generatePdf(editor.getEditorState().toJSON());
const blob = new Blob([pdfBytes], { type: "application/pdf" });
window.open(URL.createObjectURL(blob));Configuration
All options are optional.
const pdfBytes = await generatePdf(doc, {
title: "Quarterly Report",
author: "Jane Doe",
page: {
size: "A4", // "A4" | "Letter" | "Legal"
orientation: "Portrait", // "Portrait" | "Landscape"
margins: { // in points (72pt = 1 inch)
top: 72,
right: 72,
bottom: 72,
left: 72,
},
},
});PdfConfig
| Field | Type | Description |
|-------|------|-------------|
| title | string | PDF document title (metadata) |
| author | string | PDF document author (metadata) |
| page | PageConfig | Page layout options |
PageConfig
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| size | "A4" \| "Letter" \| "Legal" | "A4" | Page size |
| orientation | "Portrait" \| "Landscape" | "Portrait" | Page orientation |
| margins | Margins | 72 all sides | Page margins in points |
Worker API
For large documents, run PDF generation off the main thread:
import { createPdfWorker } from "lexical-pdf-wasm";
const worker = createPdfWorker();
// Generate as many PDFs as needed
const pdf1 = await worker.generatePdf(doc1);
const pdf2 = await worker.generatePdf(doc2, { page: { size: "Letter" } });
// Clean up when done
worker.terminate();The worker loads WASM independently — no contention with the main thread.
PdfWorker
| Method | Returns | Description |
|--------|---------|-------------|
| generatePdf(doc, config?, images?) | Promise<Uint8Array> | Generate PDF bytes |
| version() | Promise<string> | Library version |
| terminate() | void | Kill the worker and reject pending calls |
Custom worker URL
If your bundler places the worker file at a non-standard path:
const worker = createPdfWorker(new URL("./custom-path/worker.mjs", import.meta.url));React example
import { useState } from "react";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { generatePdf } from "lexical-pdf-wasm";
function ExportPdfButton() {
const [editor] = useLexicalComposerContext();
const [exporting, setExporting] = useState(false);
async function handleClick() {
setExporting(true);
try {
const state = editor.getEditorState().toJSON();
const pdfBytes = await generatePdf(state, {
title: "Exported Document",
});
const blob = new Blob([pdfBytes], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "document.pdf";
a.click();
URL.revokeObjectURL(url);
} finally {
setExporting(false);
}
}
return (
<button onClick={handleClick} disabled={exporting}>
{exporting ? "Exporting..." : "Export PDF"}
</button>
);
}Svelte 5 example
<script>
import { generatePdf } from "lexical-pdf-wasm";
let { editor } = $props();
let exporting = $state(false);
async function handleExport() {
exporting = true;
try {
const state = editor.getEditorState().toJSON();
const pdfBytes = await generatePdf(state, {
title: "Exported Document",
});
const blob = new Blob([pdfBytes], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "document.pdf";
a.click();
URL.revokeObjectURL(url);
} finally {
exporting = false;
}
}
</script>
<button onclick={handleExport} disabled={exporting}>
{exporting ? "Exporting..." : "Export PDF"}
</button>Pre-loading WASM
By default, WASM loads on the first generatePdf() or version() call. To load it earlier (e.g., while the user is editing):
import { init } from "lexical-pdf-wasm";
// Fire and forget — subsequent calls won't reload
init();Other functions
import { version } from "lexical-pdf-wasm";
const v = await version(); // e.g. "0.1.0"Supported Lexical nodes
- Paragraphs (with alignment, indentation)
- Headings (h1–h6)
- Text (bold, italic, underline, strikethrough, code, superscript, subscript)
- Links
- Blockquotes
- Lists (ordered, unordered, checklists)
- Code blocks
- Tables
- Horizontal rules
- Images
- Line breaks
Bundler notes
The package ships as ESM with a .wasm file alongside the JS bundles. Most modern bundlers handle this automatically. The WASM file is resolved at runtime relative to import.meta.url.
Vite: Works out of the box.
Webpack 5: Works out of the box. Ensure experiments.asyncWebAssembly is not interfering — this package loads WASM manually via fetch(), not as a Webpack WASM module.
Next.js: Works in client components ("use client"). The Worker API requires a browser environment and won't work during SSR.
License
MIT
