Monorepo architecture
One engine, three front-ends. The logic lives in packages/core,
pure TypeScript, zero DOM or Node dependencies. On top of that core sit
the web app (browser), the CLI (Node) and, in the future, a desktop app
(Electron wrapping the web build).
Structure
embroidery-converter/
├── packages/
│ ├── core/ ← ALL the logic. TS strict, ZERO DOM/Node deps.
│ │ ├── src/
│ │ │ ├── ir.ts Intermediate Representation
│ │ │ ├── embcompress.ts Greg-Hus expand()/compress()
│ │ │ ├── readers/
│ │ │ │ ├── vip.ts (HUS body in VIP wrapper)
│ │ │ │ ├── zhs.ts
│ │ │ │ ├── pes.ts dst.ts jef.ts vp3.ts exp.ts ...
│ │ │ ├── writers/
│ │ │ │ ├── zhs.ts ← the gem: no open writer has ever touched ZHS
│ │ │ │ ├── dst.ts pes.ts jef.ts vp3.ts exp.ts hus.ts vip.ts xxx.ts ...
│ │ │ ├── registry.ts extension → reader/writer
│ │ │ └── index.ts convert(bytes, from, to) → bytes + warnings
│ │ └── test/ Vitest, round-trip vs ../../fixtures
│ └── cli/ ← Node CLI, uses core
│ └── src/index.ts embconv in.vip out.zhs | batch folder
├── apps/
│ ├── web/ ← Astro. Drag-drop,Web Worker?, JSZip download.
│ │ └── src/ 100% client-side.
│ └── desktop/ ← (future) Electron. Loads apps/web/build and adds native batch on top.
└── fixtures/ VIP/ZHS pairs and other ground truth Shared IR
The Pattern is modeled on pyembroidery's
EmbPattern, but in strict TypeScript. See the IR
page for the exact type. It is the contract between every reader and every writer.
Stack
- pnpm workspaces, strict TypeScript everywhere.
- Astro 6 for
apps/web: a scrollytelling landing plus the/convertand/docspages. - tsup to build
coreandcli. - Vitest for tests, with fixtures taken from
/fixtures. - No backend. The web app is static, deployable to Netlify / Cloudflare Pages / any host.
Porting order (what got ported first)
- EmbCompress
expand()+ VIP reader + ZHS writer + IR. → VIP→ZHS in TS, with round-trip tests againstfixtures/. - Readers: ZHS, PES, DST, VP3. (XXX, JEF, EXP came later.)
- Writers: DST, PES (easy, well documented). Then JEF, VP3, HUS, VIP, XXX.
- Web app: drag-drop + single download + Studio (queue + panel).
- Batch CLI. (Desktop is future work.)
- Multi-color / trim ZHS writing — only after obtaining sample
.zhsfiles with trims (see ZHS).
Why a monorepo
Because the logic gets written exactly once. Add a format to the
core and, the same day, the web app, CLI and desktop know how
to use it. Updating the core means updating everything. It is the laziest,
most efficient pattern for a solo maintainer.