magus v0.4.2 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
7 min read

Nx

Warning

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:

  1. Dependencies. nx graph --file=<path> writes the project graph, whose dependencies map gives each project's in-workspace upstreams. Feed them to depends_on and magus's affected set starts matching Nx's.
  2. Per-target inputs and outputs. Nx 22.7+ has nx show target inputs <p>:<t> and nx show target outputs <p>:<t>, which report Nx's own resolved answer. Below that, nx show project --json carries inputs/outputs per target, with {projectRoot}/{workspaceRoot} tokens and namedInputs references for the spell to expand. Re-anchor both to the project directory before returning them.
  3. One call instead of N. nx show project per project is a subprocess per project. nx graph --file gets 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

nxworkspace-providermonorepotypescriptadoptionintegrationexperimental
Last updated (c92c1327)
Earlier changes on this page (5)

Full history ↗ · Blame source ↗

Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. See workspace.

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. See workspace.

Magusfile

The magusfile.buzz that declares a project's targets (as export funs) and binds its spells. See targets.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. See targets.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). See operations.

Spell

A language/runtime adapter (e.g. go, md) that maps generic targets onto a toolchain's real commands. See spells.

Module

A magus stdlib namespace a magusfile imports for host capabilities: filesystem, exec, vcs, and more. See the module reference.

Buzz

The language magusfiles are written in (the .buzz engine). See engines.

Cache

The content-addressed store magus consults before running a target, so unchanged work is skipped. See cache.

Affected

The set of projects touched by a change; magus affected <target> runs a target only over them. See affected.

Sandbox

The restricted filesystem and environment a target runs in, so builds stay reproducible and side-effect-free. See sandbox.

Daemon

The background magus host that owns shared state such as services and the warm knowledge graph. See daemon.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs - magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

Knowledge graph

The queryable graph of a workspace's spells, targets, docs, and code relationships; query it with magus query/explain/path. See knowledge.

Insight

The reports magus derives over the graph and history (hotspots, affinity, ownership, trend, volatility, unreferenced). See insight.

Hotspot

An insight lens: edit frequency times complexity, the prime refactoring targets. The project view heat-colors the dependency graph by churn; --files ranks individual files. See insight.

Ownership

An insight lens: author concentration - the primary author and their share, the distinct-author count (the bus factor), and abandonment. See insight.

Lease

One row of the lease ledger: a piece of work an orchestrating agent handed out, with its goal, the checkpoint it was cut against, and the paths it owns or must not touch. The ledger records; the agent guard is what reads those facts back when grading a write. See doctrine.

Conventions

Placeholders

Angle brackets mark a value you replace with your own - never type the brackets:

magus run <target>
magus completion <shell>    # e.g. bash, zsh, fish

<target>, <path>, <shell>, <name> and the like are stand-ins, not literal text.

Admonitions

Call-outs are rendered from GitHub-style alert blockquotes and carry a colored accent per type:

Note

Context worth knowing, but not a warning.

Warning

Something that can bite you if ignored.

The types are NOTE, TIP, IMPORTANT, WARNING, and CAUTION.