Skip to content

Monorepo structure and workspace tooling — polyglot apps/packages on moon

Status: open/accepted

  • 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.

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:

  1. 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.
  2. 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.

FactSourceImplication
Bun at root; runs from source, no build scriptpackage.json; Bun runtime per D-0006-typescript-substrateBuilt applications must ship without a consumer build step
site/ was npm-managedsite/package-lock.jsonMixed package managers; unify on Bun for JS/TS
site/ also carried a stale pnpm-lock.yamlrepo scanDual lockfiles; delete the pnpm one
Rust/Tauri app anticipatedthis decisionRunner and layout must be polyglot, not JS-only

This decision settles the structure, the runner, and the package-manager unification.

  1. 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 languagepackages/ts/, packages/rust/ — because a library is single-ecosystem. Dependency direction is governed by S-0008-apps-consume-substrate-through-published-surfaces.
  2. Adopt moon as the workspace runner. moon’s language-agnostic WASM toolchains cover both Bun and Rust, which the n ≥ 2 ecosystem assumption requires. justfile stays the human/skill command catalog; lefthook stays the git-hook driver; moon sits alongside both, owning the build graph and cache.
  3. 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-level src/) 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, so packages/<ecosystem>/ is the consistent, honest split that scales to n ecosystems.

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.

RunnerPolyglot (Rust)Shared pkgsCache / affectedBun fitAdded complexityVerdict
moonYes (WASM toolchains; cargo/rust)Yes (project graph)StrongStable in v2, off the main trailNew layer over justfile/lefthookChosen
NxPartial (plugins; JS-centric)YesStrong (+ remote)Node-centric, heavierHigh (opinionated, large)Runner-up
Bun workspacesNoYes (JS only)NoneNative (zero new tool)MinimalPM only, not the runner
TurborepoNo (JS/TS only)YesStrongWorks with BunMediumFails polyglot
pnpm + cargoSplit (two systems)Yes (two systems)None (needs Turbo)Switches PM off BunMediumSplits 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.

OptionShapeVerdict
Ecosystem-partitioned (chosen)packages/ts/, packages/rust/Consistent, honest, scales to n ecosystems
Idiomatic per-language namespackages/ (npm) + crates/ (cargo)Familiar per-language, but asymmetric top-level trees; less discoverable as n grows
Flat, mixedpackages/<name>/ regardless of languageBreaks 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.

M-0006-monorepo-restructure-and-dashboard-migration carries the implementation. Landed state:

ItemState
Wire moon: .moon/workspace.yml + .moon/toolchain.yml (Bun + Rust), per-project moon.yml; keep justfile + lefthookLanded
Migrate the Astro site to BunLanded — the site is Bun-managed at sites/df-docs/
Create the apps/ + packages/ts/ + packages/rust/ trees with workspace wiringLanded — 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 landsLanded

Per-application work (the dashboard first) lands under its own decision and tasks — see Notes.

  • 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/ and packages/<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, slug monorepo-tooling. There is no sdlc decision create verb; this file is hand-authored against the decision body template.

← Back to Decisions