How the export pipeline works
- The lesson state (
{ title, sections: [{ name, blocks: [...] }] }) is turned into adocxDocumentin@spelling-creator/core/browser/docxExport. - DOCX export packs that document to a Blob and downloads it.
- PDF print (
@spelling-creator/core/browser/pdfExport) packs the same document, converts it to HTML withmammoth, applies print styles, and renders it to PDF withhtml2pdf.js. Using one shared document builder keeps the two outputs in sync. - Import (
browser/docxImport) is the same machinery in the other direction —mammothagain, this time reading a user's file — and Save to Google Docs uploads the very same.docx.
Building a Word file is for Word files only
Those four are the whole list. Nothing else in the app touches docx or mammoth; in particular, preview does not. The editor's preview dialog renders the lesson model directly with LessonView — the same component the public /hub/:id page uses — so previewing builds no document, waits on no chunk, and shows exactly what a reader will see. (It used to build a docx and convert it back to HTML with mammoth just to fill a dialog.)
Keep it that way: a new "show me the lesson" surface should render LessonView, not the export pipeline.
On screen it follows the theme
LessonView draws a lesson in the app's own theme, light or dark, on both surfaces that show one — the public lesson page and the editor's preview dialog — exactly as interactive mode does. It keeps the export's measurements (the fitWithin image maths against DOCX_MAX_IMAGE_WIDTH, the heading sizes, the spacing), so a lesson keeps the shape it will print in; only the colours and the typeface are the theme's.
There is no second "paper" rendering to keep in sync. A lesson is read on screen far more often than it is printed, and a white sheet glaring out of a dark page is the wrong default for reading — the printout look lives in the thing that actually prints, the DOCX and PDF exports.
Question-type and spelling colours stay literal there, as they are in the editor and in interactive mode: they're content — the same colour coding the docx carries — rather than chrome.
It loads on demand
Together those libraries — docx, mammoth, html2pdf.js, html2canvas, jszip — are the largest single cluster in the dependency graph: about 390 kB gzipped. None of it is needed until someone clicks Export, Print, Import or Save to Drive, so it lives in its own chunk behind src/lib/exports/load.js, in the same shape as the git engine:
const { exportDocx } = await loadExportEngine();
await exportDocx(doc);src/lib/exports/engine.js is the chunk; nothing imports it directly. It's one chunk rather than four because the entry points share nearly all their weight — the PDF path builds the docx and converts it with mammoth, which is also what the importer uses.
The constants trap
DOCX_MAX_IMAGE_WIDTH lives in @spelling-creator/core/lessonLayout, not beside the code that first needed it. It used to sit in browser/docxExport, which meant LessonView.jsx — wanting one number, and rendering on the public /hub/:id page — pulled the entire Word toolchain into the bundle every visitor downloaded.
Being outside browser/ is also what lets the server render a lesson at all: that tier needs a DOM and is unreachable from the Worker by design, and /hub/:id is server-rendered.
If you add a shared constant to any of these modules, put it in lessonLayout and re-export it, rather than importing the module for the constant's sake.