Nx
This integration is an experiment, not a supported feature. It exists to find out whether driving an Nx workspace through magus is useful at all. It ships in no magus release, setup is entirely manual, and it may change or be removed without notice. Expect rough edges.
An Nx repo already has a project model: nx.json, a project.json (or an
inferred target set) per project, and a dependency graph Nx computes from the
source. A workspace provider lets magus
adopt that model instead of asking the repo to carry a magusfile per project.
Nx keeps doing the work. Every target magus runs shells out to Nx, so nothing gets faster. What you gain is magus's view of the repo: a project graph you can query, an affected set you can compare against Nx's, and the ownership, churn and coverage that magus derives from running the work.
Nothing is committed to the repo
The shim is three untracked files at the repo root plus magus's cache directory,
and .git/info/exclude - which is per-clone and never committed - keeps them out
of git status:
magusfile.buzz wires the provider; the root project
spells/nx.buzz the provider spell, copied from the magus repository
magus.yaml sandbox env passthrough for NX_*/NODE_*/npm_config_* (see below)
.magus/ magus's cache (relocate with MAGUS_CACHE_DIR to keep the tree cleaner)
cat >> .git/info/exclude <<'EOF'
/magusfile.buzz
/spells/
/magus.yaml
/.magus/
EOF
A teammate who never runs magus sees an unchanged repo. Nothing lands inside a
project directory, which matters: Nx's default inputs include
{projectRoot}/**/* and Nx hashes untracked files, so a file dropped into every
project directory would change every task hash on your machine and stop you
hitting the shared cache.
Setup, by hand
There is no installer and no built-in. magus ships nothing for Nx: what runs is a Buzz spell you copy into your own repo and import from a local magusfile. Updating it means copying the file again.
The canonical source is
spells/experimental/nx/spell.buzz
in the magus repository. Every step below runs at the root of your Nx workspace.
1. Copy the spell in.
mkdir -p spells
curl -fsSL -o spells/nx.buzz \
https://raw.githubusercontent.com/egladman/magus/main/spells/experimental/nx/spell.buzz
From a clone of the magus repository instead:
cp <magus-checkout>/spells/experimental/nx/spell.buzz spells/nx.buzz
2. Write the magusfile that wires it.
import "magus";
import "spells/nx";
magus\workspace.provider(nx);
That is the whole file. It declares no targets of its own: every target in the workspace is an op of the provider spell, and an empty placeholder target would be a name, not a phase - it cannot fail and nothing depends on it.
3. Write magus.yaml with the sandbox env passthrough below. It is load-bearing only when the
sandbox is enabled.
4. Append the .git/info/exclude block
above, so none of the three files reaches git status.
5. Run magus. magus ls should list your Nx projects; if it does not,
the mapping caveats below are where
to start.
Editing your copy
The copy is yours: it is a workspace-local spell like any other, and magus invalidates the provider's cached answer when it changes.
ci chains every target the base spell already exposes. Trim the chain to
what every project in the repo actually declares - nx errors on a project
that lacks one of the chained targets - or, if flavors diverge, split ci
across separate provider spells, one per flavor.
The copy is the smallest version that works. What it leaves out, in the order worth adding:
- Dependencies.
nx graph --file=<path>writes the project graph, whosedependenciesmap gives each project's in-workspace upstreams. Feed them todepends_onand magus's affected set starts matching Nx's. - Per-target inputs and outputs. Nx 22.7+ has
nx show target inputs <p>:<t>andnx show target outputs <p>:<t>, which report Nx's own resolved answer. Below that,nx show project --jsoncarriesinputs/outputsper target, with{projectRoot}/{workspaceRoot}tokens andnamedInputsreferences for the spell to expand. Re-anchor both to the project directory before returning them. - One call instead of N.
nx show projectper project is a subprocess per project.nx graph --filegets everything in one shot.
Pinning the nx that runs
npm 7+'s npx prefers the workspace-local nx over anything global. But when
nx is not installed and stdin is not a TTY (or CI is set), npx assumes
--yes and silently downloads the latest nx from the registry and runs
that instead - the wrong version, none of the workspace's plugins.
--no-install turns that into a loud failure: npx errors instead of
guessing, which is why every npx nx call in the spell carries it.
A globally installed nx delegates to the workspace-local version the same
way a gradle wrapper delegates to the pinned gradle - so nx on PATH is also
fine when you control the machines that run it. Yarn PnP repos have no
node_modules/.bin for npx to resolve against, so npx cannot find the
workspace nx there at all: use yarn nx as the op's bin instead, in
every Command.
The mapping needs nx 16.3+. nx show projects --json and
nx show project --json, both load-bearing in list_projects, landed in that
release.
Provider env hygiene
The nx cloud-onboarding prompt is TTY-gated, but its non-interactive skip was
only fixed around nx 20.2, and the nx 21 TUI is interactive-only regardless
of TTY. NX_NO_CLOUD, NX_TUI, and NX_INTERACTIVE cover all three cases at
once - belt and suspenders costs nothing here, so list_projects sets them
unconditionally around its two proc\exec calls.
A one-shot nx command also starts the nx daemon, and that daemon outlives the
magus invocation that started it: it self-terminates after 3 hours idle,
keeps its state under .nx/workspace-data, and opens its socket in the OS
temp dir. CI disables it automatically. None of that needs magus
configuration; it is nx behaving normally, not a shim concern.
Sandbox env passthrough
When the sandbox is enabled, magus rebuilds a
child process's environment from an allowlist - HOME, USER, PATH,
LANG, LC_*, TZ, TERM, and a few more. NX_*, NODE_*, and
npm_config_* are not on it, so none of them reach nx unless the workspace
passes them through - silently different behavior from running nx bare in
a shell, where those variables are simply inherited.
sandbox:
env:
passthrough:
- "NX_*"
- "NODE_*"
- "npm_config_*"
This magus.yaml block joins the untracked shim files listed
above; the .git/info/exclude snippet
there already covers it.
MGS2003 reports every dropped
variable, so a missing passthrough entry fails loud - a broken nx run with a
stripped-var notice in the log - rather than silently behaving differently.
Three things to know about the mapping
The op relies on Nx inferring the project from the working directory. magus
runs an op in the project's own directory and an op's argv is fixed before any
project is chosen, so nx build in libs/foo is what stands in for
nx run libs/foo:build. That inference is Nx behavior, not magus's: verify it
on your repo before trusting the mapping, and if a target of yours does not
infer, have the op run a small wrapper script that resolves the project from
$PWD and calls nx run itself.
Outputs that leave the project directory cannot be declared. Nx's default is
dist/{projectRoot}, which sits at the workspace root, while a Project's
outputs are project-relative. Leave them undeclared rather than guessing. magus
then replays nothing for those targets, which beats replaying the wrong thing.
Nx runs dependent tasks itself. nx run p:build already builds what
dependsOn: ["^build"] names, so magus may schedule an upstream target that Nx
then replays from its own cache. Correct, and slower. Passing
--excludeTaskDependencies hands ordering to magus instead. Try that once the
affected sets agree, not before.
What to do with it
magus ls # the Nx project set, as magus projects
magus query kind=project # ... in the knowledge graph
magus explain project:libs/foo # its edges and blast radius
magus affected --plan --base=main # compare against nx show projects --affected
magus_insight lens=hotspots # churn x complexity, which Nx does not answer
magus refs <symbol> # cross-project symbol references (needs scip-typescript)
Start with the affected-set comparison. Run both over the same real commits and score where they disagree. Agreement is evidence the mapping is faithful; a disagreement is either a bug in the shim or a dependency Nx knows about that the provider has not reported yet.
Secrets
A provided project has no magusfile body, so magus\secret.read is out of
reach for it - there is no target function to call it from. A Command can
still declare secrets, and magus injects the resolved values into that one
child process at spawn, redacted from every captured output:
fun publish(target: Target) > Command {
return Command{bin = "npx", args = ["--no-install", "nx", "release", "publish"],
secrets = {"NPM_TOKEN": "NPM_TOKEN"}};
}
export fun mgs_listTargets() > any {
return {"build": build, "test": test, "lint": lint, "ci": ci, "publish": publish};
}
See Secrets for how references resolve and what the redaction guarantee covers.
See also
spells/experimental/nx/spell.buzz: the spell this guide copies, and the only source of truth for it- Workspace providers: the mechanism
- Secrets: how a provided project reaches a credential without a magusfile body
- Sandbox model: the env allowlist a provided project's ops run under
- MGS1023 and MGS1024: the two diagnostics a provider can raise