Moon project ids — derive by default; the crate holds the bare name
Status: open/active
Summary
Section titled “Summary”- Moon resolves a project by two names: an
id(directory name, or explicitid:) and an alias taken from the manifest —Cargo.toml’s package name for Rust,package.json’s for JS/TS. Both work inmoon 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.
-
Derive by default. A project with no conflict declares no
id:. This is most projects. -
Twins: bare id to the crate,
-tsto the TypeScript package. When one library exists in both tiers, the Rust crate takes (or keeps) the bare id and the TypeScript project declaresid: '<name>-ts'. -
Renamed directories keep their id. When a directory moves but the project is the same project, declare the historical id explicitly so
moon runand CI keep working. -
Colliding containers take
-root. A solution or product root that would derive the same id as a project nested inside it takes the-rootsuffix; 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.
How to apply
Section titled “How to apply”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.
Examples
Section titled “Examples”| Project | Id | Alias | Rule |
|---|---|---|---|
packages/rust/slug | slug (derived) | slug | 1 |
packages/rust/markdown-util | markdown-util (derived) | markdown-util | 2 |
packages/ts/markdown-util | markdown-util-ts | @sksizer/markdown-util | 2 |
apps/legacy-polish | polish | — | 3 |
solutions/ontological | sdlc (via sources:) | — | 3 |
solutions/determined | determined-root | — | 4 |
Anti-examples
Section titled “Anti-examples”- Suffixing the Rust crate’s id (
markdown-util-rs) to “retire” the bare name. The Cargo alias still answers tomarkdown-util, so this adds a spelling without removing anything. - Renaming a directory and letting the id follow it, breaking
moon runand 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>:teststill 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
rustandbun/javascripttoolchains in.moon/toolchains.ymlare 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>.