Monorepo structure and workspace tooling — polyglot apps/packages on moon
Status: open/accepted
Summary
Section titled “Summary”- A frontend application consuming the substrate fires D-0001-project-structure’s “additional tool consumes substrate” transition trigger. This decision fixes how the resulting monorepo is structured and tooled; D-0001-project-structure fixes where its trees live. See Context.
- The repo is polyglot — at least two ecosystems (TypeScript and Rust, n ≥ 2). The dashboard is TypeScript; the planned Tauri desktop app brings Rust. This assumption drives every choice below. See Why.
- Settled: an
apps/+packages/<ecosystem>/workspace; moon as the workspace runner; the JS/TS ecosystem unified on Bun as package manager. See Decision. - How each application builds and ships (e.g. into the plugin) is decided in that application’s own decision, not here. See Notes.
Context
Section titled “Context”At decision time the repo was single-package: Bun at the root running
TypeScript from source, an npm-managed Astro/Starlight site under site/
(now sites/df-docs/), and no monorepo tooling. Migrating the status
dashboard to a frontend framework needed its own buildable project, and
future work needed two things that layout could not express:
- Shared packages, per ecosystem. Libraries shared across applications (a UI kit, a substrate API client — generated or authored, a Rust crate) need a home that is neither an app nor the substrate, and a library is single-ecosystem.
- Polyglot applications. Beyond TypeScript, a Rust Tauri desktop app is anticipated. The workspace must be language-agnostic.
Adopting a frontend app is one of D-0001-project-structure’s named transition triggers (“an additional tool is adopted that consumes substrate independently — a web UI”). This decision tools that move.
| Fact | Source | Implication |
|---|---|---|
| Bun at root; runs from source, no build script | package.json; Bun runtime per D-0006-typescript-substrate | Built applications must ship without a consumer build step |
site/ was npm-managed | site/package-lock.json | Mixed package managers; unify on Bun for JS/TS |
site/ also carried a stale pnpm-lock.yaml | repo scan | Dual lockfiles; delete the pnpm one |
| Rust/Tauri app anticipated | this decision | Runner and layout must be polyglot, not JS-only |
Decision
Section titled “Decision”This decision settles the structure, the runner, and the package-manager unification.
Settled
Section titled “Settled”- Adopt an
apps/+packages/<ecosystem>/workspace (introduced by D-0001-project-structure).apps/<name>/holds deployable applications. An app stays flat (not partitioned by ecosystem) because a deployable may be polyglot internally — a Tauri app pairs a Rust core with a web frontend.packages/<ecosystem>/holds shared libraries, partitioned by language —packages/ts/,packages/rust/— because a library is single-ecosystem. Dependency direction is governed by S-0008-apps-consume-substrate-through-published-surfaces.
- Adopt moon as the workspace runner. moon’s language-agnostic WASM
toolchains cover both Bun and Rust, which the n ≥ 2 ecosystem assumption
requires.
justfilestays the human/skill command catalog;lefthookstays the git-hook driver; moon sits alongside both, owning the build graph and cache. - Unify the JS/TS ecosystem on Bun as the package manager. The Astro
site is Bun-managed and lives at
sites/df-docs/; the npm and stale pnpm lockfiles are gone. (Rust uses cargo under moon’s Rust toolchain.)
- Polyglot is the load-bearing assumption. The repo will host at least two ecosystems (TypeScript and Rust, n ≥ 2). Every choice follows from it: the runner must be language-agnostic, and shared libraries partition by language.
- The trigger fired; tool the move deliberately. D-0001-project-structure
deferred the workspace shape until a concrete consumer existed. It now does,
so the structure is fixed:
apps/+packages/<ecosystem>/beside the substrate. The substrate lift (plugin/lib/→ top-levelsrc/) stays separately sequenced — adding the new trees needs no substrate move. - Polyglot rules out the JS-only runners. Bun workspaces and Turborepo are excellent for JS/TS but cannot manage a Rust Tauri app, so neither can be the sole runner. With polyglot required, the field narrows to the language-agnostic graph runners — moon and Nx — and moon is chosen (lighter than Nx, first-class Bun + Rust toolchains, the author’s pick).
- Apps flat, packages by ecosystem. A deployable is not cleanly one
language (Tauri = Rust + web), so partitioning
apps/by ecosystem would be dishonest; a shared library is one language, sopackages/<ecosystem>/is the consistent, honest split that scales to n ecosystems.
Options considered
Section titled “Options considered”Workspace runner
Section titled “Workspace runner”Weighted for the requirements: polyglot (Rust/Tauri), shared packages, task
caching / affected-detection, Bun fit, and added complexity over the existing
justfile + lefthook + sdlc CLI stack.
| Runner | Polyglot (Rust) | Shared pkgs | Cache / affected | Bun fit | Added complexity | Verdict |
|---|---|---|---|---|---|---|
| moon | Yes (WASM toolchains; cargo/rust) | Yes (project graph) | Strong | Stable in v2, off the main trail | New layer over justfile/lefthook | Chosen |
| Nx | Partial (plugins; JS-centric) | Yes | Strong (+ remote) | Node-centric, heavier | High (opinionated, large) | Runner-up |
| Bun workspaces | No | Yes (JS only) | None | Native (zero new tool) | Minimal | PM only, not the runner |
| Turborepo | No (JS/TS only) | Yes | Strong | Works with Bun | Medium | Fails polyglot |
| pnpm + cargo | Split (two systems) | Yes (two systems) | None (needs Turbo) | Switches PM off Bun | Medium | Splits the toolchain |
The polyglot requirement eliminates the JS-only runners as the sole tool; between the two language-agnostic graph runners, moon is chosen. Bun workspaces still serve as the JS package manager under moon.
Caveat (research): moon v2 (Phobos, 2026) is recent, so the workspace pins
the proven 1.41.x line (versionConstraint: '>=1.41.0' in
.moon/workspace.yml). Bun, while stable, is off moon’s most-trodden
Node/pnpm trail — the Bun toolchain is declared explicitly, workspace-wide
in .moon/toolchain.yml.
Shared-package layout
Section titled “Shared-package layout”| Option | Shape | Verdict |
|---|---|---|
| Ecosystem-partitioned (chosen) | packages/ts/, packages/rust/ | Consistent, honest, scales to n ecosystems |
| Idiomatic per-language names | packages/ (npm) + crates/ (cargo) | Familiar per-language, but asymmetric top-level trees; less discoverable as n grows |
| Flat, mixed | packages/<name>/ regardless of language | Breaks down once two languages share the tree (tooling, lockfiles) |
packages/<ecosystem>/ is chosen: a single, uniform rule that reads honestly
at n ≥ 2. The idiomatic packages/ + crates/ split is the considered
alternative — more familiar to each language’s users, but it scatters shared
code across asymmetric top-level trees.
Consequences
Section titled “Consequences”- M-0006-monorepo-restructure-and-dashboard-migration decomposes the implementation into member tasks.
- Dependency direction follows the contract recorded in
S-0008-apps-consume-substrate-through-published-surfaces (
open/active): app code reaches the substrate through a clearly-defined API — typically an API library (generated or authored) inpackages/<ecosystem>/— never by importingplugin/lib/. - S-0001-co-locate-first-promote-when-shared treats
packages/<ecosystem>/as shared-library space, not a capability home. - Moving the Astro site to Bun removed the dual-lockfile mess (Astro is Bun-compatible).
Migration / rollout
Section titled “Migration / rollout”M-0006-monorepo-restructure-and-dashboard-migration carries the implementation. Landed state:
| Item | State |
|---|---|
Wire moon: .moon/workspace.yml + .moon/toolchain.yml (Bun + Rust), per-project moon.yml; keep justfile + lefthook | Landed |
| Migrate the Astro site to Bun | Landed — the site is Bun-managed at sites/df-docs/ |
Create the apps/ + packages/ts/ + packages/rust/ trees with workspace wiring | Landed — apps/dashboard plus three packages/ts/ libraries; packages/rust/ is an empty placeholder until the Tauri work |
Promote S-0008-apps-consume-substrate-through-published-surfaces to open/active once the first app lands | Landed |
Per-application work (the dashboard first) lands under its own decision and tasks — see Notes.
Open questions
Section titled “Open questions”- moon remote cache. Local cache is default; whether to enable remote cache is deferred until CI exists.
- This is the “tooling ADR” complement to D-0001-project-structure: D-0001
fixes where
apps/andpackages/<ecosystem>/live; this fixes how the workspace is tooled. - Per-application build and ship is out of scope here. How a given application is built and how its output reaches consumers (e.g. committed into the plugin) is decided in that application’s own decision. The first such application is the status dashboard (D-0013-dashboard-app).
- Research basis (2026): moon v2 “Phobos” WASM toolchains (Bun stable, Rust via cargo); Nx is Node-centric with partial polyglot support via plugins.
- Filename follows D-0002-entity-identifier-shape:
D-0012-prefix, slugmonorepo-tooling. There is nosdlc decision createverb; this file is hand-authored against the decision body template.