office-engine

Development

bun install
bun run dev          # playground http://localhost:5173 (drag & drop, URL, samples)
bun run collab       # collaboration hub for the playground's "Share live" (port 8787)
bun test             # core tests on fixtures
bun run test:e2e     # browser tests (headless Chromium: bunx playwright install chromium; needs large-deck.pptx, see fixtures.py)
bun run build        # dist/: ESM lib + office-engine.embed.js + .d.ts
python3 -m venv .venv && .venv/bin/pip install python-docx openpyxl python-pptx Pillow
.venv/bin/python scripts/fixtures.py --large   # generate samples + heavy files (--large-deck: only the deck the browser tests need)
bun run screenshots  # README screenshots -> docs/screenshots/ (headless Chromium)

Architecture

main thread                                   worker
───────────                                   ──────
DocViewer ─ useDocument ─ DocumentClient ⇄ RPC ⇄ Engine
  ├ DocxView  (measure → paginate → virtual pages)     ├ ByteSource (Blob slice | HTTP Range + LRU)
  ├ XlsxView  (Axis prefix-sum, 2D virtual grid)       ├ ZipArchive (central dir, inflate on demand)
  └ PptxView  (virtual slides + thumbs)                ├ SaxParser / TreeBuilder (streaming)
                                                       └ DocxDocument | XlsxWorkbook | PptxPresentation
src/core     parsers, DOM-free (runs in worker / Bun)
src/react    view components
src/embed.ts mount() + <office-viewer>
src/collab   real-time collaboration (hub, session, per-format adapters)

Browser tests

e2e/*.spec.ts drive a fixture app (e2e/app, served by Vite with vite.e2e.config.ts) in headless Chromium through playwright-core, one browser and server per file (e2e/harness.ts). The app reads DocViewer props from the query string (?src=samples/sample.docx&editable=1&allowDownload=0&onSave=1…) and records callbacks in window.__events; saved files are re-opened with the engine to check what was written.

SpecCovers
viewer.spec.tsrendering of the 3 formats, outline, zoom, sheet tabs, thumbnails, slideshow, props, errors
docx.spec.tstyping, formatting, styles, lists, colors, fonts, links, tables, images, find & replace, undo
xlsx.spec.tsvalues, formulas, merges (all / across / down / unmerge), formats, rows / columns, sheets, freeze
pptx.spec.tstext, shapes (insert, move, fill, arrange, rotate, duplicate, delete), images, slides, present
file-menu.spec.tsevery File entry: New, Open (conversions), Import, Save, Rename, Download as, Details, Print
permissions.spec.tsallowSave / allowDownload combinations, image download, Ctrl+S / Ctrl+P
color.spec.tscustom colors (system picker, recent list, persistence) in the 3 formats
embed.spec.ts<office-viewer> (shadow DOM, attributes, file) and mount()
collab.spec.tstwo editors on scripts/collab-server.ts: live edits, presence, File menu in a room

bun test only runs test/ (bunfig.toml); bun run test:e2e runs e2e/.

Site (GitHub Pages)

.github/workflows/pages.yml deploys on every push to main:

One-time setup: Settings → Pages → Source: GitHub Actions. Build locally: bun run build:playground && bun run build:site → dist-site/.

"Share live" needs a collaboration hub; the static demo has none, so pass one: ?collab=https://your-hub.example.

Release

CI (.github/workflows/ci.yml) runs typecheck, tests, build and npm pack --dry-run, and the browser tests, on every push to main and every PR.

To release, push a branch named after the version:

git switch -c v0.2.0 main
git push -u origin v0.2.0

.github/workflows/release.yml then runs CI, sets package.json version to match the branch (committing chore(release): v0.2.0 onto the branch if it differs), pushes the v0.2.0 tag, creates the GitHub release with the npm tarball attached, and publishes to npm with provenance.

npm auth is Trusted Publishing (OIDC): the workflow uses no token secret. The package trusts this repo and workflow, set up once with 2FA:

npm trust github @p10node/office-engine --repo p10node/office-engine --file release.yml --allow-publish

npm trust needs npm >= 11.15.0 and only works on a package that already exists on npm, so the package was bootstrapped with a placeholder 0.0.0 published by hand.