Contributing
Thanks for helping build Folio. This page mirrors CONTRIBUTING.md at the repository root. Please also read the Code of Conduct (CODE_OF_CONDUCT.md); to report a vulnerability, follow SECURITY.md instead of opening a public issue. Background reading: the architecture decision records and the performance rules.
Before you start
Read docs/architecture.md before changing package boundaries, and skim the architecture decision records in docs/adr/. Folio has three non-negotiables: book-scale performance (an edit does work proportional to what it changed), exact, Word-like pagination, and hosts extend, never fork. Changes are judged against them.
For anything bigger than a bug fix, open an issue (or a draft PR) first so we can agree on the approach.
Repository layout
folio/
├── packages/
│ ├── model/ @nextgensoftwares/folio-model ProseMirror-JSON model, schema, validate/normalize (DOM-free)
│ ├── layout/ @nextgensoftwares/folio-layout line breaking, bidi, pagination, incremental layout (DOM-free)
│ ├── fonts/ @nextgensoftwares/folio-fonts HarfBuzz TextMeasurer; fixtures/ holds OFL test fonts
│ ├── formatter/ @nextgensoftwares/folio-formatter rules → layout hints (DOM-free)
│ ├── editor/ @nextgensoftwares/folio-editor headless editor on ProseMirror core, plugin contract
│ ├── sync/ @nextgensoftwares/folio-sync chunked loading, step sync, offline outbox, checksums
│ ├── server-node/ @nextgensoftwares/folio-server-node reference Node server: REST sync, WebSocket collab, persistence
│ ├── react/ @nextgensoftwares/folio-react view, toolbar, menus, rulers, page setup, theme editor
│ ├── plugin-media/ @nextgensoftwares/folio-plugin-media
│ ├── plugin-toc/ @nextgensoftwares/folio-plugin-toc
│ ├── plugin-collab/ @nextgensoftwares/folio-plugin-collab
│ ├── export-pdf/ @nextgensoftwares/folio-export-pdf
│ ├── export-docx/ @nextgensoftwares/folio-export-docx
│ ├── render-canvas/ @nextgensoftwares/folio-render-canvas pages → any Canvas 2D context (browser or Node) (DOM-free)
│ ├── render-node/ @nextgensoftwares/folio-render-node server page images + flattened PDFs (@napi-rs/canvas)
│ ├── plugin-protect/ @nextgensoftwares/folio-plugin-protect canvas pages, copy/print limits, tamper detection, image pager
│ └── docx/, pdf/ older stubs
├── tools/ chrome-compare.mjs (fidelity), load-memory.mjs (memory probe)
├── docs/ architecture.md, adr/
├── site/ the documentation site (VitePress)
└── .changeset/ pending release notesThe playground (folio-playground, a sibling Vite + React project) links the packages' dist builds and exercises every feature, including the stress book and the editing benchmark.
Setup
Requirements: Node 22+ and pnpm 10 (the repo pins pnpm@10.13.1; corepack enable picks it up).
pnpm install
pnpm buildUse pnpm only: no npm or yarn lockfiles.
Commands
| Command | What it does |
|---|---|
pnpm build | build every package (tsup: ESM + CJS + .d.ts), 4 at a time |
pnpm --filter @nextgensoftwares/folio-layout test | test one package (run this first) |
pnpm test | build, then test every package one package at a time |
pnpm typecheck | tsc in every package |
pnpm --filter @nextgensoftwares/folio-site dev / build | docs site (the build fails on dead links) |
node site/scripts/api-drift.mjs | exports missing from the API reference pages |
pnpm changeset | record a version bump and release note |
Every package's test script runs Vitest with --maxWorkers=4. Keep it that way, and run only the packages your change touches before running everything. Never start several full test runs in parallel.
Coding rules
- Files stay under 200 lines. Split by responsibility when a file grows.
- Core packages are DOM-free.
model,layout,formatterand the export writers compile withlib: ["ES2022"]only and must run in Node, a worker and the browser. Onlyeditorandreact(and React painters inside plugins) may touch the DOM. - Never measure text with the browser for layout. Go through
TextMeasurer. - Nodes are immutable. Never mutate document nodes; caches key on identity.
- Declare every attribute you store in a schema (ProseMirror drops the rest).
- Hosts extend, never fork. New block types, themes and rule packs belong in plugins or the host, not in the engine core.
- No code from AGPL/GPL projects (SuperDoc, OnlyOffice, LibreOffice's GPL parts…). Study them for ideas only; never copy, adapt or import their code.
- Dependencies must be permissively licensed (MIT, BSD, Apache-2.0, ISC) and justified; prefer small, focused ones. Fonts must be OFL or similar, with their license files kept next to them.
- TypeScript is strict (
noUncheckedIndexedAccess,exactOptionalPropertyTypes). Keep it that way; avoidany. - Comments explain why (a constraint, a measured cost, a spec rule), not what.
Performance checklist
Every change on a hot path (typing, layout, painting, loading) should satisfy:
- [ ] Work is proportional to the change: no new whole-document passes beyond O(blocks) bookkeeping.
- [ ] No per-page work for invisible pages: use
page.blocksand binary search; readpage.fragmentsonly for pages that are drawn or needed. - [ ] Identity is preserved: unchanged nodes, flows, pages and derived objects stay the same objects; caches are
WeakMaps keyed by object. - [ ] Layout inputs stay stable (theme, renderer maps, header/footer config).
- [ ] Long-lived closures are created by module-level helpers that capture only what they need (closures share their creating scope's context in V8).
- [ ] Lazy getters are non-enumerable.
- [ ] Availability checks (
when/enabled/active, commands withoutdispatch) stay cheap even after select-all on a book. - [ ] Transient state (progress, hover) stays out of the document.
How to measure:
pnpm build
node --expose-gc tools/load-memory.mjs 130 # heap per 10 chunks while a big book streams in
PUPPETEER_FROM=/dir/with/node_modules/puppeteer \
node tools/chrome-compare.mjs chapters.ndjson # line breaks vs headless ChromeIn the playground, load the "Stress: ~5,000 pages" sample and run the editing benchmark (bench panel): engine and frame times for typing, Enter and Backspace in the middle of the book. Include before/after numbers in the PR for performance-sensitive changes.
Tests
- Spec-driven. Tests state the behaviour the user or the spec expects (Word's pagination rules, UAX #14/#9, CSS font matching), not the current implementation's quirks. When a test and the spec disagree, fix the code.
- One package at a time, with the small fixed-advance measurer (
{ width: (t, f) => t.length * f.size * 0.5 }) and tiny themes so numbers are round; use the real fonts inpackages/fonts/fixtures/when shaping matters. - Randomized equivalence tests for anything incremental: compare the incremental result with a full recomputation after hundreds of seeded random edits (see
packages/layout/src/incremental.test.ts). Use a seeded PRNG so failures reproduce. - Regression tests for safety properties (see
packages/sync/src/integrity.test.ts): simulate the host bug and assert the system refuses it. - Every bug fix comes with a test that fails without the fix.
Changesets and releases
Packages are versioned with Changesets. For any change that affects a published package, run pnpm changeset, pick the packages and the bump (pre-1.0: minor for breaking changes, patch otherwise), and write a one-line, user-facing summary. Commit the generated file with your change.
Maintainers release with pnpm changeset version (bumps versions and changelogs) and pnpm release (builds and publishes; access is currently restricted to the project's registry).
Commit messages
Follow the existing history:
- Imperative, sentence case, no trailing period, about 70 characters:
Add @nextgensoftwares/folio-sync; fix progressive-load memory and scrolling. - Optionally prefix the area:
layout: allow custom rect roles for host renderers. - The body explains why and any measured effect (numbers welcome).
- Sign off your commits (
git commit -s, see licensing below).
Pull requests and review checklist
A PR should do one thing. Fill in the template; reviewers check:
- [ ] The change matches the architecture (or updates
docs/architecture.md/ adds an ADR when it changes a decision). - [ ] Core packages stay DOM-free; no browser text measurement for layout.
- [ ] The performance checklist holds; numbers included for hot-path changes.
- [ ] Tests cover the behaviour (spec-driven; randomized equivalence for incremental code) and pass with
--maxWorkers=4. - [ ]
pnpm typecheckpasses; files are under 200 lines. - [ ] Public API changes are reflected in the docs site (guide and API pages;
node site/scripts/api-drift.mjsis clean) and have a changeset. - [ ] No code copied from AGPL/GPL projects; new dependencies are permissive.
Licensing and prior art
Folio is licensed under the Apache License 2.0 (see LICENSE and NOTICE at the repository root).
- Inbound = outbound. By submitting a contribution you agree that it is licensed under Apache-2.0, as section 5 of the license states, and that you have the right to submit it.
- Developer Certificate of Origin. We recommend signing off every commit (
git commit -s), which adds aSigned-off-by:line certifying the DCO. - Don't add copyright headers to individual files; the
NOTICEfile covers the project. Keep third-party license files (e.g. font licenses) intact. - Prior art is for study only. SuperDoc and OnlyOffice are AGPL; never copy, adapt or import their code. Specs (ECMA-376, ISO 32000, UAX #14, UAX #9, CSS Fonts) are the references to cite.