A client-only, statically-built web UI for every conversion and editing tool in the documents.js ecosystem — convert and edit docx, pptx, xlsx, odt, odp, ods, odg, csv, svg, pdf, and markdown documents entirely in the browser, with no server component.
Private (unpublished to npm); deployed as a static site to GitHub Pages.
Requires Node.js >=20 and pnpm 11.6.0.
pnpm install
pnpm dev # vite dev serverpnpm build # tsc (app + worker tsconfigs) then vite build -> dist/
pnpm preview # serve the production build locally
pnpm typecheck # tsc across tsconfig.json, tsconfig.worker.json, tsconfig.node.json
pnpm lint # eslint . --cache --max-warnings 0
pnpm test # vitest run --project unit
pnpm test:watch # vitest --project unit
pnpm test:coverage # vitest run --project unit --coverage
pnpm test:e2e # playwright test, against the real app served by `pnpm dev`To run a single unit test file, pass its path: pnpm exec vitest run --project unit src/shared/transferables.test.ts.
test:e2e's specs live in e2e/ and drive a real Chromium instance against the Vite dev server (playwright.config.ts's own webServer), covering the routed navigation and the drag-and-drop conversion flow that unit tests can't reach — the native file-picker path and the browser's own drag events. It runs in CI (test-e2e in ci.yml) but is not yet a required check.
The app is split into a main-thread UI and a Web Worker that holds the only code allowed to touch real document bytes:
src/workers/documents.worker.tsrunssrc/rpc/router.ts, an oRPC router that is the sole caller ofdocuments.js's conversion, metadata, and font functions, and the only place a sibling codec package is imported directly — currentlymarkdown-codecalone, though the import boundary below coversodf.js,ooxml.js, andpdf-codectoo, so any of them may only ever be reached from here.src/rpc/client.tsis how everything on the main thread reaches the worker — UI code, routes, hooks, and features never importdocuments.jsor its sibling packages directly (see Conventions below).src/routes/are TanStack Router file-based routes (src/routeTree.gen.tsis generated, not hand-edited).src/ports/+src/adapters/hold a small ports-and-adapters boundary for browser capabilities that need a fallback:FileAccessPorthas anativeFileAccessimplementation (File System Access API) and afallbackFileAccessimplementation for browsers without it, selected bycreateFileAccess.ts.src/db/dexie.tsis the local IndexedDB store (recent files, preferences) via Dexie.src/hooks/wrap the RPC client and Dexie store in React Query-friendly hooks consumed bysrc/routes/andsrc/ui/.
- UI code (
src/routes/,src/features/,src/hooks/,src/ui/) may not importdocuments.js's conversion/editor functions or any sibling package (odf.js,ooxml.js,pdf-codec,markdown-codec) directly — enforced by an ESLintno-restricted-importsrule. Onlysrc/workers/**may import them; everything else goes throughsrc/rpc/client.ts. A handful ofdocuments.jsexports with no non-Zod runtime dependencies (DocumentFormatSchema,DOCUMENT_FORMATS, and the plainContent*/Diagnostic/DocumentPayloadtypes) are allowlisted for direct import since they don't pull the conversion engine into the main bundle. - Uses this org's shared
@exadev/eslint-config(exadevRecommendedTypeChecked), which bans type assertions and@ts-expect-erroroutside test files and defaults its barrel-policy rule tobanned— this app has no public npm entry point, so that default is left as-is rather than overridden. - Route files under
src/routes/**/*.tsxare exempt fromexadev/barrel-policy,react-refresh/only-export-components, and@typescript-eslint/only-throw-error— all three collide with TanStack Router's own file-based-routing conventions (index-file naming, the exportedRoute'scomponent:property, andredirect()/notFound()as thrown control-flow objects) rather than being an avoidable choice in this codebase.
- The production build is served from
/<repo-name>/on GitHub Pages (/documents.js/today) but from/in local dev —baseinvite.config.tsderives the path segment fromGITHUB_REPOSITORYwhenCIis set, falling back to the name parsed from the git remote, so it never needs a hand-maintained literal and can't silently drift from the actual deploying repository. A build produced locally withCIunset will have the wrong base path if deployed as-is. - This is a PWA (
vite-plugin-pwa,autoUpdate). The worker bundle (by far the largest built asset) is deliberately excluded from the Workbox precache list and instead cached at runtime on first use via aCacheFirstrule, so it doesn't block install or blow the default precache size budget. src/workers/documents.worker.tsis a browser Web Worker, unrelated to Cloudflare Workers — this repo has nowranglerconfig and doesn't deploy to Cloudflare, unlike some sibling packages in the ecosystem.
Release, CI, and commit-message conventions are all workspace-wide, not package-local — see the monorepo root README for the mechanism. This package is private: true, so the orchestrator versions and changelogs it without publishing to npm; its GitHub Pages deploy runs after the release job, built from the post-release commit so a release's deployed site always matches its tagged version.
- documents.js ecosystem overview — how this app relates to the sibling packages it depends on.
MIT