Skip to content

Moon project ids — derive by default; the crate holds the bare name

Status: open/active

  • Moon resolves a project by two names: an id (directory name, or explicit id:) and an alias taken from the manifest — Cargo.toml’s package name for Rust, package.json’s for JS/TS. Both work in moon run.
  • Default: declare no id: and let it derive.
  • Cross-language twins: the Rust crate keeps the bare id, the TypeScript package takes -ts. This is not arbitrary — the crate’s Cargo alias is already the bare name, so a bare id makes its two names agree.
  • A renamed directory keeps its historical id explicitly.
  • A container colliding with a project inside it takes -root.
  • Known sharp edge: adding a Rust peer to a TypeScript-first package changes what the bare name means, and no naming convention can prevent it.
  1. Derive by default. A project with no conflict declares no id:. This is most projects.

  2. Twins: bare id to the crate, -ts to the TypeScript package. When one library exists in both tiers, the Rust crate takes (or keeps) the bare id and the TypeScript project declares id: '<name>-ts'.

  3. Renamed directories keep their id. When a directory moves but the project is the same project, declare the historical id explicitly so moon run and CI keep working.

  4. Colliding containers take -root. A solution or product root that would derive the same id as a project nested inside it takes the -root suffix; the inner project keeps the bare name.

Renaming an id breaks every moon run <id>:<task>, dependsOn edge and CI step that names it. Sweep them in the same change.

An id collision is a hard error — A project already exists with the identifier <id> — so ids are a flat global namespace that needs a convention. But the id is only half the resolution story, and the half that is easy to miss is the one that decides rule 2.

Moon also registers an alias from the package manifest. packages/rust/markdown-util resolves under both markdown-util-rs-style ids and the Cargo package name; moon project markdown-util reports Project: markdown-util-rs, Alias: markdown-util. Aliases do not collide across tiers here only because npm packages are scoped (@sksizer/markdown-util) while crates are bare (markdown-util).

That is why the crate takes the bare id. The crate’s alias claims the bare name whatever id you give it, so a suffixed crate id buys a second spelling and retires nothing. Giving the crate the bare id keeps its two names in agreement and leaves exactly one way to say each project.

The symmetric-looking alternative — suffix both sides — was tried and measurably worse. It does not make the bare name fail, because the Cargo alias still answers to it; it just adds a name.

The sharp edge, stated plainly because it cannot be designed away. When a Rust peer is added to a package that was TypeScript-first, the TypeScript project must yield the bare id, and afterwards moon run <name>:<task> runs the crate — silently, via the alias, with no error. A stale script or a habit then tests the wrong tier and reports green. Convention cannot fix this; only sweeping the references at rename time can.

Adding a project: write no id:. Check nothing else derives the same name.

Adding a Rust peer to an existing TypeScript package: give the TypeScript project id: '<name>-ts' in the same change, and sweep every reference to the old bare id — dependsOn edges, CI steps, justfile recipes, and prose that documents the command. Declare the cross-tier dependsOn by hand; moon infers no edge through a shared test corpus or a { workspace = true } dependency.

Moving or renaming a directory: add id: with the historical name before the move lands.

Registering a project no glob reaches: add it to .moon/workspace.yml under sources:, and beware the double-registration trap — a sources: entry plus a matching glob registers the directory twice with no error.

Checking what a name resolves to: moon project <name> prints both the id and the alias, which is the fastest way to confirm a bare name means what you think.

ProjectIdAliasRule
packages/rust/slugslug (derived)slug1
packages/rust/markdown-utilmarkdown-util (derived)markdown-util2
packages/ts/markdown-utilmarkdown-util-ts@sksizer/markdown-util2
apps/legacy-polishpolish3
solutions/ontologicalsdlc (via sources:)3
solutions/determineddetermined-root4
  • Suffixing the Rust crate’s id (markdown-util-rs) to “retire” the bare name. The Cargo alias still answers to markdown-util, so this adds a spelling without removing anything.
  • Renaming a directory and letting the id follow it, breaking moon run and CI for a change meant to be cosmetic.
  • Resolving a collision by renaming the directory rather than setting id:, which pushes the problem into the filesystem.
  • Assuming moon run <name>:test still means what it meant last month, in a package that has since gained a peer in the other tier.
  • Aliases are a toolchain feature: the rust and bun/javascript toolchains in .moon/toolchains.yml are what read the manifests. A project in a tier with no manifest has an id and no alias.
  • This standard describes moon 2.x behaviour as verified against this workspace with moon project <name>.

← Back to Standards