Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f0d8324d6 | |||
| 523a22c4e4 | |||
| 68b8a1918d | |||
| a8ee2e4f47 | |||
| 769ff7dade | |||
| d000c83e2c |
@@ -1,88 +0,0 @@
|
||||
---
|
||||
name: performance-analyzer
|
||||
description: Use this agent during PR review to analyze the runtime performance of a PR's changes along two axes - (1) resource footprint (unnecessary CPU or memory usage) and (2) execution efficiency (does the code run quickly, avoid redundant work, and scale with workspace size). It reports a finding only when the cost is real on a hot path or scales with input size; micro-costs in cold paths are endorsed as sound so the reviewer knows performance was checked. Read-only on the worktree.
|
||||
model: inherit
|
||||
tools: Read, Grep, Glob, Bash
|
||||
---
|
||||
|
||||
# Performance Analyst
|
||||
|
||||
You evaluate the runtime cost of a PR's changes. Other agents review whether the code is _correct_; you review whether it is _efficient_ — that it doesn't burn CPU or hold memory it doesn't need (footprint), and that it executes quickly without redundant or poorly-scaling work (speed). Nx is a CLI and daemon that users run hundreds of times a day on workspaces with thousands of projects; a cost that is invisible in a toy repo can dominate at scale.
|
||||
|
||||
## Inputs (provided by the caller)
|
||||
|
||||
- `PR_NUMBER` — the PR under review in nrwl/nx
|
||||
- `WORKTREE_PATH` — an nrwl/nx checkout at the PR's HEAD
|
||||
- `BASE_REF` — the base branch (usually `master`)
|
||||
|
||||
If `.review-charter.md` exists in the worktree, read it first — it carries the maintainers' severity policy and calibrations, and they bound what you may report.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Read the diff.** `git -C "$WORKTREE_PATH" diff <BASE_REF>...HEAD`. Identify every changed code path that executes at runtime (skip tests, docs, fixtures).
|
||||
|
||||
2. **Classify each changed path as hot or cold.** This determines the bar for a finding:
|
||||
- **Hot:** anything on the critical path of every command — project-graph construction, hashing (`hasher`, `task-hasher`), the daemon and its watchers, task orchestration/scheduling, plugin workers, file-system traversal, `nx.json`/`project.json` parsing, caching, native (Rust) bindings and the JS that feeds them.
|
||||
- **Warm:** per-task or per-project work that runs once per invocation but scales with workspace size (per-project loops, executor startup, lockfile parsing).
|
||||
- **Cold:** generators, migrations, one-shot setup commands, error paths, `--help`/print paths.
|
||||
|
||||
3. **Hunt CPU waste (axis 1a).** In changed code, look for:
|
||||
- Work moved onto a hot path that previously ran lazily, once, or not at all (eager imports of heavy modules, computation hoisted out of a conditional).
|
||||
- Repeated recomputation of an invariant inside a loop — re-parsing, re-globbing, re-hashing, `JSON.parse(JSON.stringify(...))` cloning, regex compilation per iteration.
|
||||
- Accidental quadratic+ complexity: nested loops over projects/tasks/files, `Array.prototype.includes`/`find`/`indexOf` inside a loop over the same collection (should be a `Set`/`Map`), repeated `array.filter().map()` chains re-walking large arrays.
|
||||
- Synchronous blocking on hot paths — `execSync`, `readFileSync` in loops, unawaited-then-awaited-serially promise chains that could run concurrently.
|
||||
|
||||
4. **Hunt memory waste (axis 1b).** In changed code, look for:
|
||||
- Unbounded caches or maps that grow with workspace size and are never pruned (especially in the daemon, which is long-lived — a per-invocation leak in the CLI is bounded by process exit; the same leak in the daemon is not).
|
||||
- Retaining large structures longer than needed: full file contents kept when only a hash was needed, whole project-graph copies where a reference suffices, closures capturing large scopes in long-lived listeners.
|
||||
- Duplicating large collections (spread/clone of the project graph, file maps, or task graphs) when a mutation-free read would do.
|
||||
|
||||
5. **Hunt slow execution (axis 2).** In changed code, look for:
|
||||
- Serial awaits over independent work that could be `Promise.all`.
|
||||
- New file-system walks, process spawns, or network calls on paths that previously had none.
|
||||
- Debounce/polling intervals, sleeps, or retries added to interactive paths.
|
||||
- Work that could be pushed behind the daemon, memoized across calls, or delegated to the existing Rust layer instead of re-implemented in JS.
|
||||
|
||||
6. **Ground every suspect.** For each candidate finding, confirm the call frequency by reading callers (Grep for the function name; check whether it's invoked per-file, per-project, per-task, or once). Estimate the scale factor in a large workspace (e.g. "runs once per project per hash → 5,000× per command in a big monorepo"). A finding without a call-frequency argument is a hunch — drop it.
|
||||
|
||||
7. **Compare against the base when unsure.** If it's unclear whether a cost is new, read the same code on the base (`git -C "$WORKTREE_PATH" show <BASE_REF>:<path>`). Pre-existing cost the PR merely relocates is not a finding.
|
||||
|
||||
## Calibration
|
||||
|
||||
- **Hot path + scales with workspace size** → report (important; critical if it makes any command measurably slower at scale or the daemon leak is unbounded).
|
||||
- **Warm path + clearly avoidable waste** → report as important only when the fix is straightforward; otherwise endorse with a note.
|
||||
- **Cold path** → not a finding, no matter how inefficient. A generator that clones an array twice is fine.
|
||||
- Constant-factor micro-optimizations (`for` vs `forEach`, string concat style) are never findings.
|
||||
- Don't demand benchmarks — reason from call frequency and input scale, and say so.
|
||||
|
||||
## Verdicts (report exactly one)
|
||||
|
||||
- `PERFORMANCE_SOUND` — no real CPU, memory, or speed cost introduced. Write 2-4 sentences naming what you checked (which paths, hot/cold classification) so the reviewer knows performance was actually examined, not skipped.
|
||||
- `PERFORMANCE_CONCERN` — avoidable cost on a hot or warm path; a maintainer would ask for a change but the PR isn't wrong. Important-level. Include the call-frequency argument and a concrete cheaper shape.
|
||||
- `PERFORMANCE_REGRESSION` — the change makes any command measurably slower for real workspaces at scale (a single affected command is enough — a blowup confined to `nx release` is still a regression) or introduces unbounded memory growth (especially daemon-resident). Critical-level. Include the scaling argument.
|
||||
|
||||
When in doubt between `PERFORMANCE_SOUND` and `PERFORMANCE_CONCERN`, endorse — speculative performance feedback is noise.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Read-only.** Never modify the worktree, never check out other refs.
|
||||
- **Ground every claim** in call frequency and input scale, with file:line references.
|
||||
- Don't duplicate the other agents: correctness, style, tests, and error handling are not your beat — only runtime cost.
|
||||
|
||||
## Output format
|
||||
|
||||
```markdown
|
||||
### Performance analysis
|
||||
|
||||
**Verdict:** PERFORMANCE_SOUND | PERFORMANCE_CONCERN | PERFORMANCE_REGRESSION
|
||||
|
||||
**Paths examined:** <one line per changed runtime path: path — hot/warm/cold>
|
||||
|
||||
**Findings:** <for non-SOUND verdicts, one block per finding:>
|
||||
|
||||
- **<file:line>** — <the cost, the call-frequency/scale argument, and the concrete cheaper shape>
|
||||
|
||||
**CPU/memory footprint:** <one sentence: net effect on CPU and memory>
|
||||
|
||||
**Execution speed:** <one sentence: net effect on command latency>
|
||||
```
|
||||
@@ -118,43 +118,174 @@ Only attempt Level 1 for `LOCAL_TEST` or `LOCAL_NX_TARGET` scenarios. For other
|
||||
|
||||
Only attempt Level 2 when `RUN_LEVEL_2: true` is passed by the caller. Default is off — Level 2 takes ~10-15 minutes per invocation.
|
||||
|
||||
Level 2 publishes nx packages from the worktree at HEAD into a local verdaccio instance, then runs the external repro against that build **inside an isolated sandbox** — the clone/install/run is delegated to the **`reproduce-issue`** skill (Step 4), so untrusted repro code never executes on the host. This is **HEAD-only** — we do not re-publish at master for the baseline. The verdict becomes `PR_REPRO_PASSES` or `PR_REPRO_FAILS`, describing what happened _at the PR_ without trying to confirm the bug existed on master. That limitation is a deliberate trade for wall-clock time. If the caller needs a master baseline, they can run Level 2 twice manually.
|
||||
Level 2 publishes nx packages from the worktree at HEAD into a local verdaccio instance, then runs the external repro against that build. This is **HEAD-only** — we do not re-publish at master for the baseline. The verdict becomes `PR_REPRO_PASSES` or `PR_REPRO_FAILS`, describing what happened _at the PR_ without trying to confirm the bug existed on master. That limitation is a deliberate trade for wall-clock time. If the caller needs a master baseline, they can run Level 2 twice manually.
|
||||
|
||||
**Critical:** you MUST always clean up, even on failure. Use the exit-trap pattern described in step 9 below.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
1. The `nx-review-sandbox` image exists: `docker image inspect nx-review-sandbox:latest`. If not, run `setup-review-sandbox` — it carries the repo's full toolchain (node/java/dotnet/maven/rust via mise). **java + dotnet are required** because nx dogfoods the `@nx/dotnet` + `@nx/gradle` graph plugins; the build fails without them.
|
||||
2. Docker + the isolation runtime (gVisor on Linux / the Docker VM on macOS) + container networking are healthy — see the `reproduce-issue` skill's Preflight.
|
||||
1. Node 20+ and pnpm 10.28.2+ in PATH.
|
||||
2. Worktree has been built or can be built (`pnpm install` may need to run first).
|
||||
3. Port 4873 is free (or a different port is specified via `VERDACCIO_PORT`).
|
||||
4. Disk space for `dist/local-registry/storage` (~500MB-1GB).
|
||||
|
||||
If a prerequisite is missing, report and skip Level 2 — **never build or run on the host.**
|
||||
If any prerequisite is missing, report and skip Level 2 — do NOT attempt partial setup.
|
||||
|
||||
#### Steps 1–3: build the PR — INSIDE the sandbox (no host build)
|
||||
#### Step 1: Install dependencies in the worktree (if needed)
|
||||
|
||||
**Nothing builds on the host.** The build is done by the `reproduce-issue` skill's **PR-build mode** (`nx-build:<HEAD_SHA>`): the skill's sandbox container clones `nrwl/nx`, checks out that SHA, runs `mise install` + `pnpm install`, then builds + publishes nx to a verdaccio on **`localhost` inside the same container** — and reproduces against it. One container, localhost, no host verdaccio, no `WORKTREE_PATH` build.
|
||||
|
||||
So the old host Steps 1–3 are gone — the whole build → publish → reproduce happens in **Step 4's single skill call**. (`WORKTREE_PATH` is still used read-only by Levels 0–1; Level 2 never builds it.)
|
||||
|
||||
#### Step 4: Run the external repro IN THE SANDBOX (via the `reproduce-issue` skill)
|
||||
|
||||
**Do NOT clone, install, or run the untrusted repro on the host.** Its `install` scripts and repro command are arbitrary third-party code — delegate the whole thing to the **`reproduce-issue`** skill, which clones/creates → rewrites the nx deps → installs → runs the repro → classifies, **all inside an isolated container** (gVisor on Linux, the Docker VM on macOS), then destroys it. There is no host scratch dir.
|
||||
|
||||
```
|
||||
Skill(skill="reproduce-issue", args="""
|
||||
repro: repo:<REPO_URL> # EXTERNAL_REPO
|
||||
# -- or, for GENERATED_WORKSPACE:
|
||||
# repro: create:"--preset=<PRESET_FROM_ISSUE> <OTHER_FLAGS_FROM_ISSUE> --no-interactive --skipGit"
|
||||
nx-build: <HEAD_SHA> # PR-build mode: the skill builds THIS commit in-sandbox and reproduces against it
|
||||
command: <REPRO_COMMAND, verbatim from the issue>
|
||||
node-image: node:<major from the issue's Nx Report; default 22>
|
||||
expect: <the reported symptom, one line>
|
||||
setup: <files the issue says to create first, else omit>
|
||||
""")
|
||||
```bash
|
||||
cd "$WORKTREE_PATH"
|
||||
test -d node_modules || pnpm install --frozen-lockfile
|
||||
```
|
||||
|
||||
The skill returns a block whose `verdict:` is one of `PR_REPRO_PASSES | PR_REPRO_FAILS | PR_REPRO_FAILS_DIFFERENT | PR_REPRO_INCONCLUSIVE | SETUP_FAILED`, plus the exit code and an output tail. **Use that verdict directly** in your report — do not re-run anything on the host. If it returns `SETUP_FAILED`, note which step (clone / create / install) broke; do not fall back to the host.
|
||||
If `pnpm install` fails, stop and report. Do not try to continue.
|
||||
|
||||
**Registry — where the PR's nx comes from.** Target architecture: the PR is **built inside the sandbox** and served from a verdaccio on `localhost` in that same sandbox, so `nx-registry:http://localhost:<PORT>` — no host reachability, no listen-address change. That build (Steps 1–3) is being migrated off the host into the sandbox and needs the `nx-review-sandbox` image (`setup-review-sandbox`). Until the migration lands, Steps 1–3 still publish to a host verdaccio; status + the container-to-container handoff are tracked in `tmp/notes/review-in-container-plan.md`.
|
||||
#### Step 2: Start the local registry
|
||||
|
||||
Start verdaccio in the background. It must outlive the publish step but be killable on cleanup.
|
||||
|
||||
```bash
|
||||
cd "$WORKTREE_PATH"
|
||||
PORT=${VERDACCIO_PORT:-4873}
|
||||
pnpm nx local-registry @nx/nx-source --port=$PORT >/tmp/verdaccio-<PR_NUMBER>.log 2>&1 &
|
||||
VERDACCIO_PID=$!
|
||||
echo "$VERDACCIO_PID" > /tmp/verdaccio-<PR_NUMBER>.pid
|
||||
```
|
||||
|
||||
Wait up to 60s for the registry to accept connections:
|
||||
|
||||
```bash
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf http://localhost:$PORT/-/ping >/dev/null 2>&1; then break; fi
|
||||
sleep 1
|
||||
done
|
||||
curl -sf http://localhost:$PORT/-/ping >/dev/null || { echo "verdaccio failed to start"; exit 1; }
|
||||
```
|
||||
|
||||
If startup fails, kill the pid (if set), report, and exit.
|
||||
|
||||
#### Step 3: Publish nx to the local registry
|
||||
|
||||
```bash
|
||||
cd "$WORKTREE_PATH"
|
||||
NX_LOCAL_REGISTRY_PORT=$PORT \
|
||||
NX_VERBOSE_LOGGING=true \
|
||||
PUBLISHED_VERSION=${TARGET_PUBLISHED_VERSION:-major} \
|
||||
pnpm nx populate-local-registry-storage @nx/nx-source 2>&1 | tee /tmp/publish-<PR_NUMBER>.log
|
||||
```
|
||||
|
||||
This runs `pnpm nx-release --local ${PUBLISHED_VERSION}` internally — it builds all packages, versions them, and publishes to verdaccio. Takes 5-10 minutes. If it fails, capture the error and skip to cleanup.
|
||||
|
||||
After success, determine the exact published version:
|
||||
|
||||
```bash
|
||||
PUBLISHED_NX_VERSION=$(node -p "require('$WORKTREE_PATH/dist/packages/nx/package.json').version")
|
||||
echo "Published version: $PUBLISHED_NX_VERSION"
|
||||
```
|
||||
|
||||
#### Step 4: Prepare the scratch repro workspace
|
||||
|
||||
Use `/tmp/pr-<PR_NUMBER>-repro/` as the scratch dir — deliberately outside the nx repo, so the generated/cloned workspace's own nx root can't be mistaken for (or nested inside) the repo you're reviewing. Always wipe it at the start of this step:
|
||||
|
||||
```bash
|
||||
REPRO_DIR=/tmp/pr-<PR_NUMBER>-repro
|
||||
rm -rf "$REPRO_DIR"
|
||||
mkdir -p "$REPRO_DIR"
|
||||
```
|
||||
|
||||
**For `EXTERNAL_REPO`:**
|
||||
|
||||
1. Clone the repro repo:
|
||||
|
||||
```bash
|
||||
git clone --depth=1 <REPO_URL> "$REPRO_DIR"
|
||||
```
|
||||
|
||||
2. Rewrite all `nx` / `@nx/*` / `@nrwl/*` dependency versions in `$REPRO_DIR/package.json` to the exact `$PUBLISHED_NX_VERSION`:
|
||||
|
||||
```bash
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const p = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||
const v = process.argv[2];
|
||||
for (const section of ["dependencies", "devDependencies"]) {
|
||||
const deps = p[section] || {};
|
||||
for (const name of Object.keys(deps)) {
|
||||
if (name === "nx" || name.startsWith("@nx/") || name.startsWith("@nrwl/")) {
|
||||
deps[name] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(process.argv[1], JSON.stringify(p, null, 2) + "\n");
|
||||
' "$REPRO_DIR/package.json" "$PUBLISHED_NX_VERSION"
|
||||
```
|
||||
|
||||
3. If the repo has a lockfile, delete it (it's now stale after the rewrite):
|
||||
|
||||
```bash
|
||||
rm -f "$REPRO_DIR/package-lock.json" "$REPRO_DIR/pnpm-lock.yaml" "$REPRO_DIR/yarn.lock"
|
||||
```
|
||||
|
||||
4. Detect the package manager (prefer the same one used by the repo; fall back to npm):
|
||||
|
||||
```bash
|
||||
if test -f "$REPRO_DIR/pnpm-workspace.yaml"; then PM=pnpm;
|
||||
elif grep -q '"packageManager"' "$REPRO_DIR/package.json" 2>/dev/null; then
|
||||
PM=$(node -p "require('$REPRO_DIR/package.json').packageManager?.split('@')[0] || 'npm'")
|
||||
else PM=npm; fi
|
||||
```
|
||||
|
||||
5. Install with the registry env var pointing at verdaccio:
|
||||
|
||||
```bash
|
||||
cd "$REPRO_DIR"
|
||||
npm_config_registry=http://localhost:$PORT \
|
||||
BUN_CONFIG_REGISTRY=http://localhost:$PORT \
|
||||
YARN_REGISTRY=http://localhost:$PORT \
|
||||
$PM install 2>&1 | tee /tmp/install-<PR_NUMBER>.log
|
||||
```
|
||||
|
||||
If install fails, capture why. Common causes: lockfile not deleted, version mismatch the rewrite didn't catch (peer deps of sibling packages), missing `@nx/*` plugins in our publish set. Record and stop.
|
||||
|
||||
**For `GENERATED_WORKSPACE`:**
|
||||
|
||||
Instead of cloning, run `create-nx-workspace` pointed at the local registry:
|
||||
|
||||
```bash
|
||||
cd /tmp
|
||||
npm_config_registry=http://localhost:$PORT \
|
||||
BUN_CONFIG_REGISTRY=http://localhost:$PORT \
|
||||
YARN_REGISTRY=http://localhost:$PORT \
|
||||
npx --yes create-nx-workspace@$PUBLISHED_NX_VERSION \
|
||||
--name=pr-<PR_NUMBER>-repro \
|
||||
--preset=<PRESET_FROM_ISSUE> \
|
||||
--no-interactive \
|
||||
--skipGit \
|
||||
<OTHER_FLAGS_FROM_ISSUE> 2>&1 | tee /tmp/create-workspace-<PR_NUMBER>.log
|
||||
```
|
||||
|
||||
Pull `<PRESET_FROM_ISSUE>` and `<OTHER_FLAGS_FROM_ISSUE>` from the reported repro steps. If the issue doesn't specify a preset, use `apps` as a safe default and flag it in the report.
|
||||
|
||||
#### Step 5: Run the reported repro command
|
||||
|
||||
Extract the exact command from the issue body. If it references specific files to create first, create them in the scratch dir. Run with a timeout:
|
||||
|
||||
```bash
|
||||
cd "$REPRO_DIR"
|
||||
timeout 300 <REPRO_COMMAND> 2>&1 | tee /tmp/repro-<PR_NUMBER>.log
|
||||
REPRO_EXIT=$?
|
||||
```
|
||||
|
||||
Note: `timeout` may not be available on macOS by default — use `gtimeout` (from `brew install coreutils`) or emulate with a background kill. If neither is available, run without a timeout but watch carefully.
|
||||
|
||||
#### Step 6: Classify the outcome
|
||||
|
||||
Compare the output (`/tmp/repro-<PR_NUMBER>.log` + `REPRO_EXIT`) to the reported behavior:
|
||||
|
||||
- `PR_REPRO_PASSES` — the command succeeded, matching the PR's claimed fix. Verdict.
|
||||
- `PR_REPRO_FAILS_WITH_REPORTED_ERROR` — the command failed with the same error the issue describes. The PR did NOT fix the bug. Verdict `PR_REPRO_FAILS`.
|
||||
- `PR_REPRO_FAILS_DIFFERENT` — the command failed but with a different error. Flag for human review — may be env-specific or a related-but-different bug.
|
||||
- `PR_REPRO_INCONCLUSIVE` — output doesn't clearly match either direction. Capture the tail of the log and stop.
|
||||
|
||||
#### Step 7: Always clean up (cleanup trap)
|
||||
|
||||
@@ -172,15 +303,15 @@ fi
|
||||
# 2. Belt-and-suspenders: free the port even if pid is gone
|
||||
npx -y kill-port $PORT 2>/dev/null || true
|
||||
|
||||
# 3. No host scratch dir to remove — the repro lived and died inside the sandbox
|
||||
# (the reproduce-issue skill's container self-destroys via --rm). If a sandbox
|
||||
# container ever lingers, clear it with /sandbox-prune.
|
||||
# 3. Remove scratch workspace
|
||||
rm -rf /tmp/pr-<PR_NUMBER>-repro
|
||||
|
||||
# 4. Remove the ephemeral HOST logs only AFTER capturing their tails in your report.
|
||||
# Only verdaccio + publish run on the host now; the repro's own output comes
|
||||
# back inside the skill's returned block:
|
||||
# 4. Remove the ephemeral logs only AFTER capturing their tails in your report
|
||||
# Keep them on failure so the user can inspect them:
|
||||
# - /tmp/verdaccio-<PR_NUMBER>.log
|
||||
# - /tmp/publish-<PR_NUMBER>.log
|
||||
# - /tmp/install-<PR_NUMBER>.log (or create-workspace)
|
||||
# - /tmp/repro-<PR_NUMBER>.log
|
||||
```
|
||||
|
||||
Do NOT `rm -rf dist/local-registry/storage` in the nx worktree — that storage is shared state used by E2E tests. Leave it.
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
name: security-analyzer
|
||||
description: Use this agent during PR review to hunt injection-class vulnerabilities in a PR's changes - command injection, zip-slip and path traversal, prototype pollution, SSRF, credential leakage, and unsafe deserialization. It reports a finding only when untrusted data actually crosses a trust boundary into a dangerous sink; code that merely handles trusted workspace config is endorsed as sound so the reviewer knows security was checked. Read-only on the worktree.
|
||||
model: inherit
|
||||
tools: Read, Grep, Glob, Bash
|
||||
---
|
||||
|
||||
# Security Analyst
|
||||
|
||||
You evaluate whether a PR's changes introduce a security vulnerability. Other agents review correctness and cost; you review whether _untrusted data can reach a dangerous sink_. Your value is precision: nx is a build tool that by design executes arbitrary workspace code, so most "user input flows into exec" patterns are inside the trust boundary and are non-findings. A real finding shows data from OUTSIDE the workspace's trust boundary reaching a sink.
|
||||
|
||||
## Inputs (provided by the caller)
|
||||
|
||||
- `PR_NUMBER` — the PR under review in nrwl/nx
|
||||
- `WORKTREE_PATH` — an nrwl/nx checkout at the PR's HEAD
|
||||
- `BASE_REF` — the base branch (usually `master`)
|
||||
|
||||
If `.review-charter.md` exists in the worktree, read it first — it carries the maintainers' severity policy and calibrations, and they bound what you may report.
|
||||
|
||||
## The trust model (read this before flagging anything)
|
||||
|
||||
**Trusted** (attacker controlling these already owns the machine — never a finding):
|
||||
|
||||
- The workspace itself: `nx.json`, `project.json`, `package.json`, workspace source files, local plugins, executor/generator options, CLI arguments typed by the user.
|
||||
- Migration metadata and `migrations.json` — `nx migrate` runs migrations as arbitrary code by explicit design.
|
||||
- Installed node_modules content and the plugins nx loads from them.
|
||||
- The local nx cache directory and daemon socket (same-user filesystem access).
|
||||
|
||||
**Untrusted** (data crossing from here into a sink IS a finding):
|
||||
|
||||
- Network responses: npm registry metadata, GitHub/GitLab API responses, Nx Cloud / remote-cache payloads, anything fetched over HTTP.
|
||||
- Remote cache artifacts and any archive downloaded then extracted (tarballs, zips) — zip-slip territory.
|
||||
- Git data that originates from other people: commit messages, tag names, branch names, author fields (these flow into changelogs, release bodies, and shell commands).
|
||||
- Cloned reproduction repos or template repos (`create-nx-workspace` presets fetched from the network).
|
||||
- Environment content on shared CI only when the PR newly writes it somewhere privileged.
|
||||
|
||||
When in doubt whether a source is trusted, trace where it enters the process. "Comes from a function parameter" is not an answer — walk the callers to the origin.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Read the diff.** `git -C "$WORKTREE_PATH" diff <BASE_REF>...HEAD`. List every changed code path that touches a sink class below (skip tests, docs, fixtures).
|
||||
|
||||
2. **Hunt injection sinks.** In changed code, look for:
|
||||
- **Command injection:** string-built shell commands (`exec`/`execSync` with interpolation, `sh -c`, backticks in Rust `Command` misuse) where any argument originates from an untrusted source. Prefer-args-array (`execFile`, `spawn` without `shell: true`) with untrusted args is usually safe — flag only flag-injection (`--upload-pack`-style) when args reach git/npm/tar.
|
||||
- **Zip-slip / path traversal:** archive extraction (tar, zip, remote cache restore) writing entries without normalizing + containment-checking each path (`..` segments, absolute paths, symlink entries). Also path joins where an untrusted segment reaches `fs` writes/reads outside the intended root.
|
||||
- **Prototype pollution:** deep-merge/assign of untrusted JSON into objects later used for lookups or spread into options (`__proto__`, `constructor.prototype` keys).
|
||||
- **Unsafe deserialization / eval:** `eval`, `new Function`, `vm.runInContext`, YAML `load` (vs `safeLoad`-equivalent) on untrusted content.
|
||||
|
||||
3. **Hunt data-exposure sinks.** In changed code, look for:
|
||||
- **Credential leakage:** tokens/auth headers written to logs, error messages, changelogs, cache keys, or telemetry; secrets interpolated into URLs that get logged.
|
||||
- **SSRF / URL injection:** untrusted strings composed into fetch/axios URLs (registry endpoints, webhook targets) without scheme/host validation, especially when the response is then trusted.
|
||||
- **Injection into rendered output:** untrusted text (commit messages, issue titles) placed into HTML, markdown link targets, or terminal escape sequences without escaping.
|
||||
|
||||
4. **Trace every candidate end-to-end.** For each suspect, establish the full chain: origin (which untrusted source) → transformations (any sanitization on the way?) → sink (what damage). Read the actual sanitization code — do not assume a function named `sanitize`/`normalize` is sufficient; check it against the attack (e.g. does the path check run after resolving symlinks?).
|
||||
|
||||
5. **Compare against the base when unsure.** Pre-existing vulnerable patterns the PR merely moves or repeats are advisory context, not findings against this PR (note them in one line if serious). New-in-diff is your beat.
|
||||
|
||||
## Calibration
|
||||
|
||||
- **Untrusted source → sink, chain verified** → report (critical if exploitation is plausible in a default setup; important if it needs a nonstandard configuration).
|
||||
- **Sink fed only by trusted workspace data** → not a finding, even for `execSync` with interpolation. Nx executes workspace code by design.
|
||||
- **Hardening suggestions** (add validation "just in case", defense-in-depth without a traced attack path) → never a finding; the repo rejects speculative guards.
|
||||
- **Dependency CVEs / version bumps** → out of scope; dependabot's beat, not yours.
|
||||
- A finding without a complete origin-to-sink chain is a hunch — drop it.
|
||||
|
||||
## Verdicts (report exactly one)
|
||||
|
||||
- `SECURITY_SOUND` — no untrusted data reaches a dangerous sink in the changed code. Write 2-4 sentences naming what you checked (which sinks, which sources you traced) so the reviewer knows security was actually examined, not skipped.
|
||||
- `SECURITY_CONCERN` — a traced chain exists but exploitation requires a nonstandard configuration or an already-privileged position; a maintainer should fix it before merge. Important-level.
|
||||
- `SECURITY_VULNERABILITY` — a complete, plausible chain from an untrusted source to a dangerous sink in a default setup (e.g. a malicious remote-cache artifact escaping the extraction root). Critical-level. Include the concrete attack scenario.
|
||||
|
||||
When in doubt between `SECURITY_SOUND` and `SECURITY_CONCERN`, endorse — unfounded security flags erode trust in real ones.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Read-only.** Never modify the worktree, never check out other refs.
|
||||
- **Ground every claim** with the full origin → sink chain and file:line references at each hop.
|
||||
- Don't duplicate the other agents: correctness, style, tests, and performance are not your beat — only exploitability.
|
||||
- Report findings factually in the draft; do not write exploit code.
|
||||
|
||||
## Output format
|
||||
|
||||
```markdown
|
||||
### Security analysis
|
||||
|
||||
**Verdict:** SECURITY_SOUND | SECURITY_CONCERN | SECURITY_VULNERABILITY
|
||||
|
||||
**Sinks examined:** <one line per changed path that touches a sink class: path — sink class — source traced to>
|
||||
|
||||
**Findings:** <for non-SOUND verdicts, one block per finding:>
|
||||
|
||||
- **<file:line>** — <sink class; the origin → sink chain hop by hop; the attack scenario; the concrete fix>
|
||||
|
||||
**Trust-boundary summary:** <one sentence: which untrusted sources this PR newly touches, or "none — all inputs trusted workspace data">
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
This skill is disabled to encourage use of the AI prompt from the nx cloud sandboxing dashboard.
|
||||
|
||||
If you still need the original skill, you can reference it with @./claude/disabled-skills/diagnose-sandbox-report/SKILL.md directly.
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
name: diagnose-sandbox-report
|
||||
description: >
|
||||
Diagnose Nx sandbox violations from a sandbox report. Use when asked to
|
||||
"diagnose sandbox", "analyze sandbox report", "investigate sandbox violations",
|
||||
"check violations", when given a sandbox report JSON file or URL to investigate,
|
||||
or when the user pastes a staging.nx.app sandbox-report URL. Also trigger when
|
||||
discussing unexpected reads/writes in Nx task execution. Guides structured
|
||||
investigation of why tasks read/write undeclared files, determines root causes,
|
||||
and recommends fixes.
|
||||
argument-hint: '<sandbox-report.json or URL> [--filter <file|pattern|list>]'
|
||||
allowed-tools: Bash, Read, Grep, Glob
|
||||
---
|
||||
|
||||
# Diagnose Sandbox Report
|
||||
|
||||
## Overview
|
||||
|
||||
Sandbox violations occur when an Nx task reads files not declared as inputs or writes files not declared as outputs.
|
||||
|
||||
**Unexpected reads** are one of:
|
||||
|
||||
1. **Missing input** (most likely) — the process legitimately needs this file. Understand what the process does and why the access makes sense, then declare it as an input.
|
||||
2. **Potential sandboxing gap** (last resort) — the access is irrelevant to correctness and should be filtered/ignored by the sandbox. Only conclude this after exhausting every possibility for it being a missing input.
|
||||
|
||||
**Unexpected writes** follow the same logic:
|
||||
|
||||
1. **Missing output** (most likely) — the process legitimately produces this file.
|
||||
2. **Potential sandboxing gap** (last resort) — same as above.
|
||||
|
||||
The default assumption is that an unexpected access IS a missing declaration. The investigation's job is to understand WHY the process accesses the file — not to find reasons it shouldn't.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **NEVER read the sandbox report JSON directly** — these files are too large for the Read tool (50K+ tokens). Do NOT use `Read`, `cat`, `head`, `python3`, or `jq` on the raw report. All report parsing is handled by the script.
|
||||
2. **ALWAYS run the context-gathering script as the very first step** — no manual parsing, no ad-hoc python/jq on the report file. The script does everything deterministically.
|
||||
3. If the script fails, **report the error and stop**. Do not attempt manual parsing as a fallback.
|
||||
4. **Identify the inferring plugin BEFORE proposing any fix** — check `inference.plugin` in the script output or run `jq '.targets.<target>.metadata' <detail-file>`. Fixing the wrong plugin wastes entire investigation rounds.
|
||||
5. **Verify hypotheses empirically before committing to them** — see Principle 4 and the Phase 2 instrumentation guidance.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 0: Input
|
||||
|
||||
User provides one of:
|
||||
|
||||
- Path to a sandbox report JSON file
|
||||
- A URL to a sandbox report — pass it directly to the script, it handles downloading
|
||||
- A task ID + CIPE URL (fetch report via MCP if available)
|
||||
- Inline violation data
|
||||
|
||||
If a task ID is provided but no report, ask the user for the report file.
|
||||
|
||||
**Filtering**: Most invocations will focus on specific files, not the entire report. The user may specify:
|
||||
|
||||
- A single file: `e2e.log`
|
||||
- A comma-separated list: `apps/nx-cloud/e2e.log,apps/nx-cloud/build/client/assets/main.js`
|
||||
- A glob pattern: `*.tsbuildinfo`, `apps/nx-cloud/build/**`
|
||||
- A directory prefix: `apps/nx-cloud/build/client/assets`
|
||||
|
||||
When the user specifies files to focus on, pass them via `--filter` to the script. When they don't specify a filter and the report has many violations, summarize the groupings (by directory, extension) and ask which group(s) to investigate first rather than trying to investigate everything at once.
|
||||
|
||||
### Phase 1: Deterministic Pre-Processing
|
||||
|
||||
Run the context-gathering script **immediately** — this is the first tool call after reading the user's input.
|
||||
|
||||
Call it exactly as shown — do NOT append `2>&1` or `2>/dev/null` (the script manages its own stderr internally). Run in the **foreground** (no `run_in_background`) with a **3-minute timeout** — reports can be large and the script runs the task + multiple nx commands:
|
||||
|
||||
```bash
|
||||
npx tsx ${CLAUDE_SKILL_DIR}/scripts/gather-sandbox-context.ts <report.json or URL> [--filter <pattern>] [--workspace <path>]
|
||||
```
|
||||
|
||||
Pass `--filter` when the user wants to focus on specific files or patterns. The script filters violations before all downstream processing (grouping, validation, classification), so the output only contains relevant data.
|
||||
|
||||
The script produces two outputs:
|
||||
|
||||
**stdout** (~3-5KB compact brief) — everything needed to start investigating:
|
||||
|
||||
- `summary`: violation counts (total, filtered, confirmed vs undeclared)
|
||||
- `undeclaredFiles`: the actual file paths that are true violations
|
||||
- `grouping`: violations grouped by directory and extension
|
||||
- `commands`: processes with violations (pid, cmd, executable, arguments, counts) — no full file lists
|
||||
- `classificationSummary`: counts per category (cross-project, build artifacts, config files, etc.)
|
||||
- `crossProjectDependencyCheck`: whether cross-project file owners are in the task's dependency chain
|
||||
- `staleDeclarations`: grouped analysis of expectedInputsNotRead / expectedOutputsNotWritten
|
||||
- `dependentTasksOutputFiles`: extracted from target inputs config and named inputs — shows what dep output globs are declared (critical for cross-project violations)
|
||||
- `executorInfo`: executor name and resolved source path in `node_modules` — read this file to understand how the tool is invoked
|
||||
- `checkSample`: results of `--check` on up to 5 undeclared files (catches false positives early)
|
||||
- `inference` + `pluginRegistration`: plugin metadata
|
||||
- `verificationCommands`: pre-built `--check` commands with the correct task ref
|
||||
- `detailFile`: path to the full detail JSON
|
||||
|
||||
**detail file** (`/tmp/sandbox-diagnosis-detail-<project>-<target>.json`) — full data for drill-down. Structure:
|
||||
|
||||
- `processTree.processTree`: array of `{pid, cmd, parentPid}` entries
|
||||
- `processTree.processPidToCmd`: `{ "pid": "command string" }` map
|
||||
- `processTree.readsByPid`: `{ "pid": ["file1", "file2"] }` — violated reads grouped by PID
|
||||
- `processTree.writesByPid`: `{ "pid": ["file1", "file2"] }` — violated writes grouped by PID
|
||||
- `targetConfig`: full target configuration (executor, options, inputs, outputs, dependsOn)
|
||||
- `projectConfig`: full project configuration
|
||||
- `resolvedInputs`: `{ files: [...], depOutputs: [...], runtime: [...], environment: [...] }`
|
||||
- `resolvedOutputs`: `{ outputPaths: [...], expandedOutputs: [...] }`
|
||||
- `validation`: `{ reads: { confirmed: [...], undeclared: [...] }, writes: { ... } }`
|
||||
- `classification`: `{ reads: { crossProject, buildArtifacts, configFiles, ... }, writes: { ... } }`
|
||||
|
||||
Read the brief output — it has everything to start. Use `jq` on the detail file only when you need to drill into specific sections. When querying the detail file, use the structure above — do not guess the schema. Do NOT use Python, ad-hoc scripts, or the Read tool on the detail file — only `jq`.
|
||||
|
||||
For reports with many violations, use `--filter` to narrow scope. When investigating without a filter, use the `grouping` data to identify patterns and prioritize — don't try to trace every file individually.
|
||||
|
||||
If `summary.undeclaredReads` and `summary.undeclaredWrites` are both 0, all violations were resolved by the script's validation against resolved inputs/outputs. Report this to the user — no further investigation needed.
|
||||
|
||||
The `commands` array pre-parses each process — use `executable` and `arguments` to identify the tool without re-parsing `cmd`. When many files share the same root cause, group them under one finding using a glob pattern or count (e.g., "88 `.d.ts` files matching `packages/nx/dist/**/*.d.ts`").
|
||||
|
||||
### Phase 2: Command Analysis — the core investigation
|
||||
|
||||
**This is the most important phase.** The goal is to determine with 100% certainty why each process reads or writes each violated file. Do not classify violations from file names or paths alone — trace the actual causal chain from command → config → file access.
|
||||
|
||||
#### Step 1: Understand the command
|
||||
|
||||
The brief's `commands` array pre-parses each process. Use the `executable` and `arguments` fields directly — don't re-parse `cmd`. Identify:
|
||||
|
||||
- The tool (from `executable`)
|
||||
- The arguments (target files/dirs, config flags, extensions — from `arguments`)
|
||||
- The working directory (from executor options or project root)
|
||||
|
||||
#### Step 2: Trace why the command accesses each violated file
|
||||
|
||||
For each violated file, establish the **exact causal chain** that leads the command to read or write it. The approach is the same regardless of tool:
|
||||
|
||||
1. Identify the tool's config file (usually in the project root or workspace root)
|
||||
2. Read the config and trace file references: `includes`, `extends`, `presets`, entry points, plugins
|
||||
3. Follow the reference chain until you can explain exactly why the violated file is accessed
|
||||
|
||||
Common causal patterns:
|
||||
|
||||
- **Config chain walk-up**: tool reads config, config extends another, chain reaches the violated file (e.g., tsconfig `extends`, eslint config chain, jest preset chain)
|
||||
- **Directory traversal**: tool scans a directory for matching files and reads everything, including files it won't process (e.g., jest-haste-map scanning `.next/`, eslint reading `.d.ts` alongside `.ts`)
|
||||
- **Dependency resolution**: tool resolves imports/requires and follows the dependency graph to files outside the project (e.g., esbuild/vite/webpack resolving workspace packages to their dist outputs)
|
||||
- **Plugin/transformer loading**: tool loads plugins or transformers that read additional files (e.g., ts-jest loading tsconfig for TypeScript compilation)
|
||||
|
||||
For any tool, read its source code in `node_modules` to understand its file discovery behavior. Don't assume — trace the actual code.
|
||||
|
||||
**You must be able to explain the full path:** e.g., "eslint loads `.eslintrc.json` → configures `@typescript-eslint/parser` → parser resolves `parserOptions.project` → walks up to find `tsconfig.json` → reads it." If you can't trace the full path, keep investigating — do not guess.
|
||||
|
||||
**When theoretical analysis is inconclusive, verify empirically.** For difficult cases, instrument `node_modules` with interceptors to capture real stack traces. For example, patch `fs.readFileSync` in the tool's entry point to log stack traces when the violated file is accessed. A confirmed stack trace is worth more than multiple rounds of code reading.
|
||||
|
||||
#### Step 3: Confirm the violation with `--check`
|
||||
|
||||
**This step is mandatory — do not skip it.** The script already runs `--check` on a sample of up to 5 undeclared files (see `checkSample` in the brief). Review those results first — if the sample files are confirmed as inputs/outputs, the corresponding violations are false positives.
|
||||
|
||||
For files not in the sample, use the pre-generated commands from `verificationCommands` in the brief:
|
||||
|
||||
```bash
|
||||
npx nx show target inputs <project>:<target> --check <violated-read-files>
|
||||
npx nx show target outputs <project>:<target> --check <violated-write-files>
|
||||
```
|
||||
|
||||
If the commands fail because output files don't exist (e.g., the script's task run timed out), run the task first with `verificationCommands.runTask`.
|
||||
|
||||
If `--check` shows the file IS already an input/output, the violation is a false positive from the script's static analysis. If it confirms the file is NOT an input/output, proceed to classification.
|
||||
|
||||
#### Step 4: Classify
|
||||
|
||||
With the causal chain established and the violation confirmed, classify into one of these categories:
|
||||
|
||||
1. **Missing input/output** (most common) — the process legitimately needs this file. Understand why:
|
||||
- **Direct dependency** — the tool needs this file to do its job (e.g., tsc reads referenced tsconfigs, eslint loads config chain)
|
||||
- **Transitive dependency** — a config file references another file that references this one (e.g., jest preset → resolver → module). Trace the full chain.
|
||||
- **Directory traversal side effect** — the tool reads all files in a directory even if it only processes some (e.g., eslint reads `.d.ts` files while linting `.ts`). Still a legitimate access from the tool's perspective.
|
||||
|
||||
2. **Bad tool configuration** — the tool accesses a file it shouldn't because its scope is too broad. The fix is fixing the tool's config, NOT adding an input. Investigate:
|
||||
- Is the command targeting too broad a directory? (e.g., `eslint .` instead of `eslint src/`)
|
||||
- Is a config file missing ignore/exclude rules? (e.g., eslint processing a file type it should skip)
|
||||
- Is a plugin inferring a target for a project that doesn't match? (e.g., eslint target on a non-JS project)
|
||||
- Is an env var causing the tool to behave differently?
|
||||
|
||||
3. **Potential sandboxing gap** (last resort) — the access is genuinely irrelevant to correctness (PID files, temp sockets, dev server logs that no task consumes). Only conclude this after exhausting categories 1 and 2.
|
||||
|
||||
### Phase 3: Deep Investigation
|
||||
|
||||
For violations that aren't immediately obvious, investigate further:
|
||||
|
||||
#### If the target is inferred by a plugin
|
||||
|
||||
1. Identify which plugin from `inference.plugin` in the brief output, or `nx show project --json` metadata
|
||||
2. Read the plugin's `createNodesV2` implementation to understand inference logic
|
||||
3. Determine if this project should have this target at all
|
||||
4. Check if the plugin has `include`/`exclude` patterns in `nx.json` that should filter this project
|
||||
5. **Check for input override layers** — `project.json`, `package.json`, or `nx.json` `targetDefaults` may override plugin-inferred inputs, rendering plugin-level fixes invisible. Check all three before concluding a plugin fix is sufficient.
|
||||
|
||||
#### If violations come from a subprocess
|
||||
|
||||
1. Trace the process tree: which parent spawned the subprocess?
|
||||
2. Why does the subprocess exist? (dev server for e2e, worker thread, build tool subprocess)
|
||||
3. What environment does the subprocess inherit? (env vars, cwd)
|
||||
4. Does the subprocess access files in a different project's directory?
|
||||
|
||||
#### If violations involve config file reference chains
|
||||
|
||||
1. Read the config file (jest.config, tsconfig, .eslintrc)
|
||||
2. Trace all file references: `preset`, `extends`, `references`, `setupFiles`, `resolver`, `moduleNameMapper`, `transform`, etc.
|
||||
3. Recursively resolve references (preset → preset → files)
|
||||
4. Determine which referenced files are not declared as task inputs
|
||||
|
||||
#### If violations involve dependency task outputs
|
||||
|
||||
1. Check `dependsOn` to understand task dependency chain
|
||||
2. Check `dependentTasksOutputFiles` glob pattern — is it too narrow?
|
||||
3. Compare the glob against actual file types the tool reads from dependencies (e.g., `**/*.d.ts` missing `.tsbuildinfo`)
|
||||
|
||||
#### Generalizability analysis
|
||||
|
||||
After diagnosing the root cause, determine scope:
|
||||
|
||||
1. Is this violation specific to this project, or does it affect all projects using this tool/plugin?
|
||||
2. What conditions trigger it? (specific config, specific tool version, specific project structure)
|
||||
3. Should the fix be per-project (declarative input) or systemic (plugin improvement)?
|
||||
4. If the plugin can be made smarter to infer the correct inputs, that's preferable to manual declarations.
|
||||
|
||||
### Phase 4: Output
|
||||
|
||||
**You MUST present findings using the structured format below before proceeding to any implementation discussion.** Do not use free-form narrative — the structure ensures completeness and makes findings reviewable.
|
||||
|
||||
Present findings grouped by category:
|
||||
|
||||
```
|
||||
=== Sandbox Violation Diagnosis: {project}:{target} ===
|
||||
|
||||
## Summary
|
||||
Unexpected reads: N total → M validated as declared → K true violations
|
||||
Unexpected writes: N total → M validated as declared → K true violations
|
||||
|
||||
## Findings
|
||||
|
||||
### [MISSING INPUT] {short description}
|
||||
Files: {file list or pattern}
|
||||
Process: PID {pid} — {command}
|
||||
Why: {why the process legitimately needs this file}
|
||||
Scope: {project-specific or affects all projects using this tool/plugin}
|
||||
Fix: {where/how to add the input declaration — consider both declarative (add input) and systemic (improve plugin inference) options}
|
||||
|
||||
### [MISSING OUTPUT] {short description}
|
||||
Files: {file list or pattern}
|
||||
Process: PID {pid} — {command}
|
||||
Why: {why the process produces this file}
|
||||
Scope: {project-specific or affects all projects using this tool/plugin}
|
||||
Fix: {where/how to add the output declaration}
|
||||
|
||||
### [BAD TOOL CONFIG] {short description}
|
||||
Files: {file list or pattern}
|
||||
Process: PID {pid} — {command}
|
||||
Why: {why the tool accesses files it shouldn't — config too broad, missing ignore, etc.}
|
||||
Fix: {specific tool config change}
|
||||
|
||||
### [POTENTIAL SANDBOXING GAP] {short description}
|
||||
Files: {file list or pattern}
|
||||
Process: PID {pid} — {command}
|
||||
Why: {why this access is irrelevant to correctness}
|
||||
Evidence: {proof that categories 1-2 were exhausted}
|
||||
|
||||
### [INVESTIGATE] {short description}
|
||||
Files: {file list or pattern}
|
||||
Notes: {what's known, what needs more info}
|
||||
Question: {what to ask the user or team}
|
||||
|
||||
## Stale Declarations
|
||||
expectedInputsNotRead: {count and details if relevant}
|
||||
expectedOutputsNotWritten: {count and details if relevant}
|
||||
|
||||
## Verification Plan
|
||||
For each fix, provide the exact commands to verify:
|
||||
1. Run the task so output files exist on disk: `npx nx <target> <project> --skip-nx-cache`
|
||||
2. Check each violation file is now an input: `npx nx show target <project>:<target> inputs --check <space-separated files>`
|
||||
3. For plugin-level fixes: build the plugin, patch node_modules, then verify with steps 1-2
|
||||
```
|
||||
|
||||
## Principles
|
||||
|
||||
1. **Missing declaration is the default.** Most unexpected accesses are legitimate — the process needs the file, it just wasn't declared. Start from this assumption and investigate to understand WHY the access happens.
|
||||
2. **The command is the unit of analysis.** Don't classify files in isolation. Understand what the command does and whether each file access makes sense given that command's purpose.
|
||||
3. **Trace the full chain.** Plugin inference → target config → executor → command → file access. The root cause is often several layers removed from the symptom.
|
||||
4. **Empirical over theoretical.** When code analysis produces a hypothesis, verify it before acting. Instrument `node_modules`, capture stack traces, run with debug flags. Wrong theories waste entire investigation rounds.
|
||||
5. **Be thorough.** Read plugin source code, config files, executor implementations. Don't guess based on file names alone.
|
||||
6. **Potential sandboxing gaps are last resort.** Only conclude this after exhausting missing declaration and bad tool config. The access must be genuinely irrelevant to correctness.
|
||||
7. **Verify claims about Nx behavior in source code.** Any assertion about how Nx works must be traced to the actual implementation. Do not reason from theory or assumptions.
|
||||
8. **Prefer systemic fixes over per-project declarations.** If a plugin can be improved to infer correct inputs for all projects, that's better than adding manual input declarations to each project.
|
||||
|
||||
## Delegating to Subagents
|
||||
|
||||
When the investigation is complex and requires parallel research, you can delegate to subagents. Follow this pattern:
|
||||
|
||||
1. **Run the context-gathering script yourself first.** The brief output (~3-5KB) is the shared context all subagents need.
|
||||
2. **Include the brief output in each subagent prompt** along with the specific question to investigate. Subagents should NOT run the script again or try to parse the raw report.
|
||||
3. **Give subagents the detail file path** so they can `jq` specific sections (process tree, resolved inputs, etc.) without re-running the script.
|
||||
4. **Each subagent should answer one focused question**, e.g., "Why does PID 12345 (eslint) read `tsconfig.base.json`? Trace the full causal chain from the eslint config."
|
||||
5. **Subagents must still follow the skill principles** — trace full causal chains, verify empirically, use `--check`, don't guess from file names. Include these instructions in the subagent prompt.
|
||||
6. **Synthesize subagent results yourself** using the structured Phase 4 output format. Do not delegate the final classification.
|
||||
|
||||
## Reference
|
||||
|
||||
For the sandbox report data model and field definitions, see `references/data-model.md`.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Sandbox Report Data Model
|
||||
|
||||
## Raw Report Structure (JSON)
|
||||
|
||||
```typescript
|
||||
interface SandboxReport {
|
||||
taskId: string; // "project:target" or "project:target:configuration"
|
||||
sandboxReportId: string;
|
||||
inputs: string[]; // declared input patterns (globs or paths)
|
||||
outputs: string[]; // declared output patterns
|
||||
filesRead: FileAccessEntry[]; // all files actually read
|
||||
filesWritten: FileAccessEntry[]; // all files actually written
|
||||
unexpectedReads?: FileAccessEntry[]; // reads not matching any input pattern
|
||||
unexpectedWrites?: FileAccessEntry[]; // writes not matching any output pattern
|
||||
expectedInputsNotRead?: string[]; // declared inputs never accessed
|
||||
expectedOutputsNotWritten?: string[]; // declared outputs never written
|
||||
processTree?: ProcessTreeEntry[]; // process hierarchy with commands
|
||||
}
|
||||
|
||||
interface FileAccessEntry {
|
||||
path: string; // workspace-relative file path
|
||||
pid: number; // process ID that accessed the file
|
||||
}
|
||||
|
||||
interface ProcessTreeEntry {
|
||||
pid: number;
|
||||
cmd: string; // full command string
|
||||
parentPid?: number; // parent process (absent for root)
|
||||
}
|
||||
```
|
||||
|
||||
## Violation Computation
|
||||
|
||||
Violations are computed by `findUnexpectedFiles()` using `minimatch`:
|
||||
|
||||
- A file is "unexpected" if it does NOT match any declared pattern
|
||||
- Patterns without wildcards also match as directory prefixes (`pattern + '/'`)
|
||||
- If `unexpectedReads`/`unexpectedWrites` are pre-computed in the report, those are used directly
|
||||
|
||||
## Nx CLI Commands for Context
|
||||
|
||||
### `nx show target <project:target> --json`
|
||||
|
||||
Returns: executor, command, options (merged with configuration), inputs (configured, not resolved), outputs, dependsOn, cache, parallelism, configurations, metadata.
|
||||
|
||||
### `nx show target inputs <project:target> --json`
|
||||
|
||||
Returns resolved input files (requires files to exist on disk — task must have run):
|
||||
|
||||
```json
|
||||
{
|
||||
"files": ["workspace-relative paths..."],
|
||||
"runtime": ["node version checks..."],
|
||||
"environment": ["ENV_VAR_NAMES..."],
|
||||
"depOutputs": ["dependency output paths..."],
|
||||
"external": ["external package names..."]
|
||||
}
|
||||
```
|
||||
|
||||
### `nx show target inputs <project:target> --check <files...>`
|
||||
|
||||
Validates specific files against declared inputs. Exit code 0 = match, 1 = no match.
|
||||
Categories: `files`, `environment`, `runtime`, `external`, `depOutputs`.
|
||||
Also detects directory matches (directory containing N input files).
|
||||
|
||||
### `nx show target outputs <project:target> --json`
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"outputPaths": ["configured output paths..."],
|
||||
"expandedOutputs": ["glob-expanded actual paths..."],
|
||||
"unresolvedOutputs": ["{options.key} patterns that couldn't resolve..."]
|
||||
}
|
||||
```
|
||||
|
||||
### `nx show target outputs <project:target> --check <files...>`
|
||||
|
||||
Validates specific files against declared outputs. Same exit code behavior as inputs.
|
||||
|
||||
### `nx show project <project> --json`
|
||||
|
||||
Returns full project config. Key fields for sandbox analysis:
|
||||
|
||||
- `targets[name].metadata.plugin` — which plugin inferred the target
|
||||
- `targets[name].metadata.technologies` — what tech the target uses
|
||||
- `root` — project root directory
|
||||
|
||||
### `nx graph --view=tasks --targets=<target> --focus=<project> --print --file=stdout`
|
||||
|
||||
Returns task dependency graph with task IDs, dependencies, and roots.
|
||||
@@ -0,0 +1,846 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* gather-sandbox-context: Parse sandbox report + gather Nx task context
|
||||
* Produces structured JSON for the diagnose-sandbox-report skill
|
||||
*
|
||||
* Usage: npx tsx gather-sandbox-context.ts <report.json or URL> [--filter <pattern>] [--workspace <path>]
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import { resolve, basename, extname, dirname } from 'path';
|
||||
import { execSync, execFileSync } from 'child_process';
|
||||
import { minimatch } from 'minimatch';
|
||||
|
||||
// --- CLI argument parsing ---
|
||||
|
||||
interface Args {
|
||||
reportFile: string;
|
||||
filter: string | null;
|
||||
workspaceRoot: string;
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const args = process.argv.slice(2);
|
||||
let reportFile = '';
|
||||
let filter: string | null = null;
|
||||
let workspaceRoot = process.cwd();
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--filter':
|
||||
filter = args[++i];
|
||||
break;
|
||||
case '--workspace':
|
||||
workspaceRoot = args[++i];
|
||||
break;
|
||||
case '--help':
|
||||
case '-h':
|
||||
console.error(
|
||||
'Usage: gather-sandbox-context <report.json or URL> [--filter <pattern>] [--workspace <path>]'
|
||||
);
|
||||
process.exit(1);
|
||||
default:
|
||||
if (args[i].startsWith('-')) {
|
||||
console.error(`Unknown option: ${args[i]}`);
|
||||
process.exit(1);
|
||||
}
|
||||
reportFile = args[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (!reportFile) {
|
||||
console.error(
|
||||
'Usage: gather-sandbox-context <report.json or URL> [--filter <pattern>] [--workspace <path>]'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return { reportFile, filter, workspaceRoot };
|
||||
}
|
||||
|
||||
// --- Types ---
|
||||
|
||||
interface FileAccessEntry {
|
||||
path: string;
|
||||
pid: number;
|
||||
}
|
||||
|
||||
interface ProcessTreeEntry {
|
||||
pid: number;
|
||||
cmd: string;
|
||||
parentPid?: number;
|
||||
}
|
||||
|
||||
interface SandboxReport {
|
||||
taskId: string;
|
||||
unexpectedReads?: FileAccessEntry[];
|
||||
unexpectedWrites?: FileAccessEntry[];
|
||||
expectedInputsNotRead?: string[];
|
||||
expectedOutputsNotWritten?: string[];
|
||||
filesRead?: FileAccessEntry[];
|
||||
filesWritten?: FileAccessEntry[];
|
||||
processTree?: ProcessTreeEntry[];
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
function downloadUrl(url: string): string {
|
||||
const tmpPath = `/tmp/sandbox-report-${Date.now()}.json`;
|
||||
try {
|
||||
execFileSync('curl', ['-sL', '-o', tmpPath, url], { stdio: 'pipe' });
|
||||
} catch {
|
||||
console.error(`Error: Failed to download report from URL: ${url}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return tmpPath;
|
||||
}
|
||||
|
||||
function runNxCommand(
|
||||
args: string[],
|
||||
workspaceRoot: string,
|
||||
timeoutMs = 30000
|
||||
): string | null {
|
||||
try {
|
||||
return execFileSync('npx', ['nx', ...args], {
|
||||
cwd: workspaceRoot,
|
||||
timeout: timeoutMs,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeJsonParse<T>(str: string | null, fallback: T): T {
|
||||
if (!str) return fallback;
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function filterEntries(
|
||||
entries: FileAccessEntry[],
|
||||
filterStr: string | null
|
||||
): FileAccessEntry[] {
|
||||
if (!filterStr) return entries;
|
||||
|
||||
const patterns = filterStr.split(',').map((p) => p.trim());
|
||||
return entries.filter((entry) =>
|
||||
patterns.some((pattern) => {
|
||||
if (
|
||||
pattern.includes('*') ||
|
||||
pattern.includes('?') ||
|
||||
pattern.includes('[')
|
||||
) {
|
||||
// Glob pattern — if no slashes, match against basename
|
||||
if (!pattern.includes('/')) {
|
||||
return minimatch(basename(entry.path), pattern);
|
||||
}
|
||||
return minimatch(entry.path, pattern);
|
||||
}
|
||||
// Literal: exact match or directory prefix
|
||||
return entry.path === pattern || entry.path.startsWith(pattern + '/');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function groupByDirPrefix(
|
||||
paths: string[],
|
||||
depth = 3
|
||||
): { prefix: string; count: number }[] {
|
||||
const groups: Record<string, number> = {};
|
||||
for (const p of paths) {
|
||||
const prefix = p.split('/').slice(0, depth).join('/');
|
||||
groups[prefix] = (groups[prefix] || 0) + 1;
|
||||
}
|
||||
return Object.entries(groups)
|
||||
.map(([prefix, count]) => ({ prefix, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
function groupByExtension(paths: string[]): { ext: string; count: number }[] {
|
||||
const groups: Record<string, number> = {};
|
||||
for (const p of paths) {
|
||||
const ext = extname(p) || '(no ext)';
|
||||
groups[ext] = (groups[ext] || 0) + 1;
|
||||
}
|
||||
return Object.entries(groups)
|
||||
.map(([ext, count]) => ({ ext, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
function classifyFiles(
|
||||
undeclared: string[],
|
||||
projectRoot: string,
|
||||
projectRoots: Record<string, string>
|
||||
) {
|
||||
const projects = Object.entries(projectRoots).map(([project, root]) => ({
|
||||
project,
|
||||
root,
|
||||
}));
|
||||
|
||||
const isBuildArtifact = (f: string) =>
|
||||
f.startsWith('dist/') ||
|
||||
f.startsWith('build/') ||
|
||||
f.startsWith('out-tsc/') ||
|
||||
f.startsWith('.next/') ||
|
||||
f.includes('/node_modules/.cache/') ||
|
||||
f.endsWith('.tsbuildinfo') ||
|
||||
f.includes('/dist/') ||
|
||||
f.includes('/build/output/');
|
||||
|
||||
const configBasenames = new Set(['nx.json', 'project.json', 'package.json']);
|
||||
const configPrefixes = [
|
||||
'tsconfig',
|
||||
'jest.config',
|
||||
'jest.preset',
|
||||
'.eslintrc',
|
||||
'eslint.config',
|
||||
'playwright.config',
|
||||
'webpack.config',
|
||||
'vite.config',
|
||||
'babel.config',
|
||||
'.babelrc',
|
||||
'rollup.config',
|
||||
];
|
||||
const isConfigFile = (f: string) => {
|
||||
const b = basename(f);
|
||||
return (
|
||||
configBasenames.has(b) ||
|
||||
configPrefixes.some((prefix) => b.startsWith(prefix))
|
||||
);
|
||||
};
|
||||
|
||||
const isEnvFile = (f: string) => {
|
||||
const b = basename(f);
|
||||
return b === '.env' || b.startsWith('.env.');
|
||||
};
|
||||
|
||||
const classified = undeclared.map((f) => {
|
||||
const inProjectRoot = projectRoot !== '' && f.startsWith(projectRoot + '/');
|
||||
const owner = projects.find((p) => f.startsWith(p.root + '/'));
|
||||
return {
|
||||
path: f,
|
||||
inProjectRoot,
|
||||
ownerProject: owner?.project ?? null,
|
||||
isBuildArtifact: isBuildArtifact(f),
|
||||
isConfigFile: isConfigFile(f),
|
||||
isEnvFile: isEnvFile(f),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
crossProject: classified
|
||||
.filter((c) => !c.inProjectRoot)
|
||||
.map((c) => ({ path: c.path, owner: c.ownerProject })),
|
||||
buildArtifacts: classified
|
||||
.filter((c) => c.isBuildArtifact)
|
||||
.map((c) => c.path),
|
||||
configFiles: classified.filter((c) => c.isConfigFile).map((c) => c.path),
|
||||
envFiles: classified.filter((c) => c.isEnvFile).map((c) => c.path),
|
||||
inProjectRoot: classified.filter((c) => c.inProjectRoot).map((c) => c.path),
|
||||
outsideProjectRoot: classified
|
||||
.filter((c) => !c.inProjectRoot)
|
||||
.map((c) => c.path),
|
||||
total: undeclared.length,
|
||||
};
|
||||
}
|
||||
|
||||
function validateViolations(
|
||||
violations: string[],
|
||||
resolvedFiles: Set<string>
|
||||
): { confirmed: string[]; undeclared: string[] } {
|
||||
const confirmed: string[] = [];
|
||||
const undeclared: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const f of violations) {
|
||||
if (seen.has(f)) continue;
|
||||
seen.add(f);
|
||||
if (resolvedFiles.has(f)) {
|
||||
confirmed.push(f);
|
||||
} else {
|
||||
undeclared.push(f);
|
||||
}
|
||||
}
|
||||
return { confirmed, undeclared };
|
||||
}
|
||||
|
||||
function validateOutputViolations(
|
||||
violations: string[],
|
||||
resolvedOutputs: string[]
|
||||
): { confirmed: string[]; undeclared: string[] } {
|
||||
const outputSet = new Set(resolvedOutputs);
|
||||
const outputDirs = resolvedOutputs.map((o) => o + '/');
|
||||
const confirmed: string[] = [];
|
||||
const undeclared: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const f of violations) {
|
||||
if (seen.has(f)) continue;
|
||||
seen.add(f);
|
||||
if (outputSet.has(f) || outputDirs.some((d) => f.startsWith(d))) {
|
||||
confirmed.push(f);
|
||||
} else {
|
||||
undeclared.push(f);
|
||||
}
|
||||
}
|
||||
return { confirmed, undeclared };
|
||||
}
|
||||
|
||||
function extractCommands(
|
||||
processTree: ProcessTreeEntry[],
|
||||
readsByPid: Record<string, string[]>,
|
||||
writesByPid: Record<string, string[]>
|
||||
) {
|
||||
const pidToCmd: Record<string, string> = {};
|
||||
for (const entry of processTree) {
|
||||
pidToCmd[String(entry.pid)] = entry.cmd;
|
||||
}
|
||||
|
||||
return processTree
|
||||
.filter(
|
||||
(entry) =>
|
||||
(readsByPid[String(entry.pid)]?.length ?? 0) > 0 ||
|
||||
(writesByPid[String(entry.pid)]?.length ?? 0) > 0
|
||||
)
|
||||
.map((entry) => {
|
||||
const parts = entry.cmd.split(' ');
|
||||
const exe = parts[0].split('/').pop() ?? parts[0];
|
||||
return {
|
||||
pid: entry.pid,
|
||||
cmd: entry.cmd,
|
||||
parentPid: entry.parentPid ?? null,
|
||||
parentCmd: entry.parentPid
|
||||
? (pidToCmd[String(entry.parentPid)] ?? null)
|
||||
: null,
|
||||
unexpectedReadCount: readsByPid[String(entry.pid)]?.length ?? 0,
|
||||
unexpectedWriteCount: writesByPid[String(entry.pid)]?.length ?? 0,
|
||||
unexpectedReads: readsByPid[String(entry.pid)] ?? [],
|
||||
unexpectedWrites: writesByPid[String(entry.pid)] ?? [],
|
||||
executable: exe,
|
||||
arguments: parts.slice(1).join(' '),
|
||||
};
|
||||
})
|
||||
.sort(
|
||||
(a, b) =>
|
||||
b.unexpectedReadCount +
|
||||
b.unexpectedWriteCount -
|
||||
(a.unexpectedReadCount + a.unexpectedWriteCount)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveExecutorSource(
|
||||
executor: string | undefined,
|
||||
workspaceRoot: string
|
||||
): { executor: string; sourcePath: string } {
|
||||
if (
|
||||
!executor ||
|
||||
executor === 'null' ||
|
||||
executor.includes('nx:run-commands')
|
||||
) {
|
||||
return { executor: executor ?? '', sourcePath: '' };
|
||||
}
|
||||
|
||||
const lastColon = executor.lastIndexOf(':');
|
||||
const pkg = executor.substring(0, lastColon);
|
||||
const name = executor.substring(lastColon + 1);
|
||||
|
||||
try {
|
||||
const result = execFileSync(
|
||||
'node',
|
||||
[
|
||||
'-e',
|
||||
`
|
||||
try {
|
||||
const pkg = require('${pkg}/package.json');
|
||||
const executors = pkg.executors || pkg.builders;
|
||||
if (executors) {
|
||||
const p = require.resolve('${pkg}/' + executors);
|
||||
const dir = require('path').dirname(p);
|
||||
const json = require(p);
|
||||
const impl = json.executors?.['${name}']?.implementation ||
|
||||
json.builders?.['${name}']?.implementation;
|
||||
if (impl) console.log(require.resolve(dir + '/' + impl));
|
||||
}
|
||||
} catch(e) {}
|
||||
`,
|
||||
],
|
||||
{
|
||||
cwd: workspaceRoot,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 10000,
|
||||
}
|
||||
).trim();
|
||||
return { executor, sourcePath: result };
|
||||
} catch {
|
||||
return { executor, sourcePath: '' };
|
||||
}
|
||||
}
|
||||
|
||||
function extractDepTaskOutputFiles(
|
||||
targetConfig: any,
|
||||
workspaceRoot: string
|
||||
): { dependentTasksOutputFiles: any[]; namedInputs: string[] } {
|
||||
const inputs: any[] = targetConfig?.inputs ?? [];
|
||||
const depOutputs: any[] = [];
|
||||
const namedInputs: string[] = [];
|
||||
|
||||
for (const input of inputs) {
|
||||
if (
|
||||
typeof input === 'object' &&
|
||||
input !== null &&
|
||||
'dependentTasksOutputFiles' in input
|
||||
) {
|
||||
depOutputs.push({
|
||||
glob: input.dependentTasksOutputFiles,
|
||||
transitive: input.transitive ?? false,
|
||||
});
|
||||
} else if (
|
||||
typeof input === 'string' &&
|
||||
!input.startsWith('{') &&
|
||||
!input.startsWith('^') &&
|
||||
!input.includes('/') &&
|
||||
!input.includes('.')
|
||||
) {
|
||||
namedInputs.push(input);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve named inputs from nx.json
|
||||
const nxJsonPath = resolve(workspaceRoot, 'nx.json');
|
||||
if (existsSync(nxJsonPath) && namedInputs.length > 0) {
|
||||
try {
|
||||
const nxJson = JSON.parse(readFileSync(nxJsonPath, 'utf-8'));
|
||||
for (const name of namedInputs) {
|
||||
const namedDef = nxJson.namedInputs?.[name] ?? [];
|
||||
for (const entry of namedDef) {
|
||||
if (
|
||||
typeof entry === 'object' &&
|
||||
entry !== null &&
|
||||
'dependentTasksOutputFiles' in entry
|
||||
) {
|
||||
depOutputs.push({
|
||||
glob: entry.dependentTasksOutputFiles,
|
||||
transitive: entry.transitive ?? false,
|
||||
fromNamedInput: name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore nx.json parse errors
|
||||
}
|
||||
}
|
||||
|
||||
return { dependentTasksOutputFiles: depOutputs, namedInputs };
|
||||
}
|
||||
|
||||
function analyzeStaleDeclarations(
|
||||
expectedInputsNotRead: string[],
|
||||
expectedOutputsNotWritten: string[]
|
||||
) {
|
||||
const classifyPattern = (value: string) => {
|
||||
if (/[*{]/.test(value)) return 'glob';
|
||||
if (value.startsWith('^')) return 'depOutput';
|
||||
return 'file';
|
||||
};
|
||||
|
||||
const groupByType = (items: string[]) => {
|
||||
const groups: Record<string, string[]> = {};
|
||||
for (const item of items) {
|
||||
const type = classifyPattern(item);
|
||||
(groups[type] ??= []).push(item);
|
||||
}
|
||||
return Object.entries(groups).map(([type, values]) => ({
|
||||
type,
|
||||
count: values.length,
|
||||
samples: values.slice(0, 3),
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
expectedInputsNotRead: expectedInputsNotRead.length,
|
||||
expectedOutputsNotWritten: expectedOutputsNotWritten.length,
|
||||
staleInputsByType: groupByType(expectedInputsNotRead),
|
||||
staleOutputsByType: groupByType(expectedOutputsNotWritten),
|
||||
};
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs();
|
||||
let reportPath = args.reportFile;
|
||||
|
||||
// Handle URL inputs
|
||||
if (reportPath.startsWith('http')) {
|
||||
reportPath = downloadUrl(reportPath);
|
||||
}
|
||||
|
||||
if (!existsSync(reportPath)) {
|
||||
console.error(`Error: Report file not found: ${reportPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
reportPath = resolve(reportPath);
|
||||
process.chdir(args.workspaceRoot);
|
||||
|
||||
// Phase 1: Parse report (single read)
|
||||
let report: SandboxReport;
|
||||
try {
|
||||
report = JSON.parse(readFileSync(reportPath, 'utf-8'));
|
||||
} catch {
|
||||
console.error(`Error: Report file is not valid JSON: ${reportPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!report.taskId) {
|
||||
console.error('Error: Report file has no .taskId field');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [project, target, config] = report.taskId.split(':');
|
||||
const taskRef = config
|
||||
? `${project}:${target}:${config}`
|
||||
: `${project}:${target}`;
|
||||
|
||||
const unexpectedReads = report.unexpectedReads ?? [];
|
||||
const unexpectedWrites = report.unexpectedWrites ?? [];
|
||||
|
||||
// Apply filter
|
||||
const filteredReads = filterEntries(unexpectedReads, args.filter);
|
||||
const filteredWrites = filterEntries(unexpectedWrites, args.filter);
|
||||
|
||||
const readPaths = filteredReads.map((e) => e.path);
|
||||
const writePaths = filteredWrites.map((e) => e.path);
|
||||
|
||||
// Build pid → files maps
|
||||
const readsByPid: Record<string, string[]> = {};
|
||||
const writesByPid: Record<string, string[]> = {};
|
||||
for (const entry of filteredReads) {
|
||||
(readsByPid[String(entry.pid)] ??= []).push(entry.path);
|
||||
}
|
||||
for (const entry of filteredWrites) {
|
||||
(writesByPid[String(entry.pid)] ??= []).push(entry.path);
|
||||
}
|
||||
|
||||
// Phase 2: Gather Nx task context (run task + parallel nx commands)
|
||||
runNxCommand(['run', taskRef], args.workspaceRoot, 120000);
|
||||
|
||||
const [
|
||||
targetConfigStr,
|
||||
projectConfigStr,
|
||||
resolvedInputsStr,
|
||||
resolvedOutputsStr,
|
||||
graphResult,
|
||||
] = await Promise.all([
|
||||
runNxCommand(['show', 'target', taskRef, '--json'], args.workspaceRoot),
|
||||
runNxCommand(['show', 'project', project, '--json'], args.workspaceRoot),
|
||||
runNxCommand(
|
||||
['show', 'target', 'inputs', taskRef, '--json'],
|
||||
args.workspaceRoot
|
||||
),
|
||||
runNxCommand(
|
||||
['show', 'target', 'outputs', taskRef, '--json'],
|
||||
args.workspaceRoot
|
||||
),
|
||||
(() => {
|
||||
const graphPath = `/tmp/sandbox-project-graph-${Date.now()}.json`;
|
||||
runNxCommand(['graph', '--file', graphPath], args.workspaceRoot);
|
||||
try {
|
||||
return readFileSync(graphPath, 'utf-8');
|
||||
} catch {
|
||||
return '{"graph":{"nodes":{}}}';
|
||||
}
|
||||
})(),
|
||||
]);
|
||||
|
||||
const targetConfig = safeJsonParse(targetConfigStr, {} as any);
|
||||
const projectConfig = safeJsonParse(projectConfigStr, {} as any);
|
||||
const resolvedInputs = safeJsonParse(resolvedInputsStr, {} as any);
|
||||
const resolvedOutputs = safeJsonParse(resolvedOutputsStr, {} as any);
|
||||
const projectGraph = safeJsonParse(graphResult, {
|
||||
graph: { nodes: {} },
|
||||
} as any);
|
||||
|
||||
// Phase 3: Validate violations
|
||||
const resolvedInputFiles = new Set([
|
||||
...(resolvedInputs.files ?? []),
|
||||
...(resolvedInputs.depOutputs ?? []),
|
||||
]);
|
||||
const resolvedOutputFiles = [
|
||||
...(resolvedOutputs.outputPaths ?? []),
|
||||
...(resolvedOutputs.expandedOutputs ?? []),
|
||||
];
|
||||
|
||||
const checkInputs = validateViolations(readPaths, resolvedInputFiles);
|
||||
const checkOutputs = validateOutputViolations(
|
||||
writePaths,
|
||||
resolvedOutputFiles
|
||||
);
|
||||
|
||||
// Phase 3.5: Sample --check verification
|
||||
let checkSampleInputs: any = {};
|
||||
let checkSampleOutputs: any = {};
|
||||
const sampleReadFiles = checkInputs.undeclared.slice(0, 5);
|
||||
if (sampleReadFiles.length > 0) {
|
||||
const result = runNxCommand(
|
||||
[
|
||||
'show',
|
||||
'target',
|
||||
'inputs',
|
||||
taskRef,
|
||||
'--check',
|
||||
...sampleReadFiles,
|
||||
'--json',
|
||||
],
|
||||
args.workspaceRoot
|
||||
);
|
||||
checkSampleInputs = safeJsonParse(result, {});
|
||||
}
|
||||
const sampleWriteFiles = checkOutputs.undeclared.slice(0, 5);
|
||||
if (sampleWriteFiles.length > 0) {
|
||||
const result = runNxCommand(
|
||||
[
|
||||
'show',
|
||||
'target',
|
||||
'outputs',
|
||||
taskRef,
|
||||
'--check',
|
||||
...sampleWriteFiles,
|
||||
'--json',
|
||||
],
|
||||
args.workspaceRoot
|
||||
);
|
||||
checkSampleOutputs = safeJsonParse(result, {});
|
||||
}
|
||||
|
||||
// Phase 4: File classification
|
||||
const projectRoots: Record<string, string> = {};
|
||||
for (const [name, node] of Object.entries(projectGraph.graph?.nodes ?? {})) {
|
||||
projectRoots[name] = (node as any).data?.root ?? name;
|
||||
}
|
||||
const taskProjectRoot = projectRoots[project] ?? '';
|
||||
|
||||
const readClassification = classifyFiles(
|
||||
checkInputs.undeclared,
|
||||
taskProjectRoot,
|
||||
projectRoots
|
||||
);
|
||||
const writeClassification = classifyFiles(
|
||||
checkOutputs.undeclared,
|
||||
taskProjectRoot,
|
||||
projectRoots
|
||||
);
|
||||
|
||||
// Phase 5: Command extraction
|
||||
const processTree = report.processTree ?? [];
|
||||
const commands = extractCommands(processTree, readsByPid, writesByPid);
|
||||
|
||||
// Phase 6: Inference detection
|
||||
const targetMeta = projectConfig.targets?.[target]?.metadata ?? {};
|
||||
const inference = {
|
||||
isInferred: 'plugin' in targetMeta || 'technologies' in targetMeta,
|
||||
plugin: targetMeta.plugin ?? null,
|
||||
technologies: targetMeta.technologies ?? null,
|
||||
description: targetMeta.description ?? null,
|
||||
};
|
||||
|
||||
let pluginRegistration: any = {};
|
||||
const nxJsonPath = resolve(args.workspaceRoot, 'nx.json');
|
||||
if (inference.plugin && existsSync(nxJsonPath)) {
|
||||
try {
|
||||
const nxJson = JSON.parse(readFileSync(nxJsonPath, 'utf-8'));
|
||||
const plugins = (nxJson.plugins ?? []).map((p: any) =>
|
||||
typeof p === 'string' ? { plugin: p, options: {} } : p
|
||||
);
|
||||
pluginRegistration =
|
||||
plugins.find((p: any) => p.plugin === inference.plugin) ?? {};
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 6.5: dependentTasksOutputFiles + executor resolution
|
||||
const depTaskOutputs = extractDepTaskOutputFiles(
|
||||
targetConfig,
|
||||
args.workspaceRoot
|
||||
);
|
||||
const executorInfo = resolveExecutorSource(
|
||||
targetConfig.executor ?? targetConfig.command,
|
||||
args.workspaceRoot
|
||||
);
|
||||
|
||||
// Phase 7: Cross-project dependency check
|
||||
const dependsOn = (targetConfig.dependsOn ?? []).map((d: any) =>
|
||||
typeof d === 'string' ? d : (d.target ?? '')
|
||||
);
|
||||
const checkCrossProject = (classification: typeof readClassification) => {
|
||||
const owners = [
|
||||
...new Set(
|
||||
classification.crossProject
|
||||
.map((c) => c.owner)
|
||||
.filter((o): o is string => o !== null)
|
||||
),
|
||||
];
|
||||
return owners.map((owner) => ({
|
||||
project: owner,
|
||||
isDependency: dependsOn.some(
|
||||
(d: string) =>
|
||||
d === owner ||
|
||||
d === `${owner}:build` ||
|
||||
d === `^${owner}:build` ||
|
||||
d.includes(`^${owner}`)
|
||||
),
|
||||
files: classification.crossProject
|
||||
.filter((c) => c.owner === owner)
|
||||
.map((c) => c.path),
|
||||
}));
|
||||
};
|
||||
|
||||
const crossProjectDeps = {
|
||||
reads: checkCrossProject(readClassification),
|
||||
writes: checkCrossProject(writeClassification),
|
||||
};
|
||||
|
||||
// Phase 8: Stale declarations
|
||||
const staleDeclarations = analyzeStaleDeclarations(
|
||||
report.expectedInputsNotRead ?? [],
|
||||
report.expectedOutputsNotWritten ?? []
|
||||
);
|
||||
|
||||
// Assemble outputs
|
||||
const detailFile = `/tmp/sandbox-diagnosis-detail-${taskRef.replace(/[/:@]/g, '-')}.json`;
|
||||
|
||||
const detail = {
|
||||
processTree: {
|
||||
processTree,
|
||||
processPidToCmd: Object.fromEntries(
|
||||
processTree.map((e) => [String(e.pid), e.cmd])
|
||||
),
|
||||
readsByPid,
|
||||
writesByPid,
|
||||
},
|
||||
targetConfig,
|
||||
projectConfig,
|
||||
resolvedInputs,
|
||||
resolvedOutputs,
|
||||
validation: { reads: checkInputs, writes: checkOutputs },
|
||||
classification: { reads: readClassification, writes: writeClassification },
|
||||
report: {
|
||||
taskId: report.taskId,
|
||||
totalFilesRead: report.filesRead?.length ?? 0,
|
||||
totalFilesWritten: report.filesWritten?.length ?? 0,
|
||||
totalUnexpectedReads: unexpectedReads.length,
|
||||
totalUnexpectedWrites: unexpectedWrites.length,
|
||||
expectedInputsNotRead: report.expectedInputsNotRead ?? [],
|
||||
expectedOutputsNotWritten: report.expectedOutputsNotWritten ?? [],
|
||||
},
|
||||
commands,
|
||||
crossProjectDependencyCheck: crossProjectDeps,
|
||||
staleDeclarations,
|
||||
inference,
|
||||
pluginRegistration,
|
||||
dependentTasksOutputFiles: depTaskOutputs,
|
||||
executorInfo,
|
||||
};
|
||||
writeFileSync(detailFile, JSON.stringify(detail, null, 2));
|
||||
|
||||
// Brief to stdout
|
||||
const brief = {
|
||||
task: {
|
||||
ref: taskRef,
|
||||
project,
|
||||
target,
|
||||
configuration: config ?? null,
|
||||
projectRoot: taskProjectRoot,
|
||||
},
|
||||
summary: {
|
||||
unexpectedReads: unexpectedReads.length,
|
||||
unexpectedWrites: unexpectedWrites.length,
|
||||
filteredReads: filteredReads.length,
|
||||
filteredWrites: filteredWrites.length,
|
||||
filterApplied: args.filter !== null,
|
||||
filterPattern: args.filter,
|
||||
confirmedReads: checkInputs.confirmed.length,
|
||||
undeclaredReads: checkInputs.undeclared.length,
|
||||
confirmedWrites: checkOutputs.confirmed.length,
|
||||
undeclaredWrites: checkOutputs.undeclared.length,
|
||||
},
|
||||
undeclaredFiles: {
|
||||
reads: checkInputs.undeclared,
|
||||
writes: checkOutputs.undeclared,
|
||||
},
|
||||
grouping: {
|
||||
readsByDirectory: groupByDirPrefix(readPaths),
|
||||
writesByDirectory: groupByDirPrefix(writePaths),
|
||||
byExtension: {
|
||||
readsByExt: groupByExtension(readPaths),
|
||||
writesByExt: groupByExtension(writePaths),
|
||||
},
|
||||
},
|
||||
commands: commands.map(
|
||||
({
|
||||
pid,
|
||||
cmd,
|
||||
parentCmd,
|
||||
executable,
|
||||
arguments: args,
|
||||
unexpectedReadCount,
|
||||
unexpectedWriteCount,
|
||||
}) => ({
|
||||
pid,
|
||||
cmd,
|
||||
parentCmd,
|
||||
executable,
|
||||
arguments: args,
|
||||
unexpectedReadCount,
|
||||
unexpectedWriteCount,
|
||||
})
|
||||
),
|
||||
checkSample: {
|
||||
inputs: checkSampleInputs,
|
||||
outputs: checkSampleOutputs,
|
||||
},
|
||||
classificationSummary: {
|
||||
reads: {
|
||||
crossProject: readClassification.crossProject.length,
|
||||
buildArtifacts: readClassification.buildArtifacts.length,
|
||||
configFiles: readClassification.configFiles.length,
|
||||
envFiles: readClassification.envFiles.length,
|
||||
inProjectRoot: readClassification.inProjectRoot.length,
|
||||
outsideProjectRoot: readClassification.outsideProjectRoot.length,
|
||||
},
|
||||
writes: {
|
||||
crossProject: writeClassification.crossProject.length,
|
||||
buildArtifacts: writeClassification.buildArtifacts.length,
|
||||
configFiles: writeClassification.configFiles.length,
|
||||
envFiles: writeClassification.envFiles.length,
|
||||
inProjectRoot: writeClassification.inProjectRoot.length,
|
||||
outsideProjectRoot: writeClassification.outsideProjectRoot.length,
|
||||
},
|
||||
},
|
||||
crossProjectDependencyCheck: crossProjectDeps,
|
||||
staleDeclarations,
|
||||
dependentTasksOutputFiles: depTaskOutputs.dependentTasksOutputFiles,
|
||||
executorInfo,
|
||||
inference,
|
||||
pluginRegistration,
|
||||
verificationCommands: {
|
||||
checkInputs: `npx nx show target inputs ${taskRef} --check <files...>`,
|
||||
checkOutputs: `npx nx show target outputs ${taskRef} --check <files...>`,
|
||||
runTask: `npx nx run ${taskRef} --skip-nx-cache`,
|
||||
},
|
||||
detailFile,
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(brief, null, 2));
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(`Script failed: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -58,21 +58,9 @@ Run `nx run astro-docs:vale` to check the modified files.
|
||||
For ambiguous cases, suggest the fix and ask.
|
||||
- **suggestions** — mention them to the user but do not auto-fix.
|
||||
|
||||
### Step 2: Apply the guide by hand (Vale covers only a subset)
|
||||
### Step 2: Fix issues Vale doesn't catch
|
||||
|
||||
Vale enforces only the mechanical rules, and even the ones it implements are partial. A
|
||||
clean Vale run is **not** evidence the guide passed. Reading the guide is also not enough;
|
||||
you have to test your changed text against each rule.
|
||||
|
||||
For the diff you just made:
|
||||
|
||||
1. Run the guide's own "Pre-publish pass order" end to end, in order, on your changed text.
|
||||
Where a pass is a procedure (a grep, a count, a rewrite), perform it on your text rather
|
||||
than just confirming the pass exists.
|
||||
2. Then go through the rest of `STYLE_GUIDE.md` rule by rule, checking your changed lines
|
||||
against every rule the pass order did not already cover. A rule counts as checked only
|
||||
after you've read your actual sentences through it, not after you've read the rule.
|
||||
3. Fix every violation. If a rule genuinely doesn't apply to this change, move on.
|
||||
Read `astro-docs/STYLE_GUIDE.md` and check for that things that Vale may have missed.
|
||||
|
||||
### Handling false positives
|
||||
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
---
|
||||
name: reproduce-issue
|
||||
description: The single skill for reproducing an nx issue. Given a GitHub issue number (human entry) OR explicit repro parameters (agent entry), it runs the reproduction ENTIRELY inside an isolated Docker sandbox — gVisor on Linux, the Docker VM on macOS — so the untrusted repro's install scripts and commands never execute on the host, then reports whether it reproduces. Called by humans via "/reproduce-issue #N", "reproduce this bug", "does this reproduce", and by the reproduce-verifier agent (Level 2). Nothing lands on the host.
|
||||
allowed-tools: Read, Grep, Glob, Bash(uname *), Bash(gh issue view *), Bash(gh issue list *), Bash(docker run *), Bash(docker cp *), Bash(docker rm *), Bash(docker info *), Bash(docker pull *)
|
||||
---
|
||||
|
||||
# Reproduce an issue (sandboxed)
|
||||
|
||||
Reproduce an nx bug **entirely inside an isolated container** and report the outcome. The untrusted repro — its `install` (arbitrary postinstall scripts) and its repro command — runs only in the sandbox, never on the host. `--rm` destroys everything on exit; nothing touches the host filesystem.
|
||||
|
||||
This is the one reproduction engine in the repo. It has two front doors:
|
||||
|
||||
## Entry A — a GitHub issue (human: `/reproduce-issue <N>`)
|
||||
|
||||
1. Fetch the issue:
|
||||
```bash
|
||||
gh issue view <N> --repo nrwl/nx --json number,title,body,comments,labels
|
||||
```
|
||||
2. Extract from the body: the **repro repo URL** (or `create-nx-workspace` steps), the **exact command(s)** that show the bug, the **reported vs expected** behavior, and the **Nx Report** (nx version + Node version).
|
||||
3. Fill the parameters below and run the sandbox (default `nx-version` = whatever the issue reports / the repo pins; default registry = public npm).
|
||||
|
||||
## Entry B — explicit parameters (agent: reproduce-verifier Level 2)
|
||||
|
||||
The caller passes these directly:
|
||||
|
||||
- **`repro`** — `repo:<git-url>` (clone a public repo) OR `create:"<create-nx-workspace args>"`.
|
||||
- **`nx-version:<version>`** — install this **published** nx and rewrite the repro's `nx` / `@nx/*` / `@nrwl/*` deps to it. For reproducing against a released version.
|
||||
- **`nx-build:<git-ref>`** (PR-verification mode) — instead of a published version, **build nx from this `nrwl/nx` commit inside the sandbox** and reproduce against it. Uses the `nx-review-sandbox` image; the skill derives the version and serves it from a `localhost` verdaccio in the same container. Mutually exclusive with `nx-version`.
|
||||
- **`nx-registry:<url>`** (optional, `nx-version` mode only) — registry to install from. Default public npm.
|
||||
- **`command:"<repro-cmd>"`** — the command whose output/exit code decides the verdict.
|
||||
- **`node-image:<img>`** (optional) — base image matching the issue's Node (default `node:22`; public images are multi-arch → native on Apple Silicon).
|
||||
- **`expect:<reported symptom>`** (optional), **`setup:"<files/steps>"`** (optional) — files to create in the workspace first.
|
||||
|
||||
## Platform (where the sandbox boundary comes from)
|
||||
|
||||
Run `uname -s` once:
|
||||
|
||||
- **Linux** → add `--runtime=runsc` to `docker run` (gVisor is the sandbox).
|
||||
- **macOS (`Darwin`)** → **omit `--runtime=runsc`** (the Docker VM is the sandbox). Verify `docker info` works; if not, tell the user to `colima start` (or start Docker Desktop / OrbStack).
|
||||
|
||||
The command below shows the Linux form — on macOS drop `--runtime=runsc`, keep the rest.
|
||||
|
||||
## Preflight — check the environment, fail with a FIX (not a mystery)
|
||||
|
||||
Before running anything, verify prerequisites in order and **stop at the first miss, printing the one-line fix**. Most misses point at the `setup-review-sandbox` skill, which installs/builds everything.
|
||||
|
||||
1. **Docker is up:**
|
||||
|
||||
```bash
|
||||
docker info >/dev/null 2>&1 && echo up || echo MISSING
|
||||
```
|
||||
|
||||
Miss → Linux: `sudo systemctl start docker`. macOS: `colima start` (or open Docker Desktop). Or run `setup-review-sandbox`.
|
||||
|
||||
2. **Container networking works** (the check that would have caught the `veth` breakage):
|
||||
|
||||
```bash
|
||||
docker run --rm --network none alpine true # A: is the sandbox itself OK?
|
||||
docker run --rm alpine true # B: is networking OK?
|
||||
```
|
||||
|
||||
If **A passes but B fails** with `veth ... operation not supported` → networking is broken (usually a kernel update left `veth` unloadable). Fix: `sudo modprobe veth`; if that errors with a BTF/version mismatch, **reboot** (the running kernel no longer matches its modules).
|
||||
|
||||
3. **Isolation runtime (platform-specific):**
|
||||
- **Linux** — gVisor registered as a Docker runtime?
|
||||
```bash
|
||||
docker info --format '{{range $k,$v := .Runtimes}}{{$k}} {{end}}' | grep -q runsc && echo ok || echo MISSING
|
||||
```
|
||||
Miss → run `setup-review-sandbox` (installs + registers `runsc`).
|
||||
- **macOS** — the Docker VM (Colima / Docker Desktop) _is_ the sandbox; step 1 already covered it. No `runsc`.
|
||||
|
||||
4. **(PR-build mode ONLY) the toolchain image exists:**
|
||||
```bash
|
||||
docker image inspect nx-review-sandbox:latest >/dev/null 2>&1 && echo ok || echo MISSING
|
||||
```
|
||||
Miss → run `setup-review-sandbox` (builds it from `tools/review-sandbox/Dockerfile`). **Skip this check** when reproducing against a _published_ nx version — that path needs only steps 1–3 and a public `node` image.
|
||||
|
||||
If all needed checks pass, proceed.
|
||||
|
||||
## Safety rails (do NOT break these)
|
||||
|
||||
- The untrusted repro runs **only** in the container. **Never `-v` a host path in.** nx comes from a registry (or `docker cp`-ed tarballs), never a mount.
|
||||
- Always pass: `--cap-drop ALL`, `--security-opt no-new-privileges`, `--memory 4g --cpus 4 --pids-limit 2048`, `--rm`; plus `--runtime=runsc` on Linux.
|
||||
- Network is ON (clone + install need it). gVisor still protects the host kernel; on macOS the VM protects the host.
|
||||
- One `docker` command per Bash call. (Chaining inside the container's `bash -c '...'` is one host command, which is fine.)
|
||||
|
||||
## Run
|
||||
|
||||
Detect platform, then a single host command does clone/create → dep-rewrite → install → repro, all inside the sandbox:
|
||||
|
||||
```bash
|
||||
# RUNTIME="--runtime=runsc" on Linux
|
||||
# RUNTIME="" on macOS
|
||||
docker run --rm $RUNTIME \
|
||||
--cap-drop ALL --security-opt no-new-privileges \
|
||||
--memory 4g --cpus 4 --pids-limit 2048 \
|
||||
node:22 bash -c '
|
||||
set -e
|
||||
git clone --depth 1 <GIT_URL> /repro # repo: form
|
||||
# -- or -- npx --yes create-nx-workspace <ARGS> --directory /repro # create: form
|
||||
cd /repro
|
||||
|
||||
node -e '"'"'
|
||||
const fs=require("fs"),p=JSON.parse(fs.readFileSync("package.json","utf8")),v=process.argv[1];
|
||||
for (const s of ["dependencies","devDependencies"]) for (const n of Object.keys(p[s]||{}))
|
||||
if (n==="nx"||n.startsWith("@nx/")||n.startsWith("@nrwl/")) p[s][n]=v;
|
||||
fs.writeFileSync("package.json", JSON.stringify(p,null,2)+"\n");
|
||||
'"'"' <NX_VERSION>
|
||||
|
||||
rm -f package-lock.json pnpm-lock.yaml yarn.lock
|
||||
PM=npm; test -f pnpm-workspace.yaml && PM=pnpm
|
||||
npm i -g pnpm@11 >/dev/null 2>&1 || true
|
||||
npm_config_registry=<NX_REGISTRY> $PM install
|
||||
|
||||
( timeout 300 <REPRO_COMMAND> ); echo "REPRO_EXIT=$?"
|
||||
echo "kernel: $(uname -r)"
|
||||
'
|
||||
```
|
||||
|
||||
Substitute `<GIT_URL>`/`<ARGS>`, `<NX_VERSION>`, `<NX_REGISTRY>` (default `https://registry.npmjs.org`), and `<REPRO_COMMAND>`.
|
||||
|
||||
## Classify + report
|
||||
|
||||
Compare output and `REPRO_EXIT` against the reported symptom, and return this block (verdicts match the reproduce-verifier's Level 2 vocabulary):
|
||||
|
||||
```
|
||||
repro: <repo-url | create-nx-workspace ...>
|
||||
nx-version: <version> (registry: <url>)
|
||||
command: <verbatim>
|
||||
exit code: <N>
|
||||
verdict: <PR_REPRO_PASSES | PR_REPRO_FAILS | PR_REPRO_FAILS_DIFFERENT | PR_REPRO_INCONCLUSIVE | SETUP_FAILED>
|
||||
output (tail ~20 lines):
|
||||
<...>
|
||||
```
|
||||
|
||||
- succeeded (matches the claimed fix) → `PR_REPRO_PASSES`
|
||||
- failed with the reported error → `PR_REPRO_FAILS`
|
||||
- failed with a _different_ error → `PR_REPRO_FAILS_DIFFERENT` (flag for human)
|
||||
- unclear → `PR_REPRO_INCONCLUSIVE`
|
||||
- clone/create/install broke before the repro ran → `SETUP_FAILED` (say which step + tail)
|
||||
|
||||
(For a human `/reproduce-issue` run against a released version, "reproduced" vs "did not reproduce" is the plain-language answer; the verdict vocab above is for the agent.)
|
||||
|
||||
## PR-build mode — build nx from source in the sandbox (`nx-build`)
|
||||
|
||||
When `nx-build:<git-ref>` is given, do everything in **one `nx-review-sandbox` container** (it carries the mise toolchain incl. **java + dotnet**, required by nx's `@nx/dotnet`/`@nx/gradle` graph plugins). One container, `localhost` throughout — no host build, no host verdaccio, no `host.docker.internal`, no listen-address change:
|
||||
|
||||
```bash
|
||||
# RUNTIME="--runtime=runsc" on Linux, "" on macOS
|
||||
docker run --rm $RUNTIME \
|
||||
--cap-drop ALL --security-opt no-new-privileges \
|
||||
--memory 20g --cpus 6 --pids-limit 8192 --tmpfs /work:rw,exec,size=16g \
|
||||
-e CI=true -e NX_DAEMON=false \
|
||||
nx-review-sandbox:latest bash -c '
|
||||
set -e
|
||||
# 1. build nx from the PR commit
|
||||
cd /work
|
||||
git clone --filter=blob:none https://github.com/nrwl/nx nx && cd nx
|
||||
git checkout <GIT_REF>
|
||||
mise install && pnpm install --frozen-lockfile
|
||||
PORT=4873
|
||||
pnpm nx local-registry @nx/nx-source --port=$PORT >/tmp/verdaccio.log 2>&1 &
|
||||
for i in $(seq 1 60); do curl -sf http://localhost:$PORT/-/ping >/dev/null 2>&1 && break; sleep 1; done
|
||||
NX_LOCAL_REGISTRY_PORT=$PORT pnpm nx populate-local-registry-storage @nx/nx-source
|
||||
NXV=$(node -p "require(\"/work/nx/dist/packages/nx/package.json\").version")
|
||||
|
||||
# 2. reproduce against that build — same container, localhost registry
|
||||
cd /work
|
||||
git clone --depth 1 <GIT_URL> repro # or: npx --yes create-nx-workspace <ARGS> --directory repro
|
||||
cd repro
|
||||
# rewrite nx/@nx/@nrwl deps to "$NXV" (same node one-liner as the Run section)
|
||||
rm -f package-lock.json pnpm-lock.yaml yarn.lock
|
||||
npm_config_registry=http://localhost:$PORT pnpm install
|
||||
( timeout 300 <REPRO_COMMAND> ); echo "REPRO_EXIT=$?"
|
||||
echo "kernel: $(uname -r)"
|
||||
'
|
||||
```
|
||||
|
||||
Because verdaccio and the repro live in the **same** container, the registry is plain `localhost` — the reachability/listen-address problems a host verdaccio would create simply don't exist. Classify the result exactly as in "Classify + report".
|
||||
|
||||
Prerequisite: the `nx-review-sandbox` image (`setup-review-sandbox`). The nx build is heavy (~several min + several GB) — RAM-backed via the tmpfs above so it stays off the host disk.
|
||||
|
||||
## Cleanup
|
||||
|
||||
`--rm` destroys the container and everything in it on exit. Nothing persists on the host. Stray sandbox containers/images: `/sandbox-prune`.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: review-pr
|
||||
description: Deep code review of a single open PR in nrwl/nx. Sets up an isolated worktree, runs the pr-review-toolkit review agents, the reproduce-verifier agent (grounds the review in the linked issues and, when runnable locally, executes the repro on master vs PR), the alternative-approach agent (independently designs competing solutions and contrasts them with the PR's choice), the performance-analyzer agent (checks the changes don't waste CPU or memory and execute quickly at workspace scale), and the security-analyzer agent (hunts injection-class vulnerabilities — command injection, zip-slip, SSRF, credential leakage — across real trust boundaries), surfaces only critical and important findings (plus strengths; nice-to-have suggestions are dropped), and saves a GitHub-flavored draft to ~/.nx-pr-reviews/<NUMBER>.md for the reviewer to read (nothing is posted). Use when you want a thorough review of one PR.
|
||||
description: Deep code review of a single open PR in nrwl/nx. Sets up an isolated worktree, runs the pr-review-toolkit review agents, the reproduce-verifier agent (grounds the review in the linked issues and, when runnable locally, executes the repro on master vs PR), and the alternative-approach agent (independently designs competing solutions and contrasts them with the PR's choice), surfaces only critical and important findings (plus strengths; nice-to-have suggestions are dropped), and saves a GitHub-flavored draft to ~/.nx-pr-reviews/<NUMBER>.md for the reviewer to read (nothing is posted). Use when you want a thorough review of one PR.
|
||||
allowed-tools: Bash(gh pr view *), Bash(gh pr list *), Bash(gh issue view *), Bash(gh auth status*), Bash(git -C *), Bash(git worktree *), Bash(git rev-parse *), Bash(mkdir -p *), Bash(ls *), Bash(printf *), Bash(date *), Bash(cd *), Bash(test *), Bash(echo *), Bash(head *), Bash(tail *), Bash(cat *), Bash(jq *), Bash(grep *), Bash(wc *), Bash(sed *), Write(~/.nx-pr-reviews/**), Write(/tmp/**), Edit(~/.nx-pr-reviews/**), Edit(/tmp/**), Read, Grep, Glob, Skill, Agent
|
||||
argument-hint: '<PR_NUMBER> [--verify-repros]'
|
||||
---
|
||||
@@ -216,7 +216,7 @@ If all signals are cheap-negative, skip emitting the section entirely (no noise
|
||||
|
||||
### Early exit on a strong close signal
|
||||
|
||||
If **superseded (strong)** or **unnecessary (strong)** fired, skip Steps 5 through 5b entirely (toolkit, alternative-approach, performance-analyzer, security-analyzer, reproduce-verifier, reconciliation). The verdict precedence in Step 7 already decides the outcome, so agent findings can't change it — and nobody acts on code feedback for a PR that won't merge. Set `$REVIEW_BODY` to just the `### Close-without-merge check` section and continue with Steps 6-10 as normal.
|
||||
If **superseded (strong)** or **unnecessary (strong)** fired, skip Steps 5 through 5b entirely (toolkit, alternative-approach, reproduce-verifier, reconciliation). The verdict precedence in Step 7 already decides the outcome, so agent findings can't change it — and nobody acts on code feedback for a PR that won't merge. Set `$REVIEW_BODY` to just the `### Close-without-merge check` section and continue with Steps 6-10 as normal.
|
||||
|
||||
## Step 5: Run the review toolkit
|
||||
|
||||
@@ -292,60 +292,6 @@ Capture the output as `$APPROACH_REPORT` and fold it into the review body as `##
|
||||
- `BETTER_ALTERNATIVE_EXISTS` — counts as an important finding, with the sketch as the ask.
|
||||
- `APPROACH_SOUND` — fold the endorsement into **Strengths** as a one-liner; no finding.
|
||||
|
||||
## Step 5a.2: Run the performance-analyzer agent
|
||||
|
||||
In parallel with Step 5, dispatch the `performance-analyzer` agent — it answers "does this change waste CPU or memory, and does it execute quickly at workspace scale?":
|
||||
|
||||
```
|
||||
Agent(
|
||||
subagent_type="performance-analyzer",
|
||||
description="Analyze PR <NUMBER> runtime performance",
|
||||
prompt="""
|
||||
Analyze the runtime performance of PR <NUMBER> in nrwl/nx: CPU/memory footprint and execution speed.
|
||||
|
||||
Inputs:
|
||||
- PR_NUMBER: <NUMBER>
|
||||
- WORKTREE_PATH: <WORKTREE_BASE>/pr-<NUMBER>
|
||||
- BASE_REF: <BASE_REF_NAME>
|
||||
|
||||
Read .review-charter.md in the worktree first. Follow your standard workflow and return the structured report.
|
||||
"""
|
||||
)
|
||||
```
|
||||
|
||||
Capture the output as `$PERF_REPORT` and fold it into the review body as `### Performance analysis`, directly below `### Approach analysis`. Verdict influence (Step 7):
|
||||
|
||||
- `PERFORMANCE_REGRESSION` — counts as a critical finding (slower commands for real workspaces, or unbounded memory growth).
|
||||
- `PERFORMANCE_CONCERN` — counts as an important finding, with the cheaper shape as the ask.
|
||||
- `PERFORMANCE_SOUND` — fold the endorsement into **Strengths** as a one-liner; no finding.
|
||||
|
||||
## Step 5a.3: Run the security-analyzer agent
|
||||
|
||||
In parallel with Step 5, dispatch the `security-analyzer` agent — it answers "can untrusted data reach a dangerous sink through this change?" (command injection, zip-slip/path traversal, prototype pollution, SSRF, credential leakage):
|
||||
|
||||
```
|
||||
Agent(
|
||||
subagent_type="security-analyzer",
|
||||
description="Analyze PR <NUMBER> for security vulnerabilities",
|
||||
prompt="""
|
||||
Analyze PR <NUMBER> in nrwl/nx for injection-class vulnerabilities and data exposure.
|
||||
|
||||
Inputs:
|
||||
- PR_NUMBER: <NUMBER>
|
||||
- WORKTREE_PATH: <WORKTREE_BASE>/pr-<NUMBER>
|
||||
- BASE_REF: <BASE_REF_NAME>
|
||||
|
||||
Read .review-charter.md in the worktree first. Follow your standard workflow and return the structured report.
|
||||
"""
|
||||
)
|
||||
```
|
||||
|
||||
Capture the output as `$SECURITY_REPORT` and fold it into the review body as `### Security analysis`, directly below `### Performance analysis`. Verdict influence (Step 7):
|
||||
|
||||
- `SECURITY_VULNERABILITY` — counts as a critical finding (complete untrusted-source-to-sink chain in a default setup).
|
||||
- `SECURITY_CONCERN` — counts as an important finding, with the traced chain as the evidence.
|
||||
- `SECURITY_SOUND` — fold the endorsement into **Strengths** as a one-liner; no finding.
|
||||
|
||||
## Step 5a.5: Run the reproduce-verifier agent
|
||||
|
||||
In parallel with Step 5, dispatch the `reproduce-verifier` agent to ground the review in the reported bug.
|
||||
@@ -358,12 +304,12 @@ git -C "$NX_REPO_PATH" worktree add --detach "$WORKTREE_BASE/pr-<NUMBER>-verify"
|
||||
|
||||
(Detached on purpose: the `pr-<NUMBER>` branch is already checked out by the review worktree, and the verifier only ever checks out SHAs.)
|
||||
|
||||
Decide whether to opt in to Level 2 (expensive **sandboxed** reproduction — the agent builds the PR and runs the external repro inside a container, ~10-15 min per PR). Default is **off** — Level 2 only runs when:
|
||||
Decide whether to opt in to Level 2 (expensive verdaccio-based external-repo reproduction, ~10-15 min per PR). Default is **off** — Level 2 only runs when:
|
||||
|
||||
- The caller of this skill explicitly requested deep verification (e.g. invoked with the `--verify-external-repros` flag, or a manual `/review-pr <N> --verify-repros` pattern), OR
|
||||
- `$NX_REVIEW_LEVEL_2=1` is set in the environment.
|
||||
|
||||
Level 2 is for deep-dive passes where you want end-user-level proof — each run **builds nx inside the sandbox** (needs the `nx-review-sandbox` image; run `setup-review-sandbox` if missing), takes ~10-15 minutes and several GB, so opt in deliberately. Nothing in Level 2 builds or runs on the host.
|
||||
Level 2 is for deep-dive passes where you want end-user-level proof — each run takes ~10-15 minutes and ~0.5-1 GB of disk, so opt in deliberately.
|
||||
|
||||
```
|
||||
Agent(
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
name: setup-review-sandbox
|
||||
description: One-time setup of the sandbox prerequisites used by the reproduce-issue skill and the reproduce-verifier agent — Docker, the isolation runtime (gVisor on Linux / Colima on macOS), healthy container networking, and the nx-review-sandbox toolchain image (built from the repo's mise.toml). Idempotent; re-run any time to verify or repair. Use when the user says "set up the review sandbox", "install the sandbox prereqs", "build the sandbox image", or a reproduce-issue preflight reports something MISSING.
|
||||
allowed-tools: Read, Grep, Glob, Bash(uname *), Bash(docker info *), Bash(docker run *), Bash(docker build *), Bash(docker image inspect *), Bash(docker images *), Bash(command -v *), Bash(lsmod *)
|
||||
---
|
||||
|
||||
# Set up the review sandbox (one-time)
|
||||
|
||||
Installs and verifies everything the `reproduce-issue` skill / `reproduce-verifier` agent need to run untrusted PR code in isolation. Idempotent — each step checks first and only acts if needed. Steps needing `sudo` are handed to the user to run in their terminal (this skill cannot `sudo` non-interactively).
|
||||
|
||||
Run `uname -s` first — the path differs on Linux vs macOS.
|
||||
|
||||
## 1. Docker
|
||||
|
||||
```bash
|
||||
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
|
||||
```
|
||||
|
||||
- **MISSING, Linux:** install Docker Engine, then `sudo systemctl enable --now docker` and add yourself to the `docker` group (`sudo usermod -aG docker $USER`, then re-login).
|
||||
- **MISSING, macOS:** `brew install colima docker` then `colima start` (or install Docker Desktop).
|
||||
|
||||
## 2. Isolation runtime
|
||||
|
||||
### Linux — gVisor (`runsc`)
|
||||
|
||||
```bash
|
||||
docker info --format '{{range $k,$v := .Runtimes}}{{$k}} {{end}}' | grep -q runsc && echo "runsc OK" || echo "runsc MISSING"
|
||||
```
|
||||
|
||||
If MISSING, have the user run this in their terminal (needs `sudo`; their shell is fish — exit codes are `$status`):
|
||||
|
||||
```bash
|
||||
sudo apt-get update && sudo apt-get install -y apt-transport-https ca-certificates curl gnupg
|
||||
curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | sudo tee /etc/apt/sources.list.d/gvisor.list
|
||||
sudo apt-get update && sudo apt-get install -y runsc
|
||||
sudo runsc install # registers runsc as a Docker runtime
|
||||
sudo systemctl restart docker
|
||||
```
|
||||
|
||||
Then re-check the runtime line above.
|
||||
|
||||
### macOS — the Docker VM is the sandbox
|
||||
|
||||
No `runsc`. Just confirm the VM is up:
|
||||
|
||||
```bash
|
||||
docker info >/dev/null 2>&1 && echo "docker VM OK" || echo "start it: colima start"
|
||||
```
|
||||
|
||||
## 3. Container networking (catches the `veth` class of breakage)
|
||||
|
||||
```bash
|
||||
docker run --rm --network none alpine true && echo "sandbox OK"
|
||||
docker run --rm alpine true && echo "networking OK" || echo "networking BROKEN"
|
||||
```
|
||||
|
||||
If the first passes but the second fails with `veth ... operation not supported`:
|
||||
|
||||
```bash
|
||||
sudo modprobe veth
|
||||
```
|
||||
|
||||
If `modprobe` errors with a BTF / version mismatch (`failed to validate module [veth] BTF`), the running kernel no longer matches its on-disk modules (a kernel update landed while it was booted) — **reboot**, after which it auto-loads. Persist it: `echo veth | sudo tee /etc/modules-load.d/veth.conf`.
|
||||
|
||||
## 4. The toolchain image (`nx-review-sandbox`)
|
||||
|
||||
Needed only to **build an unreleased PR's nx** in the sandbox (reproduce-verifier Level 2). Reproducing against a published nx version does NOT need it.
|
||||
|
||||
```bash
|
||||
docker image inspect nx-review-sandbox:latest >/dev/null 2>&1 && echo "image OK" || echo "image MISSING"
|
||||
```
|
||||
|
||||
If MISSING, build it from the repo root (so `mise.toml` is in the build context). This installs the repo's exact toolchain — node/java/dotnet/maven/rust/bun via mise — and takes a while + several GB:
|
||||
|
||||
```bash
|
||||
docker build -t nx-review-sandbox:latest -f tools/review-sandbox/Dockerfile .
|
||||
```
|
||||
|
||||
Requires steps 1 + 3 to pass first (build needs working networking). If disk is tight, `/sandbox-prune` first.
|
||||
|
||||
## 5. Verify (smoke test)
|
||||
|
||||
Confirm the sandbox actually isolates and carries the tools:
|
||||
|
||||
```bash
|
||||
# RUNTIME="--runtime=runsc" on Linux, "" on macOS
|
||||
docker run --rm $RUNTIME nx-review-sandbox:latest bash -lc '
|
||||
echo "kernel: $(uname -r)" # Linux+gVisor: 4.19.0-gvisor ; macOS: the VM kernel
|
||||
mise ls 2>/dev/null | head
|
||||
node --version; java -version 2>&1 | head -1; dotnet --version
|
||||
'
|
||||
```
|
||||
|
||||
Green when: the kernel is NOT your host kernel, and node/java/dotnet report versions. Report a concise ✅/❌ per step and what (if anything) the user still needs to run.
|
||||
@@ -1,166 +0,0 @@
|
||||
---
|
||||
name: update-cnw-templates
|
||||
description: Update the CNW (create-nx-workspace) template repos (nrwl/empty-template, nrwl/react-template, etc.) to a target nx version via nx migrate, verify each repo, and open a PR per repo. Clones repos it needs - assumes no local checkout. Use when asked to "update the CNW templates", "migrate the templates to nx X", "bump the template repos", or given a version like "update templates to 23.2.0".
|
||||
allowed-tools: Bash, Read, Write, Edit, Grep, Glob, WebFetch
|
||||
---
|
||||
|
||||
# Update CNW Templates
|
||||
|
||||
Bump every CNW template repo to one target nx version, verify it still builds and
|
||||
still scaffolds, then open a draft PR per repo. Each template is an independent
|
||||
GitHub repo under `nrwl/`; `create-nx-workspace --template nrwl/<repo>` clones its
|
||||
`main` to scaffold a user's workspace. Each repo has a `ci.yml` that lints, tests,
|
||||
builds, typechecks, and e2es it on PRs - but the consumer path (scaffolding from `main`
|
||||
via `--template`) isn't covered there, and a force-push to `main` skips PR CI entirely
|
||||
(how the react template broke). So verify before you ship.
|
||||
|
||||
This skill makes **no assumption that the repos are checked out locally.** It clones
|
||||
what it needs. Anyone on the team can run it from a fresh machine.
|
||||
|
||||
## Input
|
||||
|
||||
- **Target nx version** - e.g. `23.2.0`. If omitted, use latest stable: `npm view nx@latest version`. Verify it exists: `npm view nx@<version> version`.
|
||||
- **Repos** - one, several, or (default) all live templates. Names may be given with or without the `-template` suffix.
|
||||
- **Work dir** - where clones land. Default `./tmp/cnw-templates/` (gitignored). Reuse an existing clone if one is already there and clean.
|
||||
|
||||
## The template repos
|
||||
|
||||
All live under `nrwl/<name>-template`, push target branch `main`. `--template` accepts
|
||||
the full `nrwl/<repo>` form for all of them. Four templates also have a bare shorthand.
|
||||
|
||||
| Template | `--template` value | Shorthand |
|
||||
| --------------- | ------------------------------- | --------- |
|
||||
| empty | `nrwl/empty-template` | `empty` |
|
||||
| typescript | `nrwl/typescript-template` | `ts` |
|
||||
| react | `nrwl/react-template` | `react` |
|
||||
| angular | `nrwl/angular-template` | `angular` |
|
||||
| react-mfe | `nrwl/react-mfe-template` | - |
|
||||
| nextjs | `nrwl/nextjs-template` | - |
|
||||
| nestjs | `nrwl/nestjs-template` | - |
|
||||
| express-api | `nrwl/express-api-template` | - |
|
||||
| astro-starlight | `nrwl/astro-starlight-template` | - |
|
||||
| remotion | `nrwl/remotion-template` | - |
|
||||
| tanstack-start | `nrwl/tanstack-start-template` | - |
|
||||
| tanstack-ai | `nrwl/tanstack-ai-template` | - |
|
||||
|
||||
Before continuing, check that all the templates are live. A repo is live if
|
||||
`GET https://api.github.com/repos/nrwl/<name>-template/commits/main` returns 200 (a sha).
|
||||
If you hit 404 report it.
|
||||
|
||||
This table may change, and the user will tell you which repos to use (defaults to all in the table).
|
||||
|
||||
## Procedure
|
||||
|
||||
### 1. Resolve version + repo set
|
||||
|
||||
```bash
|
||||
npm view nx@<version> version # confirm target exists
|
||||
# for each requested repo, confirm it's live:
|
||||
curl -s -o /dev/null -w "%{http_code}" https://api.github.com/repos/nrwl/<name>-template/commits/main
|
||||
```
|
||||
|
||||
### 1a. If in a Polygraph session, add the templates to it
|
||||
|
||||
If this skill runs inside a Polygraph session (the startup banner names a session ID),
|
||||
add every target template repo to the session so their per-repo PRs link together under
|
||||
one session. The repos are exact `owner/repo` refs, so add them directly - no discovery:
|
||||
|
||||
```
|
||||
add_repo(sessionId: "<session-id>", repoIds: ["nrwl/empty-template", "nrwl/react-template", ...])
|
||||
```
|
||||
|
||||
Add only the live repos you're actually touching. After `add_repo`, the PRs you open in
|
||||
step 5 join the session automatically - the link is the session, not any cross-reference
|
||||
in the PR bodies. If there's no session, skip this and proceed normally.
|
||||
|
||||
### 2. Clone (or reuse) each repo
|
||||
|
||||
All template repos are npm (`package-lock.json`). Clone over SSH; the working tree must
|
||||
be clean before you touch it.
|
||||
|
||||
```bash
|
||||
mkdir -p tmp/cnw-templates && cd tmp/cnw-templates
|
||||
git clone git@github.com:nrwl/<name>-template.git # or reuse an existing clean clone
|
||||
cd <name>-template
|
||||
git checkout main
|
||||
git status --porcelain # MUST be empty; if dirty, skip this repo and report
|
||||
git fetch origin main && git reset --hard origin/main # make sure we start from latest origin
|
||||
grep '"nx"' package.json # record current version
|
||||
```
|
||||
|
||||
### 3. Migrate
|
||||
|
||||
Use `CI=true` to skip prompts.
|
||||
|
||||
```bash
|
||||
CI=true npm install # node_modules at current version
|
||||
CI=true npx nx migrate <target-version> # updates package.json, writes migrations.json
|
||||
CI=true npm install # apply the dep bump
|
||||
if [ -f migrations.json ]; then
|
||||
CI=true npx nx migrate --run-migrations
|
||||
rm -f migrations.json
|
||||
fi
|
||||
```
|
||||
|
||||
### 4. Verify
|
||||
|
||||
```bash
|
||||
NX_NO_CLOUD=true NX_DAEMON=false CI=true npx nx run-many -t build test lint typecheck --skip-nx-cache
|
||||
NX_NO_CLOUD=true NX_DAEMON=false CI=true npx nx run-many -t e2e # where the repo defines it
|
||||
```
|
||||
|
||||
If any target fails, **revert that repo (`git checkout .`) and report** - never open a red PR.
|
||||
|
||||
### 5. Commit + PR (per repo)
|
||||
|
||||
Every template's `main` is a single "Initial commit" (verified across all 12 repos), so
|
||||
keep the branch to **one commit** (amend, don't stack) and squash-merge the PR.
|
||||
|
||||
```bash
|
||||
cd tmp/cnw-templates/<name>-template
|
||||
git checkout -b update-nx-<target-version>
|
||||
git add -A
|
||||
git commit -m "chore(deps): update to nx <target-version>" # never mention AI/Claude
|
||||
git push -u origin update-nx-<target-version>
|
||||
# open a draft PR to main via the GitHub API (token from env/1Password, never hardcode):
|
||||
curl -s -X POST -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/nrwl/<name>-template/pulls" \
|
||||
-d '{"title":"chore(deps): update to nx <target-version>","head":"update-nx-<target-version>","base":"main","draft":true,"body":"<per-repo summary: old->new nx, migrations run>"}'
|
||||
```
|
||||
|
||||
PR body: old -> new nx version, which migrations ran, and the verification result. For a
|
||||
not-yet-created repo (404 in step 1), skip the push - report it as "not created".
|
||||
|
||||
### 6. Sanity check after the PRs land - run-all-templates.sh
|
||||
|
||||
`run-all-templates.sh` (bundled next to this file) runs `create-nx-workspace --template
|
||||
nrwl/<repo>` for every template and reports pass/fail. It scaffolds from each repo's
|
||||
**`main`**, so run it as a **follow-up once the template PRs are merged** (or after you
|
||||
push to `main`) - a real end-to-end check that every template still scaffolds for users.
|
||||
It can't see an unpushed branch, so it's a post-merge step, not a pre-merge gate.
|
||||
|
||||
```bash
|
||||
# all templates:
|
||||
CNW_VERSION=<target-version> ./run-all-templates.sh
|
||||
# a subset:
|
||||
CNW_VERSION=<target-version> ONLY="empty-template react-template" ./run-all-templates.sh
|
||||
```
|
||||
|
||||
### 7. Report
|
||||
|
||||
One table across all repos:
|
||||
|
||||
```
|
||||
| Template | Previous | Updated | Files | Status |
|
||||
| --------------- | -------- | ------- | ----- | -------------- |
|
||||
| empty-template | 23.1.0 | 23.2.0 | 2 | PR #NN (draft) |
|
||||
| nuxt-template | 23.1.0 | - | - | not created |
|
||||
```
|
||||
|
||||
Be ready to explain any change - which migration produced it and why.
|
||||
|
||||
## Notes
|
||||
|
||||
- **Always `CI=true`** for nx/npm commands so nothing blocks on a prompt.
|
||||
- **Never push without confirmation.** Open PRs as **drafts**; the owner reviews and marks ready.
|
||||
- Patch bumps are usually just `package.json` + lockfile (no `migrations.json`). Minor/major can rewrite source - review the non-dep diff before committing.
|
||||
@@ -1,95 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Run create-nx-workspace against every CNW template, non-interactively.
|
||||
#
|
||||
# Each template clones into its own subdirectory under an output base dir, so
|
||||
# no two runs collide ("The directory '<name>' already exists" -> CnwError
|
||||
# DIRECTORY_EXISTS). Existing per-template dirs are removed before each run so
|
||||
# the script is idempotent.
|
||||
#
|
||||
# Usage:
|
||||
# ./run-all-templates.sh [OUTPUT_DIR]
|
||||
#
|
||||
# Env:
|
||||
# CNW_VERSION create-nx-workspace version/tag (default: latest)
|
||||
# ONLY space-separated subset of template repos to run
|
||||
#
|
||||
# Examples:
|
||||
# ./run-all-templates.sh
|
||||
# CNW_VERSION=22.7.0 ./run-all-templates.sh /tmp/cnw-out
|
||||
# ONLY="nextjs-template react-template" ./run-all-templates.sh
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
CNW_VERSION="${CNW_VERSION:-latest}"
|
||||
OUTPUT_DIR="${1:-$PWD/cnw-runs-$(date +%Y%m%d-%H%M%S)}"
|
||||
|
||||
# Template GitHub repos under the nrwl org. --template requires the full
|
||||
# nrwl/<repo> form except for the 4 shorthands (empty/react/angular/typescript).
|
||||
# Listing the full repo name for all keeps it uniform.
|
||||
TEMPLATES=(
|
||||
empty-template
|
||||
typescript-template
|
||||
react-template
|
||||
angular-template
|
||||
react-mfe-template
|
||||
nextjs-template
|
||||
nestjs-template
|
||||
express-api-template
|
||||
astro-starlight-template
|
||||
remotion-template
|
||||
tanstack-start-template
|
||||
tanstack-ai-template
|
||||
)
|
||||
|
||||
if [ -n "${ONLY:-}" ]; then
|
||||
# shellcheck disable=SC2206
|
||||
TEMPLATES=($ONLY)
|
||||
fi
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
cd "$OUTPUT_DIR" || exit 1
|
||||
|
||||
echo "CNW version : $CNW_VERSION"
|
||||
echo "Output dir : $OUTPUT_DIR"
|
||||
echo "Templates : ${#TEMPLATES[@]}"
|
||||
echo
|
||||
|
||||
declare -a PASS=()
|
||||
declare -a FAIL=()
|
||||
|
||||
for repo in "${TEMPLATES[@]}"; do
|
||||
# workspace name = repo without the -template suffix (valid npm pkg name)
|
||||
name="${repo%-template}"
|
||||
target="$OUTPUT_DIR/$name"
|
||||
|
||||
echo "=================================================================="
|
||||
echo ">> $repo -> $name"
|
||||
echo "=================================================================="
|
||||
|
||||
# avoid DIRECTORY_EXISTS: clear any prior run for this template
|
||||
rm -rf "$target"
|
||||
|
||||
CI=true npx --yes "create-nx-workspace@${CNW_VERSION}" "$name" \
|
||||
--template "nrwl/$repo" \
|
||||
--nxCloud=skip \
|
||||
--no-interactive
|
||||
|
||||
if [ $? -eq 0 ] && [ -d "$target" ]; then
|
||||
PASS+=("$repo")
|
||||
echo "OK: $repo"
|
||||
else
|
||||
FAIL+=("$repo")
|
||||
echo "FAILED: $repo"
|
||||
fi
|
||||
echo
|
||||
done
|
||||
|
||||
echo "=================================================================="
|
||||
echo "SUMMARY"
|
||||
echo "=================================================================="
|
||||
echo "Passed (${#PASS[@]}): ${PASS[*]:-none}"
|
||||
echo "Failed (${#FAIL[@]}): ${FAIL[*]:-none}"
|
||||
echo "Output: $OUTPUT_DIR"
|
||||
|
||||
[ ${#FAIL[@]} -eq 0 ]
|
||||
@@ -15,7 +15,7 @@ env:
|
||||
PNPM_HOME: ~/.pnpm
|
||||
# Pin corepack to the pnpm version from packageManager. Without this, corepack
|
||||
# falls back to "latest" in directories that have no packageManager field
|
||||
# (e.g. e2e temp dirs) instead of the repo's pinned pnpm.
|
||||
# (e.g. e2e temp dirs), pulling pnpm 11 and breaking install.
|
||||
COREPACK_DEFAULT_TO_LATEST: '0'
|
||||
|
||||
jobs:
|
||||
@@ -92,8 +92,18 @@ jobs:
|
||||
run:
|
||||
pnpm nx report
|
||||
|
||||
- name: Enable core dumps for segfault diagnostics
|
||||
run: |
|
||||
sudo mkdir -p /tmp/cores && sudo chmod 777 /tmp/cores
|
||||
# Runner default pipes cores to apport, which discards them.
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/cores/core.%e.%p'
|
||||
|
||||
- name: Run Checks/Lint/Test/Build
|
||||
run: |
|
||||
# Intermittent SIGSEGV kills of nx clients happen in this step
|
||||
# (e.g. runs 29067764140, 29103579727); keep cores so the
|
||||
# "Collect segfault cores" step can capture a native backtrace.
|
||||
ulimit -c unlimited
|
||||
pids=()
|
||||
|
||||
pnpm nx record -- nx format:check &
|
||||
@@ -115,6 +125,38 @@ jobs:
|
||||
wait "$pid"
|
||||
done
|
||||
timeout-minutes: 100
|
||||
- name: Collect segfault cores
|
||||
if: failure()
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
cores=(/tmp/cores/core.*)
|
||||
if [ ${#cores[@]} -eq 0 ]; then
|
||||
echo 'No core dumps found'
|
||||
exit 0
|
||||
fi
|
||||
command -v gdb >/dev/null || { sudo apt-get update -q && sudo apt-get install -y -q gdb; }
|
||||
kept=0
|
||||
for c in "${cores[@]}"; do
|
||||
echo "===== backtrace for $c ====="
|
||||
gdb -q -batch -ex 'set pagination off' -ex bt -ex 'info threads' \
|
||||
"$(command -v node)" "$c" 2>&1 | head -150 | tee -a /tmp/backtraces.txt
|
||||
if [ "$kept" -lt 3 ]; then
|
||||
gzip -f "$c" && kept=$((kept+1)) || true
|
||||
else
|
||||
rm -f "$c"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload segfault cores
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: segfault-cores-linux
|
||||
path: |
|
||||
/tmp/backtraces.txt
|
||||
/tmp/cores/*.gz
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Fix CI
|
||||
run: pnpm nx fix-ci
|
||||
if: failure()
|
||||
|
||||
@@ -15,7 +15,7 @@ env:
|
||||
CYPRESS_CACHE_FOLDER: ${{ github.workspace }}/.cypress
|
||||
# Pin corepack to the pnpm version from packageManager. Without this, corepack
|
||||
# falls back to "latest" in directories that have no packageManager field
|
||||
# (e.g. e2e temp dirs) instead of the repo's pinned pnpm.
|
||||
# (e.g. e2e temp dirs), pulling pnpm 11 and breaking install.
|
||||
COREPACK_DEFAULT_TO_LATEST: '0'
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -78,8 +78,7 @@ const matrixData: MatrixData = {
|
||||
package_managers: ['npm', 'pnpm', 'yarn'],
|
||||
// TODO: re-add '26.0.0' once playwright ships the yauzl fix for node 26 extract hang.
|
||||
// See https://github.com/microsoft/playwright/issues/40724
|
||||
// Floors track @angular/cli engines (^22.22.3 || ^24.15.0): ng new refuses older.
|
||||
node_versions: ['22.22.3', '24.15.0'],
|
||||
node_versions: ['22.13.0', '24.0.0'],
|
||||
excluded: ['e2e-detox', 'e2e-react-native', 'e2e-expo']
|
||||
},
|
||||
// Docker is not supported on ARM-based macOS runners (no nested virtualization)
|
||||
@@ -87,7 +86,7 @@ const matrixData: MatrixData = {
|
||||
// We may want to look into adding intel only for this docker case, at least until vm-in-vm works on latest macos
|
||||
// TODO: re-add '26.0.0' once playwright ships the yauzl fix for node 26 extract hang.
|
||||
// See https://github.com/microsoft/playwright/issues/40724
|
||||
{ os: 'macos-latest', os_name: 'MacOS', os_timeout: 90, package_managers: ['npm'], node_versions: ['24.15.0'], excluded: ['e2e-docker'] }
|
||||
{ os: 'macos-latest', os_name: 'MacOS', os_timeout: 90, package_managers: ['npm'], node_versions: ['24.0.0'], excluded: ['e2e-docker'] }
|
||||
// TODO (Jack): Fix Windows support as gradle fails when running nx build https://staging.nx.app/runs/LgD4vxGn8w?utm_source=pull-request&utm_medium=comment
|
||||
// { os: 'windows-latest', os_name: 'WinOS', os_timeout: 180, package_managers: ['npm'], node_versions: ['24.0.0'], excluded: ['e2e-detox', 'e2e-react-native', 'e2e-expo'] }
|
||||
]
|
||||
|
||||
@@ -16,13 +16,9 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup dev tools with mise
|
||||
uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3
|
||||
|
||||
- name: Enable corepack and install pnpm
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare --activate
|
||||
- uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d # v4.1.0
|
||||
with:
|
||||
version: 11.2.2 # Aligned with root package.json (pnpm/action-setup will helpfully error if out of sync)
|
||||
|
||||
- name: Run a security audit
|
||||
run: pnpm dlx audit-ci --critical --report-type summary
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
name: SIGSEGV AB Hunt
|
||||
|
||||
# Diagnostic workflow for the intermittent SIGSEGV kills of nx clients in
|
||||
# main-linux (see e.g. run 29103579727). Reproduces the crash conditions
|
||||
# (concurrent cloud-recording nx clients, cold shared V8 compile cache,
|
||||
# heartbeat spawn window) in a loop, across three legs:
|
||||
# - node 26.3.0 (current CI default; expected to crash)
|
||||
# - node 24.11.0 (pre-Jun-3 pin; expected clean)
|
||||
# - node 26.3.0 + NX_COMPILE_CACHE=false (isolates the compile cache)
|
||||
# Captures core dumps and prints gdb backtraces so the first crash yields a
|
||||
# native stack. Report-only: legs always end green; read the step summary.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- ci-segv-ab
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
iterations:
|
||||
description: Loop count per leg
|
||||
default: '60'
|
||||
|
||||
env:
|
||||
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
NX_CLOUD_ENABLE_METRICS_COLLECTION: 'true'
|
||||
PNPM_HOME: ~/.pnpm
|
||||
COREPACK_DEFAULT_TO_LATEST: '0'
|
||||
|
||||
jobs:
|
||||
# Proves the capture pipeline in ci.yml's "Collect segfault cores" end to
|
||||
# end: a real node process is SIGSEGV'd on purpose under the identical
|
||||
# core_pattern/ulimit setup, and the job FAILS unless a core appears and
|
||||
# gdb yields usable frames. Runs on every push; the expensive hunt legs
|
||||
# only run on manual dispatch.
|
||||
canary:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
NODE_VERSION: 26.3.0
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup dev tools with mise
|
||||
uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3
|
||||
|
||||
- name: Enable core dumps exactly like the ci.yml catcher
|
||||
run: |
|
||||
sudo mkdir -p /tmp/cores && sudo chmod 777 /tmp/cores
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/cores/core.%e.%p'
|
||||
|
||||
- name: Crash a real node process on purpose
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
ec=0
|
||||
node -e 'process.kill(process.pid, "SIGSEGV")' || ec=$?
|
||||
echo "node exited with $ec (expect 139)"
|
||||
ls -la /tmp/cores/
|
||||
|
||||
- name: Validate a core was captured and gdb yields frames
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
cores=(/tmp/cores/core.*)
|
||||
if [ ${#cores[@]} -eq 0 ]; then
|
||||
echo '::error::CANARY FAILED: no core dump captured'
|
||||
exit 1
|
||||
fi
|
||||
command -v gdb >/dev/null || { sudo apt-get update -q && sudo apt-get install -y -q gdb; }
|
||||
# `command -v node` resolves to the mise SHIM (a Rust binary), and
|
||||
# symbolizing against the wrong executable yields garbage frames —
|
||||
# the first canary run proved it. Read the real executable path
|
||||
# from the core itself.
|
||||
exe=$(file -b "${cores[0]}" | sed -n "s/.*execfn: '\([^']*\)'.*/\1/p")
|
||||
[ -x "$exe" ] || exe=$(mise which node 2>/dev/null || command -v node)
|
||||
echo "symbolizing against: $exe"
|
||||
gdb -q -batch -ex 'set pagination off' -ex bt -ex 'info threads' \
|
||||
"$exe" "${cores[0]}" 2>&1 | tee /tmp/bt.txt
|
||||
frames=$(grep -c '^#' /tmp/bt.txt || true)
|
||||
echo "backtrace frames: $frames"
|
||||
if [ "$frames" -lt 3 ]; then
|
||||
echo '::error::CANARY FAILED: gdb produced fewer than 3 frames'
|
||||
exit 1
|
||||
fi
|
||||
# A raise()-induced SIGSEGV with the CORRECT executable must
|
||||
# resolve real symbols (libc raise/pthread_kill or node/v8/libuv).
|
||||
if ! grep -qE 'raise|pthread_kill|node::|v8::|uv_' /tmp/bt.txt; then
|
||||
echo '::error::CANARY FAILED: no recognizable symbols — wrong executable?'
|
||||
exit 1
|
||||
fi
|
||||
echo "Canary OK: core captured, $frames frames, symbols resolve against $exe" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
hunt:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- leg: node26
|
||||
node-version: 26.3.0
|
||||
nx-compile-cache: 'true'
|
||||
- leg: node24
|
||||
node-version: 24.11.0
|
||||
nx-compile-cache: 'true'
|
||||
- leg: node26-no-compile-cache
|
||||
node-version: 26.3.0
|
||||
nx-compile-cache: 'false'
|
||||
env:
|
||||
NODE_VERSION: ${{ matrix.node-version }}
|
||||
NX_COMPILE_CACHE: ${{ matrix.nx-compile-cache }}
|
||||
NX_DAEMON: 'true'
|
||||
NX_PERF_LOGGING: 'false'
|
||||
NX_NATIVE_LOGGING: 'false'
|
||||
NX_CI_EXECUTION_ENV: 'linux'
|
||||
NX_CLOUD_NO_TIMEOUTS: 'true'
|
||||
NX_CLOUD_VERBOSE_LOGGING: 'true'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
filter: tree:0
|
||||
|
||||
- name: Fetch Master
|
||||
run: git fetch origin master:master
|
||||
|
||||
- name: Set SHAs
|
||||
uses: nrwl/nx-set-shas@310288c04d90696f9f1bc27c5e3caea6642b53d4 # v5.0.0
|
||||
with:
|
||||
main-branch-name: 'master'
|
||||
|
||||
- name: Setup dev tools with mise
|
||||
uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3
|
||||
|
||||
- name: Enable corepack and install pnpm
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare --activate
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install project dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Crash hunt loop
|
||||
run: |
|
||||
set -uo pipefail
|
||||
sudo mkdir -p /tmp/cores && sudo chmod 777 /tmp/cores
|
||||
# Runner default pipes cores to apport, which discards them.
|
||||
sudo sysctl -w kernel.core_pattern='/tmp/cores/core.%e.%p'
|
||||
ulimit -c unlimited
|
||||
command -v gdb >/dev/null || { sudo apt-get update -q && sudo apt-get install -y -q gdb; }
|
||||
|
||||
# Rounds 1-2 (idle box) produced 0 crashes in 180 exercised
|
||||
# heartbeat windows. Production crashes happen while the full
|
||||
# affected build/test/e2e wave loads the runner, so recreate that
|
||||
# pressure: CPU contention + memory pressure force frequent V8 GC,
|
||||
# which is where latent heap corruption actually manifests.
|
||||
command -v stress-ng >/dev/null || sudo apt-get install -y -q stress-ng
|
||||
stress-ng --cpu 2 --vm 2 --vm-bytes 40% --quiet &
|
||||
STRESS_PID=$!
|
||||
trap 'kill $STRESS_PID 2>/dev/null || true' EXIT
|
||||
|
||||
ITER='${{ inputs.iterations }}'
|
||||
# Cache-busted iterations execute tasks for real (~2-3 min each
|
||||
# instead of ~25s replays), so the push-triggered default is lower.
|
||||
ITER="${ITER:-20}"
|
||||
crashes=0
|
||||
kept_cores=0
|
||||
hb_windows=0
|
||||
|
||||
# Production parity: the crashing step always runs inside a CIPE
|
||||
# started at the top of the job.
|
||||
npx nx-cloud@next start-ci-run || true
|
||||
|
||||
for i in $(seq 1 "$ITER"); do
|
||||
# Re-open the heartbeat spawn window: without this, iteration 1's
|
||||
# heartbeat process survives and later iterations skip the spawn
|
||||
# path where the crash lands. Ephemeral runner, targeted pattern.
|
||||
pkill -f 'heartbeat/background-process' 2>/dev/null || true
|
||||
rm -rf /tmp/run-group-* 2>/dev/null || true
|
||||
# Cold shared compile cache each iteration = the racy write window.
|
||||
rm -rf /tmp/node-compile-cache 2>/dev/null || true
|
||||
# Bust the nx cache (nx.json sharedGlobals includes this env var)
|
||||
# so every iteration EXECUTES tasks — real fork/exec churn, cache
|
||||
# writes, and terminal-output uploads — instead of cache replays.
|
||||
export NX_AB_SALT="salt-$i"
|
||||
|
||||
# Direct bin invocation (not `pnpm nx`): pnpm collapses a child's
|
||||
# signal death into exit 1, which would hide 128+SIGSEGV from wait.
|
||||
pids=(); names=()
|
||||
node_modules/.bin/nx run-many -t check-imports check-lock-files check-codeowners --parallel=1 --no-dte > /tmp/iter-runmany.log 2>&1 &
|
||||
pids+=($!); names+=(runmany)
|
||||
node_modules/.bin/nx run-many -t check-lock-files --parallel=1 --no-dte > /tmp/iter-lockfiles.log 2>&1 &
|
||||
pids+=($!); names+=(lockfiles)
|
||||
node_modules/.bin/nx sync:check > /tmp/iter-sync.log 2>&1 &
|
||||
pids+=($!); names+=(sync)
|
||||
node_modules/.bin/nx record -- echo "iter-$i-a" > /tmp/iter-reca.log 2>&1 &
|
||||
pids+=($!); names+=(reca)
|
||||
node_modules/.bin/nx record -- echo "iter-$i-b" > /tmp/iter-recb.log 2>&1 &
|
||||
pids+=($!); names+=(recb)
|
||||
|
||||
iter_crash=0
|
||||
for idx in "${!pids[@]}"; do
|
||||
ec=0; wait "${pids[$idx]}" || ec=$?
|
||||
if [ "$ec" -ge 128 ]; then
|
||||
iter_crash=1
|
||||
sig=$((ec - 128))
|
||||
echo "::warning::iter $i: ${names[$idx]} died with signal $sig (exit $ec)"
|
||||
echo "===== iter $i ${names[$idx]} log tail ====="
|
||||
tail -50 "/tmp/iter-${names[$idx]}.log" || true
|
||||
fi
|
||||
done
|
||||
# A core from ANY process (client, daemon, heartbeat child) or
|
||||
# segfault text in a log also counts as a crash.
|
||||
if compgen -G '/tmp/cores/core.*' > /dev/null 2>&1; then
|
||||
ls /tmp/cores/core.* 2>/dev/null | grep -qv '\.gz$' && iter_crash=1
|
||||
fi
|
||||
# Case-sensitive: the branch's own lowercase commit title
|
||||
# ("...sigsegv ab hunt...") echoes into Nx Cloud metadata in every
|
||||
# log, so -i produced 100% false positives on the first run.
|
||||
if grep -laE 'SIGSEGV|Segmentation fault' /tmp/iter-*.log > /dev/null 2>&1; then
|
||||
iter_crash=1
|
||||
echo "::warning::iter $i: segfault text found in logs"
|
||||
grep -aE 'SIGSEGV|Segmentation fault' /tmp/iter-*.log | head -5 || true
|
||||
fi
|
||||
# Probe: confirm the heartbeat spawn window was actually exercised.
|
||||
if grep -laq 'heartbeat background process' /tmp/iter-*.log 2>/dev/null; then
|
||||
hb_windows=$((hb_windows+1))
|
||||
fi
|
||||
|
||||
if [ "$iter_crash" = 1 ]; then
|
||||
crashes=$((crashes+1))
|
||||
sudo dmesg 2>/dev/null | grep -iE 'segv|segfault' | tail -5 || true
|
||||
for core in /tmp/cores/core.*; do
|
||||
[ -e "$core" ] || continue
|
||||
case "$core" in *.gz) continue ;; esac
|
||||
echo "===== backtrace for $core ====="
|
||||
exe=$(file -b "$core" | sed -n "s/.*execfn: '\([^']*\)'.*/\1/p")
|
||||
[ -x "$exe" ] || exe=$(mise which node 2>/dev/null || command -v node)
|
||||
gdb -q -batch -ex 'set pagination off' -ex bt -ex 'info threads' \
|
||||
"$exe" "$core" 2>&1 | head -150 | tee -a /tmp/backtraces.txt
|
||||
if [ "$kept_cores" -lt 2 ]; then
|
||||
gzip -f "$core" && kept_cores=$((kept_cores+1)) || true
|
||||
else
|
||||
rm -f "$core"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo "iter $i/$ITER done (crashes so far: $crashes)"
|
||||
done
|
||||
|
||||
{
|
||||
echo "## Leg ${{ matrix.leg }} (node $NODE_VERSION, NX_COMPILE_CACHE=$NX_COMPILE_CACHE)"
|
||||
echo ""
|
||||
echo "**$crashes / $ITER iterations had signal deaths**"
|
||||
echo ""
|
||||
echo "Heartbeat spawn window exercised in $hb_windows / $ITER iterations"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "RESULT leg=${{ matrix.leg }}: $crashes/$ITER iterations had signal deaths (heartbeat window in $hb_windows)"
|
||||
|
||||
- name: Upload crash evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: crash-evidence-${{ matrix.leg }}
|
||||
path: |
|
||||
/tmp/backtraces.txt
|
||||
/tmp/cores/*.gz
|
||||
if-no-files-found: ignore
|
||||
@@ -7,9 +7,8 @@ common-env-vars: &common-env-vars
|
||||
NX_NATIVE_LOGGING: 'nx::native::db'
|
||||
# Pin corepack to the pnpm version from packageManager. Without this, corepack
|
||||
# falls back to "latest" in directories that have no packageManager field
|
||||
# (e.g. e2e temp dirs created by create-nx-workspace) instead of the repo's
|
||||
# pinned pnpm. Same treatment as .github/workflows/{ci,e2e-matrix}.yml, which
|
||||
# pair it with `corepack prepare --activate` (see the init step below).
|
||||
# (e.g. e2e temp dirs created by create-nx-workspace), pulling pnpm 11 and
|
||||
# breaking install. Same treatment as .github/workflows/{ci,e2e-matrix}.yml.
|
||||
COREPACK_DEFAULT_TO_LATEST: '0'
|
||||
# These are need for build and link validation for next.js and astro apps
|
||||
NEXT_PUBLIC_ASTRO_URL: 'https://master--nx-docs.netlify.app'
|
||||
@@ -32,20 +31,11 @@ common-init-steps: &common-init-steps
|
||||
- name: Setup toolchains
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-mise/main.yaml'
|
||||
|
||||
# Make the repo's pinned pnpm corepack's default so e2e temp dirs (no
|
||||
# packageManager field) resolve it too, instead of corepack's bundled
|
||||
# last-known-good version.
|
||||
- name: Activate repo pnpm via corepack
|
||||
script: |
|
||||
corepack enable
|
||||
corepack prepare --activate
|
||||
|
||||
- name: Verify toolchain versions
|
||||
script: |
|
||||
echo "mise: $(mise --version)"
|
||||
echo "node: $(node --version)"
|
||||
echo "pnpm: $(pnpm --version)"
|
||||
echo "pnpm outside repo: $(cd $(mktemp -d) && pnpm --version)"
|
||||
echo "bun: $(bun --version)"
|
||||
echo "rust: $(rustc --version) - $(cargo --version)"
|
||||
echo "dotnet: $(dotnet --version)"
|
||||
|
||||
@@ -15,12 +15,7 @@
|
||||
<a href=""><img src="https://img.shields.io/npm/l/nx.svg?style=for-the-badge" alt="License"></a>
|
||||
<a href="https://go.nx.dev/community"><img src="https://img.shields.io/discord/1143497901675401286?label=discord&style=for-the-badge" alt="Discord"></a>
|
||||
<a href="https://x.com/nxdevtools"><img src="https://img.shields.io/badge/@nxdevtools-555?style=for-the-badge&logo=x" alt="X (Twitter)"></a>
|
||||
<a href="https://nx.dev/nx-cloud"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fstaging.nx.app%2Fnx-cloud%2Fbadge%2F62d013ea0852fe0a2df74438%2Fhours-saved.json&style=for-the-badge" alt="Hours saved"></a>
|
||||
<a href="https://nx.dev/nx-cloud"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fstaging.nx.app%2Fnx-cloud%2Fbadge%2F62d013ea0852fe0a2df74438%2Fcache-hit-rate.json&style=for-the-badge" alt="Cache hit rate"></a>
|
||||
<a href="https://nx.dev/docs/features/ci-features/sandboxing"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fstaging.nx.app%2Fnx-cloud%2Fbadge%2F62d013ea0852fe0a2df74438%2Fsandbox.json&style=for-the-badge" alt="Nx Sandboxing"></a>
|
||||
<a href="https://nx.dev/nx-cloud"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fstaging.nx.app%2Fnx-cloud%2Fbadge%2F62d013ea0852fe0a2df74438%2Fremote-cache.json&style=for-the-badge" alt="Remote caching"></a>
|
||||
<a href="https://nx.dev/nx-cloud"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fstaging.nx.app%2Fnx-cloud%2Fbadge%2F62d013ea0852fe0a2df74438%2Fself-healing.json&style=for-the-badge" alt="Self-healing CI"></a>
|
||||
<a href="https://nx.dev/nx-cloud"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fstaging.nx.app%2Fnx-cloud%2Fbadge%2F62d013ea0852fe0a2df74438%2Fflaky-detection.json&style=for-the-badge" alt="Flaky task retries"></a>
|
||||
<a href="https://nx.dev/docs/features/ci-features/sandboxing"><img src="https://staging.nx.app/workspaces/62d013ea0852fe0a2df74438/sandbox-badge.svg?style=for-the-badge" alt="Nx Sandboxing"></a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
@@ -38,10 +38,6 @@ Bounties are not paid out for OSS findings.
|
||||
|
||||
### Process
|
||||
|
||||
**Important:** All attached reports MUST be in a plaintext format. You can attach text/markdown files (.txt or .md with no embedded images).
|
||||
We are no longer accepting PDF or other document formats. If you need to attach images, you can do so to the initial email. We do not guarantee
|
||||
any response reminding submitters of this requirement and emails sent with these attached files may be rejected without response.
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message.
|
||||
|
||||
Nx follows the principle of Coordinated Vulnerability Disclosure.
|
||||
|
||||
@@ -76,10 +76,6 @@ exceptions:
|
||||
- Turborepo
|
||||
- Lerna
|
||||
- Bazel
|
||||
- Depot
|
||||
- Blacksmith
|
||||
- Buildkite
|
||||
- Develocity
|
||||
- JSON
|
||||
- YAML
|
||||
- TOML
|
||||
|
||||
@@ -44,14 +44,6 @@ export default defineConfig({
|
||||
redirects: {
|
||||
'/knowledge-base/installation':
|
||||
'/docs/knowledge-base/installation-and-updates',
|
||||
'/guides/nx-cloud/source-control-integration/github':
|
||||
'/docs/features/ci-features/github-integration',
|
||||
'/concepts/decisions/overview':
|
||||
'/docs/concepts/decisions/monorepo-vs-polyrepo',
|
||||
'/concepts/decisions/why-monorepos':
|
||||
'/docs/concepts/decisions/what-is-a-monorepo',
|
||||
'/features/maintain-typescript-monorepos':
|
||||
'/docs/technologies/typescript/introduction',
|
||||
'/guides/nx-cloud/ci-resource-usage':
|
||||
'/docs/features/ci-features/resource-usage',
|
||||
'/reference/remote-cache-plugins':
|
||||
|
||||
@@ -72,26 +72,6 @@ to = "/docs/features/ci-features/self-healing-ci"
|
||||
from = "/docs/guides/nx-cloud/manual-dte"
|
||||
to = "/docs/guides/nx-cloud/bring-your-own-compute"
|
||||
|
||||
# DOC-549: GitHub source control guide merged into the GitHub Actions integration page
|
||||
[[redirects]]
|
||||
from = "/docs/guides/nx-cloud/source-control-integration/github"
|
||||
to = "/docs/features/ci-features/github-integration"
|
||||
|
||||
# DOC-549: decisions overview renamed to monorepo-vs-polyrepo
|
||||
[[redirects]]
|
||||
from = "/docs/concepts/decisions/overview"
|
||||
to = "/docs/concepts/decisions/monorepo-vs-polyrepo"
|
||||
|
||||
# DOC-549: why-monorepos renamed to what-is-a-monorepo
|
||||
[[redirects]]
|
||||
from = "/docs/concepts/decisions/why-monorepos"
|
||||
to = "/docs/concepts/decisions/what-is-a-monorepo"
|
||||
|
||||
# DOC-549: maintain-typescript-monorepos merged into the TypeScript introduction
|
||||
[[redirects]]
|
||||
from = "/docs/features/maintain-typescript-monorepos"
|
||||
to = "/docs/technologies/typescript/introduction"
|
||||
|
||||
[[redirects]]
|
||||
from = "/docs/extending-nx/recipes/create-preset"
|
||||
to = "/docs/extending-nx/create-preset"
|
||||
@@ -100,11 +80,6 @@ to = "/docs/extending-nx/create-preset"
|
||||
from = "/docs/guides/adopting-nx/adding-to-monorepos"
|
||||
to = "/docs/guides/adopting-nx/adding-to-monorepo"
|
||||
|
||||
# Turborepo comparison moved to Comparisons section (#36275)
|
||||
[[redirects]]
|
||||
from = "/docs/guides/adopting-nx/nx-vs-turborepo"
|
||||
to = "/docs/guides/comparisons/nx-vs-turborepo"
|
||||
|
||||
# Angular multiple workspace migration page removed (DOC-419)
|
||||
[[redirects]]
|
||||
from = "/docs/technologies/angular/migration/angular-multiple"
|
||||
|
||||
@@ -317,6 +317,10 @@ const learnGroups: SidebarItems = [
|
||||
label: 'Preserving Git histories',
|
||||
link: 'guides/adopting-nx/preserving-git-histories',
|
||||
},
|
||||
{
|
||||
label: 'Nx vs Turborepo',
|
||||
link: 'guides/adopting-nx/nx-vs-turborepo',
|
||||
},
|
||||
{
|
||||
label: 'Migrating from Turborepo',
|
||||
link: 'guides/adopting-nx/from-turborepo',
|
||||
@@ -626,19 +630,19 @@ const knowledgeBaseGroups: SidebarItems = [
|
||||
link: 'guides/tips-n-tricks/yarn-pnp',
|
||||
},
|
||||
{
|
||||
label: 'npm workspaces',
|
||||
label: 'Use npm workspaces with Nx',
|
||||
link: 'guides/tips-n-tricks/npm-workspaces',
|
||||
},
|
||||
{
|
||||
label: 'pnpm workspaces',
|
||||
label: 'Use pnpm workspaces with Nx',
|
||||
link: 'guides/tips-n-tricks/pnpm-workspaces',
|
||||
},
|
||||
{
|
||||
label: 'Yarn workspaces',
|
||||
label: 'Use Yarn workspaces with Nx',
|
||||
link: 'guides/tips-n-tricks/yarn-workspaces',
|
||||
},
|
||||
{
|
||||
label: 'Bun workspaces',
|
||||
label: 'Use Bun workspaces with Nx',
|
||||
link: 'guides/tips-n-tricks/bun-workspaces',
|
||||
},
|
||||
{
|
||||
@@ -754,12 +758,12 @@ const knowledgeBaseGroups: SidebarItems = [
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
label: 'What is a monorepo',
|
||||
link: 'concepts/decisions/what-is-a-monorepo',
|
||||
label: 'Why monorepos',
|
||||
link: 'concepts/decisions/why-monorepos',
|
||||
},
|
||||
{
|
||||
label: 'Monorepo or polyrepo',
|
||||
link: 'concepts/decisions/monorepo-vs-polyrepo',
|
||||
link: 'concepts/decisions/overview',
|
||||
},
|
||||
{
|
||||
label: 'Dependency management',
|
||||
@@ -965,7 +969,7 @@ const knowledgeBaseGroups: SidebarItems = [
|
||||
link: 'guides/tasks--caching/change-cache-location',
|
||||
},
|
||||
{
|
||||
label: 'Self-hosted remote cache',
|
||||
label: 'Self-hosted caching',
|
||||
link: 'guides/tasks--caching/self-hosted-caching',
|
||||
},
|
||||
{
|
||||
@@ -1001,6 +1005,10 @@ const knowledgeBaseGroups: SidebarItems = [
|
||||
label: 'TypeScript',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Maintain TypeScript monorepos',
|
||||
link: 'features/maintain-typescript-monorepos',
|
||||
},
|
||||
...getTechnologyKBItems('typescript'),
|
||||
{
|
||||
label: 'Buildable and publishable libraries',
|
||||
@@ -1086,31 +1094,6 @@ const knowledgeBaseGroups: SidebarItems = [
|
||||
collapsed: true,
|
||||
items: [...getTechnologyKBItems('vitest', 'test-tools')],
|
||||
},
|
||||
{
|
||||
label: 'Comparisons',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Nx vs Turborepo',
|
||||
link: 'guides/comparisons/nx-vs-turborepo',
|
||||
},
|
||||
{ label: 'Nx vs Vite+', link: 'guides/comparisons/nx-vs-vite-plus' },
|
||||
{ label: 'Nx vs Bazel', link: 'guides/comparisons/nx-vs-bazel' },
|
||||
{ label: 'Nx vs Depot', link: 'guides/comparisons/nx-vs-depot' },
|
||||
{
|
||||
label: 'Nx vs Blacksmith',
|
||||
link: 'guides/comparisons/nx-vs-blacksmith',
|
||||
},
|
||||
{
|
||||
label: 'Nx vs Develocity',
|
||||
link: 'guides/comparisons/nx-vs-develocity',
|
||||
},
|
||||
{
|
||||
label: 'Nx vs Buildkite',
|
||||
link: 'guides/comparisons/nx-vs-buildkite',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 178 KiB |
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Folder Structure
|
||||
description: Learn how to organize the projects in your Nx monorepo with grouping folders by scope, and why the structure stays easy to change later.
|
||||
description: Learn about organizing your Nx monorepo with effective folder structures, and how to easily move or remove projects as your organization evolves.
|
||||
filter: 'type:Concepts'
|
||||
---
|
||||
|
||||
@@ -8,15 +8,27 @@ Nx can work with any folder structure you choose, but it is good to have a plan
|
||||
|
||||
Projects are often grouped by _scope_. A project's scope is either the application to which it belongs or (for larger applications) a section within that application.
|
||||
|
||||
## Can you change the folder structure later?
|
||||
## Move generator
|
||||
|
||||
Yes. Don't be too anxious about choosing the exact right structure from the beginning.
|
||||
Moving or renaming a project folder is a regular `mv`, and deleting one is a regular `rm`, the same as in any repository.
|
||||
Tooling doesn't factor into the decision, so pick the structure that helps developers find things, and change it when your organization changes.
|
||||
Don't be too anxious about choosing the exact right folder structure from the beginning. Projects can be moved or renamed using the [`@nx/workspace:move` generator](/docs/reference/workspace/generators#move).
|
||||
|
||||
For instance, if a project under the `booking` folder is now being shared by multiple apps, you can move it to the shared folder like this:
|
||||
|
||||
```shell
|
||||
nx g move --project booking-some-project shared/some-project
|
||||
```
|
||||
|
||||
## Remove generator
|
||||
|
||||
Similarly, if you no longer need a project, you can remove it with the [`@nx/workspace:remove` generator](/docs/reference/workspace/generators#remove).
|
||||
|
||||
```shell
|
||||
nx g remove booking-some-project
|
||||
```
|
||||
|
||||
## Example workspace
|
||||
|
||||
Let's use Acme Airlines as an example organization. This organization has two apps, `booking` and `check-in`. In the Nx workspace, projects related to `booking` are grouped under a `libs/booking` folder, projects related to `check-in` are grouped under a `libs/check-in` folder and projects used in both applications are placed in `libs/shared`. You can also have nested grouping folders, (i.e. `libs/shared/seatmap`).
|
||||
Let's use Nrwl Airlines as an example organization. This organization has two apps, `booking` and `check-in`. In the Nx workspace, projects related to `booking` are grouped under a `libs/booking` folder, projects related to `check-in` are grouped under a `libs/check-in` folder and projects used in both applications are placed in `libs/shared`. You can also have nested grouping folders, (i.e. `libs/shared/seatmap`).
|
||||
|
||||
The purpose of these folders is to help with organizing by scope. We recommend grouping projects together which are (usually) updated together. It helps minimize the amount of time a developer spends navigating the folder tree to find the right file.
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
---
|
||||
title: 'Monorepo vs Polyrepo'
|
||||
description: 'Monorepo vs polyrepo: a side-by-side comparison of code sharing, CI, and ownership tradeoffs, plus a framework for choosing the right repo strategy.'
|
||||
filter: 'type:Concepts'
|
||||
---
|
||||
|
||||
A monorepo keeps many projects in one repository, while a polyrepo splits them across separate repositories.
|
||||
With modern build tooling, the [technical challenges](/docs/concepts/decisions/what-is-a-monorepo) of maintaining a large repository are solvable, so the choice is mostly organizational: how your teams want to share code, review changes, and release.
|
||||
|
||||
## Monorepo vs polyrepo at a glance
|
||||
|
||||
Monorepos optimize for atomic changes and shared tooling, while polyrepos optimize for team isolation.
|
||||
|
||||
| Dimension | Monorepo | Polyrepo |
|
||||
| ------------------- | --------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| Code sharing | Import shared code directly from the same repository | Publish shared code as versioned packages |
|
||||
| Atomic changes | One PR can update an API and every consumer | Cross-project changes span multiple PRs and releases |
|
||||
| Release cadence | Projects build together, artifacts deploy on any schedule | Independent by default, each repository on its own |
|
||||
| CI cost | One pipeline that needs tooling to run only what changed | Many small pipelines, each maintained separately |
|
||||
| Access control | Everyone sees all code, with per-folder ownership rules | Repository-level permissions per team |
|
||||
| Dependency versions | A single version policy is enforceable, but optional | A single policy can't be enforced, versions drift |
|
||||
| Team autonomy | Teams follow shared conventions | Each team picks its own workflow, conventions, and release cadence |
|
||||
|
||||
A monorepo doesn't force everything to release together.
|
||||
Projects build from the same commit, but each build artifact or Docker image carries its own tag, and teams control when a tag reaches production.
|
||||
What a monorepo does force is that every project builds against the latest shared code, so projects stay current with each other - an advantage or a burden depending on what your teams need.
|
||||
A polyrepo makes independent cadences the default because everything is already split.
|
||||
|
||||
## When should you choose a polyrepo?
|
||||
|
||||
Choose a polyrepo when strict repository-level access control is a hard requirement, or when teams share little code and nothing should force them to move together.
|
||||
|
||||
The cost is that sharing code becomes harder and every maintenance task, from dependency upgrades to CI changes, has to be repeated across all the repositories in the organization.
|
||||
|
||||
## Can AI agents work across polyrepos?
|
||||
|
||||
AI agents can work across polyrepos, but an agent working in one repository can't see the consumers of the code it's changing, so cross-repository changes need extra coordination.
|
||||
Agents work best when they can see every project affected by a change, which a monorepo gives them by default.
|
||||
[Meta-harnesses](https://metaharness.tools) like [Polygraph](https://trypolygraph.com/), from the Nx team, give AI agents cross-repository context in polyrepo setups.
|
||||
|
||||
## Organizational decisions
|
||||
|
||||
For teams to work together in a monorepo, they need to agree on how that repository is going to be managed.
|
||||
These questions can be answered in many different ways, but if the developers in the repository can't agree on the answers, then they'll need to work in separate repositories.
|
||||
|
||||
- [Dependency Management](/docs/concepts/decisions/dependency-management) - Should there be an enforced single version policy or should each project maintain their own dependency versions independently?
|
||||
- [Code Ownership](/docs/concepts/decisions/code-ownership) - What is the code review process? Who is responsible for reviewing changes to each portion of the repository?
|
||||
- [Project Dependency Rules](/docs/concepts/decisions/project-dependency-rules) - What are the restrictions on dependencies between projects? Which projects can depend on which other projects?
|
||||
- [Folder Structure](/docs/concepts/decisions/folder-structure) - What is the folder structure and naming convention for projects in the repository?
|
||||
- Git Workflow - What Git workflow should be used? Will you use trunk-based development or long running feature branches?
|
||||
- CI Pipeline - How is the CI pipeline managed? Who is responsible for maintaining it?
|
||||
- Deployment - How are deployments managed? Does each project deploy independently or do they all deploy at once?
|
||||
|
||||
## How many repositories?
|
||||
|
||||
Once you have a good understanding of where people stand on these questions, you'll need to choose between one of the following setups:
|
||||
|
||||
### One monorepo to rule them all
|
||||
|
||||
If everyone can agree on how to run the repository, a single monorepo provides the most benefit.
|
||||
Every project can share code, and maintenance tasks and anything else that needs coordination can be performed in one PR for the entire organization.
|
||||
|
||||
Nx addresses the technical challenges of a repository this size, so the limiting factors in how large it grows are organizational rather than technical.
|
||||
Once the repository scales to hundreds of developers, you need to take proactive steps to ensure that your decisions about code review and [project dependency restrictions](/docs/features/enforce-module-boundaries) do not inhibit the velocity of your teams.
|
||||
Also, any shared code and tooling (like the CI pipeline or a shared component library) need to be maintained by a dedicated team to help everyone in the monorepo.
|
||||
|
||||
### Polyrepos - a repository for each project
|
||||
|
||||
If every project is placed in its own repository, each team can make their own organizational decisions without the need to consult with other teams.
|
||||
Unfortunately, this also means that each team has to make their own organizational decisions instead of focusing on feature work that provides business value.
|
||||
|
||||
Nx can still be useful with this organizational structure.
|
||||
Tooling and maintenance tasks can be centralized through shared [Nx plugins](/docs/concepts/nx-plugins) that each repository can opt-in to using.
|
||||
Since creating repositories is a frequent occurrence in this scenario, Nx [generators](/docs/features/generate-code) can be used to quickly scaffold out the repository with reasonable tooling defaults.
|
||||
|
||||
Polyrepos also fragment the context AI agents work with.
|
||||
The [Polygraph](https://trypolygraph.com/) meta-harness addresses this by linking your repositories into a synthetic monorepo, so one agent session works across all of them with shared context and coordinated changes.
|
||||
|
||||
### Multiple monorepos
|
||||
|
||||
Somewhere between the single monorepo and the full polyrepo solutions exists the multiple monorepo setup.
|
||||
The split usually follows a real boundary: an internal codebase next to a public open source one, product lines in different domains that share no code, or groups of teams that answer the organizational questions above differently.
|
||||
Each monorepo is configured in the way that best suits the teams working in it.
|
||||
|
||||
Compared to a single monorepo, this setup adds overhead: multiple CI pipelines and repeated tooling maintenance.
|
||||
In exchange, each team works in a repository optimized for how they work.
|
||||
|
||||
[Polygraph](https://trypolygraph.com/) helps with the multiple monorepo setup by linking the repositories and treating them as one, so work and AI agent sessions can span all of them.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: 'Monorepo vs Polyrepo: How to Choose'
|
||||
description: Monorepo vs polyrepo comes down to how your teams agree to manage code, dependencies, ownership, and CI, not just technical limits.
|
||||
filter: 'type:Concepts'
|
||||
---
|
||||
|
||||
A monorepo keeps many projects in one repository, while a polyrepo splits them across separate ones. Both work well with Nx, so choosing between them is mostly an organizational decision rather than a technical one. Nx and Nx Cloud address the [technical challenges](/docs/concepts/decisions/why-monorepos) of maintaining a large monorepo, so the limiting factors in how large your monorepo grows are interpersonal.
|
||||
|
||||
In order for teams to work together in a monorepo, they need to agree on how that repository is going to be managed. These questions can be answered in many different ways, but if the developers in the repository can't agree on the answers, then they'll need to work in separate repositories.
|
||||
|
||||
**Organizational Decisions:**
|
||||
|
||||
- [Dependency Management](/docs/concepts/decisions/dependency-management) - Should there be an enforced single version policy or should each project maintain their own dependency versions independently?
|
||||
- [Code Ownership](/docs/concepts/decisions/code-ownership) - What is the code review process? Who is responsible for reviewing changes to each portion of the repository?
|
||||
- [Project Dependency Rules](/docs/concepts/decisions/project-dependency-rules) - What are the restrictions on dependencies between projects? Which projects can depend on which other projects?
|
||||
- [Folder Structure](/docs/concepts/decisions/folder-structure) - What is the folder structure and naming convention for projects in the repository?
|
||||
- [Project Size](/docs/concepts/decisions/project-size) - What size should projects be before they need to be split into separate projects?
|
||||
- Git Workflow - What Git workflow should be used? Will you use trunk-based development or long running feature branches?
|
||||
- CI Pipeline - How is the CI pipeline managed? Who is responsible for maintaining it?
|
||||
- Deployment - How are deployments managed? Does each project deploy independently or do they all deploy at once?
|
||||
|
||||
## How many repositories?
|
||||
|
||||
Once you have a good understanding of where people stand on these questions, you'll need to choose between one of the following setups:
|
||||
|
||||
### One monorepo to rule them all
|
||||
|
||||
If everyone can agree on how to run the repository, having [a single monorepo will provide a lot of benefits](/docs/concepts/decisions/why-monorepos). Every project can share code and maintenance tasks can be performed in one PR for the entire organization. Any task that involves coordination becomes much easier.
|
||||
|
||||
Once the repository scales to hundreds of developers, you need to take proactive steps to ensure that your decisions about [code review](/docs/concepts/decisions/code-ownership) and [project dependency restrictions](/docs/features/enforce-module-boundaries) do not inhibit the velocity of your teams. Also, any shared code and tooling (like the CI pipeline or a shared component library) need to be maintained by a dedicated team to help everyone in the monorepo.
|
||||
|
||||
### Polyrepos - a repository for each project
|
||||
|
||||
If every project is placed in its own repository, each team can make their own organizational decisions without the need to consult with other teams. Unfortunately, this also means that each team has to make their own organizational decisions instead of focusing on feature work that provides business value. Sharing code is difficult with this set up and every maintenance task needs to be repeated across all the repositories in the organization.
|
||||
|
||||
Nx can still be useful with this organizational structure. Tooling and maintenance tasks can be centralized through shared [Nx plugins](/docs/concepts/nx-plugins) that each repository can opt-in to using. Since creating repositories is a frequent occurrence in this scenario, Nx [generators](/docs/features/generate-code) can be used to quickly scaffold out the repository with reasonable tooling defaults.
|
||||
|
||||
### Multiple monorepos
|
||||
|
||||
Somewhere between the single monorepo and the full polyrepo solutions exists the multiple monorepo setup. Typically when there are disagreements about organizational decisions, there are two or three factions that form. These factions can naturally be allocated to separate monorepos that have been configured in a way that best suits the teams that will be working in them.
|
||||
|
||||
Compared to the single monorepo setup, this setup requires some extra overhead cost - maintaining multiple CI pipelines and performing the same tooling maintenance tasks on multiple repositories, but this cost could be offset by the extra productivity boost provided by the fact that each team can work in a repository that is optimized for the way that they work.
|
||||
@@ -1,90 +0,0 @@
|
||||
---
|
||||
title: 'What is a Monorepo?'
|
||||
description: 'A monorepo is a single repository containing multiple projects. Learn the benefits, how it differs from a monolith, and why AI agents work better in one.'
|
||||
filter: 'type:Concepts'
|
||||
---
|
||||
|
||||
A **monorepo** is a single repository containing multiple distinct projects, with well-defined relationships between them.
|
||||
The projects can be applications, libraries, or tools, and they can use the same stack or different ones.
|
||||
A monorepo is not a monolith: each project inside it stays separate and can be built, tested, and deployed on its own.
|
||||
|
||||
The "well-defined relationships" part matters.
|
||||
Putting many projects in one repository is easy, but without tooling that understands how they depend on each other, the repository gets slower and messier as it grows.
|
||||
Whether one repository or many is right for your organization is a separate decision, covered in [Monorepo vs polyrepo](/docs/concepts/decisions/monorepo-vs-polyrepo).
|
||||
|
||||
## What are the benefits of a monorepo?
|
||||
|
||||
- **Shared code without publishing overhead** - Sharing a library is as simple as creating a folder: no versioned packages to publish and no waiting for consumers to upgrade. Reuse validation logic, UI components, and types across your entire organization, including between the backend and the frontend.
|
||||
|
||||
- **Single source of truth** - Common code lives in one place, so every project sees what already exists instead of rebuilding it, and a bug is fixed once for every consumer.
|
||||
|
||||
- **Atomic changes** - Change a server API and every application that consumes it in the same commit. There's no coordinating a chain of pull requests across repositories, and no window where consumers are broken.
|
||||
|
||||
- **Enforceable conventions at scale** - Lint rules, code style, and dependency policy apply everywhere automatically, so consistency is the default instead of a per-repository chore.
|
||||
|
||||
- **Developer mobility** - Build and test every project the same way, regardless of the tools it uses. Developers can contribute to another team's application and verify their changes are safe without learning a new setup.
|
||||
|
||||
- **Single set of dependencies** - [Keep every project on the same version of each third-party dependency](/docs/concepts/decisions/dependency-management). The policy is optional, but a monorepo is the only place you can enforce it. Less actively developed applications still get framework and tooling updates instead of falling years behind.
|
||||
|
||||
## Is a monorepo the same as a monolith?
|
||||
|
||||
No. A monolith is a single application that ships as one deployable unit.
|
||||
A monorepo is a version-control strategy: one repository holding many projects that are built, tested, and deployed independently.
|
||||
Teams often adopt a monorepo specifically to break a monolith into smaller projects while keeping all the code in one place.
|
||||
|
||||
## Monorepo examples
|
||||
|
||||
Some of the largest codebases in the world are monorepos:
|
||||
|
||||
- **Google** keeps most of its code in one repository, described in the 2016 ACM paper "Why Google Stores Billions of Lines of Code in a Single Repository."
|
||||
- **Meta** develops its main products in a monorepo and built the Sapling version control system to keep it fast at that scale.
|
||||
- **Microsoft** hosts the Windows codebase in one of the largest Git repositories in existence and built dedicated tooling (VFS for Git, later Scalar) to keep cloning and fetching it practical.
|
||||
|
||||
You don't need to be at that scale to benefit: shared code, atomic changes, and one set of dependencies apply just as well to a repository with five projects.
|
||||
|
||||
## Why isn't code collocation enough?
|
||||
|
||||
A naive implementation of a monorepo is code collocation: combining the code from multiple repositories into one without adding any tooling.
|
||||
Large companies that use monorepos don't just put all the code in one place, and without tooling to coordinate everything, problems arise as the repository grows:
|
||||
|
||||
- **Running unnecessary tests** - CI runs every test in the repository on every change to make sure nothing breaks, including tests for projects the change can't possibly affect.
|
||||
|
||||
- **No code boundaries** - Any team can change or depend on any code, including code you intended to keep private. Once another project depends on your internals, you can't change them without breaking that project.
|
||||
|
||||
- **Inconsistent tooling** - Each project keeps its own commands for testing, building, serving, and linting. Every project switch means relearning how to run things.
|
||||
|
||||
Package manager workspaces ([npm](/docs/guides/tips-n-tricks/npm-workspaces), [pnpm](/docs/guides/tips-n-tricks/pnpm-workspaces), [Yarn](/docs/guides/tips-n-tricks/yarn-workspaces), and [Bun](/docs/guides/tips-n-tricks/bun-workspaces)) solve a different problem: they install and link local packages so projects can import each other.
|
||||
They don't build a task graph, cache results, or detect which projects a change affects.
|
||||
Lerna adds versioning and publishing on top, and modern Lerna delegates task running to Nx.
|
||||
|
||||
## Why do AI coding agents work better in monorepos?
|
||||
|
||||
AI coding agents get more from a monorepo than from scattered repositories:
|
||||
|
||||
- **Full context** - Agents read the actual source of every project they touch and navigate directly between the frontend, backend, and libraries, instead of guessing against out-of-date API specs in other repositories.
|
||||
|
||||
- **A queryable project graph** - Tooling exposes which projects exist, how they relate, and what tasks run, so agents don't burn tokens exploring the repository.
|
||||
|
||||
- **Tight feedback loops** - Affected-only task runs and caching verify every agent change quickly.
|
||||
|
||||
- **Guardrails** - Module boundary rules keep agent-written code inside your architecture, and generators produce consistent, convention-matching scaffolding.
|
||||
|
||||
- **CI that keeps up** - As agents raise PR volume, distributed task execution and self-healing CI keep the pipeline from becoming the bottleneck.
|
||||
|
||||
To learn how Nx gives agents this context and these guardrails, see [Enhance your AI coding agent](/docs/features/enhance-ai).
|
||||
To configure your Nx workspace for AI agents, see [AI setup](/docs/getting-started/ai-setup).
|
||||
|
||||
## How Nx makes a monorepo scale
|
||||
|
||||
Without graph-aware tooling, a monorepo makes CI slower, not faster, because every pipeline runs every task.
|
||||
Nx adds the layer that solves those collocation problems and keeps the repository fast as it grows:
|
||||
|
||||
- **Affected detection** - [Run tasks only for the projects impacted by a change](/docs/features/ci-features/affected) instead of rebuilding and retesting everything on every commit.
|
||||
|
||||
- **Caching** - [Cache task results](/docs/features/cache-task-results) locally and remotely, so no one on your team or in CI rebuilds something that has already been built.
|
||||
|
||||
- **Module boundaries** - [Enforce which projects can depend on which](/docs/features/enforce-module-boundaries), so private code stays private and your architecture survives contact with deadlines.
|
||||
|
||||
- **Code generation** - [Generate projects and components](/docs/features/generate-code) that follow your organization's conventions instead of documenting a seven-step setup in a `README`.
|
||||
|
||||
- **Nx Cloud** - Distribute tasks across machines in CI, [detect and re-run flaky tasks](/docs/features/ci-features/flaky-tasks), fix failing tasks with [self-healing CI](/docs/features/ci-features/self-healing-ci), and run AI agents in [isolated sandboxes](/docs/features/ci-features/sandboxing).
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Monorepos
|
||||
description: Understand the benefits of monorepos including shared code, atomic changes, developer mobility, and consistent dependencies across your organization.
|
||||
filter: 'type:Concepts'
|
||||
---
|
||||
|
||||
A monorepo is a single git repository that holds the source code for multiple applications and libraries, along with the tooling for them.
|
||||
|
||||
## What are the benefits of a monorepo?
|
||||
|
||||
- **Shared code and visibility** - [Keeps your code DRY across your entire organization.](/docs/concepts/decisions/code-ownership) Reuse validation code, UI components, and types across the codebase. Reuse code between the backend, the frontend, and utility libraries.
|
||||
|
||||
- **Atomic changes** - Change a server API and modify the downstream applications that consume that API in the same commit. You can change a button component in a shared library and the applications that use that component in the same commit. A monorepo saves the pain of trying to coordinate commits across multiple repositories.
|
||||
|
||||
- **Developer mobility** - Get a consistent way of building and testing applications written using different tools and technologies. Developers can confidently contribute to other teams' applications and verify that their changes are safe.
|
||||
|
||||
- **Single set of dependencies** - [Use a single version of all third-party dependencies](/docs/concepts/decisions/dependency-management), reducing inconsistencies between applications. Less actively developed applications are still kept up-to-date with the latest version of a framework, library, or build tool.
|
||||
|
||||
## Why not just code collocation?
|
||||
|
||||
A naive implementation of a monorepo is code collocation, where you combine all the code from multiple repositories into the same repo. Many large companies that use monorepos don't "simply" put all the code in one place. **That's not enough**. Without adequate tooling to coordinate everything, problems arise with simply collocating code.
|
||||
|
||||
- **Running unnecessary tests** - All tests in the entire repository run to ensure nothing breaks from a given change. Even code in projects that are unrelated to the actual change.
|
||||
|
||||
- **No code boundaries** - Bugs and inconsistencies are added by a developer from another team changing code in your project. Or worse, another team uses code that you only intended for private use in their application. Now another project code depends on it, keeping you from making changes that may break their application.
|
||||
|
||||
- **Inconsistent tooling** - Each project uses its own set of commands for running tests, building, serving, linting, deploying, and so forth. Inconsistency creates mental overhead remembering which commands to use from project to project.
|
||||
|
||||
Tools like Lerna and Yarn Workspaces help optimize the installation of node modules, but they **do not** enable Monorepo-style development. In other words, they solve an orthogonal problem and can even be used in combination with Nx. Read more on it [here](https://blog.nrwl.io/why-you-should-switch-from-lerna-to-nx-463bcaf6821).
|
||||
|
||||
## Nx + code collocation = monorepo
|
||||
|
||||
Nx provides tools to give you the benefits of a monorepo without the drawbacks of simple code collocation.
|
||||
|
||||
### Scaling your monorepo with Nx
|
||||
|
||||
- **Consistent Command Execution** - Executors allow for consistent commands to test, serve, build, and lint each project using various tools.
|
||||
|
||||
- **Consistent Code Generation** - Generators allow you to customize and standardize organizational conventions and structure, removing the need to perform the same manual setup tasks repetitively.
|
||||
|
||||
- **Affected Commands** - [Nx affected commands](/docs/reference/nx-commands#nx-affected) analyze your source code, the context of the changes, and only runs tasks on the affected projects impacted by the source code changes.
|
||||
|
||||
- **Remote Caching** - Nx provides local caching and support for remote caching of command executions. With remote caching, when someone on your team runs a command, everyone else gets access to those artifacts to speed up their command executions, bringing them down from minutes to seconds. Nx helps you scale your development to massive applications and libraries even more with distributed task execution and incremental builds.
|
||||
|
||||
### Scaling your organization with Nx
|
||||
|
||||
- **Controlled Code Sharing** - While sharing code becomes much easier to share, there should also be constraints of when and how code should be depended on. Libraries are defined with specific enforced APIs. Rules should be put in place to define which libraries can depend on each other. Also, even though everyone has access to the repo does not mean that anyone should change any project. Projects should have owners such that changes to that project requires their approval. This can be defined using a `CODEOWNERS` file.
|
||||
|
||||
- **Consistent Code Generation** - Generators allow you to automate code creation and modification tasks. Instead of writing a 7 steps guide in a readme file, you can create a generator to prompt the developer for inputs and modify the code directly. Nrwl provides plugins containing useful executors and generators for many popular tools. Also, Nx workspaces are extended further through a growing number of community-provided plugins.
|
||||
|
||||
- **Accurate Architecture Diagram** - Most architecture diagrams become obsolete in an instant. And every diagram becomes out of date as soon as the code changes. Because Nx understands your code, it generates an up-to-date and accurate diagram of how projects depend on each other. The Nx project dependencies are also pluggable to extend to other programming languages and ecosystems.
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
...
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
filter: tree:0
|
||||
@@ -289,9 +289,9 @@ jobs:
|
||||
- run: pnpm dlx nx start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build"
|
||||
|
||||
# Cache node_modules
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
...
|
||||
|
||||
|
||||
@@ -1,136 +1,70 @@
|
||||
---
|
||||
title: GitHub Actions integration
|
||||
description: 'Speed up GitHub Actions for your monorepo with Nx: run only affected projects, share a remote cache, and add PR insights and self-healing CI with Nx Cloud.'
|
||||
title: GitHub Integration
|
||||
description: Connect Nx Cloud with GitHub for seamless onboarding, PR insights, and access control
|
||||
sidebar:
|
||||
order: 18
|
||||
filter: 'type:Features'
|
||||
---
|
||||
|
||||
GitHub Actions runs every job on every push by default, so CI time in a monorepo grows with the
|
||||
size of the repository.
|
||||
Nx keeps GitHub Actions fast by running tasks only for the projects affected by each pull request,
|
||||
restoring unchanged results from a remote cache, and distributing the remaining work across
|
||||
machines.
|
||||
Any CI tool requires tight integration with your existing version control system. Nx Cloud offers first class integration with GitHub in the following ways.
|
||||
|
||||
## How does Nx speed up GitHub Actions?
|
||||
## Easy workspace setup
|
||||
|
||||
Nx speeds up GitHub Actions in three layers:
|
||||

|
||||
|
||||
- `nx affected` runs lint, test, and build only for projects impacted by a pull request.
|
||||
- Nx Cloud restores results that were already computed from a remote cache.
|
||||
- Nx Agents distribute what's left across multiple machines.
|
||||
Get started quickly with Nx Cloud with our GitHub connection process. Connect your workspace by selecting your repo and organization from GitHub, and Nx Cloud will create a pull request with all the necessary configuration. User access is automatically connected to GitHub, and a PR is created to connect your workspace. Your repo now has [distributed caching](/docs/features/ci-features/remote-cache) in less than 5 minutes.
|
||||
|
||||
Affected runs work without an Nx Cloud account, so you can adopt each layer separately.
|
||||
You can also create a new workspace from a template for experimentation. This workspace will come pre-configured with Nx Cloud and examples of core Nx concepts. Run `npx create-nx-workspace@latest` and choose a template to get started.
|
||||
|
||||
## Run Nx on GitHub Actions
|
||||
[Connect your Nx Cloud account to GitHub](/docs/features/ci-features/github-integration#connect-to-github) to use this feature.
|
||||
|
||||
A complete GitHub Actions workflow for an Nx monorepo:
|
||||
## Pull request insights
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci.yml
|
||||
name: CI
|
||||

|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
Good CI checks require fast and easy access to results. That's why Nx Cloud will update your PR with the current running status of your tasks and a convenient link to your Nx Cloud results and logs. Take advantage of the enhanced developer experience of structured and searchable logs. Quick insight to PR task progress, so you're not stuck waiting for every task to complete. And with Nx Replay, developers can quickly replay tasks locally to avoid running tasks that CI has already completed.
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
This feature is available in workspaces with the [Nx Cloud GitHub App installed](/docs/guides/nx-cloud/source-control-integration/github#install-the-app).
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
filter: tree:0
|
||||
fetch-depth: 0
|
||||
## Access control
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||

|
||||
|
||||
- run: npm ci
|
||||
Nx Cloud organization access can be linked to a Github organization, so that memberships are automatically synced. This allows Nx Cloud to fit in to any existing on-boarding or off-boarding process. There's no need to manually manage users separately. Get your engineers Nx Cloud access right alongside their GitHub access so they can get to work fast. Use [personal access tokens](/docs/guides/nx-cloud/personal-access-tokens) to further enhance your security.
|
||||
|
||||
- uses: nrwl/nx-set-shas@v5
|
||||
[Connect your Nx Cloud account to GitHub](/docs/features/ci-features/github-integration#connect-to-github) to use this feature. Members of your GitHub organization will also need to connect their GitHub accounts to access the organization.
|
||||
|
||||
- run: npx nx affected -t lint test build
|
||||
```
|
||||
## Connect to GitHub
|
||||
|
||||
This workflow works without Nx Cloud.
|
||||
To generate it instead of writing it by hand, run `nx g @nx/workspace:ci-workflow --ci=github`.
|
||||
For a walkthrough of each step, see [setting up CI](/docs/getting-started/setup-ci).
|
||||
Get started by connecting your Nx Cloud account to GitHub. This will allow you to access GitHub-powered organizations that you're a member of, easily connect workspaces, and configure automatic access control through GitHub.
|
||||
|
||||
## Run only affected projects in GitHub Actions
|
||||
|
||||
Use `nx affected` instead of `nx run-many` in CI, and Nx compares your changes against a base
|
||||
commit to skip projects that couldn't have been broken.
|
||||
Two pieces of the workflow above make this work:
|
||||
|
||||
- `fetch-depth: 0` on the checkout step gives Nx access to the full git history, which it needs
|
||||
to compute the changed file set.
|
||||
- `nrwl/nx-set-shas@v5` sets the `NX_BASE` and `NX_HEAD` environment variables that `nx affected`
|
||||
reads.
|
||||
On a pull request, the base is the branch you're merging into.
|
||||
On a push to `main`, the action sets `NX_BASE` to the commit of the last successful workflow
|
||||
run, so commits that land while CI is red still get verified.
|
||||
|
||||
## Add remote caching and task distribution
|
||||
|
||||
Affected pruning skips projects that didn't change.
|
||||
[Remote caching](/docs/features/ci-features/remote-cache) goes further by reusing results for
|
||||
tasks whose inputs are identical to an earlier run, whether that run happened in CI or on a
|
||||
teammate's machine.
|
||||
Connect your workspace by running this command:
|
||||
|
||||
```shell
|
||||
npx nx connect
|
||||
```
|
||||
|
||||
Or follow the [Nx Cloud getting started guide](/docs/getting-started/nx-cloud).
|
||||
|
||||
Once connected, one extra line
|
||||
[distributes tasks across multiple machines](/docs/features/ci-features/distribute-task-execution),
|
||||
and `npx nx fix-ci` lets self-healing CI propose fixes when tasks fail.
|
||||
Run `start-ci-run` as early as possible, after checkout but before dependencies are installed:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci.yml
|
||||
# ... checkout step as above
|
||||
|
||||
- run: npx nx start-ci-run --distribute-on="3 linux-medium-js"
|
||||
|
||||
# ... install and nx-set-shas steps as above
|
||||
|
||||
- run: npx nx affected -t lint test build
|
||||
- run: npx nx fix-ci
|
||||
if: always()
|
||||
```
|
||||
|
||||
You can also enable [task sandboxing](/docs/features/ci-features/sandboxing) to run each
|
||||
distributed task in an isolated sandbox, and track per-agent CPU and memory with
|
||||
[resource usage](/docs/features/ci-features/resource-usage) to right-size your agents.
|
||||
|
||||
## What Nx adds to your GitHub PRs
|
||||
|
||||
With the Nx Cloud GitHub App installed, every pull request gets:
|
||||
|
||||

|
||||
|
||||
- A comment with the live status of each task in the run, updated as tasks complete, so you see
|
||||
which check failed without waiting for the whole workflow to finish.
|
||||
- Links to structured, searchable logs for every task instead of one raw CI log.
|
||||
- Links to each run in Nx Cloud, where you can rerun a command locally and pull the outputs CI
|
||||
already computed with Nx Replay instead of recomputing them.
|
||||
- Proposed fixes from [self-healing CI](/docs/features/ci-features/self-healing-ci) when a task
|
||||
fails, which you can review and apply directly from the PR.
|
||||
|
||||
## Connect your repository
|
||||
|
||||
{% call_to_action title="Get started with Nx Cloud" url="https://cloud.nx.app/get-started/" icon="nxcloud" description="Connect your repository to Nx Cloud" %}
|
||||
Get started with Nx Cloud
|
||||
{% call_to_action title="Connect to GitHub" url="https://cloud.nx.app/profile/vcs-integrations" icon="nxcloud" description="Connect your Nx Cloud account to GitHub in your profile settings" %}
|
||||
Connect to GitHub
|
||||
{% /call_to_action %}
|
||||
|
||||
Note that it doesn't matter what method you use to log into Nx Cloud, connecting your GitHub account is a separate step.
|
||||
|
||||
## Connect to GitHub during initial setup
|
||||
|
||||
1. Visit [Nx Cloud](https://cloud.nx.app) and click **Connect a workspace** at the top.
|
||||
2. Select **Connect existing repository** from the dropdown.
|
||||
3. Follow the prompts to select a repo.
|
||||
4. If that repo is controlled by a GitHub organization, you will be prompted to use that organization.
|
||||
5. Follow the prompts to create a pull request to complete your connection to Nx Cloud.
|
||||
|
||||
{% call_to_action title="Connect a workspace to Nx Cloud" url="https://cloud.nx.app/setup/connect-workspace/github/select" icon="nxcloud" description="Connect an Nx workspace in GitHub to Nx Cloud" %}
|
||||
Connect an Nx workspace in GitHub to Nx Cloud
|
||||
{% /call_to_action %}
|
||||
|
||||
## Connect an organization to GitHub after initial setup
|
||||
|
||||
If you already have an organization in Nx Cloud, and you'd like to use your GitHub organization to manage access to it:
|
||||
|
||||
1. Go to the organization in Nx Cloud while logged in as an admin user.
|
||||
2. Click on **Settings** in the top menu
|
||||
3. Go to **Connect GitHub organization in the sidebar**
|
||||
4. Follow the prompts there to connect to GitHub. Note that for every workspace in the Nx Cloud organization, there must be a corresponding repo in the GitHub organization.
|
||||
|
||||
## Connect a workspace to GitHub after initial setup
|
||||
|
||||
If you already have a workspace connected to Nx Cloud, and you'd like to connect it to a GitHub repo to enable PR insights, [install the Nx Cloud GitHub App](/docs/guides/nx-cloud/source-control-integration/github#install-the-app).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 'Resource Usage'
|
||||
description: 'Upload and view CPU and memory metrics for your CI runs to find bottlenecks, debug out-of-memory errors, and right-size your agents.'
|
||||
description: 'Upload and view per-agent CPU and memory metrics for distributed task execution to find bottlenecks, debug out-of-memory errors, and right-size your agents.'
|
||||
keywords:
|
||||
[
|
||||
resource usage,
|
||||
@@ -19,9 +19,10 @@ sidebar:
|
||||
filter: 'type:Features'
|
||||
---
|
||||
|
||||
The resource usage add-on records CPU and memory metrics while your tasks run and surfaces them in
|
||||
Nx Cloud. Use this data to find resource bottlenecks, debug out-of-memory (OOM) errors, and pick the
|
||||
right agent size for your workload, all the way down to which task caused a spike.
|
||||
The resource usage add-on records per-agent CPU and memory metrics during distributed task
|
||||
execution and surfaces them in Nx Cloud. Use this data to find resource bottlenecks, debug
|
||||
out-of-memory (OOM) errors, and pick the right agent size for your workload, all the way down to
|
||||
which task caused a spike.
|
||||
|
||||
{% aside type="note" title="Nx Cloud add-on" %}
|
||||
Resource usage is a standalone Nx Cloud add-on. Enable it under [**Settings > Add-ons**](https://cloud.nx.app/go/organization/add-ons) for your
|
||||
@@ -37,15 +38,13 @@ Resource usage requires Nx 22.1 or later.
|
||||
Enable the add-on under [**Settings > Add-ons**](https://cloud.nx.app/go/organization/add-ons), or from the **Enable resource profiling** prompt on
|
||||
the **Analysis** tab of any CI pipeline execution.
|
||||
|
||||
Once the add-on is active, metrics are collected and uploaded automatically, whether you distribute
|
||||
your tasks with [Nx Agents](/docs/features/ci-features/distribute-task-execution) or run them on a
|
||||
single machine. There's nothing else to configure.
|
||||
Once the add-on is active:
|
||||
|
||||
{% aside type="note" title="Bringing your own compute" %}
|
||||
If you [bring your own compute](/docs/guides/nx-cloud/bring-your-own-compute) and run the agents on
|
||||
your own CI, add a single CLI step per agent job to upload metrics. See
|
||||
[the section below](#resource-metrics-when-you-bring-your-own-compute).
|
||||
{% /aside %}
|
||||
- **With [Nx Agents](/docs/features/ci-features/distribute-task-execution) on Nx Cloud compute**, metrics are collected
|
||||
and uploaded automatically for every agent and task. There's nothing else to configure.
|
||||
- **When you [bring your own compute](/docs/guides/nx-cloud/bring-your-own-compute)** (running the agents on your own CI),
|
||||
add a single CLI step per agent job to upload metrics. See
|
||||
[the section below](#resource-metrics-when-you-bring-your-own-compute).
|
||||
|
||||
When a CI pipeline execution doesn't yet have the add-on, Nx Cloud shows a preview with sample data
|
||||
and a prompt to enable it, including a note on the
|
||||
@@ -54,11 +53,9 @@ CPU issues.
|
||||
|
||||
## Viewing resource usage
|
||||
|
||||
### Runs with Nx Agents
|
||||
|
||||
Open any CI pipeline execution and go to the **Analysis** tab.
|
||||
|
||||
#### Agent resource usage summary
|
||||
### Agent resource usage summary
|
||||
|
||||
The **Agent resource usage** table lists every agent in the run with its average and maximum CPU and
|
||||
memory, plus the machine specs (cores and RAM) of its resource class. It's the fastest way to spot an
|
||||
@@ -66,7 +63,7 @@ agent that ran hot.
|
||||
|
||||

|
||||
|
||||
#### Resource usage over time
|
||||
### Resource usage over time
|
||||
|
||||
Click an agent to open its **Resource usage over time** view. Separate memory and CPU charts plot
|
||||
utilization across the agent's lifetime, with reference lines for the machine's capacity and peak
|
||||
@@ -95,13 +92,6 @@ The detail view has a few controls for digging in:
|
||||
|
||||
- **Download CSV** - export the raw per-process data for deeper analysis.
|
||||
|
||||
### Runs without Nx Agents
|
||||
|
||||
For a run that isn't distributed on Nx Agents, open the run details and go to the **Resource usage**
|
||||
tab.
|
||||
|
||||

|
||||
|
||||
## Common use cases
|
||||
|
||||
- **Find memory-hungry tasks** - figure out which project eats the most memory when running in
|
||||
|
||||
@@ -448,20 +448,20 @@ jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
filter: tree:0
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- run: pnpm dlx nx start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="e2e-ci"
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
---
|
||||
title: 'Maintain TypeScript Monorepos'
|
||||
description: 'Learn how Nx simplifies TypeScript monorepo maintenance by auto-configuring tools, managing project references, and enhancing tooling for better monorepo support.'
|
||||
sidebar:
|
||||
order: 6
|
||||
filter: 'type:Features'
|
||||
---
|
||||
|
||||
Keeping all the industry-standard tools involved in a large TypeScript monorepo correctly configured and working well together is a difficult task. And the more tools you add, the more opportunity there is for tools to conflict with each other in some way.
|
||||
|
||||
In addition to [generating default configuration files](/docs/features/generate-code) and [automatically updating dependencies](/docs/features/automate-updating-dependencies) to versions that we know work together, Nx makes managing all the tools in your monorepo easier in two ways:
|
||||
|
||||
- Rather than adding another tool that you have to configure, Nx configures itself to match the existing configuration of other tools.
|
||||
- Nx also enhances certain tools to be more usable in a monorepo context.
|
||||
|
||||
## Auto-configuration
|
||||
|
||||
Whenever possible, Nx will detect the existing configuration settings of other tools and update itself to match.
|
||||
|
||||
### Project detection with workspaces
|
||||
|
||||
If your repository is using package manager workspaces, Nx will use those settings to find all the [projects](/docs/reference/project-configuration) in your repository. So you don't need to define a project for your package manager and separately identify the project for Nx. The `workspaces` configuration allows Nx to detect the project graph.
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"workspaces": ["apps/*", "packages/*"]
|
||||
}
|
||||
```
|
||||
|
||||
{% graph height="200px" title="Project View" %}
|
||||
|
||||
```json
|
||||
{
|
||||
"composite": false,
|
||||
"projects": [
|
||||
{
|
||||
"name": "product-state",
|
||||
"type": "lib",
|
||||
"data": {
|
||||
"root": "packages/cart/product-state",
|
||||
"tags": ["scope:cart", "type:state"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ui-buttons",
|
||||
"type": "lib",
|
||||
"data": {
|
||||
"root": "packages/ui/buttons",
|
||||
"tags": ["scope:shared", "type:ui"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cart",
|
||||
"type": "app",
|
||||
"data": {
|
||||
"root": "apps/cart",
|
||||
"tags": ["type:app", "scope:cart"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"product-state": [],
|
||||
"ui-buttons": [],
|
||||
"cart": [
|
||||
{ "source": "cart", "target": "product-state", "type": "static" },
|
||||
{ "source": "cart", "target": "ui-buttons", "type": "static" }
|
||||
]
|
||||
},
|
||||
"workspaceLayout": {
|
||||
"appsDir": "apps",
|
||||
"libsDir": "libs"
|
||||
},
|
||||
"affectedProjectIds": [],
|
||||
"focus": null,
|
||||
"groupByFolder": false,
|
||||
"exclude": [],
|
||||
"enableTooltips": false
|
||||
}
|
||||
```
|
||||
|
||||
{% /graph %}
|
||||
|
||||
### Inferred tasks with tooling plugins
|
||||
|
||||
Nx [plugins](/docs/concepts/nx-plugins) for tools like Vite, TypeScript, Playwright, and Jest automatically [infer task configuration](/docs/concepts/inferred-tasks) from your existing tooling config files — keeping them as the single source of truth.
|
||||
|
||||
In the example below, because the `/apps/cart/vite.config.ts` file exists, Nx knows that the `cart` project can run a `build` task using Vite. If you expand the `build` task, you can also see that Nx configured the output directory for the [cache](/docs/features/cache-task-results) to match the `build.outDir` provided in the Vite configuration file.
|
||||
|
||||
```ts
|
||||
// /apps/cart/vite.config.ts
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/apps/cart',
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: './dist',
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
{% project_details %}
|
||||
|
||||
```json
|
||||
{
|
||||
"project": {
|
||||
"name": "cart",
|
||||
"type": "app",
|
||||
"data": {
|
||||
"root": "apps/cart",
|
||||
"targets": {
|
||||
"build": {
|
||||
"options": {
|
||||
"cwd": "apps/cart",
|
||||
"command": "vite build"
|
||||
},
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": [
|
||||
"production",
|
||||
"^production",
|
||||
{
|
||||
"externalDependencies": ["vite"]
|
||||
}
|
||||
],
|
||||
"outputs": ["{projectRoot}/dist"],
|
||||
"executor": "nx:run-commands",
|
||||
"configurations": {},
|
||||
"metadata": {
|
||||
"technologies": ["vite"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "cart",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "apps/cart/src",
|
||||
"projectType": "application",
|
||||
"tags": [],
|
||||
"implicitDependencies": [],
|
||||
"metadata": {
|
||||
"technologies": ["react"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"sourceMap": {
|
||||
"root": ["apps/cart/project.json", "nx/core/project-json"],
|
||||
"targets": ["apps/cart/project.json", "nx/core/project-json"],
|
||||
"targets.build": ["apps/cart/vite.config.ts", "@nx/vite/plugin"],
|
||||
"targets.build.command": ["apps/cart/vite.config.ts", "@nx/vite/plugin"],
|
||||
"targets.build.options": ["apps/cart/vite.config.ts", "@nx/vite/plugin"],
|
||||
"targets.build.cache": ["apps/cart/vite.config.ts", "@nx/vite/plugin"],
|
||||
"targets.build.dependsOn": ["apps/cart/vite.config.ts", "@nx/vite/plugin"],
|
||||
"targets.build.inputs": ["apps/cart/vite.config.ts", "@nx/vite/plugin"],
|
||||
"targets.build.outputs": ["apps/cart/vite.config.ts", "@nx/vite/plugin"],
|
||||
"targets.build.options.cwd": [
|
||||
"apps/cart/vite.config.ts",
|
||||
"@nx/vite/plugin"
|
||||
],
|
||||
"name": ["apps/cart/project.json", "nx/core/project-json"],
|
||||
"$schema": ["apps/cart/project.json", "nx/core/project-json"],
|
||||
"sourceRoot": ["apps/cart/project.json", "nx/core/project-json"],
|
||||
"projectType": ["apps/cart/project.json", "nx/core/project-json"],
|
||||
"tags": ["apps/cart/project.json", "nx/core/project-json"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{% /project_details %}
|
||||
|
||||
## Enhance tools for monorepos
|
||||
|
||||
Nx does not just reduce its own configuration burden, it also improves the functionality of your existing tools so that they work better in a monorepo context.
|
||||
|
||||
### Keep TypeScript project references in sync
|
||||
|
||||
TypeScript provides a feature called [Project References](https://www.typescriptlang.org/docs/handbook/project-references.html) that allows the TypeScript compiler to build and typecheck each project independently. When each project is typechecked, the TypeScript compiler will output an intermediate `*.tsbuildinfo` file that can be used by other projects instead of re-typechecking all dependencies. This feature can provide [significant performance improvements](/docs/concepts/typescript-project-linking#typescript-project-references-performance-benefits), particularly in a large monorepo.
|
||||
|
||||
The main downside of this feature is that you have to manually define each project's references (dependencies) in the appropriate `tsconfig.*.json` file. This process is tedious to set up and very difficult to maintain as the repository changes over time. Nx can help by using a [sync generator](/docs/concepts/sync-generators) to automatically update the references defined in the `tsconfig.json` files based on the project graph it already knows about.
|
||||
|
||||
{% graph height="200px" title="Project View" %}
|
||||
|
||||
```json
|
||||
{
|
||||
"composite": false,
|
||||
"projects": [
|
||||
{
|
||||
"name": "product-state",
|
||||
"type": "lib",
|
||||
"data": {
|
||||
"root": "packages/cart/product-state",
|
||||
"tags": ["scope:cart", "type:state"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ui-buttons",
|
||||
"type": "lib",
|
||||
"data": {
|
||||
"root": "packages/ui/buttons",
|
||||
"tags": ["scope:shared", "type:ui"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cart",
|
||||
"type": "app",
|
||||
"data": {
|
||||
"root": "apps/cart",
|
||||
"tags": ["type:app", "scope:cart"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"product-state": [],
|
||||
"ui-buttons": [],
|
||||
"cart": [
|
||||
{ "source": "cart", "target": "product-state", "type": "static" },
|
||||
{ "source": "cart", "target": "ui-buttons", "type": "static" }
|
||||
]
|
||||
},
|
||||
"workspaceLayout": {
|
||||
"appsDir": "apps",
|
||||
"libsDir": "libs"
|
||||
},
|
||||
"affectedProjectIds": [],
|
||||
"focus": null,
|
||||
"groupByFolder": false,
|
||||
"exclude": [],
|
||||
"enableTooltips": false
|
||||
}
|
||||
```
|
||||
|
||||
{% /graph %}
|
||||
|
||||
```jsonc
|
||||
// apps/cart/tsconfig.json
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [], // intentionally empty
|
||||
"references": [
|
||||
// UPDATED BY NX SYNC
|
||||
// All project dependencies
|
||||
{
|
||||
"path": "../../packages/product-state",
|
||||
},
|
||||
{
|
||||
"path": "../../packages/ui/buttons",
|
||||
},
|
||||
// This project's other tsconfig.*.json files
|
||||
{
|
||||
"path": "./tsconfig.lib.json",
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json",
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Later, if someone adds another dependency to the `cart` app and then runs the `build` task, Nx will detect that the project references are out of sync and ask if the references should be updated.
|
||||
|
||||
```plaintext {% title="nx build cart" frame="terminal" %}
|
||||
NX The workspace is out of sync
|
||||
|
||||
[@nx/js:typescript-sync]: Some TypeScript configuration files are missing project references to the projects they depend on or contain outdated project references.
|
||||
|
||||
This will result in an error in CI.
|
||||
|
||||
? Would you like to sync the identified changes to get your workspace up to date? …
|
||||
❯ Yes, sync the changes and run the tasks
|
||||
No, run the tasks without syncing the changes
|
||||
```
|
||||
@@ -60,7 +60,7 @@ Let's take a look at the structure of our new Nx workspace:
|
||||
- package-lock.json
|
||||
- package.json
|
||||
- tsconfig.base.json
|
||||
- vitest.config.ts
|
||||
- vitest.workspace.ts
|
||||
|
||||
{%/filetree%}
|
||||
|
||||
@@ -262,7 +262,7 @@ Running the above command should lead to the following directory structure:
|
||||
- nx.json
|
||||
- package.json
|
||||
- tsconfig.base.json
|
||||
- vitest.config.ts
|
||||
- vitest.workspace.ts
|
||||
|
||||
{%/filetree %}
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ For TypeScript workspaces, the recommended setup uses three levels of `tsconfig.
|
||||
}
|
||||
```
|
||||
|
||||
This setup gives editors and language servers accurate type information per-project, enables incremental builds (only recompile what changed), and creates clear boundaries between projects. For more details, see [maintain TypeScript monorepos](/docs/technologies/typescript/introduction#typescript-project-references-kept-in-sync).
|
||||
This setup gives editors and language servers accurate type information per-project, enables incremental builds (only recompile what changed), and creates clear boundaries between projects. For more details, see [maintain TypeScript monorepos](/docs/features/maintain-typescript-monorepos).
|
||||
|
||||
{% aside type="note" title="Workspaces using tsconfig path aliases" %}
|
||||
Some workspaces use TypeScript `paths` in `tsconfig.base.json` to link projects. This works but is not recommended for new workspaces. Path aliases were not designed for project linking, and solution-style project references work better with editors and build tools. See the [migration guide](/docs/technologies/typescript/guides/switch-to-workspaces-project-references) to switch.
|
||||
|
||||
@@ -2489,13 +2489,13 @@ jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
filter: tree:0
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
- name: Set up JDK 21 for x64
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
|
||||
@@ -61,7 +61,7 @@ Let's take a look at the structure of our new Nx workspace:
|
||||
- package.json
|
||||
- tsconfig.base.json
|
||||
- tsconfig.json
|
||||
- vitest.config.ts
|
||||
- vitest.workspace.ts
|
||||
|
||||
{%/filetree%}
|
||||
|
||||
@@ -246,7 +246,7 @@ Running the above commands should lead to the following directory structure:
|
||||
- package.json
|
||||
- tsconfig.base.json
|
||||
- tsconfig.json
|
||||
- vitest.config.ts
|
||||
- vitest.workspace.ts
|
||||
|
||||
{% /filetree %}
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ Try adding a plugin to your workspace:
|
||||
nx add @nx/vite
|
||||
```
|
||||
|
||||
This installs `@nx/vite` and registers it in `nx.json`. Some plugins may also need `nx sync` to update workspace configuration files (e.g., TypeScript project references). See [maintain TypeScript monorepos](/docs/technologies/typescript/introduction#typescript-project-references-kept-in-sync) for details.
|
||||
This installs `@nx/vite` and registers it in `nx.json`. Some plugins may also need `nx sync` to update workspace configuration files (e.g., TypeScript project references). See [maintain TypeScript monorepos](/docs/features/maintain-typescript-monorepos) for details.
|
||||
|
||||
After installing, check what tasks were inferred for one of your projects:
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ Let's take a look at the structure of our new Nx workspace:
|
||||
- package.json
|
||||
- tsconfig.base.json
|
||||
- tsconfig.json
|
||||
- vitest.config.ts
|
||||
- vitest.workspace.ts
|
||||
|
||||
{% /filetree %}
|
||||
|
||||
@@ -91,7 +91,7 @@ Running these commands should lead to new directories and files in your workspac
|
||||
- animal/
|
||||
- zoo/
|
||||
- ...
|
||||
- vitest.config.ts
|
||||
- vitest.workspace.ts
|
||||
|
||||
{%/filetree %}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ Use `nx affected` to run tasks only for projects impacted by the PR's changes:
|
||||
|
||||
```yaml {% meta="{5}" %}
|
||||
# .github/workflows/ci.yml
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: npx nx affected -t lint test build
|
||||
|
||||
@@ -357,7 +357,7 @@ jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
filter: tree:0
|
||||
@@ -367,9 +367,9 @@ jobs:
|
||||
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
|
||||
# Connect your workspace by running "nx connect" and uncomment this
|
||||
- run: npx nx start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build"
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- uses: nrwl/nx-set-shas@v5
|
||||
|
||||
@@ -375,7 +375,7 @@ jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
filter: tree:0
|
||||
@@ -385,9 +385,9 @@ jobs:
|
||||
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
|
||||
# Connect your workspace by running "nx connect" and uncomment this
|
||||
- run: npx nx start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build"
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- uses: nrwl/nx-set-shas@v5
|
||||
|
||||
@@ -7,7 +7,7 @@ sidebar:
|
||||
---
|
||||
|
||||
{% aside type="note" title="Looking for a comparison?" %}
|
||||
For a data-driven comparison of Nx and Turborepo covering setup complexity, CI performance, and advanced capabilities, see [Nx vs Turborepo](/docs/guides/comparisons/nx-vs-turborepo).
|
||||
For a data-driven comparison of Nx and Turborepo covering setup complexity, CI performance, and advanced capabilities, see [Nx vs Turborepo](/docs/guides/adopting-nx/nx-vs-turborepo).
|
||||
{% /aside %}
|
||||
|
||||
Nx is a superset of Turborepo, so migrating requires minimal effort. The diff is tiny: an `nx.json` file (equivalent to `turbo.json`), the `nx` package added to `package.json`, and a `.gitignore` entry for the Nx cache. No changes to your existing projects or scripts are needed.
|
||||
|
||||
@@ -1,52 +1,39 @@
|
||||
---
|
||||
title: Nx vs Turborepo
|
||||
description: Nx vs Turborepo on the same workspace, with distributed CI, task sandboxing, polyglot builds, generators, and release management.
|
||||
description: Nx vs Turborepo on the same workspace, with distributed CI in 9m 20s vs 19m 18s, task sandboxing, polyglot builds, generators, and release management.
|
||||
filter: 'type:Guides'
|
||||
sidebar:
|
||||
label: Nx vs Turborepo
|
||||
---
|
||||
|
||||
Both Nx and Turborepo cover similar ground. They provide task scheduling, caching (local and remote), and affected detection. The differences emerge as your needs grow. While **Turbo covers the basics**, **Nx provides solutions along the entire software growth lifecycle**, even when you need more advanced features such as distributed CI, polyglot builds, or AI-powered CI workflows. That's where the gap widens.
|
||||
Both Nx and Turborepo seem to cover very similar ground. They provide task scheduling, caching (local and remote), and affected detection. The differences emerge as your needs grow. While **Turbo covers just the basics**, **Nx provides solutions along the entire software growth lifecycle**, even when you need more advanced features such as distributed CI, polyglot builds, or AI-powered CI workflows. That's where the gap widens.
|
||||
|
||||
## What is Nx?
|
||||
|
||||
Nx is a build system that runs your `package.json` scripts with task scheduling and local and remote caching. On top of that baseline, plugins configure tasks from your existing tool configs so you maintain fewer scripts, `nx affected` scopes work to your change, and Nx Cloud distributes tasks across CI machines. It spans JS/TS, JVM, .NET, and more in one graph.
|
||||
|
||||
## What is Turborepo?
|
||||
|
||||
Turborepo is a task runner for JavaScript and TypeScript monorepos, maintained by Vercel. It runs your `package.json` scripts with local and remote caching (via Vercel Remote Cache), task scheduling, and change detection, configured through a `turbo.json` file.
|
||||
|
||||
## Quick takeaway
|
||||
|
||||
Both tools are easy to set up and get started, and both cover the same basics: task scheduling, local and remote caching, and affected detection. On top of those, Nx adds:
|
||||
|
||||
- Task sandboxing, code generation, polyglot support, and release management.
|
||||
- A CI platform with distribution, flaky-task handling, and self-healing.
|
||||
|
||||
Adoption isn't the trade-off it sounds like: Nx works with your existing scripts and is adopted incrementally, so a Turborepo workspace can move over without a rewrite.
|
||||
This doesn't come at the cost of adoption complexity though. Nx is designed to be modular from the ground up and can be **adopted incrementally as you need more**.
|
||||
|
||||
{% aside type="note" title="Benchmarks" %}
|
||||
All benchmarks on this page use the same [pnpm workspace](https://github.com/meeroslav/pnpm-workspace-baseline) migrated with both tools.
|
||||
{% /aside %}
|
||||
|
||||
| Topic | Nx | Turborepo |
|
||||
| ----------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| [Onboarding](#onboarding) | Zero-config or guided `nx init` | Manual `turbo.json` configuration |
|
||||
| [Running tasks](#running-tasks) | Runs `package.json` scripts, optional plugin-based task configuration | Runs `package.json` scripts, requires `turbo.json` config |
|
||||
| [Caching](#caching) | Explicit opt-in, composable `namedInputs` | Cached by default, flat input lists |
|
||||
| [Task sandboxing](#task-sandboxing) | Sandboxed execution for cache integrity | Not available |
|
||||
| [Code generation](#code-generation) | Programmatic generators with AST transforms and graph awareness | Template-based file scaffolding (Plop) |
|
||||
| [Module boundary rules](#module-boundary-rules) | Tag-based lint rule + conformance rules (polyglot) | Experimental `turbo boundaries` (since 2.4) |
|
||||
| [Polyglot support](#polyglot-support) | First-party plugins for Gradle, Maven, .NET; community for Python, Rust, Go | Any CLI via `package.json` scripts, no native graph |
|
||||
| [AI integration](#ai-integration) | Agent skills, MCP, `configure-ai-agents`, self-healing CI | Official skill, no MCP or CI integration |
|
||||
| [CI solution](#running-nx-vs-turbo-on-ci) | Nx Cloud: distribution, self-healing, flaky detection | Remote caching, no distributed agents or self-healing |
|
||||
| [Release management](#release-management) | Built-in versioning, changelogs, and publishing | Requires manual setup or 3rd party tools |
|
||||
| [Observability](#observability) | Integrated dashboards and AI-powered run analysis | Experimental OpenTelemetry (OTLP) export |
|
||||
| [Developer experience](#developer-experience) | TUI, IDE extensions, and interactive project graph | Basic TUI and LSP support |
|
||||
This page starts with the basics, like onboarding, and progressively moves into more advanced capabilities.
|
||||
|
||||
| Topic | Nx | Turborepo |
|
||||
| ----------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| [Onboarding](#onboarding) | Zero-config or guided `nx init` (+3 lines) | Manual `turbo.json` (+144 lines) |
|
||||
| [Running tasks](#running-tasks) | Runs `package.json` scripts, optional plugin-based task inference | Runs `package.json` scripts, requires `turbo.json` config |
|
||||
| [Caching](#caching) | Explicit opt-in, composable `namedInputs` | Cached by default, flat input lists |
|
||||
| [Task sandboxing](#task-sandboxing) | IO tracing + cache poisoning protection | Not available |
|
||||
| [Code generation](#code-generation) | Programmatic generators with AST transforms and graph awareness | Template-based file scaffolding (Plop) |
|
||||
| [Module boundary rules](#module-boundary-rules) | Tag-based lint rule + conformance rules (polyglot) | Experimental `turbo boundaries` (since 2024) |
|
||||
| [Polyglot support](#polyglot-support) | Native support for JS/TS, Java, .NET, Python, Rust | Any CLI via `package.json` scripts, no native graph |
|
||||
| [AI integration](#ai-integration) | Agent skills, MCP, `configure-ai-agents`, self-healing CI | Official skill, no MCP or CI integration |
|
||||
| [CI solution](#running-nx-vs-turbo-on-ci) | Nx Cloud: distribution (9m 20s), self-healing, flaky detection | No CI solution (19m 18s with manual binning) |
|
||||
| [Release management](#release-management) | Built-in versioning, changelogs, and publishing | Requires manual setup or 3rd party tools |
|
||||
| [Observability](#observability) | Integrated dashboards and AI-powered run analysis | Experimental OpenTelemetry (OTLP) export |
|
||||
| [Developer experience](#developer-experience) | TUI, IDE extensions, and interactive project graph | Basic TUI and LSP support |
|
||||
|
||||
## Onboarding
|
||||
|
||||
Nx works with your existing `package.json` scripts out of the box. Add the `nx` package to your workspace and you immediately get task orchestration, affected detection, and local caching, without writing any task configuration. Running `npx nx init` detects your tooling, adds the relevant plugins, and scaffolds `nx.json`.
|
||||
Nx works with your existing `package.json` scripts out of the box. Add the `nx` package to your workspace and you immediately get task orchestration, affected detection, and local caching, without writing any task configuration.
|
||||
|
||||
Turborepo requires every task to be explicitly declared in `turbo.json` before anything runs, even if the same tasks already work with your package manager directly:
|
||||
|
||||
@@ -56,42 +43,21 @@ Turborepo requires every task to be explicitly declared in `turbo.json` before a
|
||||
╰─▶ × Could not find task `build` in project
|
||||
```
|
||||
|
||||
The difference shows up in how much configuration each tool needs before a `build` is cached correctly. Nx plugins read your tool config and automatically configure each build's inputs and outputs, so turning on caching is a single flag:
|
||||
For a more guided experience, run `npx nx init`. The interactive setup detects your existing tooling, asks which tasks should be cacheable, and scaffolds an `nx.json` with the right configuration. If you have Next.js and ESLint, for example, Nx automatically infers `build`, `dev`, `start`, and `lint` targets without you declaring them.
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
{
|
||||
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
||||
"targetDefaults": {
|
||||
"build": {
|
||||
"cache": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
Looking at the raw impact on a repository using the same [pnpm workspace](https://github.com/meeroslav/pnpm-workspace-baseline) migrated with both tools:
|
||||
|
||||
Turborepo caches by default, but without input scoping a change to any file, tests included, invalidates the build cache. Scoping it means enumerating the inputs on each task:
|
||||
|
||||
```jsonc
|
||||
// turbo.json
|
||||
{
|
||||
"$schema": "https://turborepo.dev/schema.json",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": ["dist/**"],
|
||||
"inputs": ["$TURBO_DEFAULT$", "!**/*.test.*", "!**/*.spec.*"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Nx configures the inputs and outputs for you. Turborepo has you declare them, and repeat them on every cacheable task. The [Caching](#caching) section shows how that compounds across a workspace.
|
||||
| Setup | Config written | Net impact on codebase |
|
||||
| ------------ | ------------------ | ------------------------------------------------------------------------- |
|
||||
| Nx (minimal) | 3 lines | [+3 lines](https://github.com/meeroslav/pnpm-workspace-baseline/pull/4) |
|
||||
| Nx (guided) | 85 lines | [-15 lines](https://github.com/meeroslav/pnpm-workspace-baseline/pull/2) |
|
||||
| Turborepo | 122 lines (manual) | [+144 lines](https://github.com/meeroslav/pnpm-workspace-baseline/pull/3) |
|
||||
|
||||
For a full walkthrough, see [Adding Nx to your Existing Project](/docs/guides/adopting-nx/adding-to-existing-project). If you're coming from Turborepo, see [Migrating from Turborepo to Nx](/docs/guides/adopting-nx/from-turborepo).
|
||||
|
||||
## Running tasks
|
||||
|
||||
Once installed, both tools run your existing `package.json` scripts. If your project has a `build` script, `nx build` runs it, like `turbo run build`. No rewiring needed.
|
||||
Once installed, both tools run your existing `package.json` scripts. If your project has a `build` script, `nx build` runs it, just like `turbo run build`. No rewiring needed.
|
||||
|
||||
```shell
|
||||
nx run-many -t build test lint
|
||||
@@ -99,15 +65,15 @@ nx run-many -t build test lint
|
||||
|
||||
Nx also provides [`nx affected`](/docs/features/ci-features/affected) to run only tasks affected by your current changes, which works immediately without configuration.
|
||||
|
||||
Where Nx goes further is with [plugins](/docs/plugin-registry). Adding an Nx plugin like `@nx/vite` automatically configures tasks from your existing tool configuration (e.g. `vite.config.mts`), so you don't need to maintain manual script definitions. Plugins also read your tool's config to set cache `inputs` and `outputs` automatically, meaning caching works correctly from the start without manual tuning.
|
||||
Where Nx goes further is with [plugins](/docs/concepts/inferred-tasks). Adding an Nx plugin like `@nx/vite` automatically infers tasks from your existing tool configuration (e.g. `vite.config.mts`), so you don't need to maintain manual script definitions. Plugins also read your tool's config to set cache `inputs` and `outputs` automatically, meaning caching works correctly from the start without manual tuning.
|
||||
|
||||
## Caching
|
||||
|
||||
Both tools cache task results, but the defaults and depth of caching support differ significantly. Better cache configuration means fewer false positives, fewer unnecessary re-runs, and faster CI.
|
||||
|
||||
**Turborepo enables caching by default for the tasks you register**, and you opt out with `cache: false` on tasks like `dev`. It stores file artifacts only when a task declares `outputs`, and caches only the task's logs without them. **Nx caches only targets marked cacheable**, either explicitly with `cache: true` or through inferred plugin configuration.
|
||||
**Turborepo caches every task by default**, so you opt out of caching with `cache: false` on tasks like `dev`. **Nx does the opposite: nothing is cached unless you explicitly set `cache: true`.** This is a more cautious opt-in model, since not all tasks are cacheable by default.
|
||||
|
||||
Nx also provides [`namedInputs`](/docs/reference/inputs), reusable input patterns that you can compose across targets. You define a pattern once (like "production sources") and reference it everywhere. Turborepo added task `extends` in 2.7, but it still has no named input pattern you define once and reference across targets, so the exclusions get repeated.
|
||||
Nx also provides [`namedInputs`](/docs/reference/inputs), reusable input patterns that you can compose across targets. You define a pattern once (like "production sources") and reference it everywhere. Turborepo's `inputs` are flat lists with no composition, so every target repeats the same exclusions.
|
||||
|
||||
Here's the same workspace configured with both tools:
|
||||
|
||||
@@ -240,15 +206,15 @@ Here's the same workspace configured with both tools:
|
||||
|
||||
The `production` pattern in Nx is defined once and reused across `build` and `test`. A change to a spec file won't invalidate the build cache because `production` explicitly excludes test files.
|
||||
|
||||
In Turborepo, the same exclusion list is repeated across `build`, `build:prod`, and `check-types`. Task `extends` can share a base config, but not a reusable named input pattern.
|
||||
In Turborepo, the same exclusion list is repeated across `build`, `build:prod`, and `check-types`. There's no way to define it once and reuse it.
|
||||
|
||||
## Task sandboxing
|
||||
|
||||
A cache is only valuable if you can trust it. Turborepo has no task sandboxing. During execution, tasks can read and write anywhere on the filesystem. A task can read files that aren't declared as inputs and produce undeclared outputs that get cached and replayed into a different context. The result: false cache hits, missing artifacts, and hard-to-trace failures.
|
||||
|
||||
Nx provides [task sandboxing](/docs/features/ci-features/sandboxing) that runs each task in a sandbox and surfaces undeclared reads or writes, with an opt-in strict mode that fails the task. Undeclared dependencies show up automatically rather than through debugging production incidents.
|
||||
Nx provides [task sandboxing](/docs/features/ci-features/sandboxing) that monitors filesystem access during execution and flags any reads or writes outside declared `inputs` and `outputs`. Undeclared dependencies are surfaced automatically rather than discovered through debugging production incidents.
|
||||
|
||||
This matters for security too. [CVE-2025-36852](https://www.cve.org/CVERecord?id=CVE-2025-36852) (CREEP) showed how remote caches without branch isolation, where the first run to populate a key wins, let a contributor with PR access poison artifacts that protected branches later reuse. It's a property of bucket-style shared caches, not of any one tool. Nx Cloud prevents it through branch-scoped cache isolation. For more details, see [cache security](/docs/concepts/ci-concepts/cache-security).
|
||||
This matters for security too. [CVE-2025-36852](https://www.cve.org/CVERecord?id=CVE-2025-36852) (CREEP) demonstrated that build systems without cache isolation are vulnerable to cache poisoning, where any contributor with PR access can inject compromised artifacts into production. Nx Cloud prevents this through branch-scoped cache isolation. For more details, see [cache security](/docs/concepts/ci-concepts/cache-security).
|
||||
|
||||
Task sandboxing is an architectural difference, not a configuration problem. There's no workaround on the Turborepo side.
|
||||
|
||||
@@ -256,7 +222,7 @@ Task sandboxing is an architectural difference, not a configuration problem. The
|
||||
|
||||
Both tools offer code generation, but the depth differs significantly.
|
||||
|
||||
Turborepo provides `turbo gen`, built on [Plop.js](https://plopjs.com/). It scaffolds files from templates and supports custom action functions for programmatic steps. What it doesn't have is AST-level code modification, awareness of the project graph, or a migration and codemod system.
|
||||
Turborepo provides `turbo gen`, a thin wrapper around [Plop.js](https://plopjs.com/). It can scaffold new workspaces and create files from Handlebars templates, but it's limited to template-based file creation and simple string append/prepend operations. There's no AST-level code modification, no awareness of the project graph, and no migration/codemod system.
|
||||
|
||||
Nx generators are built on top of [Nx Devkit](/docs/extending-nx/intro), a full programmatic API for workspace manipulation. Generators can read and modify the project graph, perform AST-level TypeScript transforms, and compose with other generators. You can create [local workspace generators](/docs/features/generate-code#creating-custom-generators) that encode your team's specific patterns.
|
||||
|
||||
@@ -270,33 +236,33 @@ Nx has provided [module boundary rules](/docs/features/enforce-module-boundaries
|
||||
|
||||
This becomes especially important with AI coding agents. Boundary rules act as guardrails, preventing agents from creating arbitrary cross-project dependencies that violate your architecture.
|
||||
|
||||
Turborepo added experimental [`turbo boundaries`](https://turborepo.dev/docs/reference/boundaries) in 2.4 (early 2025), which can define allowed dependencies in `turbo.json` and visualize them in their devtools graph view.
|
||||
Turborepo added experimental [`turbo boundaries`](https://turborepo.dev/docs/reference/boundaries) in 2024, which can define allowed dependencies in `turbo.json` and visualize them in their devtools graph view.
|
||||
|
||||
## Polyglot support
|
||||
|
||||
Nx provides [first-party plugins](/docs/plugin-registry) for Maven, Gradle, .NET, and Docker, plus community plugins for Python (UV, Poetry), Rust (Cargo), Go, and PHP.
|
||||
Each plugin provides automatic dependency detection, target configuration, caching, affected detection, and distribution.
|
||||
Each plugin provides automatic dependency detection, target inference, caching, affected detection, and distribution.
|
||||
|
||||
Turborepo can orchestrate any language by wrapping CLI commands in `package.json` scripts. However, non-JS projects still require a `package.json`, and Turborepo provides no automatic dependency graph analysis or target configuration for those languages. You must define everything manually.
|
||||
Turborepo can orchestrate any language by wrapping CLI commands in `package.json` scripts. However, non-JS projects still require a `package.json`, and Turborepo provides no automatic dependency graph analysis or target inference for those languages. You must define everything manually.
|
||||
|
||||
**This difference is critical for AI readiness.** When your backend is in Go and your frontend is in Next.js, an AI agent with Nx can see the full cross-language dependency chain. With Turborepo, those services are "islands," and an agent has no way to reason about how a change in the Go API affects the frontend.
|
||||
|
||||
## AI integration
|
||||
|
||||
Nx actively embraces AI and autonomous agents across the entire development lifecycle, not only individual features. Running [`nx configure-ai-agents`](/blog/nx-ai-agent-skills) sets up everything your AI agent needs in one command: agent skills, an MCP server, and `CLAUDE.md` / `AGENTS.md` guidelines. It works across **Claude Code, Cursor, GitHub Copilot, Gemini, Codex, and OpenCode**.
|
||||
Nx actively embraces AI and autonomous agents across the entire development lifecycle, not just individual features. Running [`nx configure-ai-agents`](/blog/nx-ai-agent-skills) sets up everything your AI agent needs in one command: agent skills, an MCP server, and `CLAUDE.md` / `AGENTS.md` guidelines. It works across **Claude Code, Cursor, GitHub Copilot, Gemini, Codex, and OpenCode**.
|
||||
|
||||
- **[Agent skills](/blog/why-we-deleted-most-of-our-mcp-tools)** teach agents _how_ to work in your monorepo: when to use generators, how to explore the project graph, how to run tasks efficiently. Skills are loaded incrementally, keeping context focused and token-efficient.
|
||||
- **[Self-healing CI](/docs/features/ci-features/self-healing-ci)** is a specialized AI agent that runs on CI, monitors runs, diagnoses broken tasks, provides verified fixes, and automatically identifies and [re-runs flaky tasks](/docs/features/ci-features/flaky-tasks).
|
||||
- Dedicated skills and an **[MCP server](/docs/reference/nx-mcp)** allow the local coding agent to connect and [coordinate with the remote CI agent](/blog/autonomous-ai-workflows-with-nx), creating fully autonomous push-fix-verify loops.
|
||||
- The **Nx CLI is [optimized for agentic use](/blog/making-nx-agent-ready)**: commands like `nx init`, `nx import`, and `create-nx-workspace` detect when they're called by an agent and emit structured JSON output instead of interactive prompts, reducing wasted tokens and retries.
|
||||
|
||||
Turborepo provides an [official skill](https://skills.sh/vercel/turborepo) covering task configuration and caching strategies, plus a `turbo docs` command. However, no CI integration for agents, and no AI powered self-healing CI system.
|
||||
Turborepo provides an [official skill](https://skills.sh/vercel/turborepo-skills) covering task configuration and caching strategies, plus a `turbo docs` command. However, no CI integration for agents, and no AI powered self-healing CI system.
|
||||
|
||||
## Running Nx vs Turbo on CI
|
||||
|
||||
Nx works on any CI provider out of the box. Run `nx affected` or `nx run-many` in your existing pipeline and you get caching, affected detection, and task orchestration without additional setup. For teams that need more, [Nx Cloud](/docs/features/ci-features) layers on remote caching, intelligent task distribution across machines, self-healing CI, and flaky task detection, all integrated directly into your existing CI provider.
|
||||
|
||||
Turborepo supports CI through remote caching (Vercel Remote Cache) plus flags like `--affected` and `--filter` to scope what runs. What it doesn't provide is built-in distributed CI agents, self-healing CI, or flaky-test recovery.
|
||||
Turborepo has no CI-specific solution. It runs tasks on CI the same way it does locally, with no built-in distribution, failure recovery, or CI-aware features.
|
||||
|
||||

|
||||
|
||||
@@ -312,7 +278,7 @@ Its Rust-powered task scheduler produces a more optimal execution order, and its
|
||||
| Nx | 21m 56s | N/A |
|
||||
| Turborepo | 25m 32s | ~16% slower |
|
||||
|
||||
A 16% gap may sound modest, but on a 30-minute pipeline that's nearly 4 minutes saved on every run. Note that these numbers are without any cache optimization, with both tools running out of the box on the same codebase.
|
||||
A 16% gap may sound modest, but on a 30-minute pipeline that's nearly 4 minutes saved on every run. Note that these numbers are without any cache optimization, just both tools running out of the box on the same codebase.
|
||||
|
||||
### Distributed CI
|
||||
|
||||
@@ -417,6 +383,10 @@ Both tools can visualize the dependency graph, but the implementations differ si
|
||||
|
||||
The Nx graph is also available inside [Nx Console](/docs/getting-started/editor-setup), so you can explore dependencies without leaving your editor.
|
||||
|
||||
{% aside type="tip" title="Ready to migrate?" %}
|
||||
For step-by-step migration instructions, including configuration mapping and command equivalents, see [Migrating from Turborepo to Nx](/docs/guides/adopting-nx/from-turborepo).
|
||||
{% /aside %}
|
||||
|
||||
### Terminal UI
|
||||
|
||||
Nx ships with a full [terminal UI](/docs/guides/tasks--caching/terminal-ui) that adapts to what you're running.
|
||||
@@ -433,27 +403,3 @@ You can run tasks, explore the project graph, scaffold with generators, and insp
|
||||
It also includes a language server that provides autocompletion in `nx.json` and `project.json` files.
|
||||
|
||||
Turborepo provides basic LSP support for `turbo.json`.
|
||||
|
||||
## Who should pick which
|
||||
|
||||
Nx fits when any of these apply:
|
||||
|
||||
- You want task running and caching that grow into distributed CI, sandboxing, and release management.
|
||||
- Your repository spans more than JavaScript, with Java, .NET, Python, or Rust alongside it.
|
||||
- You want code generation, automated migrations, or self-healing and flaky-task handling on CI.
|
||||
- You want editor integration and an interactive project graph.
|
||||
|
||||
Turborepo is enough only when:
|
||||
|
||||
- Your workspace is JS/TS-only and you want a focused task runner with caching.
|
||||
- Remote caching and `--affected` cover your CI needs, without distribution or self-healing.
|
||||
- You prefer a minimal configuration surface over a broader platform.
|
||||
|
||||
Nx adopts incrementally on your existing scripts, so a Turborepo workspace can move over without a rewrite.
|
||||
|
||||
## Resources
|
||||
|
||||
{% cards cols=2 %}
|
||||
{% card title="Migrate from Turborepo to Nx" description="Step-by-step migration with config and command mapping" url="/docs/guides/adopting-nx/from-turborepo" /%}
|
||||
{% card title="Add Nx to an existing project" description="Adopt Nx incrementally without rewriting tool configuration" url="/docs/guides/adopting-nx/adding-to-existing-project" /%}
|
||||
{% /cards %}
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: GitHub Integration
|
||||
description: Learn how to integrate Nx Cloud with GitHub for PR integration.
|
||||
filter: 'type:Guides'
|
||||
sidebar:
|
||||
label: GitHub
|
||||
---
|
||||
|
||||
Integrate Nx Cloud with your source control platform to access run results, logs, and build insights directly from your pull requests or merge requests. This integration is required to use core Nx Cloud features like task distribution and flaky task re-trying.
|
||||
|
||||
The [Nx Cloud GitHub App](https://github.com/marketplace/official-nx-cloud-app) lets you access the result of every run—with all its logs and build insights—straight from your PR. It will comment on your PR with the results of the latest CI run, with a summary of the results and links to detailed, structured logs. If you’re using Self-Healing CI, Nx Cloud will comment with proposed fixes for CI failures.
|
||||
|
||||
## Install the app
|
||||
|
||||
For the best experience, install the [Nx Cloud GitHub App](https://github.com/marketplace/official-nx-cloud-app). Using the app provides the most seamless authentication experience. This is not required if you wish to authenticate with a personal access token that you generate yourself.
|
||||
|
||||
For a detailed breakdown of each permission the GitHub App requires and why, see the [GitHub App Permissions](/docs/guides/nx-cloud/source-control-integration/github-app-permissions) reference.
|
||||
|
||||
## Connecting your workspace
|
||||
|
||||
Once you have installed the Nx Cloud GitHub App, you must link your workspace to the installation. To do this, sign in to Nx Cloud and navigate to the VCS Integrations setup page. This page can be found in your workspace settings, you need to be admin of the organization in order to access it.
|
||||
Once on the VCS Integrations setup page, you can choose what VCS you want to connect to your workspace.
|
||||
|
||||

|
||||
|
||||
### Choosing an authentication method
|
||||
|
||||
The easiest way to configure the Nx Cloud GitHub Integration is through the Nx Cloud GitHub App, and this method should be preferred for users on [https://cloud.nx.app](https://cloud.nx.app). Users with strict privacy considerations may wish to generate a personal access token (PAT) instead.
|
||||
|
||||
#### Using the GitHub app
|
||||
|
||||
To use the Nx Cloud GitHub App for authentication, select the **Use GitHub application** radio button and then click **Connect**.
|
||||
This will verify that Nx Cloud can connect to your repo. Upon a successful test, your configuration is saved.
|
||||
Check if there's any [additional setup required for your CI platform](/docs/guides/nx-cloud/source-control-integration#ci-platform-considerations), then your setup is complete.
|
||||
|
||||

|
||||
|
||||
#### Using a personal access token
|
||||
|
||||
Note that users who authenticate with a PAT will not receive Nx Cloud comments with command results and self-healing CI fixes.
|
||||
|
||||
Github supports two [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#about-personal-access-tokens) types: classic and fine-grained.
|
||||
|
||||
To use a personal access token for authentication, one must be generated with proper permissions. The minimum required permissions are shown in the screenshot below.
|
||||
|
||||
{% tabs %}
|
||||
{% tabitem label="Classic Token" %}
|
||||
|
||||

|
||||
|
||||
{% /tabitem %}
|
||||
|
||||
{% tabitem label="Fine-Grained Token" %}
|
||||
|
||||

|
||||
|
||||
{% /tabitem %}
|
||||
{% /tabs %}
|
||||
|
||||
Once this token is created, select the radio button for providing a personal access token, paste the value, and then click "Connect". This will verify that Nx Cloud can connect to your repo. Upon a successful test, your configuration is saved. Check the "_CI Platform Considerations_" section below, and if there are no additional instructions for your platform of choice, setup is complete.
|
||||
|
||||
### Advanced configuration
|
||||
|
||||
If your company runs a self-hosted GitHub installation, you may need to override the default URL that Nx Cloud uses to connect to the GitHub API. To do so, check the box labeled "Override GitHub API URL" and enter the correct URL for your organization.
|
||||
|
||||
### Connect to GitHub for more features
|
||||
|
||||
Get access to [easy workspace setup](/docs/features/ci-features/github-integration#easy-workspace-setup) and [access control through GitHub organizations](/docs/features/ci-features/github-integration#access-control) when you [connect your GitHub account to Nx Cloud](/docs/features/ci-features/github-integration#connect-to-github).
|
||||
|
||||
## CI platform considerations
|
||||
|
||||
If you are using CircleCI, TravisCI, GitHub Actions or GitHub, there is nothing else you need to do. If you are using other CI providers, you need to set the `NX_BRANCH` environment variable in your CI configuration. The variable has to be set to a PR number.
|
||||
|
||||
For instance, this is an example of doing it in Azure pipelines.
|
||||
|
||||
### Azure pipelines
|
||||
|
||||
```yml
|
||||
// azure-pipelines.yml
|
||||
variables:
|
||||
NX_BRANCH: $(System.PullRequest.PullRequestNumber)
|
||||
```
|
||||
|
||||
### CircleCI
|
||||
|
||||
Make sure [GitHub checks are enabled](https://circleci.com/docs/2.0/enable-checks/#to-enable-github-checks).
|
||||
|
||||
### Jenkins
|
||||
|
||||
[Install the Jenkins plugin](https://plugins.jenkins.io/github-checks/).
|
||||
|
||||
Ensure this step from the plugin instructions is followed:
|
||||
|
||||
Prerequisite: only GitHub App with proper permissions can publish checks, this guide helps you authenticate your Jenkins as a GitHub App.
|
||||
|
||||
## GitHub status checks
|
||||
|
||||
The Nx Cloud GitHub Integration updates your PR with commit statuses that reflect the real-time progress of your runs. These statuses are generated dynamically based on your running commands. Enforcing these dynamically-named checks within your branch protection rules is not recommended, as it can result in stuck checks displaying `Waiting for status to be reported`.
|
||||
|
||||
From your repository, go to `Settings -> Branches -> Protect matching branches` and ensure that no Nx Cloud status checks are listed in the `Require status checks to pass before merging` list. Enforcing that status checks pass on your default branch is sufficient.
|
||||

|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
name: Nx Cloud - Main Job
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# We need to fetch all branches and commits so that Nx affected has a base to compare against.
|
||||
fetch-depth: 0
|
||||
@@ -51,9 +51,9 @@ jobs:
|
||||
package-json-path: '${{ github.workspace }}/package.json'
|
||||
|
||||
- name: Use the package manager cache if available
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
agent: [1, 2, 3]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Set node/npm/yarn versions using volta
|
||||
- uses: volta-cli/action@v4
|
||||
@@ -93,9 +93,9 @@ jobs:
|
||||
package-json-path: '${{ github.workspace }}/package.json'
|
||||
|
||||
- name: Use the package manager cache if available
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
filter: tree:0
|
||||
fetch-depth: 0
|
||||
@@ -58,9 +58,9 @@ jobs:
|
||||
# - run: npx nx start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build"
|
||||
|
||||
# Cache node_modules
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- run: npm ci
|
||||
|
||||
@@ -146,15 +146,15 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
filter: tree:0
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -225,14 +225,14 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -383,12 +383,12 @@ jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 'Self-hosted remote cache'
|
||||
description: 'Set up a self-hosted remote cache server for Nx using the OpenAPI specification, or use Nx Cloud for a fully managed remote cache.'
|
||||
title: 'Remote Cache'
|
||||
description: 'Learn how to set up remote cache with Nx Cloud or a self-hosted solution.'
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
@@ -22,17 +22,26 @@ You'll also get access to advanced CI features:
|
||||
- [Detection and re-running of flaky tasks](/docs/features/ci-features/flaky-tasks)
|
||||
- [Self-healing CI and other AI features](https://nx.dev/ai)
|
||||
|
||||
[Get Started](https://cloud.nx.app/get-started/)
|
||||
{% /aside %}
|
||||
|
||||
{% call_to_action title="Get started with Nx Cloud" url="https://cloud.nx.app/get-started/" icon="nxcloud" description="Fully managed remote cache with Nx Replay" %}
|
||||
Get started with Nx Cloud
|
||||
{% /call_to_action %}
|
||||
{% aside type="note" title="Nx Enterprise" %}
|
||||
|
||||
For single-tenant, dedicated-region, or on-prem hosting, see [Nx Enterprise](https://nx.dev/enterprise).
|
||||
Recommended for large organizations.
|
||||
|
||||
Includes everything from Nx Cloud, plus:
|
||||
|
||||
- Work hand-in-hand with the Nx team for continual improvement
|
||||
- Run on the Nx Cloud servers in any region or run fully self-contained, on-prem
|
||||
- SOC 2 type 1 and 2 compliant and comes with single-tenant, dedicated EU region hosting as well as on-premise
|
||||
|
||||
[Reach out for an Enterprise trial](https://nx.dev/enterprise/trial)
|
||||
|
||||
{% /aside %}
|
||||
|
||||
## Build your own caching server
|
||||
|
||||
You can build your own caching server using the OpenAPI specification below and tailor it to your needs. The server manages all aspects of the remote cache, including storage, retrieval, and authentication.
|
||||
Starting in Nx version 20.8, you can build your own caching server using the OpenAPI specification below. This allows you to create a custom remote cache server tailored to your specific needs. The server manages all aspects of the remote cache, including storage, retrieval, and authentication.
|
||||
|
||||
Implementation is up to you, but the server must adhere to the OpenAPI specification below to ensure compatibility with Nx caching mechanism. The endpoints transfer tar archives as binary data. Note that while the underlying data format may change in future Nx versions, the OpenAPI specification should remain stable.
|
||||
|
||||
@@ -41,6 +50,7 @@ You can implement your server in any programming language or framework, as long
|
||||
### Open API specification
|
||||
|
||||
```json
|
||||
// Nx 20.8+
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
---
|
||||
title: 'Bun Workspaces: Setup, Commands, and Best Practices'
|
||||
description: 'Learn Bun workspaces: setup, linking packages with workspace:*, running scripts with --filter, and best practices for fast monorepos.'
|
||||
title: Use Bun Workspaces with Nx
|
||||
description: Set up a Bun workspace and add Nx in one command to get cached tasks, affected-only builds, and faster CI, with your existing package.json scripts working out of the box.
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
Bun workspaces let you manage multiple packages in a single repository (or a monorepo) using the same `workspaces` field npm and Yarn read. `bun install` resolves every package in a single pass, dedupes shared dependencies to the root `node_modules`, and links local packages declared with the `workspace:` protocol.
|
||||
|
||||
For the full configuration reference, see the [Bun workspaces documentation](https://bun.com/docs/install/workspaces).
|
||||
A Bun workspace lets you manage multiple packages in a single repository. Adding Nx to a Bun workspace gives you cached tasks, affected-only builds, and faster CI, while Bun keeps managing installs. Set up a Bun workspace first, then add Nx.
|
||||
|
||||
## Set up a Bun workspace
|
||||
|
||||
### 1. List packages in the workspaces field
|
||||
|
||||
A Bun workspace is a repository whose root `package.json` has a `workspaces` field listing the directories that hold `package.json` files. Bun supports full glob syntax here, including negative patterns like `!**/excluded/**`:
|
||||
A Bun workspace is a repository whose root `package.json` has a `workspaces` field listing the directories that hold `package.json` files:
|
||||
|
||||
```jsonc
|
||||
// package.json
|
||||
@@ -23,9 +19,7 @@ A Bun workspace is a repository whose root `package.json` has a `workspaces` fie
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Add the packages
|
||||
|
||||
Every directory matched by those globs that contains a `package.json` becomes a workspace package. A common layout separates applications from shared packages:
|
||||
A common layout separates applications from shared packages:
|
||||
|
||||
{% filetree %}
|
||||
|
||||
@@ -40,9 +34,7 @@ Every directory matched by those globs that contains a `package.json` becomes a
|
||||
|
||||
{% /filetree %}
|
||||
|
||||
### 3. Link local packages with workspace:\*
|
||||
|
||||
To depend on another package in the workspace, reference it with the `workspace:` protocol so Bun resolves it to the local package. Bun supports `workspace:*`, `workspace:^`, and `workspace:~`, and replaces them with real semver versions on publish:
|
||||
To depend on another package in the workspace, reference it with the `workspace:` protocol so Bun resolves it to the local package:
|
||||
|
||||
```jsonc
|
||||
// apps/web/package.json
|
||||
@@ -57,47 +49,23 @@ To depend on another package in the workspace, reference it with the `workspace:
|
||||
|
||||
Run `bun install` once at the root to install every package's dependencies and link the internal ones.
|
||||
|
||||
## Install every package with one bun install
|
||||
## Add Nx in one command
|
||||
|
||||
A single `bun install` at the root installs and dedupes dependencies for all workspaces, so shared packages exist once in the root `node_modules`. Install speed is the headline Bun feature: per the Bun team's benchmarks, `bun install` runs up to 28x faster than `npm install`, which makes full reinstalls in CI and fresh clones noticeably cheaper in a workspace with many packages.
|
||||
|
||||
## Target packages with --filter
|
||||
|
||||
The `--filter` flag narrows both installs and script runs to matching packages. For `bun run`, place `--filter` before the script name:
|
||||
|
||||
```shell
|
||||
bun run --filter '*' build # run build in every package
|
||||
bun run --filter web build # run build in a single package
|
||||
bun install --filter 'pkg-*' # install deps only for matching packages
|
||||
```
|
||||
|
||||
Filters accept globs and compose: pass `--filter` multiple times, and prefix a pattern with `!` to exclude it, as in `bun install --filter 'pkg-*' --filter '!pkg-c'`.
|
||||
|
||||
## How mature are Bun workspaces?
|
||||
|
||||
Bun workspaces are the newest of the four implementations. Setup, linking, the `workspace:` protocol, and `--filter` cover the everyday workflows, but there's no filtering by dependency relationship or by what changed in git, which the pnpm `--filter` flag offers. Bun workspaces also have less production mileage overall, and some ecosystem tools expect an npm, Yarn, or pnpm lockfile rather than the Bun lockfile.
|
||||
|
||||
If your workspace leans on publishing workflows or unusual install hooks, test those paths before migrating and check the [Bun issue tracker](https://github.com/oven-sh/bun/issues) for open workspace issues.
|
||||
|
||||
## Do Bun workspaces replace a monorepo tool?
|
||||
|
||||
No. Bun workspaces make installs fast and link local packages, but they don't cache task results or detect which projects a commit affects, so CI reruns every task on every push no matter how fast the install was.
|
||||
|
||||
When that becomes the bottleneck, add Nx on top without changing how Bun works:
|
||||
Nx layers task running and caching on top of your existing Bun workspace. Bun still installs and resolves packages. Nx makes your tasks cacheable and your CI affected-aware. Add it with one command, which works on any npm, Yarn, pnpm, or Bun workspace:
|
||||
|
||||
```shell
|
||||
npx nx@latest init
|
||||
```
|
||||
|
||||
Your existing `package.json` scripts keep working and Bun keeps managing installs.
|
||||
Your existing `package.json` scripts keep working. Nx infers a project for each `package.json` in your workspace and runs its scripts with caching, with no `project.json` or extra configuration required.
|
||||
|
||||
### Run tasks before and after Nx
|
||||
## Run tasks before and after Nx
|
||||
|
||||
Before Nx, Bun runs a script across packages, but it runs every task every time and has no notion of which packages a change affects:
|
||||
Before Nx, Bun runs a script across packages, but it runs every task every time and has no notion of which packages a change affects. Place `--filter` before the script:
|
||||
|
||||
```shell
|
||||
bun run --filter '*' build # every package
|
||||
bun run --filter web build # a single package
|
||||
bun run --filter '*' build # every package
|
||||
bun run --filter web build # a single package
|
||||
```
|
||||
|
||||
After `nx init`, run the same scripts through Nx:
|
||||
@@ -110,9 +78,9 @@ nx affected -t build # only projects touched by your changes
|
||||
|
||||
The first run executes your scripts. A second run with no changes is [restored from the cache](/docs/features/cache-task-results), and [`nx affected`](/docs/features/ci-features/affected) skips the projects your change does not touch.
|
||||
|
||||
### Configure caching and dev servers
|
||||
## How task caching works
|
||||
|
||||
`nx init` writes an `nx.json` with `targetDefaults` that control how targets behave:
|
||||
`nx init` writes an `nx.json` with `targetDefaults` that control which targets are cached:
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
@@ -122,17 +90,12 @@ The first run executes your scripts. A second run with no changes is [restored f
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
},
|
||||
"dev": {
|
||||
"continuous": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`cache: true` makes a target cacheable: Nx hashes each project's inputs (source files, dependencies, and config) and restores its outputs from the cache when nothing has changed. Nothing is cached unless you opt in, so you stay in control. `dependsOn: ["^build"]` builds a project's dependencies first. `continuous: true` marks tasks that don't exit, like dev servers and watchers, so tasks that depend on them don't wait for them to complete.
|
||||
`cache: true` makes a target cacheable: Nx hashes each project's inputs (source files, dependencies, and config) and restores its outputs from the cache when nothing has changed. `dependsOn: ["^build"]` builds a project's dependencies first. Nothing is cached unless you opt in, so you stay in control.
|
||||
|
||||
### Speed up CI with Nx Cloud
|
||||
In CI, turn on [remote caching](/docs/features/ci-features/remote-cache) and [task distribution](/docs/features/ci-features/distribute-task-execution) to share the cache across machines and parallelize tasks.
|
||||
|
||||
Nx Cloud adds more ways to speed up CI for a Bun workspace: [remote caching](/docs/features/ci-features/remote-cache) shares the cache across CI runs and teammates, [Nx Agents](/docs/features/ci-features/distribute-task-execution) distribute tasks across machines, and [self-healing CI](/docs/features/ci-features/self-healing-ci) proposes fixes when tasks fail. For the full walkthrough, see [Adding Nx to an existing monorepo](/docs/guides/adopting-nx/adding-to-monorepo) and [CI setup](/docs/getting-started/setup-ci).
|
||||
|
||||
Working with a different package manager? See [pnpm workspaces](/docs/guides/tips-n-tricks/pnpm-workspaces), [npm workspaces](/docs/guides/tips-n-tricks/npm-workspaces), or [Yarn workspaces](/docs/guides/tips-n-tricks/yarn-workspaces). New to monorepos? Start with [what a monorepo is and why teams use one](/docs/concepts/decisions/what-is-a-monorepo).
|
||||
For a deeper walkthrough, see [Adding Nx to an NPM/Yarn/PNPM Workspace](/docs/guides/adopting-nx/adding-to-monorepo).
|
||||
|
||||
@@ -76,14 +76,6 @@ We recommend nesting your **app** specific `env` files in `apps/your-app`, and c
|
||||
for workspace-specific settings (like the [Nx Cloud token](/docs/guides/nx-cloud/access-tokens)).
|
||||
{% /aside %}
|
||||
|
||||
{% aside type="caution" title="Package managers can set variables before Nx runs" %}
|
||||
The rule above also applies to variables you didn't set yourself. A package manager can put variables into the process before Nx reads any `.env` file, and Nx keeps those values.
|
||||
|
||||
For example, the npm `node-options` setting (from a project, user, or global `.npmrc`) becomes a `NODE_OPTIONS` variable when you run `npm run nx ...` or `npx nx ...`. If you also set `NODE_OPTIONS` in `.env`, the `.npmrc` value wins, while running `nx` directly uses the `.env` value.
|
||||
|
||||
The package manager sets the value, not Nx. If you rely on a variable from `.env`, make sure nothing else sets it first.
|
||||
{% /aside %}
|
||||
|
||||
{% aside type="caution" title="Env files are not loaded in batch mode" %}
|
||||
The task-specific `.env` files described above are **not** loaded for tasks run with [batch mode](/docs/reference/glossary#batch-mode) (Gradle and Maven tasks run this way be default). Batch processes only receive the variables present in the current environment and root .env files, like `.env` and `.env.local`, so variables defined in files like `.env.[target-name]` won't be available.
|
||||
{% /aside %}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
---
|
||||
title: 'npm Workspaces: Setup, Commands, and Best Practices'
|
||||
description: 'Learn npm workspaces: the workspaces field, installing and linking local packages, running scripts with --workspace flags, and best practices that scale.'
|
||||
title: Use npm Workspaces with Nx
|
||||
description: Set up an npm workspace and add Nx in one command to get cached tasks, affected-only builds, and faster CI, with your existing package.json scripts working out of the box.
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
npm workspaces manage multiple packages in a single repository (or a monorepo) from one root `package.json`. You list package folders in the `workspaces` field, and `npm install` installs all of their dependencies in one pass, symlinking local packages into the root `node_modules` so they can depend on each other without publishing to a registry.
|
||||
An npm workspace lets you manage multiple packages in a single repository. Adding Nx to an npm workspace gives you cached tasks, affected-only builds, and faster CI, while npm keeps managing installs. Set up an npm workspace first, then add Nx.
|
||||
|
||||
For the full configuration reference, see the [npm workspaces documentation](https://docs.npmjs.com/cli/using-npm/workspaces).
|
||||
## Set up an npm workspace
|
||||
|
||||
## Set up npm workspaces
|
||||
|
||||
### 1. Add a workspaces field to package.json
|
||||
|
||||
An npm workspace is a repository whose root `package.json` has a `workspaces` field listing the directories that hold `package.json` files. The root must be `private` so it can't be published by accident:
|
||||
An npm workspace is a repository whose root `package.json` has a `workspaces` field listing the directories that hold `package.json` files:
|
||||
|
||||
```jsonc
|
||||
// package.json
|
||||
@@ -23,9 +19,7 @@ An npm workspace is a repository whose root `package.json` has a `workspaces` fi
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create the package folders
|
||||
|
||||
Every directory matched by those globs that contains a `package.json` becomes a workspace. A common layout separates applications from shared packages:
|
||||
A common layout separates applications from shared packages:
|
||||
|
||||
{% filetree %}
|
||||
|
||||
@@ -40,9 +34,7 @@ Every directory matched by those globs that contains a `package.json` becomes a
|
||||
|
||||
{% /filetree %}
|
||||
|
||||
### 3. Depend on local packages by name
|
||||
|
||||
npm doesn't use a `workspace:` protocol. To depend on another package in the workspace, reference it by name with `*` or a semver range that matches the local version, and npm symlinks the local package instead of fetching it from the registry:
|
||||
npm does not use a `workspace:` protocol. To depend on another package in the workspace, reference it by name with `*` so npm links the local package:
|
||||
|
||||
```jsonc
|
||||
// apps/web/package.json
|
||||
@@ -57,49 +49,23 @@ npm doesn't use a `workspace:` protocol. To depend on another package in the wor
|
||||
|
||||
Run `npm install` once at the root to install every package's dependencies and link the internal ones.
|
||||
|
||||
## Run scripts with --workspace and --workspaces
|
||||
## Add Nx in one command
|
||||
|
||||
The `--workspace` flag (short form `-w`) targets one workspace, and `--workspaces` targets all of them. Without `--if-present`, npm errors on any workspace that's missing the script:
|
||||
|
||||
```shell
|
||||
npm run build --workspaces --if-present # every package with a build script
|
||||
npm run build --workspace=web # a single package
|
||||
npm install axios -w web # add a dependency to one package
|
||||
```
|
||||
|
||||
Pass `--workspace` multiple times to target several packages in one command. `npm install b -w a` where `b` is another workspace adds it as a symlinked local dependency.
|
||||
|
||||
## Can npm workspaces filter by changed packages?
|
||||
|
||||
No. npm targets workspaces by name, but it has no equivalent of the pnpm `--filter` flag for selecting packages by dependency relationship or by what changed in git. Your options are to run every script on every push, maintain a wrapper script around `git diff`, or adopt a build tool that computes affected projects from a dependency graph.
|
||||
|
||||
Running everything is fine while the repository is small. The cost grows linearly with package count, and it's usually CI time that forces the move to one of the other two options.
|
||||
|
||||
## How npm hoists and dedupes dependencies
|
||||
|
||||
npm installs workspace dependencies into the root `node_modules` wherever versions allow, so shared dependencies exist once. When two packages need conflicting versions, the extra version is nested inside the individual package's `node_modules`. Run `npm dedupe` to flatten duplicates that accumulate over time.
|
||||
|
||||
Hoisting has a downside: any package can import any hoisted dependency, including ones it never declared. These phantom dependencies break when a package is extracted from the workspace or the hoisting layout shifts. Declare everything a package imports in its own `package.json`.
|
||||
|
||||
## Do npm workspaces replace a monorepo tool?
|
||||
|
||||
No. npm workspaces install and link packages, but they don't order tasks by dependency, cache results, or detect which projects a commit affects, so CI reruns every task on every push. Workspaces solve linking, and a build tool solves orchestration once task times grow.
|
||||
|
||||
When that becomes the bottleneck, add Nx on top without changing how npm works:
|
||||
Nx layers task running and caching on top of your existing npm workspace. npm still installs and resolves packages. Nx makes your tasks cacheable and your CI affected-aware. Add it with one command, which works on any npm, Yarn, pnpm, or Bun workspace:
|
||||
|
||||
```shell
|
||||
npx nx@latest init
|
||||
```
|
||||
|
||||
Your existing `package.json` scripts keep working and npm keeps managing installs.
|
||||
Your existing `package.json` scripts keep working. Nx infers a project for each `package.json` in your workspace and runs its scripts with caching, with no `project.json` or extra configuration required.
|
||||
|
||||
### Run tasks before and after Nx
|
||||
## Run tasks before and after Nx
|
||||
|
||||
Before Nx, npm runs a script across packages, but it runs every task every time and has no notion of which packages a change affects:
|
||||
|
||||
```shell
|
||||
npm run build --workspaces --if-present # every package
|
||||
npm run build --workspace=web # a single package
|
||||
npm run build --workspaces # every package
|
||||
npm run build --workspace=web # a single package
|
||||
```
|
||||
|
||||
After `nx init`, run the same scripts through Nx:
|
||||
@@ -112,9 +78,9 @@ nx affected -t build # only projects touched by your changes
|
||||
|
||||
The first run executes your scripts. A second run with no changes is [restored from the cache](/docs/features/cache-task-results), and [`nx affected`](/docs/features/ci-features/affected) skips the projects your change does not touch.
|
||||
|
||||
### Configure caching and dev servers
|
||||
## How task caching works
|
||||
|
||||
`nx init` writes an `nx.json` with `targetDefaults` that control how targets behave:
|
||||
`nx init` writes an `nx.json` with `targetDefaults` that control which targets are cached:
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
@@ -124,17 +90,12 @@ The first run executes your scripts. A second run with no changes is [restored f
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
},
|
||||
"dev": {
|
||||
"continuous": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`cache: true` makes a target cacheable: Nx hashes each project's inputs (source files, dependencies, and config) and restores its outputs from the cache when nothing has changed. Nothing is cached unless you opt in, so you stay in control. `dependsOn: ["^build"]` builds a project's dependencies first. `continuous: true` marks tasks that don't exit, like dev servers and watchers, so tasks that depend on them don't wait for them to complete.
|
||||
`cache: true` makes a target cacheable: Nx hashes each project's inputs (source files, dependencies, and config) and restores its outputs from the cache when nothing has changed. `dependsOn: ["^build"]` builds a project's dependencies first. Nothing is cached unless you opt in, so you stay in control.
|
||||
|
||||
### Speed up CI with Nx Cloud
|
||||
In CI, turn on [remote caching](/docs/features/ci-features/remote-cache) and [task distribution](/docs/features/ci-features/distribute-task-execution) to share the cache across machines and parallelize tasks.
|
||||
|
||||
Nx Cloud adds more ways to speed up CI for a npm workspace: [remote caching](/docs/features/ci-features/remote-cache) shares the cache across CI runs and teammates, [Nx Agents](/docs/features/ci-features/distribute-task-execution) distribute tasks across machines, and [self-healing CI](/docs/features/ci-features/self-healing-ci) proposes fixes when tasks fail. For the full walkthrough, see [Adding Nx to an existing monorepo](/docs/guides/adopting-nx/adding-to-monorepo) and [CI setup](/docs/getting-started/setup-ci).
|
||||
|
||||
Working with a different package manager? See [pnpm workspaces](/docs/guides/tips-n-tricks/pnpm-workspaces), [Yarn workspaces](/docs/guides/tips-n-tricks/yarn-workspaces), or [Bun workspaces](/docs/guides/tips-n-tricks/bun-workspaces). New to monorepos? Start with [what a monorepo is and why teams use one](/docs/concepts/decisions/what-is-a-monorepo).
|
||||
For a deeper walkthrough, see [Adding Nx to an NPM/Yarn/PNPM Workspace](/docs/guides/adopting-nx/adding-to-monorepo).
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
---
|
||||
title: 'pnpm Workspaces: Setup, Commands, and Best Practices'
|
||||
description: 'Learn pnpm workspaces: pnpm-workspace.yaml setup, the workspace: protocol, running scripts with --filter, catalogs, and best practices that scale.'
|
||||
title: Use pnpm Workspaces with Nx
|
||||
description: Set up a pnpm workspace and add Nx in one command to get cached tasks, affected-only builds, and faster CI, with your existing package.json scripts working out of the box.
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
pnpm workspaces let you develop multiple packages in a single repository (or a monorepo). You declare package locations in `pnpm-workspace.yaml`, link local packages with the `workspace:` protocol, and pnpm installs everything into one content-addressable store, so each package gets a strict `node_modules` without duplicating dependencies on disk.
|
||||
|
||||
For the full configuration reference, see the [pnpm workspaces documentation](https://pnpm.io/workspaces).
|
||||
A pnpm workspace lets you manage multiple packages in a single repository. Adding Nx to a pnpm workspace gives you cached tasks, affected-only builds, and faster CI, while pnpm keeps managing installs. Set up a pnpm workspace first, then add Nx.
|
||||
|
||||
## Set up a pnpm workspace
|
||||
|
||||
### 1. Create pnpm-workspace.yaml
|
||||
|
||||
A pnpm workspace is any repository with a `pnpm-workspace.yaml` file at its root. Add it next to a root `package.json` marked `"private": true` so the root is never published by accident. The `packages` field lists the directories that hold `package.json` files:
|
||||
A pnpm workspace is a repository whose root contains a `pnpm-workspace.yaml` file. Its `packages` field tells pnpm which directories hold `package.json` files:
|
||||
|
||||
```yaml
|
||||
# pnpm-workspace.yaml
|
||||
@@ -21,9 +17,7 @@ packages:
|
||||
- 'packages/*'
|
||||
```
|
||||
|
||||
### 2. Add packages
|
||||
|
||||
Every directory matched by those globs that contains a `package.json` becomes a workspace package. A common layout separates applications from shared packages:
|
||||
A common layout separates applications from shared packages:
|
||||
|
||||
{% filetree %}
|
||||
|
||||
@@ -39,9 +33,7 @@ Every directory matched by those globs that contains a `package.json` becomes a
|
||||
|
||||
{% /filetree %}
|
||||
|
||||
### 3. Link packages with the workspace: protocol
|
||||
|
||||
To depend on another package in the workspace, reference it with the `workspace:` protocol so pnpm always resolves it to the local package:
|
||||
To depend on another package in the workspace, reference it with the `workspace:` protocol so pnpm resolves it to the local package:
|
||||
|
||||
```jsonc
|
||||
// apps/web/package.json
|
||||
@@ -54,67 +46,19 @@ To depend on another package in the workspace, reference it with the `workspace:
|
||||
}
|
||||
```
|
||||
|
||||
The suffix controls what `pnpm publish` writes into the published `package.json`. With the workspace at version `1.5.0`, `workspace:*` becomes the exact version `1.5.0`, `workspace:^` becomes `^1.5.0`, and `workspace:~` becomes `~1.5.0`. During local development all of them use the local package.
|
||||
|
||||
Run `pnpm install` once at the root to install every package's dependencies and link the internal ones.
|
||||
|
||||
## Run scripts across packages with --filter
|
||||
## Add Nx in one command
|
||||
|
||||
`pnpm -r run build` runs the `build` script in every package. The `--filter` flag narrows that to a subset, and it understands the workspace's dependency relationships:
|
||||
|
||||
```shell
|
||||
pnpm --filter web run build # a single package
|
||||
pnpm --filter "web..." run build # web and everything it depends on
|
||||
pnpm --filter "...shared-ui" run test # shared-ui and everything that depends on it
|
||||
```
|
||||
|
||||
Filters also select packages by what changed in git. In CI, test only the packages a PR touches plus their dependents:
|
||||
|
||||
```shell
|
||||
pnpm --filter "...[origin/main]" run test
|
||||
```
|
||||
|
||||
`[origin/main]` selects packages with changes since that ref, and the leading `...` adds their dependents.
|
||||
|
||||
## Share dependency versions with pnpm catalogs
|
||||
|
||||
Catalogs (pnpm 9.5+) define a dependency version once in `pnpm-workspace.yaml` and reference it everywhere, so packages can't drift onto different versions of the same dependency:
|
||||
|
||||
```yaml
|
||||
# pnpm-workspace.yaml
|
||||
packages:
|
||||
- 'apps/*'
|
||||
- 'packages/*'
|
||||
catalog:
|
||||
react: ^19.0.0
|
||||
react-dom: ^19.0.0
|
||||
```
|
||||
|
||||
```jsonc
|
||||
// apps/web/package.json
|
||||
{
|
||||
"name": "web",
|
||||
"dependencies": {
|
||||
"react": "catalog:",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Upgrading React across the whole repository is now a one-line change, and version bumps stop producing merge conflicts in every `package.json`.
|
||||
|
||||
## Do pnpm workspaces replace a monorepo tool?
|
||||
|
||||
No. pnpm workspaces handle installing and linking packages, and that's where they stop. There's no task graph and no caching, and git-based `--filter` compares changed files rather than task inputs, so CI still reruns tasks a change couldn't have affected.
|
||||
|
||||
When task time becomes the bottleneck, add Nx on top without changing how pnpm works:
|
||||
Nx layers task running and caching on top of your existing pnpm workspace. pnpm still installs and resolves packages. Nx makes your tasks cacheable and your CI affected-aware. Add it with one command, which works on any npm, Yarn, pnpm, or Bun workspace:
|
||||
|
||||
```shell
|
||||
npx nx@latest init
|
||||
```
|
||||
|
||||
Your existing `package.json` scripts keep working and pnpm keeps managing installs.
|
||||
Your existing `package.json` scripts keep working. Nx infers a project for each `package.json` in your workspace and runs its scripts with caching, with no `project.json` or extra configuration required.
|
||||
|
||||
### Run tasks before and after Nx
|
||||
## Run tasks before and after Nx
|
||||
|
||||
Before Nx, pnpm runs a script across packages, but it runs every task every time and has no notion of which packages a change affects:
|
||||
|
||||
@@ -133,9 +77,9 @@ nx affected -t build # only projects touched by your changes
|
||||
|
||||
The first run executes your scripts. A second run with no changes is [restored from the cache](/docs/features/cache-task-results), and [`nx affected`](/docs/features/ci-features/affected) skips the projects your change does not touch.
|
||||
|
||||
### Configure caching and dev servers
|
||||
## How task caching works
|
||||
|
||||
`nx init` writes an `nx.json` with `targetDefaults` that control how targets behave:
|
||||
`nx init` writes an `nx.json` with `targetDefaults` that control which targets are cached:
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
@@ -145,17 +89,12 @@ The first run executes your scripts. A second run with no changes is [restored f
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
},
|
||||
"dev": {
|
||||
"continuous": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`cache: true` makes a target cacheable: Nx hashes each project's inputs (source files, dependencies, and config) and restores its outputs from the cache when nothing has changed. Nothing is cached unless you opt in, so you stay in control. `dependsOn: ["^build"]` builds a project's dependencies first. `continuous: true` marks tasks that don't exit, like dev servers and watchers, so tasks that depend on them don't wait for them to complete.
|
||||
`cache: true` makes a target cacheable: Nx hashes each project's inputs (source files, dependencies, and config) and restores its outputs from the cache when nothing has changed. `dependsOn: ["^build"]` builds a project's dependencies first. Nothing is cached unless you opt in, so you stay in control.
|
||||
|
||||
### Speed up CI with Nx Cloud
|
||||
In CI, turn on [remote caching](/docs/features/ci-features/remote-cache) and [task distribution](/docs/features/ci-features/distribute-task-execution) to share the cache across machines and parallelize tasks.
|
||||
|
||||
Nx Cloud adds more ways to speed up CI for a pnpm workspace: [remote caching](/docs/features/ci-features/remote-cache) shares the cache across CI runs and teammates, [Nx Agents](/docs/features/ci-features/distribute-task-execution) distribute tasks across machines, and [self-healing CI](/docs/features/ci-features/self-healing-ci) proposes fixes when tasks fail. For the full walkthrough, see [Adding Nx to an existing monorepo](/docs/guides/adopting-nx/adding-to-monorepo) and [CI setup](/docs/getting-started/setup-ci), or the [From pnpm Workspaces to Distributed CI](https://nx.dev/courses/pnpm-nx-next/lessons-00-overview) course.
|
||||
|
||||
Working with a different package manager? See [npm workspaces](/docs/guides/tips-n-tricks/npm-workspaces), [Yarn workspaces](/docs/guides/tips-n-tricks/yarn-workspaces), or [Bun workspaces](/docs/guides/tips-n-tricks/bun-workspaces). New to monorepos? Start with [what a monorepo is and why teams use one](/docs/concepts/decisions/what-is-a-monorepo).
|
||||
For a deeper walkthrough, see [Adding Nx to an NPM/Yarn/PNPM Workspace](/docs/guides/adopting-nx/adding-to-monorepo) or the [From pnpm Workspaces to Distributed CI](/courses/pnpm-nx-next/lessons-00-overview) course.
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
---
|
||||
title: 'Yarn Workspaces: Setup, Commands, and Best Practices'
|
||||
description: 'Learn Yarn workspaces: setup, linking packages, running scripts with yarn workspaces foreach, hoisting behavior, and best practices for growing monorepos.'
|
||||
title: Use Yarn Workspaces with Nx
|
||||
description: Set up a Yarn workspace and add Nx in one command to get cached tasks, affected-only builds, and faster CI, with your existing package.json scripts working out of the box.
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
Yarn workspaces let a single repository (or a monorepo) hold multiple packages that are developed together. You declare them in the root `package.json` `workspaces` field, and `yarn install` resolves the whole project at once, hoisting shared dependencies to the root and linking workspace packages to each other.
|
||||
|
||||
For the full configuration reference, see the [Yarn workspaces documentation](https://yarnpkg.com/features/workspaces).
|
||||
A Yarn workspace lets you manage multiple packages in a single repository. Adding Nx to a Yarn workspace gives you cached tasks, affected-only builds, and faster CI, while Yarn keeps managing installs. Set up a Yarn workspace first, then add Nx.
|
||||
|
||||
{% aside type="note" title="Looking for Yarn PnP?" %}
|
||||
This guide covers the workspaces feature. If you use the Plug'n'Play install mode, see the [Yarn PnP guide](/docs/guides/tips-n-tricks/yarn-pnp).
|
||||
This recipe covers Yarn's workspaces feature. If you use Yarn's Plug'n'Play install mode, see [Using Yarn PnP with Nx](/docs/guides/tips-n-tricks/yarn-pnp).
|
||||
{% /aside %}
|
||||
|
||||
## Set up Yarn workspaces
|
||||
## Set up a Yarn workspace
|
||||
|
||||
### 1. Declare workspaces in package.json
|
||||
|
||||
A Yarn workspace is a repository whose root `package.json` has a `workspaces` field listing the directories that hold `package.json` files. Mark the root `private` so it can't be published:
|
||||
A Yarn workspace is a repository whose root `package.json` has a `workspaces` field listing the directories that hold `package.json` files:
|
||||
|
||||
```jsonc
|
||||
// package.json
|
||||
@@ -27,9 +23,7 @@ A Yarn workspace is a repository whose root `package.json` has a `workspaces` fi
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create the workspace packages
|
||||
|
||||
Every directory matched by those globs that contains a `package.json` becomes a workspace. A common layout separates applications from shared packages:
|
||||
A common layout separates applications from shared packages:
|
||||
|
||||
{% filetree %}
|
||||
|
||||
@@ -44,9 +38,7 @@ Every directory matched by those globs that contains a `package.json` becomes a
|
||||
|
||||
{% /filetree %}
|
||||
|
||||
### 3. Link workspaces with the workspace: protocol
|
||||
|
||||
To depend on another workspace, reference it with the `workspace:` protocol (Yarn 2 and later) so Yarn always resolves it to the local package. On publish, Yarn replaces `workspace:*` with the package's exact version:
|
||||
To depend on another package in the workspace, reference it with the `workspace:` protocol so Yarn resolves it to the local package:
|
||||
|
||||
```jsonc
|
||||
// apps/web/package.json
|
||||
@@ -59,47 +51,25 @@ To depend on another workspace, reference it with the `workspace:` protocol (Yar
|
||||
}
|
||||
```
|
||||
|
||||
On Yarn 1 (Classic) there's no `workspace:` protocol, so reference the package by a version range that matches the local version, as npm workspaces do.
|
||||
|
||||
Run `yarn install` once at the root to install every package's dependencies and link the internal ones.
|
||||
|
||||
## Run scripts with yarn workspaces foreach
|
||||
## Add Nx in one command
|
||||
|
||||
`yarn workspaces foreach` runs a command across workspaces. The `-A` (`--all`) flag selects every workspace, `-p` runs in parallel, and `-t` orders execution topologically so dependencies build first:
|
||||
|
||||
```shell
|
||||
yarn workspaces foreach -A run build # every workspace
|
||||
yarn workspaces foreach -Apt run build # parallel, dependencies first
|
||||
yarn workspace web run build # a single workspace
|
||||
```
|
||||
|
||||
Yarn 4 ships `foreach` out of the box. On Yarn 3, add it first with `yarn plugin import workspace-tools`. On Yarn 1 (Classic), the equivalent is `yarn workspaces run build`.
|
||||
|
||||
## Do Yarn workspaces hoist dependencies?
|
||||
|
||||
Yes. With the default node-modules linker, Yarn hoists shared dependencies to the root `node_modules`, so each version is installed once and packages resolve it by walking up the directory tree. Only conflicting versions get nested inside an individual workspace's `node_modules` folder.
|
||||
|
||||
Hoisting means a workspace can import a dependency it never declared, which breaks the moment that package is published or moved. Declare every import in the workspace's own `package.json`. To restrict hoisting, set `nmHoistingLimits` in `.yarnrc.yml` (Yarn 2+). Yarn 1 used the `nohoist` field instead. Yarn PnP sidesteps hoisting entirely by resolving imports from a lockfile-driven map instead of `node_modules`.
|
||||
|
||||
## Do Yarn workspaces replace a monorepo tool?
|
||||
|
||||
No. Yarn workspaces install and link packages, `foreach -t` orders scripts, and `--since` selects workspaces changed since a git ref, but nothing is cached and that change detection compares files, not task inputs, so CI still reruns tasks a change couldn't have affected.
|
||||
|
||||
When that becomes the bottleneck, add Nx on top without changing how Yarn works:
|
||||
Nx layers task running and caching on top of your existing Yarn workspace. Yarn still installs and resolves packages. Nx makes your tasks cacheable and your CI affected-aware. Add it with one command, which works on any npm, Yarn, pnpm, or Bun workspace:
|
||||
|
||||
```shell
|
||||
npx nx@latest init
|
||||
```
|
||||
|
||||
Your existing `package.json` scripts keep working and Yarn keeps managing installs.
|
||||
Your existing `package.json` scripts keep working. Nx infers a project for each `package.json` in your workspace and runs its scripts with caching, with no `project.json` or extra configuration required.
|
||||
|
||||
### Run tasks before and after Nx
|
||||
## Run tasks before and after Nx
|
||||
|
||||
Before Nx, Yarn runs a script across workspaces, but it runs every task every time and has no notion of which workspaces a change affects:
|
||||
Before Nx, Yarn runs a script across packages, but it runs every task every time and has no notion of which packages a change affects:
|
||||
|
||||
```shell
|
||||
yarn workspaces foreach -A run build # every workspace
|
||||
yarn workspace web run build # a single workspace
|
||||
yarn workspaces foreach --all run build # every workspace
|
||||
yarn workspace web run build # a single workspace
|
||||
```
|
||||
|
||||
After `nx init`, run the same scripts through Nx:
|
||||
@@ -112,9 +82,9 @@ nx affected -t build # only projects touched by your changes
|
||||
|
||||
The first run executes your scripts. A second run with no changes is [restored from the cache](/docs/features/cache-task-results), and [`nx affected`](/docs/features/ci-features/affected) skips the projects your change does not touch.
|
||||
|
||||
### Configure caching and dev servers
|
||||
## How task caching works
|
||||
|
||||
`nx init` writes an `nx.json` with `targetDefaults` that control how targets behave:
|
||||
`nx init` writes an `nx.json` with `targetDefaults` that control which targets are cached:
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
@@ -124,17 +94,12 @@ The first run executes your scripts. A second run with no changes is [restored f
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"],
|
||||
},
|
||||
"dev": {
|
||||
"continuous": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`cache: true` makes a target cacheable: Nx hashes each project's inputs (source files, dependencies, and config) and restores its outputs from the cache when nothing has changed. Nothing is cached unless you opt in, so you stay in control. `dependsOn: ["^build"]` builds a project's dependencies first. `continuous: true` marks tasks that don't exit, like dev servers and watchers, so tasks that depend on them don't wait for them to complete.
|
||||
`cache: true` makes a target cacheable: Nx hashes each project's inputs (source files, dependencies, and config) and restores its outputs from the cache when nothing has changed. `dependsOn: ["^build"]` builds a project's dependencies first. Nothing is cached unless you opt in, so you stay in control.
|
||||
|
||||
### Speed up CI with Nx Cloud
|
||||
In CI, turn on [remote caching](/docs/features/ci-features/remote-cache) and [task distribution](/docs/features/ci-features/distribute-task-execution) to share the cache across machines and parallelize tasks.
|
||||
|
||||
Nx Cloud adds more ways to speed up CI for a Yarn workspace: [remote caching](/docs/features/ci-features/remote-cache) shares the cache across CI runs and teammates, [Nx Agents](/docs/features/ci-features/distribute-task-execution) distribute tasks across machines, and [self-healing CI](/docs/features/ci-features/self-healing-ci) proposes fixes when tasks fail. For the full walkthrough, see [Adding Nx to an existing monorepo](/docs/guides/adopting-nx/adding-to-monorepo) and [CI setup](/docs/getting-started/setup-ci).
|
||||
|
||||
Working with a different package manager? See [pnpm workspaces](/docs/guides/tips-n-tricks/pnpm-workspaces), [npm workspaces](/docs/guides/tips-n-tricks/npm-workspaces), or [Bun workspaces](/docs/guides/tips-n-tricks/bun-workspaces). New to monorepos? Start with [what a monorepo is and why teams use one](/docs/concepts/decisions/what-is-a-monorepo).
|
||||
For a deeper walkthrough, see [Adding Nx to an NPM/Yarn/PNPM Workspace](/docs/guides/adopting-nx/adding-to-monorepo).
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
---
|
||||
title: Nx vs Bazel
|
||||
description: Bazel offers rigorous caching and remote execution at extreme scale, at the cost of an upfront migration and ongoing build engineering. Nx delivers most of the same CI wins on your existing tooling, with a platform layer Bazel doesn't have.
|
||||
filter: 'type:Guides'
|
||||
sidebar:
|
||||
label: Nx vs Bazel
|
||||
---
|
||||
|
||||
Bazel gives you rigorous, hermetic caching and remote execution. It also asks for an explicit migration, typically weeks, and ongoing upkeep. Nx targets the same CI wins on the tooling you already run, adopted incrementally.
|
||||
|
||||
## What is Nx?
|
||||
|
||||
Nx is a build system that orchestrates the tools you already use. Plugins configure tasks from your existing configs, caching works locally and remotely, `nx affected` runs only what your change touches, and Nx Cloud distributes tasks across CI machines. It spans JS/TS, JVM, .NET, and more in one graph.
|
||||
|
||||
## What is Bazel?
|
||||
|
||||
Bazel is the open-source version of Google's internal build system, built for hermetic, reproducible builds across large polyglot codebases. It caches and distributes at the level of individual build actions with strong correctness guarantees, and requires explicit `BUILD` files describing every target.
|
||||
|
||||
## Quick takeaway
|
||||
|
||||
Bazel is strong at hermetic caching, remote execution, and reproducibility, and a small number of massive, heavily polyglot organizations need that depth. For nearly everyone else the cost outweighs the payoff, and it lands up front:
|
||||
|
||||
- **Bazel pays off only after the work is done.** `BUILD` files, hermeticity, and a migration measured in weeks (longer across teams), plus ongoing upkeep that Aspect, maintainer of Bazel's JavaScript rulesets, estimates at [0.75 to 1 full-time build engineer](https://aspect.build/blog/estimating-bazel-cicd).
|
||||
- **Nx delivers the same CI wins from day one.** Caching, affected detection, and distribution on your existing scripts, adopted incrementally, scaling to large monorepos without a dedicated build team, plus generators, migrations, release, and AI-assisted CI that Bazel has no native answer for.
|
||||
|
||||
| Topic | Nx | Bazel |
|
||||
| --------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------- |
|
||||
| [Adoption](#adoption) | Incremental on existing tooling via `nx init` | `BUILD` files per package, weeks to migrate |
|
||||
| [Caching](#caching) | Task-level, with sandboxed execution for cache integrity | Action-level, sandbox-enforced by construction |
|
||||
| [Distribution](#distribution) | Task-level Nx Agents plus Atomizer, no rewrite required | Action-level remote execution via third-party REAPI providers |
|
||||
| [JavaScript ecosystem](#javascript-ecosystem) | First-party plugins for Vite, Jest, Next.js, Playwright | `rules_js`/`rules_ts` maintained by a single small vendor |
|
||||
| [Polyglot support](#polyglot-support) | JS/TS native, plugins for Gradle, Maven, .NET | First-class C++, Java, Go, Rust, Python |
|
||||
| [Module boundaries](#module-boundaries) | Tag-based lint rule plus conformance rules | Native per-target visibility, enforced at build time |
|
||||
| [Affected detection](#affected-detection) | `nx affected` built in | `bazel query` scripting or target-determinator, DIY |
|
||||
| [Tooling layer](#tooling-layer) | Generators, migrations, release, TUI, Nx Console, AI | Not provided, assembled from third-party pieces |
|
||||
|
||||
## Adoption
|
||||
|
||||
Bazel requires the build to be described up front: a `BUILD` file per package with declared inputs, plus the hermeticity work of breaking undeclared dependencies. Generators like Gazelle and `aspect configure` automate much of it, and AI codemods help more, so a realistic migration runs several weeks, growing when many teams move in parallel. [Airbnb's web migration](https://medium.com/airbnb-engineering/adopting-bazel-for-web-at-scale-a784b2dbe325) shows the shape of the pre-work at a large org: `BUILD` files for roughly a thousand top-level directories, custom generation tooling, and repo-wide dependency-cycle cleanup. There is no supported incremental path: a dependency chain converts fully before caching pays off.
|
||||
|
||||
Nx adopts in place. Existing scripts run unchanged, [plugins automatically configure tasks](/docs/concepts/inferred-tasks) from the tool configs you already have, and caching starts paying off per task as you opt tasks in. See [adding Nx to an existing project](/docs/guides/adopting-nx/adding-to-existing-project).
|
||||
|
||||
## Caching
|
||||
|
||||
Bazel's caching is rigorous. It caches per action, and its execution sandbox blocks undeclared file reads by construction, so cache hits are structurally trustworthy. The caveats are operational rather than architectural: hermeticity depends on team discipline (`no-sandbox` tags, unsandboxed repository rules, host toolchain leaks), and there's no sandboxing on Windows.
|
||||
|
||||
Nx caches per task, with inputs derived from tool configuration by plugins and composable [`namedInputs`](/docs/reference/inputs). Cache integrity is backed by [Nx Cloud task sandboxing](/docs/features/ci-features/sandboxing), which runs tasks in a sandbox and surfaces undeclared reads or writes, with an opt-in strict mode that fails the task. The difference from Bazel is when it applies: Bazel enforces at every action by default, while Nx sandboxing is an opt-in layer through Nx Cloud. [Nx Replay](/docs/features/ci-features/remote-cache) provides the managed remote cache with [branch-scoped isolation](/docs/concepts/ci-concepts/cache-security).
|
||||
|
||||
## Distribution
|
||||
|
||||
Bazel's remote execution ships individual actions to worker fleets over the open REAPI protocol, the finest-grained distribution model there is, served by providers like BuildBuddy and EngFlow or self-hosted clusters. It requires full Bazel adoption to use.
|
||||
|
||||
[Nx Agents](/docs/features/ci-features/distribute-task-execution) distribute at task granularity with dynamic balancing from timing history, and [Atomizer](/docs/features/ci-features/split-e2e-tasks) narrows the granularity gap by splitting slow e2e suites into per-file tasks. Enablement is a single flag on top of unchanged tooling, and [flaky task detection](/docs/features/ci-features/flaky-tasks) plus [self-healing CI](/docs/features/ci-features/self-healing-ci) handle the failure side.
|
||||
|
||||
## JavaScript ecosystem
|
||||
|
||||
Bazel's JavaScript story depends on `rules_js` and `rules_ts`, actively maintained but by a single small vendor (Aspect), after Google stopped supporting the original `rules_nodejs`. The rules require pnpm with hoisting disabled, put `node_modules` inside `bazel-out`, and have no first-party Vitest ruleset, and dev servers run through wrapper tooling. Google's own Angular team shipped Bazel support in the CLI and then [retracted it](https://dev.to/bazel/angular-bazel-leaving-angular-labs-51ja), concluding most Angular applications don't have the problem Bazel solves.
|
||||
|
||||
Nx grew up in the JS ecosystem: first-party plugins for Vite, Jest, Vitest, Next.js, Playwright, Storybook, and the rest, with dev servers, HMR, and framework upgrades handled through [automated migrations](/docs/features/automate-updating-dependencies).
|
||||
|
||||
## Polyglot support
|
||||
|
||||
Bazel treats C++, Java, Go, Rust, and Python as first-class citizens with mature rulesets, which is why heavily polyglot organizations at large scale reach for it. Polyglot coverage in Nx is younger and plugin-based: [`@nx/gradle`](/docs/technologies/java/gradle/introduction), Maven, and .NET plugins parse those builds into the graph, with community plugins for [Python](https://www.npmjs.com/package/@nxlv/python), Rust, and Go. A non-JavaScript repository doesn't need a root `package.json`: [install Nx globally](/docs/getting-started/installation#global-installation) and a Gradle or .NET workspace runs without JS scaffolding. For a JVM-and-JS repository the coverage is comparable. For a C++-heavy one, Bazel is ahead.
|
||||
|
||||
## Module boundaries
|
||||
|
||||
Credit where due: Bazel's per-target `visibility` is stronger enforcement than a lint rule. Targets are private by default and violations fail the build, in every language. Nx [enforces boundaries](/docs/features/enforce-module-boundaries) with tags at lint time for JS/TS, extended by [conformance rules](/docs/enterprise/conformance) for other languages, softer enforcement, but requiring no per-target declarations.
|
||||
|
||||
## Affected detection
|
||||
|
||||
`nx affected` is built in and works from the first day. Bazel can compute the same information through `bazel query` scripting or the community target-determinator project, and large Bazel shops all build this glue, but it's glue you own and maintain.
|
||||
|
||||
## Tooling layer
|
||||
|
||||
Above the build sits everything else a monorepo team touches daily, and here the gap runs the other way. Nx ships [code generators](/docs/features/generate-code), [automated dependency migrations](/docs/features/automate-updating-dependencies), [release management](/docs/features/manage-releases), a terminal UI, [Nx Console](/docs/getting-started/editor-setup) for VS Code and JetBrains, and AI integration through the [MCP server](/docs/reference/nx-mcp) and agent skills. Bazel provides none of these natively. JetBrains now maintains a solid IntelliJ plugin, and the rest is assembled from third-party or internal tooling.
|
||||
|
||||
## Who should pick which
|
||||
|
||||
Nx fits when any of these apply:
|
||||
|
||||
- You want caching, affected detection, and distribution without a migration project.
|
||||
- Your stack is JS/TS-first, or mixes JavaScript with Gradle, Maven, or .NET.
|
||||
- You want generators, automated migrations, release management, and AI-assisted CI in one tool.
|
||||
- Your team can't dedicate engineers to running a build system.
|
||||
|
||||
Bazel is worth its cost only when:
|
||||
|
||||
- You run a large, heavily polyglot codebase across C++, Go, Java, and Rust.
|
||||
- Strict hermeticity, reproducibility, or compliance is a hard requirement.
|
||||
- You have a funded build team to own the migration and ongoing maintenance.
|
||||
|
||||
Unless you are in that extreme-scale, deeply polyglot case, Nx alone covers the ground Bazel does without the migration or the standing build team.
|
||||
|
||||
## Resources
|
||||
|
||||
{% cards cols=2 %}
|
||||
{% card title="Add Nx to an existing project" description="Adopt Nx incrementally without rewriting tool configuration" url="/docs/guides/adopting-nx/adding-to-existing-project" /%}
|
||||
{% card title="Nx plugin registry" description="First-party and community plugins across languages and frameworks" url="/docs/plugin-registry" /%}
|
||||
{% /cards %}
|
||||
@@ -1,133 +0,0 @@
|
||||
---
|
||||
title: Nx vs Blacksmith
|
||||
description: Blacksmith replaces GitHub Actions runners with faster machines. Nx reduces how much work runs at all with affected detection, remote task caching, and dynamic distribution, the larger lever in a monorepo.
|
||||
filter: 'type:Guides'
|
||||
sidebar:
|
||||
label: Nx vs Blacksmith
|
||||
---
|
||||
|
||||
Blacksmith makes your CI machines faster. Nx makes far less work run on them at all, through affected detection, remote caching, and task distribution, the bigger win in a monorepo.
|
||||
|
||||
## What is Nx?
|
||||
|
||||
Nx is a monorepo platform: a task runner, remote caching, distributed CI across machines, e2e test splitting, flaky-task handling, self-healing CI, and editor integration. It layers onto whatever CI provider you already run.
|
||||
|
||||
## What is Blacksmith?
|
||||
|
||||
Blacksmith is a drop-in replacement for GitHub Actions runners. It moves your jobs onto faster bare-metal machines with accelerated dependency and Docker layer caches.
|
||||
|
||||
## Quick takeaway
|
||||
|
||||
Blacksmith and Nx work on **different layers**:
|
||||
|
||||
- **Blacksmith is a runner provider.** Swap the `runs-on` label and every job runs on a faster machine.
|
||||
- **Nx is a monorepo platform.** It reads your project graph to skip unaffected tasks, replay cached results, and distribute the rest across machines.
|
||||
|
||||
Blacksmith gives a **one-time raw-speed bump**, and past that there is little more to squeeze. **Nx keeps compounding**: task distribution, flaky re-runs, and task-level caching, plus it drops the [manual matrix and sharding](#distribution) that become the real maintenance cost once simple parallelism stops scaling.
|
||||
|
||||
| Topic | Nx Cloud | Blacksmith |
|
||||
| ----------------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------- |
|
||||
| [Model](#model) | Task orchestration on any CI provider | Faster runners for GitHub Actions |
|
||||
| [Remote caching](#remote-caching) | Content-addressed task cache, shared with local dev | Accelerated `actions/cache` and per-job sticky disks |
|
||||
| [Distribution](#distribution) | Task-level, dynamically balanced from timing history | Static workflow matrix with manual sharding |
|
||||
| [Test splitting and flaky tests](#test-splitting-and-flaky-tests) | Atomizer, flaky re-runs, self-healing CI | Not available |
|
||||
| [Observability](#observability) | Task-level cache, timing, and agent utilization analytics | Job-level performance and cost dashboards, log search |
|
||||
| [Pricing model](#pricing-model) | Credit-based plans | Per-minute usage billing with a monthly free tier |
|
||||
|
||||
## Model
|
||||
|
||||
Blacksmith's pitch is speed per job: ephemeral Firecracker microVMs on high-clock CPUs, fast cache downloads, and its flagship Docker layer caching. Migration is a one-line label change, with a wizard that rewrites your workflows. If your workflows are slow because GitHub's hosted runners are slow, it delivers.
|
||||
|
||||
Blacksmith runs whatever your workflow matrix declares, on every push, whether or not the change affected those projects. Nx Cloud starts from the task graph: prune what the change can't reach, replay what already ran, distribute the rest.
|
||||
|
||||
## Remote caching
|
||||
|
||||
Blacksmith transparently accelerates the `actions/cache` API and offers sticky disks, persistent volumes remounted between jobs, for dependencies and Docker layers. This caching is coarse and manual: you declare which paths to store under a key you compute yourself (`path: node_modules`, `key: ${{ hashFiles(...) }}`), so it restores files by key match rather than understanding a task's inputs and outputs. It speeds up restoring state, but a task that already ran with identical inputs still reruns.
|
||||
|
||||
[Nx Replay](/docs/features/ci-features/remote-cache) caches at the task level and derives the cache key from each task's actual inputs, so there are no paths or keys to maintain. A build or test computed once, on CI or on a developer laptop, replays everywhere else with its terminal output and artifacts, with [access-controlled, branch-isolated writes](/docs/concepts/ci-concepts/cache-security).
|
||||
|
||||
## Distribution
|
||||
|
||||
With Nx, distribution is a single job. `nx run-many` runs every check, and one `--distribute-on` line hands tasks to [Nx Agents](/docs/features/ci-features/distribute-task-execution), assigned continuously from historical timing data, with no matrix and no shard count to maintain.
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci.yml (Nx)
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: npm ci
|
||||
- run: npx nx-cloud start-ci-run --distribute-on="8 linux-medium-js"
|
||||
- run: npx nx run-many -t lint test build e2e
|
||||
```
|
||||
|
||||
Blacksmith is a runner provider, so distribution is on you. Swap `runs-on` for a faster machine, then hand-write a job per check plus a static matrix to shard slow suites. Each job is a fresh, isolated runner, so every check repeats the checkout and install, and the shard count is fixed before the run, so a slow shard can't hand work to an idle one.
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci.yml (Blacksmith)
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: npm ci
|
||||
- run: npx eslint .
|
||||
test:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: npm ci
|
||||
- run: npx jest
|
||||
build:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
e2e:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
strategy:
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4] # shard count fixed before the run, rebalanced by hand
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: npm ci
|
||||
- run: npx playwright test --shard=${{ matrix.shard }}/4
|
||||
```
|
||||
|
||||
## Test splitting and flaky tests
|
||||
|
||||
Blacksmith has no test splitting and no automated flaky-test detection or re-running. Its test analytics only report failing tests in PR comments, which neither shortens a slow suite nor stops a flake from failing the build.
|
||||
|
||||
Nx Cloud acts on what it detects. [Atomizer](/docs/features/ci-features/split-e2e-tasks) splits e2e suites into per-file tasks that distribute across agents, [flaky task detection](/docs/features/ci-features/flaky-tasks) retries nondeterministic tasks in isolation instead of failing the pipeline, and [self-healing CI](/docs/features/ci-features/self-healing-ci) proposes verified fixes for real failures.
|
||||
|
||||
## Observability
|
||||
|
||||
Blacksmith provides job-level dashboards: performance and cost per team, run history, global log search, and SSH into live jobs. Nx Cloud's analytics operate at the task level: cache hit rates, per-task timings, and agent utilization breakdowns per run, the data that shows which task to tune when CI slows down.
|
||||
|
||||
## Pricing model
|
||||
|
||||
Blacksmith bills per minute with a free tier. Nx Cloud also has a free tier and usage-based billing, so billing isn't a Blacksmith advantage. The difference is what you get for it: task-level caching, distribution, and flaky re-runs cut CI further than faster minutes alone.
|
||||
|
||||
## Who should pick which
|
||||
|
||||
Nx fits when any of these apply:
|
||||
|
||||
- Most tasks on most PRs are unaffected or already cached and you want to skip them.
|
||||
- CI time calls for dynamic distribution, e2e splitting, or flaky-task handling.
|
||||
- You want a task cache shared across CI and developer machines, plus task-level analytics.
|
||||
|
||||
Blacksmith alone is enough only when:
|
||||
|
||||
- You want a one-line runner swap and nothing beyond faster machines.
|
||||
- The bottleneck is raw machine speed on a repository small enough to run everything each push.
|
||||
|
||||
Nx runs on your existing CI, so you can cut CI time and add distribution without switching runner providers. Nx Agents also run on sized resource classes, so you get faster machines too, not just a one-time raw-speed bump. If you want CI that scales with your team, [set up Nx Agents](/docs/features/ci-features/distribute-task-execution#enable-nx-agents).
|
||||
|
||||
## Resources
|
||||
|
||||
{% cards cols=2 %}
|
||||
{% card title="Nx Cloud CI features" description="Remote caching, distribution, splitting, and self-healing CI" url="/docs/features/ci-features" /%}
|
||||
{% card title="Connect your CI" description="Add Nx Cloud to an existing pipeline" url="/docs/getting-started/setup-ci" /%}
|
||||
{% /cards %}
|
||||
@@ -1,92 +0,0 @@
|
||||
---
|
||||
title: Nx vs Buildkite
|
||||
description: Buildkite is a CI platform you migrate to. Nx is a task-graph layer that runs on any CI, including Buildkite. Compare caching, affected detection, distribution, and test splitting, or use them together.
|
||||
filter: 'type:Guides'
|
||||
sidebar:
|
||||
label: Nx vs Buildkite
|
||||
---
|
||||
|
||||
Buildkite is a CI platform you migrate to. Nx is a task-graph layer that runs on any CI, Buildkite included, adding caching, affected detection, and distribution without switching CI providers.
|
||||
|
||||
## What is Nx?
|
||||
|
||||
Nx is a monorepo platform: a task-running CLI, remote caching, distributed CI across machines, e2e test splitting, flaky-task handling, self-healing CI, and editor integration. It layers onto whatever CI provider you already run.
|
||||
|
||||
## What is Buildkite?
|
||||
|
||||
Buildkite is a CI/CD platform with a hosted control plane and agents that run on your own infrastructure. It includes Test Engine for test analytics and splitting, package registries, and scales to very high job concurrency.
|
||||
|
||||
## Quick takeaway
|
||||
|
||||
Buildkite and Nx aren't alternatives. They sit at different layers:
|
||||
|
||||
- **Buildkite replaces your CI platform.** You move pipelines onto it, and its hybrid model keeps source code on agents you operate.
|
||||
- **Nx replaces nothing.** It adds task-graph intelligence, caching, affected detection, and distribution, to whatever CI you already run.
|
||||
|
||||
They overlap only on monorepo awareness, test splitting, and how work spreads across machines, and Buildkite itself ships an [`nx-set-shas` plugin](https://buildkite.com/resources/plugins/buildkite-plugins/nx-set-shas-buildkite-plugin/), a sign Buildkite users adopt Nx.
|
||||
|
||||
| Topic | Nx Cloud | Buildkite |
|
||||
| ----------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------- |
|
||||
| [Model](#model) | Task-graph layer on your existing CI | CI platform with hybrid control plane + your agents |
|
||||
| [Remote caching](#remote-caching) | Nx Replay, content-addressed, shared with local dev | No task-result cache, dependency caches via plugins |
|
||||
| [Affected detection](#affected-detection) | Transitive, from the project graph | `monorepo-diff` plugin, folder-path matching |
|
||||
| [Distribution](#distribution) | Task-level Nx Agents, dynamically balanced | Job-level `parallelism`, declared manually |
|
||||
| [Test splitting and flaky tests](#test-splitting-and-flaky-tests) | Atomizer per-file tasks, task-level flaky re-runs | Test Engine: per-test splitting and quarantine via collectors |
|
||||
| [AI integration](#ai-integration) | Self-healing CI proposes verified fixes | MCP server and agentic workflow building blocks |
|
||||
|
||||
## Model
|
||||
|
||||
Buildkite's hybrid architecture is its defining strength: the SaaS control plane schedules work, while source code, secrets, and deploy credentials stay on the self-hosted agents you run (Buildkite also offers hosted agents). It scales to extremes, with Buildkite's largest customers running at very high agent concurrency. The cost is that Buildkite is a migration: pipelines get rewritten in its YAML, and the platform becomes your CI.
|
||||
|
||||
Nx requires no platform switch. It plugs into GitHub Actions, GitLab, Jenkins, Buildkite, or any other provider, and its intelligence comes from the Nx task graph rather than the pipeline definition. On Nx Enterprise, [Nx Agents can run on your own compute](/docs/guides/nx-cloud/bring-your-own-compute), preserving the code-stays-on-your-infrastructure property.
|
||||
|
||||
## Remote caching
|
||||
|
||||
Buildkite has no task-result cache. Dependency caches can be shared across pipelines with the community cache plugin (S3 or GCS), and hosted-agent cache volumes offer best-effort local disk, but neither replays a computed task, so a task that ran on one agent reruns everywhere else.
|
||||
|
||||
[Nx Replay](/docs/features/ci-features/remote-cache) is a content-addressed task cache shared across every CI machine and every developer laptop, with [branch-scoped isolation against cache poisoning](/docs/concepts/ci-concepts/cache-security).
|
||||
|
||||
## Affected detection
|
||||
|
||||
Buildkite's monorepo answer is the `monorepo-diff` plugin: watch folder paths, trigger pipelines when files under them change. Path matching can't see that a shared library change breaks an app three dependency hops away, so teams either over-trigger or miss affected projects.
|
||||
|
||||
[`nx affected`](/docs/features/ci-features/affected) computes reachability through the project graph, catching transitive impact and skipping everything else. Buildkite's own `nx-set-shas` plugin exists precisely to feed this command the right base commit on Buildkite pipelines.
|
||||
|
||||
## Distribution
|
||||
|
||||
Buildkite parallelism is declared per job (`parallelism: N`) and maintained by hand. The control plane schedules jobs, not tasks. [Nx Agents](/docs/features/ci-features/distribute-task-execution) distribute individual tasks from the graph across machines, ordering them by task dependencies and balancing from historical timing data, with no static assignments to maintain as the workspace grows.
|
||||
|
||||
## Test splitting and flaky tests
|
||||
|
||||
Buildkite Test Engine is a mature test observability product: per-framework collectors feed timing data to the `bktec` CLI for splitting, and flaky management can auto-label, quarantine (mute or skip a single test), notify, and auto-restore tests. Its per-test granularity goes further than Nx in one respect: you can quarantine one test case. It requires instrumenting each runner with a collector SDK, and split plans live in pipeline YAML.
|
||||
|
||||
Nx works at the task level with no runner SDKs. [Atomizer](/docs/features/ci-features/split-e2e-tasks) turns an e2e suite into per-file tasks through the plugin-configured task graph, and each split task is independently cacheable and distributable. [Flaky task detection](/docs/features/ci-features/flaky-tasks) spots nondeterministic tasks from history and retries them on a different agent automatically.
|
||||
|
||||
## AI integration
|
||||
|
||||
Buildkite ships agentic building blocks: a remote MCP server, model providers for connecting pipelines to LLMs, and universal triggers. What you build with them is up to you.
|
||||
|
||||
Nx Cloud ships the finished loop: [self-healing CI](/docs/features/ci-features/self-healing-ci) analyzes failed tasks, proposes a fix, verifies it, and posts it to the PR, and the [Nx MCP server](/docs/reference/nx-mcp) gives local coding agents the same workspace and CI context.
|
||||
|
||||
## Who should pick which
|
||||
|
||||
Nx fits when any of these apply:
|
||||
|
||||
- You want to optimize a monorepo without switching CI providers.
|
||||
- You need build-graph-aware distribution, remote task caching, or affected detection.
|
||||
- You want e2e splitting, flaky-task handling, or self-healing CI.
|
||||
|
||||
Buildkite is the right call only when:
|
||||
|
||||
- You're replacing your CI platform outright, not layering onto an existing one.
|
||||
- Code must stay on your own infrastructure at very high job concurrency.
|
||||
- You want a single platform spanning pipelines, test analytics, and package registries.
|
||||
|
||||
Nx layers onto whatever CI you already run, so you get graph-aware caching and distribution without adopting a new platform.
|
||||
|
||||
## Resources
|
||||
|
||||
{% cards cols=2 %}
|
||||
{% card title="Nx Cloud CI features" description="Remote caching, distribution, splitting, and self-healing CI" url="/docs/features/ci-features" /%}
|
||||
{% card title="Connect your CI" description="Add Nx Cloud to an existing pipeline" url="/docs/getting-started/setup-ci" /%}
|
||||
{% /cards %}
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
title: Nx vs Depot
|
||||
description: Depot makes every CI minute faster and cheaper with better runners and colocated caches. Nx removes minutes entirely with affected detection, remote task caching, and distributed execution. They solve different layers, and only one of them removes work.
|
||||
filter: 'type:Guides'
|
||||
sidebar:
|
||||
label: Nx vs Depot
|
||||
---
|
||||
|
||||
Depot makes every CI minute faster. Nx makes fewer minutes run at all, by caching, pruning, and distributing work across your task graph. They solve different layers, and only one of them removes work.
|
||||
|
||||
## What is Nx?
|
||||
|
||||
Nx is a monorepo platform: a task-running CLI, remote caching, distributed CI across machines, e2e test splitting, flaky-task handling, self-healing CI, and editor integration. It layers onto whatever CI provider you already run.
|
||||
|
||||
## What is Depot?
|
||||
|
||||
Depot sells faster CI compute: GitHub Actions runners on newer CPUs, accelerated container builds, a remote cache for tools like Turborepo and Bazel, and its own CI engine. It speeds up the machines your jobs run on.
|
||||
|
||||
## Quick takeaway
|
||||
|
||||
Depot and Nx aren't the same kind of product:
|
||||
|
||||
- **Depot makes the machine faster.** Better CPUs, colocated caches, and quick container builds, behind a one-line runner swap.
|
||||
- **Nx sits above the machine and decides what runs.** It prunes unaffected tasks, replays cached results across CI and developer laptops, and distributes the rest.
|
||||
|
||||
Depot Cache doesn't even support Nx, and for a monorepo raw machine speed matters far less than the work Nx removes before it runs.
|
||||
|
||||
| Topic | Nx Cloud | Depot |
|
||||
| ----------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| [Model](#model) | Task-graph platform on top of your existing CI | Faster runners, container builds, and a CI engine |
|
||||
| [Remote caching](#remote-caching) | Nx Replay caches Nx tasks across developers and CI | Depot Cache supports Turborepo, Bazel, Gradle, but not Nx |
|
||||
| [Cache security](#cache-security) | Enforced branch-scoped isolation against cache poisoning | Shared namespace, isolation left to your cache keys |
|
||||
| [Affected detection](#affected-detection) | `nx affected` prunes tasks before anything runs | Not available |
|
||||
| [Distribution](#distribution) | Task-level distribution with dynamic balancing | Job-level scheduling, sharding is manual |
|
||||
| [Test splitting and flaky tests](#test-splitting-and-flaky-tests) | Atomizer, flaky detection, self-healing CI | Not available |
|
||||
| [Observability](#observability) | Task-level cache and utilization analytics | Job and machine metrics, log search |
|
||||
|
||||
## Model
|
||||
|
||||
Depot's runners are a one-line `runs-on` swap that puts your jobs on 4th-generation AMD CPUs with RAM-disk options and high-throughput colocated caches, billed per second. Depot CI, released in 2026, goes further with its own orchestrator that runs GitHub Actions-compatible pipelines with fast job startup and SSH debugging. If your CI is slow because the machines are slow, Depot fixes that directly.
|
||||
|
||||
What Depot doesn't see is your workspace. Every job still runs whatever your pipeline tells it to, whether or not the change affected those projects, and whether or not the same task already ran with identical inputs. Nx Cloud starts from the task graph, so those two questions get answered before compute is spent.
|
||||
|
||||
## Remote caching
|
||||
|
||||
Depot Cache is a remote cache backend for Turborepo, Bazel, Gradle, sccache, and Pants. Nx is not a supported tool. For Nx workspaces, Depot's docs point to filesystem caching paired with generic CI cache actions.
|
||||
|
||||
[Nx Replay](/docs/features/ci-features/remote-cache) is built for Nx tasks: every developer machine and CI run shares one cache, plugins derive correct inputs and outputs from your tool configuration, and replayed tasks restore terminal output and artifacts exactly.
|
||||
|
||||
## Cache security
|
||||
|
||||
Depot's GitHub Actions cache integration scopes entries by repository but not by branch, and its docs leave cache-key hygiene to you. That is the surface [CVE-2025-36852](https://www.cve.org/CVERecord?id=CVE-2025-36852) (CREEP) describes: without branch isolation, anyone who can open a PR can potentially poison artifacts that later ship from a protected branch. Depot's build-tool cache backend scopes by org and project token instead.
|
||||
|
||||
Nx Cloud enforces branch-scoped cache isolation so PR-produced artifacts can't be written into a trusted scope. See [cache security](/docs/concepts/ci-concepts/cache-security) for the model.
|
||||
|
||||
## Affected detection
|
||||
|
||||
Fast runners run everything faster, including the tasks a one-library PR never needed. [`nx affected`](/docs/features/ci-features/affected) compares your change against the base branch and hands CI only the impacted tasks. This pruning happens before any cache lookup or scheduling, and it compounds with faster hardware rather than competing with it.
|
||||
|
||||
## Distribution
|
||||
|
||||
Depot schedules at the job level: your workflow defines the jobs, and parallelizing a test suite means maintaining a matrix and shard assignments by hand. [Nx Agents](/docs/features/ci-features/distribute-task-execution) distribute individual tasks across machines, balancing dynamically from historical timing data and rebalancing as your workspace grows, with no shard boundaries to maintain.
|
||||
|
||||
## Test splitting and flaky tests
|
||||
|
||||
Depot has no test-level features. [Atomizer](/docs/features/ci-features/split-e2e-tasks) splits slow e2e suites into per-file tasks that distribute across agents. [Flaky task detection](/docs/features/ci-features/flaky-tasks) identifies flaky tests from history and retries them in isolation instead of failing the pipeline. [Self-healing CI](/docs/features/ci-features/self-healing-ci) analyzes failures and proposes verified fixes on the PR.
|
||||
|
||||
## Observability
|
||||
|
||||
Depot provides job and machine-level analytics: CPU and memory per job, step timings, and cross-run log search. Nx Cloud's analytics are task-aware: cache hit rates per task, agent utilization per run, and task timing trends.
|
||||
|
||||
## Who should pick which
|
||||
|
||||
Nx fits when any of these apply:
|
||||
|
||||
- Most tasks on most PRs are redundant and you want to skip them, not run them faster.
|
||||
- CI time calls for task distribution, e2e splitting, or flaky-task handling.
|
||||
- You want a task cache shared across CI and developer machines with access control.
|
||||
|
||||
Depot alone is enough only when:
|
||||
|
||||
- Your pipelines are dominated by container builds and you want Depot's build acceleration.
|
||||
- The bottleneck is raw machine speed on a repository small enough to run everything each push.
|
||||
- You want a runner swap with almost no setup.
|
||||
|
||||
Nx runs on your existing CI without switching providers, and Nx Agents run on sized resource classes, so faster machines are on the table too, not just cheaper minutes. Container-heavy pipelines can add [Docker layer caching](/docs/features/ci-features/docker-layer-caching). If you want CI that scales with your team, [set up Nx Agents](/docs/features/ci-features/distribute-task-execution#enable-nx-agents).
|
||||
|
||||
## Resources
|
||||
|
||||
{% cards cols=2 %}
|
||||
{% card title="Nx Cloud CI features" description="Remote caching, distribution, splitting, and self-healing CI" url="/docs/features/ci-features" /%}
|
||||
{% card title="Connect your CI" description="Add Nx Cloud to an existing pipeline" url="/docs/getting-started/setup-ci" /%}
|
||||
{% /cards %}
|
||||
@@ -1,106 +0,0 @@
|
||||
---
|
||||
title: Nx vs Develocity
|
||||
description: Develocity (formerly Gradle Enterprise) accelerates and observes JVM builds. Nx covers the whole monorepo, JS/TS and JVM and beyond, with one graph, one cache, and task-level distribution.
|
||||
filter: 'type:Guides'
|
||||
sidebar:
|
||||
label: Nx vs Develocity
|
||||
---
|
||||
|
||||
Develocity accelerates and observes JVM builds in depth. Nx covers the whole monorepo, JS/TS, JVM, and more, with one graph, one cache, and task-level distribution.
|
||||
|
||||
## What is Nx?
|
||||
|
||||
Nx is a monorepo platform: a task-running CLI, remote caching, distributed CI across machines, e2e test splitting, flaky-task handling, self-healing CI, and editor integration. It layers onto whatever CI provider you already run.
|
||||
|
||||
## What is Develocity?
|
||||
|
||||
Develocity, formerly Gradle Enterprise, is a mature build acceleration and observability platform for the JVM ecosystem: build caching for Gradle and Maven, test distribution, ML-based test selection, and deep per-build forensics through Build Scan.
|
||||
|
||||
## Quick takeaway
|
||||
|
||||
Develocity and Nx pull in different directions:
|
||||
|
||||
- **Develocity goes deep on the JVM.** Build cache, test distribution, and ML-based test selection for Gradle and Maven, with deep Build Scan introspection. It observes JS builds without caching or distributing them.
|
||||
- **Nx goes wide across the repository.** One graph across JavaScript, JVM, .NET, and more, caching, pruning, and distributing every task in it.
|
||||
|
||||
For a polyglot repository the choice is deep on one language or wide across all of them.
|
||||
|
||||
| Topic | Nx Cloud | Develocity |
|
||||
| --------------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| [Scope](#scope) | One graph across JS/TS, JVM, .NET, and more | Gradle, Maven, Bazel, sbt (npm observed, not accelerated) |
|
||||
| [Build caching](#build-caching) | Every cacheable task in the workspace | Gradle, Maven, Bazel, and sbt builds |
|
||||
| [Test selection](#test-selection) | Deterministic `nx affected` at project level, all languages | ML-based Predictive Test Selection, Gradle and Maven only |
|
||||
| [Distribution](#distribution) | Any task type across dynamically balanced agents | JVM test executions on JUnit Platform |
|
||||
| [E2E splitting](#e2e-splitting) | Atomizer splits Playwright/Cypress suites per file | Not available |
|
||||
| [Flaky tests](#flaky-tests) | Detection and isolated re-runs | Detection, retries, and org-wide trends |
|
||||
| [AI integration](#ai-integration) | Self-healing CI proposes and verifies fixes | MCP servers and failure grouping provide context |
|
||||
| [Adoption and pricing](#adoption-and-pricing) | Self-serve, public pricing, free tier, single-tenant option | Per-committer enterprise contracts, mature on-prem story |
|
||||
|
||||
## Scope
|
||||
|
||||
The Develocity compatibility matrix draws the boundary: Build Scan, caching, test distribution, and Predictive Test Selection are available for Gradle and Maven, with scans and caching for Bazel and sbt. Its npm agent captures Build Scans, test insights, and failure analytics for JS builds, but the compatibility matrix lists no build caching, distribution, or test selection for npm. There is no repository-level project graph. Agents wrap individual build invocations.
|
||||
|
||||
Nx starts from the repository. The [`@nx/gradle`](/docs/technologies/java/gradle/introduction) plugin puts Gradle projects in the same graph as your JS/TS projects, alongside plugins for Maven, .NET, and community plugins for Python, Rust, and Go. Every Nx Cloud feature below applies to that whole graph.
|
||||
|
||||
## Build caching
|
||||
|
||||
Develocity's build cache for Gradle and Maven is mature and fine-grained, caching at the task and goal level inside a build. They built the original Gradle build cache, and it shows.
|
||||
|
||||
[Nx Replay](/docs/features/ci-features/remote-cache) applies the same idea to every cacheable task in the workspace: JS builds, tests, lint runs, Gradle tasks orchestrated through Nx, and anything else with declared inputs and outputs, shared across all developers and CI with [enforced branch-scoped isolation](/docs/concepts/ci-concepts/cache-security).
|
||||
|
||||
## Test selection
|
||||
|
||||
Predictive Test Selection is a Develocity capability with no Nx equivalent: an ML model trained on your build history scores individual test classes and skips ones unlikely to fail, with selectable risk profiles. The trade-off is scope and determinism. PTS covers Gradle and Maven tests only, and as a probabilistic model it can skip a test that would have failed.
|
||||
|
||||
[`nx affected`](/docs/features/ci-features/affected) is deterministic: it computes which projects a change can reach through the graph and skips everything else, for every task type in every language. Nothing reachable is skipped, and nothing unreachable runs.
|
||||
|
||||
## Distribution
|
||||
|
||||
Develocity Test Distribution fans JVM test executions out to remote agents, with partitioning based on historical timing. It requires tests on the JUnit Platform and covers Gradle test tasks and Maven Surefire/Failsafe goals. Compilation, linting, packaging, and non-JVM work stay on the build machine.
|
||||
|
||||
[Nx Agents](/docs/features/ci-features/distribute-task-execution) distribute any task in the graph, builds, tests, lint, e2e, and Docker included, across machines that balance dynamically from timing history.
|
||||
|
||||
## E2E splitting
|
||||
|
||||
Develocity has no support for JS e2e runners. [Atomizer](/docs/features/ci-features/split-e2e-tasks) splits Playwright and Cypress suites into per-file tasks so the slowest suites spread across agents instead of serializing a pipeline.
|
||||
|
||||
## Flaky tests
|
||||
|
||||
Develocity's flaky test management runs deep: detection across Gradle, Maven, Bazel, and npm builds, retry integration, and org-wide flakiness trends.
|
||||
|
||||
Nx Cloud [detects flaky tasks](/docs/features/ci-features/flaky-tasks) from execution history and re-runs them in isolation automatically, covering every task type, so a flaky Playwright spec is handled the same way as a flaky JUnit class.
|
||||
|
||||
## AI integration
|
||||
|
||||
Develocity's 2026 AI direction is context: MCP servers that let coding agents query build history, AI-powered failure grouping, and failure classification skills, all of which give agents context rather than fixing failures themselves.
|
||||
|
||||
Nx Cloud's [self-healing CI](/docs/features/ci-features/self-healing-ci) acts: it analyzes the failed task, proposes a fix, verifies it, and surfaces it on the PR. Nx also ships an [MCP server](/docs/reference/nx-mcp) and agent skills for local coding agents.
|
||||
|
||||
## Adoption and pricing
|
||||
|
||||
Develocity is sold per committer on annual enterprise contracts with no public pricing, deployed SaaS, hybrid, bring-your-own-cloud, or self-hosted. Its on-prem and air-gapped story is long-established.
|
||||
|
||||
Nx Cloud has public pricing with a free tier and self-serve signup, and [single-tenant hosted or on-prem options](/docs/enterprise/single-tenant/overview) for enterprises.
|
||||
|
||||
## Who should pick which
|
||||
|
||||
Nx fits when any of these apply:
|
||||
|
||||
- Your repository spans more than the JVM, mixing JavaScript, .NET, or other languages with Gradle or Maven.
|
||||
- You want affected detection, e2e splitting, and self-healing CI across every task type.
|
||||
- You want self-serve adoption and a free tier rather than an enterprise sales cycle.
|
||||
|
||||
Develocity fits only when:
|
||||
|
||||
- Your codebase is purely Gradle or Maven with large JVM test suites.
|
||||
- You need ML-based Predictive Test Selection or deep JVM build forensics.
|
||||
- A strict on-prem or air-gapped deployment is a hard requirement.
|
||||
|
||||
For a polyglot repository, Nx covers the whole graph in one platform rather than splitting tooling by language.
|
||||
|
||||
## Resources
|
||||
|
||||
{% cards cols=2 %}
|
||||
{% card title="Nx Gradle plugin" description="Add Gradle projects to the Nx graph with caching and distribution" url="/docs/technologies/java/gradle/introduction" /%}
|
||||
{% card title="Nx Cloud CI features" description="Remote caching, distribution, splitting, and self-healing CI" url="/docs/features/ci-features" /%}
|
||||
{% /cards %}
|
||||
@@ -1,114 +0,0 @@
|
||||
---
|
||||
title: Nx vs Vite+
|
||||
description: Nx vs Vite+ on task running, caching, and CI. Vite+ ships a local-only task cache in beta. Nx runs the same Vite toolchain with remote caching, affected detection, and distributed CI.
|
||||
filter: 'type:Guides'
|
||||
sidebar:
|
||||
label: Nx vs Vite+
|
||||
---
|
||||
|
||||
Vite+ makes the toolchain on your machine fast. Nx makes that same toolchain fast across every machine, each teammate's laptop and every CI run, by adding remote caching, affected detection, and distributed execution on top of it.
|
||||
|
||||
## What is Nx?
|
||||
|
||||
Nx is a build system that orchestrates the tools you already use. Plugins configure tasks from your existing configs, caching works locally and remotely, `nx affected` runs only what your change touches, and Nx Cloud distributes tasks across CI machines. It spans JS/TS, JVM, .NET, and more in one graph.
|
||||
|
||||
## What is Vite+?
|
||||
|
||||
[Vite+](https://viteplus.dev/) (also written vite-plus) is VoidZero's unified distribution of the Vite toolchain: one `vp` CLI for builds (Vite 8), tests (Vitest), and linting (Oxlint/Oxfmt), plus a monorepo task runner (`vp run`) with local caching. It reached beta in July 2026 and is MIT-licensed, and VoidZero joined Cloudflare in June 2026.
|
||||
|
||||
## Quick takeaway
|
||||
|
||||
Nx and Vite+ share the basics: task running, local caching, and package filtering. They differ on non-local runs, like CI:
|
||||
|
||||
- **Vite+ is roadmap or absent there.** A shared cache, task distribution, and cross-language support are missing or unshipped.
|
||||
- **Nx has run these in production for years.** Remote caching, Nx Agents distribution, and affected detection across languages.
|
||||
|
||||
Because the [`@nx/vite`](/docs/technologies/build-tools/vite/introduction) plugin runs the Vite toolchain rather than replacing it, choosing Nx keeps Vite and Vitest. Nx fits a small workspace as well as a large one, and holds up as the monorepo scales.
|
||||
|
||||
{% aside type="note" title="Vite+ is in beta" %}
|
||||
Vite+ reached beta in July 2026 and is evolving quickly. The capabilities below reflect the [Vite+ beta announcement](https://voidzero.dev/posts/announcing-vite-plus-beta) and its published roadmap.
|
||||
{% /aside %}
|
||||
|
||||
| Topic | Nx | Vite+ |
|
||||
| ----------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------ |
|
||||
| [Toolchain](#toolchain) | Plugin-configured tasks for Vite, Vitest, and dozens of tools | Unified `vp` CLI for Vite, Vitest, Oxlint |
|
||||
| [Task running and caching](#task-running-and-caching) | Opt-in caching with composable `namedInputs` | Cached config tasks, automatic input tracking |
|
||||
| [Sharing the cache](#sharing-the-cache) | Nx Replay: shared across developers and CI, access-controlled | Local only, remote caching on the roadmap |
|
||||
| [Affected detection](#affected-detection) | `nx affected` runs only tasks impacted by your changes | Not available, `--filter` selects by name or directory |
|
||||
| [Running on CI](#running-on-ci) | Nx Agents distribution, Atomizer, self-healing, flaky detection | Runs the same commands as local, no CI features |
|
||||
| [Polyglot support](#polyglot-support) | Native plugins for Java, .NET, Python, Rust | JS/TS only, graph from `package.json` workspaces |
|
||||
| [Module boundary rules](#module-boundary-rules) | Tag-based lint rule + conformance rules | Not available |
|
||||
| [Release management](#release-management) | Built-in versioning, changelogs, and publishing | `vp pm stage` publishing workflow |
|
||||
|
||||
## Toolchain
|
||||
|
||||
Vite+ bundles the toolchain into one CLI: `vp check` runs linting, formatting, and type checking in a single pass, `vp test` runs Vitest, and `vp build` runs Vite 8 on Rolldown. One consistent entry point for the whole JS stack is convenient.
|
||||
|
||||
Choosing Nx doesn't mean giving that toolchain up. Nx plugins read your existing `vite.config` and `vitest.config` files and [automatically configure tasks](/docs/concepts/inferred-tasks) with correct cache inputs and outputs, so the same Vite 8 builds and Vitest runs execute under Nx orchestration. Tools Nx doesn't have a plugin for, including `vp` subcommands, run as regular `package.json` scripts.
|
||||
|
||||
## Task running and caching
|
||||
|
||||
Both tools run your `package.json` scripts with `dependsOn` ordering across the workspace. Vite+ derives cache inputs automatically by recording file reads and writes during execution, a good default that avoids manual configuration. Its cache is content-based, stored in `node_modules/.vite/task-cache`, and replays terminal output on a hit.
|
||||
|
||||
Nx takes a declare-and-verify approach. Caching is [opt-in per task](/docs/concepts/how-caching-works), plugins set inputs and outputs from your tool configuration, and reusable [`namedInputs`](/docs/reference/inputs) patterns compose across targets, so a spec file change doesn't invalidate your build cache. [Task sandboxing](/docs/features/ci-features/sandboxing) runs each task in a sandbox that surfaces undeclared reads or writes, with an opt-in strict mode that fails the task, protecting cache integrity.
|
||||
|
||||
## Sharing the cache
|
||||
|
||||
A task cache pays off most when a task computed once runs nowhere else: not on a teammate's machine, and not again on CI. That requires a remote cache.
|
||||
|
||||
The Vite+ cache is local-only. Remote caching for `vp run` is listed on the [road to 1.0](https://voidzero.dev/posts/announcing-vite-plus-beta), and there is no documented workflow for sharing the task cache between developers or between development machines and CI. The official `setup-vp` GitHub Action caches package manager dependencies, not task results, so every CI run starts with a cold task cache.
|
||||
|
||||
[Nx Replay](/docs/features/ci-features/remote-cache) shares the cache across every developer and CI machine, with access control and isolation designed against cache poisoning attacks like [CVE-2025-36852](https://www.cve.org/CVERecord?id=CVE-2025-36852). See [cache security](/docs/concepts/ci-concepts/cache-security) for how the multi-tier model works.
|
||||
|
||||
## Affected detection
|
||||
|
||||
Vite+ has no affected command. Its `--filter` flag selects packages by name, glob, directory, or dependency relationships, but nothing computes which packages a git diff impacts. On CI, the full task graph runs and the local cache decides what to skip. Without a remote cache, that means running everything.
|
||||
|
||||
[`nx affected`](/docs/features/ci-features/affected) compares your changes against a base branch and runs only the impacted tasks. On a typical PR touching one library, this cuts the task graph before any cache lookup happens.
|
||||
|
||||
## Running on CI
|
||||
|
||||
The Vite+ CI story is that CI runs the same commands developers run locally, with the `setup-vp` action installing the toolchain. There is no task distribution, test splitting, or flaky test handling.
|
||||
|
||||
Nx works the same way on any CI provider, and [Nx Cloud](/docs/features/ci-features) layers on what single-machine execution can't provide: [Nx Agents](/docs/features/ci-features/distribute-task-execution) distribute tasks across machines based on historical timings, [Atomizer](/docs/features/ci-features/split-e2e-tasks) splits slow e2e suites into per-file tasks, [flaky task detection](/docs/features/ci-features/flaky-tasks) retries flaky tests in isolation, and [self-healing CI](/docs/features/ci-features/self-healing-ci) proposes verified fixes for failures.
|
||||
|
||||
## Polyglot support
|
||||
|
||||
The Vite+ task graph derives from `package.json` workspaces, which makes it JS/TS-only. A Go or Java service can't participate in the graph.
|
||||
|
||||
Nx provides [first-party plugins](/docs/plugin-registry) for Maven, Gradle, .NET, and Docker, plus community plugins for Python, Rust, and Go, each with dependency detection, caching, and affected support.
|
||||
|
||||
## Module boundary rules
|
||||
|
||||
Vite+ has no mechanism for constraining dependencies between projects.
|
||||
|
||||
Nx enforces [module boundaries](/docs/features/enforce-module-boundaries) through tags and a lint rule, with [conformance rules](/docs/enterprise/conformance) covering languages where ESLint doesn't reach. In workspaces where AI agents write a growing share of the code, these rules are the guardrail that keeps generated changes inside your architecture.
|
||||
|
||||
## Release management
|
||||
|
||||
Vite+ ships `vp pm stage`, a publishing workflow for getting packages onto a registry.
|
||||
|
||||
[`nx release`](/docs/features/manage-releases) covers the full lifecycle: determining versions from conventional commits or version plans, updating dependent packages, generating changelogs, and publishing, all from one configurable command.
|
||||
|
||||
## Who should pick which
|
||||
|
||||
Nx fits when any of these apply:
|
||||
|
||||
- Your cache should be shared across every developer and CI run, not confined to one machine.
|
||||
- CI time is a bottleneck and you need distribution, e2e splitting, or flaky-test handling.
|
||||
- Your repository mixes Go, Java, or .NET with JavaScript.
|
||||
- You want the platform layer: generators, release management, and self-healing CI.
|
||||
|
||||
Vite+ is enough only when:
|
||||
|
||||
- You want a single unified CLI for building, testing, and linting.
|
||||
- Your workspace is JS/TS-only and doesn't need a shared cache, CI distribution, or the wider platform.
|
||||
|
||||
Nx runs the same Vite toolchain, so choosing Nx keeps Vite and Vitest while giving you the orchestration, caching, and CI that Vite+ leaves as roadmap.
|
||||
|
||||
## Resources
|
||||
|
||||
{% cards cols=2 %}
|
||||
{% card title="Add Nx to an existing project" description="Adopt Nx incrementally without rewriting tool configuration" url="/docs/guides/adopting-nx/adding-to-existing-project" /%}
|
||||
{% card title="Nx Vite plugin" description="Automatically configure tasks from your vite.config with caching built in" url="/docs/technologies/build-tools/vite/introduction" /%}
|
||||
{% /cards %}
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: Comparisons
|
||||
description: How Nx and Nx Cloud compare to other build tools, task runners, and CI acceleration products
|
||||
sidebar:
|
||||
hidden: true
|
||||
pagefind: false
|
||||
---
|
||||
|
||||
{% cards cols=2 %}
|
||||
{% card title="Nx vs Turborepo" description="Task running, caching, distributed CI, and the platform around them" url="/docs/guides/comparisons/nx-vs-turborepo" /%}
|
||||
{% card title="Nx vs Vite+" description="The Vite toolchain plus remote caching, affected detection, and CI" url="/docs/guides/comparisons/nx-vs-vite-plus" /%}
|
||||
{% card title="Nx vs Bazel" description="Incremental adoption versus a full build-system migration" url="/docs/guides/comparisons/nx-vs-bazel" /%}
|
||||
{% card title="Nx vs Depot" description="Faster runners versus a task-graph platform that removes work" url="/docs/guides/comparisons/nx-vs-depot" /%}
|
||||
{% card title="Nx vs Blacksmith" description="Faster machines versus distribution, caching, and flaky handling" url="/docs/guides/comparisons/nx-vs-blacksmith" /%}
|
||||
{% card title="Nx vs Develocity" description="JVM build acceleration versus the whole monorepo in one graph" url="/docs/guides/comparisons/nx-vs-develocity" /%}
|
||||
{% card title="Nx vs Buildkite" description="A CI platform versus a task-graph layer on any CI" url="/docs/guides/comparisons/nx-vs-buildkite" /%}
|
||||
{% /cards %}
|
||||
@@ -368,11 +368,11 @@ jobs:
|
||||
agent: [1, 2, 3]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- ... # other setup steps you may need
|
||||
@@ -396,11 +396,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- ... # other setup steps you may need
|
||||
|
||||
@@ -234,7 +234,7 @@ When some targets matching an entry in the map should have different defaults th
|
||||
"test": [
|
||||
{ "cache": true },
|
||||
{
|
||||
"filter": { "plugin": "@nx/vitest" },
|
||||
"filter": { "plugin": "@nx/vite" },
|
||||
"inputs": ["default", "^production"]
|
||||
},
|
||||
{
|
||||
@@ -246,14 +246,14 @@ When some targets matching an entry in the map should have different defaults th
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| key | `string` | The map key: a target name (e.g. `test`), a glob (e.g. `e2e-ci--*`), or an executor identifier (e.g. `@nx/js:tsc`). |
|
||||
| value | `object` \| `array` | A plain configuration object, or an ordered array of filtered entries. |
|
||||
| `filter.plugin` | `string` | Optional. Restricts the entry to targets inferred by a specific plugin (e.g. `@nx/vitest`). Useful when two plugins expose a target with the same name (e.g. `@nx/vitest:test` and `@nx/jest:test`). |
|
||||
| `filter.projects` | `string` \| `string[]` | Optional. Restricts the entry to matching projects. Accepts project names, globs, directory patterns, tags (`tag:foo`), and negation (`!foo`) — anything `findMatchingProjects` understands. |
|
||||
| `filter.executor` | `string` | Optional. Restricts the entry to targets that resolve to this executor. |
|
||||
| other | any field from [Target Configuration](/docs/reference/project-configuration) | The defaults applied when the entry matches. |
|
||||
| Field | Type | Description |
|
||||
| ----------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| key | `string` | The map key: a target name (e.g. `test`), a glob (e.g. `e2e-ci--*`), or an executor identifier (e.g. `@nx/js:tsc`). |
|
||||
| value | `object` \| `array` | A plain configuration object, or an ordered array of filtered entries. |
|
||||
| `filter.plugin` | `string` | Optional. Restricts the entry to targets inferred by a specific plugin (e.g. `@nx/vite`). Useful when two plugins expose a target with the same name (e.g. `@nx/vite:test` and `@nx/jest:test`). |
|
||||
| `filter.projects` | `string` \| `string[]` | Optional. Restricts the entry to matching projects. Accepts project names, globs, directory patterns, tags (`tag:foo`), and negation (`!foo`) — anything `findMatchingProjects` understands. |
|
||||
| `filter.executor` | `string` | Optional. Restricts the entry to targets that resolve to this executor. |
|
||||
| other | any field from [Target Configuration](/docs/reference/project-configuration) | The defaults applied when the entry matches. |
|
||||
|
||||
Within a key's array, entries apply in document order. **Later entries override prior entries**. An entry with **no `filter`** is a catch-all baseline that applies to every variant of that target; an entry **with a `filter`** applies only where its `plugin`, `projects`, and `executor` match. Because a later entry overrides an earlier one, a trailing catch-all can reset a value set above it. Target defaults apply after targets have been inferred by plugins but before project configuration in `project.json` or `package.json`.
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
[Docker](https://www.docker.com/) packages applications into portable container images. In a Docker monorepo, Nx builds, caches, and publishes those images alongside the rest of your projects.
|
||||
|
||||
The Nx Plugin for Docker contains executors and utilities for building and publishing docker images within an Nx workspace.
|
||||
Using the `@nx/docker` [Inference Plugin](/docs/concepts/inferred-tasks), Nx will automatically detect `Dockerfile`'s in your workspace and provide a `docker:build` and `docker:run` target for each.
|
||||
It will also provide a `nx-release-publish` target for publishing docker images to a registry.
|
||||
|
||||
@@ -7,9 +7,7 @@ weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
[esbuild](https://esbuild.github.io/api/) is an extremely fast JavaScript bundler. In an esbuild monorepo, Nx builds and caches projects across your workspace so only what changed gets rebuilt.
|
||||
|
||||
The Nx Plugin for esbuild runs these builds through the `@nx/esbuild:esbuild` executor, with type-checking and asset handling layered on top.
|
||||
The Nx Plugin for [esbuild](https://esbuild.github.io/api/), an extremely fast JavaScript bundler. Use it to build and cache projects in an esbuild monorepo, with type-checking and asset handling layered on top.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@ weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
[Rollup](https://rollupjs.org/) is a module bundler for JavaScript libraries and applications. In a Rollup monorepo, Nx builds and caches projects across your workspace and rebuilds only what a change affects.
|
||||
|
||||
The Nx Plugin for Rollup contains executors and generators that support building applications with Rollup.
|
||||
The Nx Plugin for Rollup contains executors and generators that support building applications with Rollup across a Rollup monorepo.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@ weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
[Rsbuild](https://rsbuild.dev/) is an Rspack-based build tool for web applications. In an Rsbuild monorepo, Nx lets you build, serve, and cache every project from a single workspace.
|
||||
|
||||
The Nx Plugin for Rsbuild contains executors and generators that support building applications using Rsbuild, wiring each project into Nx.
|
||||
The Nx Plugin for Rsbuild contains executors and generators that support building applications using Rsbuild. In an Rsbuild monorepo, it wires each project into Nx so you can build, serve, and cache them from a single workspace.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -1,34 +1,21 @@
|
||||
---
|
||||
title: Nx with Rspack
|
||||
description: Rspack is a Rust-based bundler with a webpack-compatible API. Learn how to use Rspack in an Nx monorepo with caching, affected builds, and fast CI.
|
||||
description: Use Rspack with Nx to build and cache projects across your monorepo, with affected-only builds and distributed CI.
|
||||
sidebar:
|
||||
label: Introduction
|
||||
weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
Rspack is a Rust-based JavaScript bundler with a webpack-compatible API, built as a drop-in replacement for webpack with much faster builds.
|
||||
Teams with existing webpack configurations get that speedup without rewriting their setup.
|
||||
The `@nx/rspack` plugin adds caching, affected builds, and task inference so Rspack scales across a monorepo.
|
||||
|
||||
## What is Rspack?
|
||||
|
||||
[Rspack](https://rspack.rs) is a bundler written in Rust that implements the webpack API.
|
||||
Per the Rspack team's [benchmarks](https://github.com/rstackjs/build-tools-performance) (as of Rspack 2), it builds and serves projects several times faster than webpack.
|
||||
In a monorepo those savings compound, and Nx layers caching and affected-only builds on top so unchanged projects don't build at all.
|
||||
|
||||
### Is Rspack a drop-in replacement for webpack?
|
||||
|
||||
Mostly - existing loaders and the majority of webpack plugins run unchanged, so many configurations migrate with few or no edits.
|
||||
Plugins that depend on webpack internals may not work, so test complex setups when you migrate.
|
||||
The Nx Plugin for Rspack contains executors, generators, and utilities for managing Rspack projects in an Nx Workspace. It helps you run, cache, and scale builds across an Rspack monorepo, with executors and generators that wire each project into the workspace.
|
||||
|
||||
## Requirements
|
||||
|
||||
The `@nx/rspack` plugin supports Rspack 1 and 2 (as of Nx v23).
|
||||
The `@nx/rspack` plugin supports the following package versions.
|
||||
|
||||
| Package | Supported Versions | Default Installed |
|
||||
| -------------- | ---------------------- | ----------------- |
|
||||
| `@rspack/core` | `^1.0.0` \|\| `^2.0.0` | `2.0.4` |
|
||||
| `@rspack/core` | `^1.0.0` \|\| `^2.0.0` | `2.0.3` |
|
||||
|
||||
[Nx generators](/docs/features/generate-code) install the latest supported version automatically when scaffolding new projects. When `@rspack/core` is already installed in your workspace, generators detect the installed version and keep it in place rather than overwriting it.
|
||||
|
||||
@@ -100,17 +87,12 @@ nx g @nx/react:app my-app --bundler=rspack
|
||||
|
||||
### Modify an existing React project to use Rspack
|
||||
|
||||
You can use the `@nx/rspack:configuration` generator to change your React project to use Rspack. This generator will modify your project's configuration to use Rspack, and it will also install all the necessary dependencies, including the `@nx/rspack` plugin.
|
||||
You can use the `@nx/rspack:configuration` generator to change your React to use Rspack. This generator will modify your project's configuration to use Rspack, and it will also install all the necessary dependencies, including the `@nx/rspack` plugin.
|
||||
|
||||
You can read more about this generator on the [`@nx/rspack:configuration`](/docs/technologies/build-tools/rspack/generators#configuration) generator page.
|
||||
|
||||
## Why use Rspack in a monorepo?
|
||||
## Set up CI for your Rspack monorepo
|
||||
|
||||
Rspack speeds up each build, and Nx cuts down how many builds run.
|
||||
In a monorepo with many Rspack projects:
|
||||
|
||||
- [Caching](/docs/features/cache-task-results) skips builds whose inputs haven't changed, locally and in CI.
|
||||
- [`nx affected`](/docs/features/ci-features/affected) rebuilds and retests only the projects a change touches.
|
||||
- Because the API is webpack-compatible, you can migrate webpack projects to Rspack one at a time instead of converting every configuration in the repo at once.
|
||||
In CI, Nx runs [`nx affected`](/docs/features/ci-features/affected) to rebuild and retest only the projects a change touches, and [caches](/docs/features/cache-task-results) results to skip repeated work.
|
||||
|
||||
For a complete pipeline, see [Set up CI](/docs/getting-started/setup-ci).
|
||||
|
||||
@@ -7,9 +7,9 @@ weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
[Webpack](https://webpack.js.org/) is a static module bundler for modern JavaScript applications. In a Webpack monorepo, Nx brings smart task running and caching to every project.
|
||||
The Nx plugin for [webpack](https://webpack.js.org/) brings smart task running and caching to a Webpack monorepo.
|
||||
|
||||
The `@nx/webpack` plugin provides executors that allow you to build and serve your projects using webpack, plus an executor for SSR.
|
||||
[Webpack](https://webpack.js.org/) is a static module bundler for modern JavaScript applications. The `@nx/webpack` plugin provides executors that allow you to build and serve your projects using webpack, plus an executor for SSR.
|
||||
|
||||
You can [customize your webpack configuration](/docs/technologies/build-tools/webpack/guides/webpack-config-setup) for your projects. Nx also provides [a number of webpack plugins](/docs/technologies/build-tools/webpack/guides/webpack-plugins) for supporting Nx and other frameworks.
|
||||
|
||||
|
||||
@@ -1,248 +1,35 @@
|
||||
---
|
||||
title: Migrate from .eslintrc to ESLint Flat Config
|
||||
description: 'Migrate .eslintrc to ESLint flat config step by step: eslint.config.mjs setup, FlatCompat for legacy configs, Next.js gotchas, and Nx automation.'
|
||||
title: Switching to ESLint's Flat Config Format
|
||||
description: Learn how to migrate your Nx workspace to ESLint's new flat configuration format, understanding the benefits and implementation details.
|
||||
sidebar:
|
||||
label: Migrate to flat config
|
||||
label: Switching to ESLint's flat config format
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
{% llm_copy_prompt title="Let an AI agent migrate it for you" %}
|
||||
ESLint introduced a new configuration format called [Flat Config](https://eslint.org/docs/latest/use/configure/configuration-files-new) in version 8 and made it the default in version 9. Since Nx requires ESLint v9 or later, new workspaces use flat config by default. The purpose of this format is to:
|
||||
|
||||
Migrate this Nx workspace from .eslintrc to ESLint flat config, following the instructions on the page below.
|
||||
|
||||
1. Run `nx g @nx/eslint:convert-to-flat-config`. Convert any JS-based eslintrc files (`.eslintrc.js`, `.eslintrc.cjs`) yourself using the page's migration steps.
|
||||
2. Handle rules removed in typescript-eslint v8 and the ESLint v9 runtime changes as the page describes: drop removed formatting rules, apply the renames and the ban-types split, update plugins that crash, and fix removed formatters and CLI flags.
|
||||
3. Find the lint target: check `nx.json` plugins for `@nx/eslint/plugin` and its `targetName` (default `lint`), and check `project.json` / `package.json` files for explicit lint targets.
|
||||
4. Run `nx reset`, then `nx run-many -t <lint-target>` and fix failures until every project passes. Disable newly preset-enabled rules you never configured instead of editing source to satisfy them, and never weaken a rule the workspace configured explicitly.
|
||||
|
||||
Page: {pageUrl}
|
||||
{% /llm_copy_prompt %}
|
||||
|
||||
ESLint flat config replaces `.eslintrc` with an `eslint.config.mjs` file that exports an
|
||||
array of config objects.
|
||||
Flat config has been the default since ESLint 9, and ESLint 10 no longer reads
|
||||
`.eslintrc` files at all, so any project still on the legacy format has to migrate before
|
||||
upgrading.
|
||||
|
||||
## What is ESLint flat config?
|
||||
|
||||
Flat config is a JavaScript-first configuration system that lives in `eslint.config.mjs` (or
|
||||
`.js`/`.cjs`) files - in a monorepo, typically one per project plus a shared base config.
|
||||
It makes three changes to the legacy format:
|
||||
|
||||
- One file format replaces the JSON, YAML, and JS `.eslintrc` variants.
|
||||
- Explicit `import` statements replace implicit string-based loading of plugins and parsers.
|
||||
- Config objects scoped by `files` patterns replace nested `overrides`.
|
||||
|
||||
## Convert files automatically
|
||||
|
||||
In an Nx workspace, the
|
||||
[@nx/eslint:convert-to-flat-config generator](/docs/technologies/eslint/generators#convert-to-flat-config)
|
||||
converts every project's ESLint configuration in one pass:
|
||||
|
||||
```shell
|
||||
nx g @nx/eslint:convert-to-flat-config
|
||||
```
|
||||
|
||||
The generator walks all projects, converts each `.eslintrc.json` to flat config, converts the
|
||||
base config at the workspace root, and folds `.eslintignore` files into `ignores` entries.
|
||||
It does not convert JavaScript-based eslintrc files (`.eslintrc.js`, `.eslintrc.cjs`), so
|
||||
convert those by hand using the steps below.
|
||||
New Nx workspaces generate flat config by default, and the
|
||||
[@nx/eslint:lint executor](/docs/technologies/eslint/executors#lint) runs it without extra
|
||||
configuration.
|
||||
|
||||
The generator aims to produce a flat config that behaves exactly like your original JSON
|
||||
config, so depending on the complexity of the original it may wrap parts in `FlatCompat`.
|
||||
Review the converted configs before deleting anything: check that each override kept its
|
||||
`parser` - a dropped parser surfaces as `Parsing error: Unexpected token` on TypeScript files.
|
||||
You can convert those sections to native flat config afterwards, by hand or with AI assistance
|
||||
(recommended): convert when the plugin documents a flat preset (typescript-eslint,
|
||||
`eslint-plugin-react`), and keep the shim for shared configs that don't ship one.
|
||||
|
||||
## Convert files manually
|
||||
|
||||
As an alternative to the generator, or for the JS-based eslintrc files it skips, convert each
|
||||
config yourself.
|
||||
The [ESLint migration guide](https://eslint.org/docs/latest/use/configure/migration-guide) maps
|
||||
every legacy option to its flat config equivalent.
|
||||
|
||||
1. Create `eslint.config.mjs` at the root of your repository.
|
||||
String references like `"eslint:recommended"` become imports:
|
||||
|
||||
```js
|
||||
// eslint.config.mjs
|
||||
import js from '@eslint/js';
|
||||
|
||||
export default [js.configs.recommended];
|
||||
```
|
||||
|
||||
1. Convert `env` and `parser` settings to `languageOptions`.
|
||||
Environments like `env: { node: true }` become imported globals:
|
||||
|
||||
```js
|
||||
// eslint.config.mjs
|
||||
import globals from 'globals';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
|
||||
export default [
|
||||
{
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
globals: { ...globals.node },
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
1. Replace `overrides` with config objects scoped by `files`.
|
||||
Each entry in the legacy `overrides` array becomes a top-level object.
|
||||
Flat config matches patterns against the full path relative to the config file, while legacy
|
||||
`overrides` matched slash-less patterns against the file basename, so `*.ts` becomes `**/*.ts`:
|
||||
|
||||
```js
|
||||
// eslint.config.mjs
|
||||
export default [
|
||||
{
|
||||
files: ['**/*.spec.ts'],
|
||||
rules: { 'max-lines': 'off' },
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
1. Bridge legacy shareable configs with `FlatCompat` from `@eslint/eslintrc`.
|
||||
Only do this for configs that don't publish a flat config export yet:
|
||||
|
||||
```js
|
||||
// eslint.config.mjs
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
|
||||
const compat = new FlatCompat({ baseDirectory: import.meta.dirname });
|
||||
|
||||
export default [...compat.extends('some-legacy-shareable-config')];
|
||||
```
|
||||
|
||||
`import.meta.dirname` requires Node.js 20.11 or later.
|
||||
On older versions, derive the directory with `path.dirname(fileURLToPath(import.meta.url))`.
|
||||
|
||||
Delete the `.eslintrc.*` files once `npx eslint .` runs clean in each project with the new config.
|
||||
Move `.eslintignore` patterns into an `ignores` entry - flat config doesn't read
|
||||
`.eslintignore`, and ESLint 10 removed support for it entirely.
|
||||
|
||||
## Handle rules removed in typescript-eslint v8
|
||||
|
||||
typescript-eslint v8 removed several rules, and a flat config that references a removed rule
|
||||
fails to load.
|
||||
Before editing, confirm your workspace is on typescript-eslint v8 or later (check
|
||||
`typescript-eslint` or `@typescript-eslint/eslint-plugin` in `package.json`) - on v7 the rules
|
||||
still exist.
|
||||
|
||||
- **Formatting rules are gone** - `@typescript-eslint/indent`, `quotes`, `semi`, `brace-style`,
|
||||
`comma-dangle`, `comma-spacing`, `key-spacing`, `keyword-spacing`, `member-delimiter-style`,
|
||||
`no-extra-parens`, `no-extra-semi`, `object-curly-spacing`,
|
||||
`padding-line-between-statements`, `space-before-blocks`, `space-before-function-paren`,
|
||||
`space-infix-ops`, `type-annotation-spacing`, and the other spacing rules were dropped.
|
||||
Remove them from your configs, or adopt [ESLint Stylistic](https://eslint.style) if you want
|
||||
lint-enforced formatting.
|
||||
- **Two rules were renamed** - replace `@typescript-eslint/no-throw-literal` with
|
||||
`@typescript-eslint/only-throw-error`, and `@typescript-eslint/no-useless-template-literals`
|
||||
with `@typescript-eslint/no-unnecessary-template-expression`, keeping your options.
|
||||
`only-throw-error` requires typed linting: the config block for TypeScript files must set
|
||||
`parserOptions.projectService: true` (or `parserOptions.project`), or ESLint fails to load
|
||||
the rule.
|
||||
- **`@typescript-eslint/ban-types` split into three rules** -
|
||||
`@typescript-eslint/no-empty-object-type` (the `{}` type),
|
||||
`@typescript-eslint/no-unsafe-function-type` (the `Function` type), and
|
||||
`@typescript-eslint/no-wrapper-object-types` (`String`, `Number`, `Boolean`, and the other
|
||||
wrappers).
|
||||
If the old entry was just an error level, set all three to that level.
|
||||
If it customized `types`, translate each banned type to whichever successor rule covers it.
|
||||
|
||||
## What else changes in ESLint v9?
|
||||
|
||||
Flat config is only part of the v9 upgrade. These runtime changes surface during the same migration:
|
||||
|
||||
- **Preset defaults shifted** - The ESLint v9 and typescript-eslint v8 recommended sets enable rules they didn't before, so a previously passing workspace can newly fail. Disable a newly reported rule you never configured, with a short comment, instead of editing source files to satisfy it.
|
||||
- **A crash is not a finding** - Errors like `TypeError: context.getAncestors is not a function` or `Could not find "<rule>" in plugin` mean an installed plugin predates ESLint v9. Update the plugin and prefer its flat entry point (for example `eslint-plugin-cypress/flat`) instead of disabling its rules.
|
||||
- **Some output formatters were removed** - `stylish`, `html`, `json`, and `json-with-metadata` remain built in. For a removed one like `junit`, install the community package (`eslint-formatter-junit`) and reference it by package name in the lint target's `format` option.
|
||||
- **Removed CLI flags** - `--rulesdir`, `--ext`, and `--resolve-plugins-relative-to` are gone, along with the matching `@nx/eslint:lint` options. Move file targeting into `files` and `ignores` in the config.
|
||||
- **Local rule API moved to `SourceCode`** - If the workspace authors its own rules, `context.getScope()` becomes `sourceCode.getScope(node)` (and similar for `getAncestors`, `getDeclaredVariables`, `getText`, and `parserServices`), and a rule that accepts options must declare `meta.schema`.
|
||||
|
||||
After the migration, run `nx reset` so renamed configs are re-detected, then confirm `nx run-many -t lint` passes across the workspace.
|
||||
|
||||
## Migrate a Next.js project to flat config
|
||||
|
||||
`eslint-config-next` 16 and later ships flat config natively, so you don't need
|
||||
`FlatCompat`.
|
||||
Import `eslint-config-next/core-web-vitals` in `eslint.config.mjs` and spread it into your
|
||||
config array:
|
||||
|
||||
```js
|
||||
// eslint.config.mjs
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import nextVitals from 'eslint-config-next/core-web-vitals';
|
||||
|
||||
export default defineConfig([
|
||||
...nextVitals,
|
||||
globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']),
|
||||
]);
|
||||
```
|
||||
|
||||
For TypeScript projects, add `eslint-config-next/typescript` to the array.
|
||||
Next.js 16 also removed the `next lint` command, so run `npx eslint .` directly.
|
||||
For rule reference and monorepo `rootDir` settings, see the
|
||||
[Next.js ESLint documentation](https://nextjs.org/docs/app/api-reference/config/eslint).
|
||||
|
||||
### Fix: TypeError: Converting circular structure to JSON with FlatCompat and next/core-web-vitals
|
||||
|
||||
Wrapping `next/core-web-vitals` with `FlatCompat` throws
|
||||
`TypeError: Converting circular structure to JSON`.
|
||||
The config includes `eslint-plugin-react-hooks`, whose configuration references itself, and
|
||||
expanding it through `FlatCompat` produces a circular structure:
|
||||
|
||||
```js
|
||||
// eslint.config.mjs
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
|
||||
const compat = new FlatCompat({ baseDirectory: import.meta.dirname });
|
||||
|
||||
// Throws: TypeError: Converting circular structure to JSON
|
||||
export default [...compat.extends('next/core-web-vitals')];
|
||||
```
|
||||
|
||||
Drop `FlatCompat` and use the native flat config export instead, upgrading to
|
||||
`eslint-config-next` 16 or later if your version doesn't provide it:
|
||||
|
||||
```js
|
||||
// eslint.config.mjs
|
||||
import nextVitals from 'eslint-config-next/core-web-vitals';
|
||||
|
||||
export default [...nextVitals];
|
||||
```
|
||||
|
||||
If you can't upgrade, register `@next/eslint-plugin-next` as a plugin directly and spread its
|
||||
recommended rules into a config object rather than extending through `FlatCompat`.
|
||||
|
||||
## Compare eslintrc and flat config formats
|
||||
|
||||
Compare the same Nx workspace configuration in flat, JSON, and JS formats:
|
||||
- push towards a single configuration format (in contrast to the existing `JSON`, `Yaml` and `JS`-based configs)
|
||||
- enforce explicit native loading (instead of the implicit imports in `JSON` and `Yaml`)
|
||||
- use a flat cascading of rules (instead of a mix of rules and overrides)
|
||||
|
||||
See below a direct comparison between `JSON`, `JS` and `Flat` config:
|
||||
{% tabs %}
|
||||
{% tabitem label="Flat" %}
|
||||
|
||||
```js
|
||||
// eslint.config.mjs
|
||||
// flat config imports plugins and parsers explicitly instead of resolving string names
|
||||
import nxPlugin from '@nx/eslint-plugin';
|
||||
import js from '@eslint/js';
|
||||
import baseConfig from './eslint.base.config.mjs';
|
||||
import globals from 'globals';
|
||||
import jsoncParser from 'jsonc-eslint-parser';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
// eslint.config.cjs
|
||||
// the older versions were magically interpreting all the imports
|
||||
// in flat config we do it explicitly
|
||||
const nxPlugin = require('@nx/eslint-plugin');
|
||||
const js = require('@eslint/js');
|
||||
const baseConfig = require('./eslint.base.config.cjs');
|
||||
const globals = require('globals');
|
||||
const jsoncParser = require('jsonc-eslint-parser');
|
||||
const tsParser = require('@typescript-eslint/parser');
|
||||
|
||||
export default [
|
||||
module.exports = [
|
||||
js.configs.recommended,
|
||||
// spreads the config objects from the base config
|
||||
// this will spread the export blocks from the base config
|
||||
...baseConfig,
|
||||
{ plugins: { '@nx': nxPlugin } },
|
||||
{
|
||||
@@ -256,16 +43,16 @@ export default [
|
||||
'@typescript-eslint/explicit-module-boundary-types': ['error'],
|
||||
},
|
||||
},
|
||||
// config objects scoped by files replace nested overrides
|
||||
// there are no overrides, all the config blocks are "flat"
|
||||
{
|
||||
files: ['**/*.json'],
|
||||
files: ['*.json'],
|
||||
languageOptions: {
|
||||
parser: jsoncParser,
|
||||
},
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
|
||||
rules: {
|
||||
'@nx/enforce-module-boundaries': [
|
||||
'error',
|
||||
@@ -375,3 +162,21 @@ module.exports = {
|
||||
|
||||
{% /tabitem %}
|
||||
{% /tabs %}
|
||||
|
||||
For additional details, head over to [ESLint's official blog post](https://eslint.org/blog/2022/08/new-config-system-part-2/).
|
||||
|
||||
Since version 16.8.0, Nx supports the usage of flat config in the [@nx/eslint:lint](/docs/technologies/eslint/executors#lint) executor and `@nx/*` generators, and provides an automated config conversion from `.eslintrc.json` config files.
|
||||
|
||||
## Converting workspace from .eslintrc.json to flat config
|
||||
|
||||
To convert workspace ESLint configurations from `.eslintrc.json` to flat config you need to run:
|
||||
|
||||
```shell
|
||||
nx g @nx/eslint:convert-to-flat-config
|
||||
```
|
||||
|
||||
The generator will go through all the projects and convert their configurations to the new format. It will also convert the base `.eslintrc.json` and `.eslintignore`.
|
||||
|
||||
## Correctness and best practices
|
||||
|
||||
The purpose of this generator is to create a flat config that works the same way as the original `JSON` config did. Depending on the complexity of your original config, it may be using the `FlatCompat` utility to provide a compatibility wrapper around parts of the original config. You can improve those by following the [official migration guide](https://eslint.org/docs/latest/use/configure/migration-guide).
|
||||
|
||||
@@ -7,9 +7,7 @@ weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
[ESLint](https://eslint.org/) statically analyzes your code to find and fix problems. In an ESLint monorepo setup, Nx runs lint tasks with caching enabled and re-lints only the projects a change affects.
|
||||
|
||||
The ESLint plugin integrates ESLint with Nx and includes code generators to set up ESLint in your workspace.
|
||||
The ESLint plugin integrates [ESLint](https://eslint.org/) with Nx. Run ESLint through Nx with caching enabled, and use the included code generators to set up ESLint in your workspace.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
[Gradle](https://gradle.org/) is a build automation tool for JVM-based projects. Nx brings task running, caching, and graph analysis to a Gradle monorepo.
|
||||
[Gradle](https://gradle.org/) is a build automation tool for JVM-based projects, and the `@nx/gradle` plugin brings Nx task running, caching, and graph analysis to a Gradle monorepo.
|
||||
|
||||
The `@nx/gradle` plugin registers Gradle projects in the Nx graph so you can [set up Gradle in Nx](#setup), [run local tasks](#local-development), [configure task inference](#configuration), and [scale in CI](#ci-considerations).
|
||||
|
||||
|
||||
@@ -1,179 +1,184 @@
|
||||
---
|
||||
title: What is Micro Frontend Architecture?
|
||||
description: Micro frontends split a web app into independently deployed modules. Learn when to use them, when not to, and how to build them with Module Federation.
|
||||
sidebar:
|
||||
label: Micro Frontend Architecture
|
||||
title: Micro Frontend Architecture
|
||||
description: Explore how Nx supports Micro Frontend architecture with Module Federation, enabling independent deployment while managing associated challenges.
|
||||
filter: 'type:Concepts'
|
||||
---
|
||||
|
||||
Micro frontend (also written microfrontend or MFE) architecture splits a web application's frontend into
|
||||
smaller applications that separate teams develop, test, and deploy independently.
|
||||
Each micro frontend owns a vertical slice of the product, such as checkout or search, and the
|
||||
pieces compose in the browser at runtime.
|
||||
The pattern trades a single coordinated release for independent release cadences per team.
|
||||
Nx provides out-of-the-box [Module Federation](/docs/technologies/module-federation/concepts/faster-builds-with-module-federation) support to both
|
||||
React and Angular. The Micro Frontend (MFE) architecture builds on top of Module Federation by providing _independent
|
||||
deployability_.
|
||||
|
||||
A common way to implement micro frontends in JavaScript is
|
||||
[Module Federation](/docs/technologies/module-federation/introduction).
|
||||
The sections below build a working setup with the `@module-federation/vite` plugin, not just
|
||||
architecture diagrams.
|
||||
If you have not read the [Module Federation guide](/docs/technologies/module-federation/concepts/faster-builds-with-module-federation)
|
||||
yet, we recommend that you read it
|
||||
before continuing with this MFE guide.
|
||||
|
||||
## When should you use micro frontends?
|
||||
## When should I use micro frontend architecture?
|
||||
|
||||
Use micro frontends when independent deployment is a hard requirement: several teams ship to the
|
||||
same application, and waiting on a shared release train costs more than the added architectural
|
||||
complexity.
|
||||
If your teams can deploy together and you mainly want faster builds, runtime composition is the
|
||||
wrong tool for that problem.
|
||||
We recommend MFE for teams that require applications to be deployed independently. It is important to consider the cost
|
||||
of MFEs and decide whether it makes sense for your own teams.
|
||||
|
||||
For the build-speed use case, see
|
||||
[faster builds with Module Federation](/docs/technologies/module-federation/concepts/faster-builds-with-module-federation),
|
||||
which uses the same federation mechanics without independent deployments.
|
||||
- Version mismatches where applications are deployed with different versions of shared libraries, which can lead to
|
||||
incompatibility issues.
|
||||
- Independent deployments can lead to unexpected errors, such as any host-level changes to orchestration/coordination
|
||||
logic that breaks compatibility with remotes.
|
||||
|
||||
## When should you avoid micro frontends?
|
||||
If you are looking at optimizing builds and do not need independent deployments, we recommend reading our guide on
|
||||
[Faster Builds with Module Federation](/docs/technologies/module-federation/concepts/faster-builds-with-module-federation).
|
||||
|
||||
Avoid micro frontends in the following situations:
|
||||
If you need to use MFEs, keep reading, and we'll examine the architecture and strategies to deal with shared libraries
|
||||
and
|
||||
deployments.
|
||||
|
||||
- A single team owns the whole frontend. Independent deployment solves a team-coordination
|
||||
problem you don't have.
|
||||
- Your teams already deploy together. Federation adds runtime failure modes without removing any
|
||||
release bottleneck.
|
||||
- The application is small. Module boundaries inside one deployable give you code ownership
|
||||
without version skew.
|
||||
- You can't fund the ongoing coordination work: shared dependency contracts, cross-app
|
||||
integration testing, and rollback procedures for non-atomic releases.
|
||||
## Architectural overview
|
||||
|
||||
## Micro frontend benefits and tradeoffs
|
||||
With MFE architecture, a large application is split into:
|
||||
|
||||
Micro frontends buy team autonomy and independent releases, and they charge for it in runtime
|
||||
overhead and coordination complexity.
|
||||
1. A single **Host** application that references external...
|
||||
2. **Remote** applications, which handle a single domain or feature.
|
||||
|
||||
| Benefit | Tradeoff |
|
||||
| ------------------------------------------------ | --------------------------------------------------------------------------- |
|
||||
| Each team deploys on its own cadence | Releases are not atomic, so app versions can skew between deployments |
|
||||
| Teams own their slice end to end | Cross-team contracts move from compile-time checks to runtime agreements |
|
||||
| Each team builds and ships a smaller application | Shared dependencies duplicate in the bundle unless you federate them |
|
||||
| A broken deploy is scoped to one micro frontend | Integration bugs only surface when specific app versions meet in production |
|
||||
In a normal Module Federation setup,
|
||||
we [recommend setting up implicit dependencies](/docs/technologies/module-federation/concepts/faster-builds-with-module-federation#architectural-overview)
|
||||
from the host application to remote applications. However, in an MFE architecture you _do not_ want these dependencies
|
||||
to exist between host and remotes.
|
||||
|
||||
## Micro frontends with Module Federation
|
||||
For example, if you have a `shell` host application, with three remotes -- `about`, `cart`, `shop` -- and a shared
|
||||
`ui-button` library, then your project graph might look something like this.
|
||||
|
||||
Module Federation lets one application load modules that another application exposes at runtime,
|
||||
while dependencies you mark as shared, such as `react`, can load once as singletons.
|
||||
Nx uses two roles for this: a **consumer** loads federated components, and a **provider** exposes
|
||||
them.
|
||||
As of Nx v23, the `host` and `remote` generators are replaced by `consumer` and `provider`.
|
||||
For the full generator surface and migration steps, see
|
||||
[consumer and provider](/docs/technologies/module-federation/consumer-and-provider).
|
||||

|
||||
|
||||
The applications stay independent in the project graph.
|
||||
A `shell` consumer with three providers, `about`, `cart`, and `shop`, plus a shared `ui-button`
|
||||
library looks like this:
|
||||
Keeping the applications independent allows them to be deployed on different cadences, which is the whole point of MFEs.
|
||||
|
||||

|
||||
## Generating applications
|
||||
|
||||
There are no dependencies between the applications themselves, only on shared libraries, which is
|
||||
what lets each one deploy independently.
|
||||
The generator for MFEs is the same as with basic Module Federation. You can use `nx g host` to create a new host
|
||||
application, and `nx g remote` for remote applications.
|
||||
|
||||
### Generate a consumer and providers
|
||||
{% tabs %}
|
||||
{% tabitem label="React" %}
|
||||
|
||||
```shell
|
||||
nx g @nx/react:consumer apps/shell --bundler=vite --providerNames=shop,cart
|
||||
nx g @nx/react:provider apps/about --bundler=vite --consumer=shell
|
||||
nx g @nx/react:host apps/shell --remotes=shop,cart
|
||||
nx g @nx/react:remote apps/about --host=shell
|
||||
```
|
||||
|
||||
The consumer registers providers at runtime through the `PROVIDERS` list in its `src/mf.ts`, so
|
||||
adding the standalone `about` provider is an edit to that list, not a rebuild of every app.
|
||||
Run `nx serve shop` to develop a provider.
|
||||
Its `serve` target depends on the consumer's `serve` target, so the shell comes up alongside it.
|
||||
The `--consumer` flag wires up the same behavior for providers generated on their own, like
|
||||
`about` above.
|
||||
{% /tabitem %}
|
||||
{% tabitem label="Angular" %}
|
||||
|
||||
### Share as few libraries as possible
|
||||
|
||||
Because deployments are not atomic, mismatched shared library versions are a leading failure mode
|
||||
of micro frontends.
|
||||
The safest strategy is to share only the libraries that must be singletons, such as your UI
|
||||
framework and any cross-app communication layer, and let everything else stay bundled per app:
|
||||
|
||||
```ts
|
||||
// apps/about/vite.config.ts
|
||||
import { federation } from '@module-federation/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
plugins: [
|
||||
federation({
|
||||
name: 'about',
|
||||
filename: 'remoteEntry.js',
|
||||
exposes: {
|
||||
'./App': './src/App.tsx',
|
||||
},
|
||||
// Share only libraries that must load once at runtime.
|
||||
// Everything else stays bundled per application.
|
||||
shared: {
|
||||
react: { singleton: true },
|
||||
'react-dom': { singleton: true },
|
||||
'@acme/pub-sub': { singleton: true },
|
||||
},
|
||||
}),
|
||||
react(),
|
||||
],
|
||||
}));
|
||||
```shell
|
||||
nx g @nx/angular:host apps/shell --remotes=shop,cart
|
||||
nx g @nx/angular:remote apps/about --host=shell
|
||||
```
|
||||
|
||||
Not sharing a library means each app bundles its own copy, which increases download size.
|
||||
Start with a small set of singletons and expand it when duplication becomes measurable.
|
||||
For a complete walkthrough of this plugin setup, see
|
||||
[Vite Module Federation](/docs/technologies/module-federation/vite-module-federation).
|
||||
A worked reference workspace covering Vite, Rsbuild, and Rspack lives in the
|
||||
[mf-examples repository](https://github.com/nrwl/mf-examples).
|
||||
{% /tabitem %}
|
||||
{% /tabs %}
|
||||
|
||||
## Why build micro frontends in a monorepo?
|
||||
That is! You can now run `nx serve shell` to develop on the `shell` application, while keeping all remotes static. To
|
||||
develop on one or more remote applications, you can run `nx serve shop` or `nx run-many -t serve -p shop,cart` to start
|
||||
both remotes. The remotes' `serve` target depends on the `shell:serve` target, and therefore shell will be started automatically.
|
||||
|
||||
A monorepo removes two of the biggest micro frontend pains: shared dependency drift and cross-app
|
||||
integration testing.
|
||||
Every application builds against the same library versions at the same commit, and CI exercises
|
||||
the composed application before any team deploys.
|
||||
Deployments stay independent even though the repository is shared.
|
||||
## Deployment strategies
|
||||
|
||||
- Run [affected](/docs/features/ci-features/affected) tasks to test and deploy only the
|
||||
applications a change touches.
|
||||
- Use [module boundary rules](/docs/features/enforce-module-boundaries) to keep each team's slice
|
||||
from reaching into another team's internals.
|
||||
How applications are deployed depends on the teams and organizational requirements. There are two approaches:
|
||||
|
||||
## How do you deploy micro frontends?
|
||||
1. À la carte deployments - Each application is deployed according to a release schedule, and can have different cadences.
|
||||
2. Affected deployments - When changes are merged, use Nx to test and deploy the affected applications automatically.
|
||||
|
||||
Teams deploy micro frontends in one of two ways, and many mix them: a la carte deployments, where
|
||||
each application follows its own release cadence, and affected deployments, where CI
|
||||
automatically tests and deploys only the applications a merge touches.
|
||||
Often times, teams mix both approaches so deployments to staging (or other shared environments) are automatic. Then,
|
||||
promotion from staging to production occurs on a set cadence (e.g. weekly releases). It is also recommended to agree on
|
||||
a process to handle changes to core libraries (i.e. ones that are shared between applications). Since the core changes
|
||||
affect all applications, it also blocks all other releases, thus should not occur too frequently.
|
||||
|
||||
A common mix is automatic deployment to staging on every merge, with promotion to production on a
|
||||
set schedule such as weekly releases.
|
||||
Agree on a process for changes to shared core libraries: those changes affect every application,
|
||||
so they block other releases and should ship infrequently.
|
||||
You may also choose to fully automate deployments, even to production. This type of pipeline requires good end-to-end
|
||||
testing to provide higher confidence that the applications behave correctly. You will also need good rollback mechanisms
|
||||
in case of a bad deployment.
|
||||
|
||||
Fully automated production deployment also works, but it needs end-to-end tests you trust and a
|
||||
rollback mechanism for bad deploys.
|
||||
As the number of applications grows, distribute those CI runs with
|
||||
[Nx Agents](/docs/features/ci-features/distribute-task-execution) to keep pipeline times flat.
|
||||
## Shared libraries
|
||||
|
||||
## Micro frontends with Angular
|
||||
Since deployments with MFEs are not atomic, there is a chance that shared libraries -- both external (npm) and workspace --
|
||||
between the host and remotes are mismatched. The default Nx setup configures all libraries as singletons, which requires
|
||||
that all affected applications be deployed for any given changeset, and makes à la carte deployments riskier.
|
||||
|
||||
As of Nx v23, the `@nx/angular` `host` and `remote` generators are deprecated.
|
||||
For Angular micro frontends, look to
|
||||
[`@angular-architects/native-federation`](https://www.npmjs.com/package/@angular-architects/native-federation),
|
||||
which implements the same runtime composition model on top of the Angular CLI build.
|
||||
Nx still runs, caches, and orchestrates the Angular apps as regular projects in the workspace.
|
||||
There are mitigation strategies that can minimize mismatch errors. One such strategy is to share as little as possible
|
||||
between applications.
|
||||
|
||||
For example, you can create a base configuration file that only shares core libraries that _have_ to be shared.
|
||||
|
||||
```javascript
|
||||
// module-federation.config.ts
|
||||
import { ModuleFederationConfig } from '@nx/module-federation';
|
||||
// Core libraries such as react, angular, redux, ngrx, etc. must be
|
||||
// singletons. Otherwise the applications will not work together.
|
||||
const coreLibraries = new Set([
|
||||
'react',
|
||||
'react-dom',
|
||||
'react-router-dom',
|
||||
// A workspace library for a publish/subscribe
|
||||
// system of communication.
|
||||
'@acme/pub-sub',
|
||||
]);
|
||||
|
||||
export const config: ModuleFederationConfig = {
|
||||
// Share core libraries, and avoid everything else
|
||||
shared: (libraryName, defaultConfig) => {
|
||||
if (coreLibraries.has(libraryName)) {
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
// Returning false means the library is not shared.
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
Then, in the `shell` and remote applications, you can extend from the base configuration.
|
||||
|
||||
```javascript
|
||||
// apps/shell/module-federation.config.ts
|
||||
import { ModuleFederationConfig } from '@nx/module-federation';
|
||||
import baseConfig from '../../module-federation.config';
|
||||
|
||||
export const config: ModuleFederationConfig = {
|
||||
...baseConfig,
|
||||
name: 'shell',
|
||||
remotes: ['shop', 'cart', 'about'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
{% aside type="note" title="More details" %}
|
||||
You can return any configuration [object that webpack/rspack's Module Federation supports](https://webpack.js.org/plugins/module-federation-plugin/#sharing-hints).
|
||||
{% /aside %}
|
||||
|
||||
There are downsides to not sharing a library (such as increasing network traffic due to duplication), so consider what
|
||||
you share carefully. If you are not sure, then start with a small set of core libraries, and expand it as needed.
|
||||
|
||||
## Strategic collaboration over micro frontend anarchy
|
||||
|
||||
[Micro frontend anarchy](https://www.thoughtworks.com/en-ca/radar/techniques/micro-frontend-anarchy)
|
||||
is a setup that mixes competing technologies, such as Angular in some applications and React in
|
||||
others.
|
||||
Module Federation makes this possible, but every extra framework multiplies the shared dependency
|
||||
surface and splits your developers into camps that can't review each other's code.
|
||||
[Micro frontend anarchy](https://www.thoughtworks.com/en-ca/radar/techniques/micro-frontend-anarchy) refers to an MFE
|
||||
setup that mixes a range of competing technologies together. For example, using Angular in some applications, and React
|
||||
in another. Although it is possible to do this mixing with MFEs, we recommend choosing strategic collaboration instead.
|
||||
|
||||
Agree on one set of adopted technologies per concern: UI framework, styling approach, and state
|
||||
management.
|
||||
Mixing competing technologies makes sense only as a deliberate transition strategy, such as an
|
||||
incremental migration from one framework to another.
|
||||
Teams should agree upon a set of adopted technologies, such as UI/backend framework, styling solutions (CSS vs CSS-in-JS),
|
||||
etc. Standardizing technologies enable developers to collaborate across teams more easily, since there is consistency
|
||||
in each vertical. The only time mixing competing technologies make sense is as a part of a deliberate transition strategy,
|
||||
such as migrating from React to Vue, for example.
|
||||
|
||||
## Summary
|
||||
|
||||
While Module Federation enables faster builds by vertically slicing your application into smaller ones, the
|
||||
MFE architecture layers _independent deployments_ on top of federation. Teams should only choose MFEs
|
||||
if they want to deploy their host and remotes on different cadences.
|
||||
|
||||
Teams should consider a process for changes to core libraries that require deploying all applications. These types of
|
||||
changes should occur infrequently as to not disrupt other releases for bug fixes or new features.
|
||||
|
||||
Since deployments are not atomic, there can be cases of mismatched libraries between the host and remotes. We recommend
|
||||
that teams deploy their applications whenever changes to a shared library affects them. You can further mitigate mismatch
|
||||
issues by minimizing the amount of libraries you share (using the `shared` configuration option in
|
||||
`module-federation.config.ts`).
|
||||
|
||||
Teams should also avoid MFE anarchy, where competing technologies are mixed together. Instead, teams should agree upon
|
||||
the adopted technologies, which allows easier collaboration across teams.
|
||||
|
||||
@@ -7,9 +7,7 @@ weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
[Node.js](https://nodejs.org/) is a JavaScript runtime for building servers, CLIs, and backend services. Nx scales your Node monorepo with caching, distributed task execution, and affected-only builds.
|
||||
|
||||
The Node plugin contains generators and executors to manage Node applications within an Nx workspace.
|
||||
The Node Plugin contains generators and executors to manage Node applications within an Nx workspace, making it the foundation for running a Node.js monorepo. It provides:
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
[React](https://react.dev/) is a popular library for building user interfaces. In a React monorepo, every project benefits from Nx [caching](/docs/features/cache-task-results), [task orchestration](/docs/features/run-tasks), and the [project graph](/docs/features/explore-graph).
|
||||
The React plugin for Nx, `@nx/react`, helps you build and scale a React monorepo. It provides generators for [applications and libraries](#generate-react-applications-and-libraries), executors for [Module Federation](/docs/technologies/react/guides/module-federation-with-ssr), and [library build support](/docs/concepts/buildable-and-publishable-libraries). It integrates with popular bundlers and test runners so you can configure each project to match your team's toolchain.
|
||||
|
||||
The React plugin for Nx, `@nx/react`, provides generators for [applications and libraries](#generate-react-applications-and-libraries), executors for [Module Federation](/docs/technologies/react/guides/module-federation-with-ssr), and [library build support](/docs/concepts/buildable-and-publishable-libraries). It integrates with popular bundlers and test runners so you can configure each project to match your team's toolchain. You don't need the plugin to use React with Nx - it simplifies scaffolding and code generation.
|
||||
You don't need the plugin to use React with Nx. Any project already benefits from [caching](/docs/features/cache-task-results), [task orchestration](/docs/features/run-tasks), and the [project graph](/docs/features/explore-graph). The plugin simplifies scaffolding and code generation.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
[Remix](https://remix.run/) is a full stack React framework for building web applications. In a Remix monorepo, every project benefits from Nx [caching](/docs/features/cache-task-results), [task orchestration](/docs/features/run-tasks), and the [project graph](/docs/features/explore-graph).
|
||||
The Remix plugin for Nx, `@nx/remix`, helps you build and scale a Remix monorepo. It automatically [infers `build`, `dev`, `start`, and `typecheck` tasks](#how-nxremix-infers-tasks) from your Remix configuration and provides [generators for applications, libraries, routes, loaders, actions, and meta functions](#develop-remix-applications).
|
||||
|
||||
The Remix plugin for Nx, `@nx/remix`, automatically [infers `build`, `dev`, `start`, and `typecheck` tasks](#how-nxremix-infers-tasks) from your Remix configuration and provides [generators for applications, libraries, routes, loaders, actions, and meta functions](#develop-remix-applications). You don't need the plugin to use Remix with Nx - it adds automatic task inference, route scaffolding, and simplified configuration.
|
||||
You don't need the plugin to use Remix with Nx. Any project already benefits from [caching](/docs/features/cache-task-results), [task orchestration](/docs/features/run-tasks), and the [project graph](/docs/features/explore-graph). The plugin adds automatic task inference, route scaffolding, and simplified configuration.
|
||||
|
||||
{% aside type="note" %}
|
||||
React Router v7 is the successor to Remix. `@nx/remix` supports Remix v2; [`@nx/react`](/docs/technologies/react/guides/react-router) handles React Router v7. For new projects, use React Router instead. Existing Remix v2 projects continue to work with `@nx/remix`.
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
title: Use TypeScript 7.0 alongside TypeScript 6.0
|
||||
description: Run TypeScript 7.0 through Nx tasks while keeping TypeScript 6.0 available for tools that need the compiler API.
|
||||
sidebar:
|
||||
label: Use TypeScript 7.0
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
TypeScript 7.0 provides a faster native compiler, but it does not yet ship a programmatic API. Some tools, including the `@nx/js/typescript` plugin, `vite`, and `typescript-eslint`, still need that API. Install TypeScript 6.0 and 7.0 side by side: TypeScript 6.0 supplies the API, while TypeScript 7.0 supplies the `tsc` executable used by your Nx tasks.
|
||||
|
||||
## Install TypeScript 6.0 and TypeScript 7.0
|
||||
|
||||
Add two aliases to the root `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"devDependencies": {
|
||||
"@typescript/native": "npm:typescript@^7.0.2",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The alias names are intentional:
|
||||
|
||||
- `typescript` resolves to `@typescript/typescript6`, so tools that import the TypeScript API continue to receive TypeScript 6.0.
|
||||
- `@typescript/native` resolves to TypeScript 7.0. Its `tsc` binary is available on the workspace path, while TypeScript 6.0 exposes `tsc6` without a naming conflict.
|
||||
|
||||
Install dependencies with your package manager, then confirm both compilers are available:
|
||||
|
||||
```shell
|
||||
npx tsc --version
|
||||
npx tsc6 --version
|
||||
```
|
||||
|
||||
`tsc` should report TypeScript 7.0 and `tsc6` should report TypeScript 6.0.
|
||||
|
||||
{% aside type="note" title="Why use aliases?" %}
|
||||
|
||||
Installing only `typescript@npm:@typescript/typescript6` preserves API compatibility but exposes only `tsc6`. Adding the TypeScript 7.0 alias restores `tsc`, letting commands that already invoke `tsc` use TypeScript 7.0 without changing their names.
|
||||
|
||||
{% /aside %}
|
||||
|
||||
## Run TypeScript 7.0 with Nx
|
||||
|
||||
The `@nx/js/typescript` plugin infers `typecheck` and `build` tasks that run `tsc`. With the aliases above, those tasks use TypeScript 7.0 while Nx continues to analyze TypeScript configuration through the TypeScript 6.0 API.
|
||||
|
||||
Run the inferred tasks as usual:
|
||||
|
||||
```shell
|
||||
nx run my-library:typecheck
|
||||
nx run my-library:build
|
||||
```
|
||||
|
||||
### Legacy workspaces using the `@nx/js:tsc` executor
|
||||
|
||||
No additional configuration is required. The `typescript` alias above makes `require('typescript')` resolve to TypeScript 6.0, which keeps API-dependent executors working.
|
||||
|
||||
## Prepare your configuration for TypeScript 7.0
|
||||
|
||||
TypeScript 7.0 is designed to match TypeScript 6.0's type-checking and command-line behavior. Before switching, update your workspace for TypeScript 6.0 and remove any temporary `ignoreDeprecations` setting. In particular, review TypeScript 6.0's changed defaults and deprecated compiler options.
|
||||
|
||||
TypeScript 7.0 does not yet support workflows that need its programmatic API, including many language-service integrations and embedded-language tools. Angular, Vue, MDX, Astro, and Svelte projects may need TypeScript 6.0 for editor or template tooling even when they use TypeScript 7.0 for CLI type checking. Keep `tsc6` available as a fallback while those tools add TypeScript 7.0 support.
|
||||
|
||||
For the complete compatibility model and current limitations, see Microsoft's [TypeScript 7 announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6.0).
|
||||
|
||||
## Validate the migration
|
||||
|
||||
Start with a representative project, then validate the affected workspace:
|
||||
|
||||
```shell
|
||||
nx affected -t build,test,lint
|
||||
nx affected -t e2e
|
||||
```
|
||||
|
||||
Run your full validation suite before committing the dependency change. This catches API-dependent tooling that still requires TypeScript 6.0 while retaining TypeScript 7.0 for the tasks that can use it.
|
||||
@@ -9,126 +9,9 @@ weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
A TypeScript/JavaScript (TS/JS) monorepo keeps many packages in one repository, sharing types and code directly instead of through published packages. Nx runs and maintains that monorepo at scale: it detects projects from your package manager workspaces, keeps TypeScript project references in sync automatically, and runs typechecking and builds with caching and affected detection.
|
||||
The TypeScript plugin for Nx, `@nx/js`, helps you run and maintain a TypeScript monorepo. It provides [generators for creating TypeScript and JavaScript projects](#generate-and-manage-typescript-libraries), and the `@nx/js/typescript` plugin automatically infers [`typecheck` and `build` tasks](#how-the-typescript-plugin-infers-tasks) for your projects using package manager workspaces and TypeScript project references. Maintaining TypeScript project references in a monorepo can be cumbersome, the `@nx/js` plugin comes with a [sync generator](/docs/concepts/sync-generators) to automatically upkeep these references.
|
||||
|
||||
The `@nx/js` plugin provides [generators for creating TypeScript and JavaScript projects](#generate-and-manage-typescript-libraries), and its `@nx/js/typescript` plugin infers [`typecheck` and `build` tasks](#how-the-typescript-plugin-infers-tasks) from your tsconfig files. You don't need the plugin to use TypeScript with Nx, any project already benefits from [caching](/docs/features/cache-task-results), [task orchestration](/docs/features/run-tasks), and the [project graph](/docs/features/explore-graph). The plugin can help simplify setups and maintenance of TypeScript projects at scale.
|
||||
|
||||
## How Nx maintains a TypeScript monorepo
|
||||
|
||||
Keeping all the tools in a large TypeScript monorepo correctly configured and working together is a difficult task, and every added tool is a new chance for conflicts. Nx approaches this from two directions: it configures itself to match the existing configuration of your other tools, and it enhances certain tools to work better in a monorepo.
|
||||
|
||||
### Project detection with package manager workspaces
|
||||
|
||||
If your repository uses package manager workspaces, Nx uses those settings to find all the [projects](/docs/reference/project-configuration) in your repository. You don't define projects for your package manager and again for Nx: the `workspaces` configuration is enough for Nx to build the project graph.
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"workspaces": ["apps/*", "packages/*"]
|
||||
}
|
||||
```
|
||||
|
||||
{% graph height="200px" title="Project View" %}
|
||||
|
||||
```json
|
||||
{
|
||||
"composite": false,
|
||||
"projects": [
|
||||
{
|
||||
"name": "product-state",
|
||||
"type": "lib",
|
||||
"data": {
|
||||
"root": "packages/cart/product-state",
|
||||
"tags": ["scope:cart", "type:state"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ui-buttons",
|
||||
"type": "lib",
|
||||
"data": {
|
||||
"root": "packages/ui/buttons",
|
||||
"tags": ["scope:shared", "type:ui"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cart",
|
||||
"type": "app",
|
||||
"data": {
|
||||
"root": "apps/cart",
|
||||
"tags": ["type:app", "scope:cart"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"product-state": [],
|
||||
"ui-buttons": [],
|
||||
"cart": [
|
||||
{ "source": "cart", "target": "product-state", "type": "static" },
|
||||
{ "source": "cart", "target": "ui-buttons", "type": "static" }
|
||||
]
|
||||
},
|
||||
"workspaceLayout": {
|
||||
"appsDir": "apps",
|
||||
"libsDir": "libs"
|
||||
},
|
||||
"affectedProjectIds": [],
|
||||
"focus": null,
|
||||
"groupByFolder": false,
|
||||
"exclude": [],
|
||||
"enableTooltips": false
|
||||
}
|
||||
```
|
||||
|
||||
{% /graph %}
|
||||
|
||||
### Tasks inferred from your tooling configuration
|
||||
|
||||
Nx [plugins](/docs/concepts/nx-plugins) for tools like TypeScript, Vite, Playwright, and Jest [infer task configuration](/docs/concepts/inferred-tasks) from your existing tooling config files, keeping those files as the single source of truth. Because a config file like `vite.config.ts` exists, Nx knows the project can run a `build` task with Vite, and it reads the config to set the correct [cache](/docs/features/cache-task-results) outputs. The TypeScript-specific inference is covered in [how the TypeScript plugin infers tasks](#how-the-typescript-plugin-infers-tasks).
|
||||
|
||||
### TypeScript project references kept in sync
|
||||
|
||||
TypeScript [Project References](https://www.typescriptlang.org/docs/handbook/project-references.html) let the compiler build and typecheck each project independently, reusing intermediate `*.tsbuildinfo` files instead of re-typechecking every dependency. This provides [significant performance improvements](/docs/concepts/typescript-project-linking#typescript-project-references-performance-benefits), particularly in a large monorepo.
|
||||
|
||||
The downside is that each project's references have to be defined by hand in the appropriate `tsconfig.*.json` file, which is tedious to set up and hard to maintain as the repository changes. Nx uses a [sync generator](/docs/concepts/sync-generators) to update the references automatically from the project graph it already knows about:
|
||||
|
||||
```jsonc
|
||||
// apps/cart/tsconfig.json
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [], // intentionally empty
|
||||
"references": [
|
||||
// UPDATED BY NX SYNC
|
||||
// All project dependencies
|
||||
{
|
||||
"path": "../../packages/product-state",
|
||||
},
|
||||
{
|
||||
"path": "../../packages/ui/buttons",
|
||||
},
|
||||
// This project's other tsconfig.*.json files
|
||||
{
|
||||
"path": "./tsconfig.lib.json",
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json",
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
If someone adds another dependency to the `cart` app and runs the `build` task, Nx detects that the project references are out of sync and asks whether to update them:
|
||||
|
||||
```plaintext {% title="nx build cart" frame="terminal" %}
|
||||
NX The workspace is out of sync
|
||||
|
||||
[@nx/js:typescript-sync]: Some TypeScript configuration files are missing project references to the projects they depend on or contain outdated project references.
|
||||
|
||||
This will result in an error in CI.
|
||||
|
||||
? Would you like to sync the identified changes to get your workspace up to date? …
|
||||
❯ Yes, sync the changes and run the tasks
|
||||
No, run the tasks without syncing the changes
|
||||
```
|
||||
You don't need the plugin to use TypeScript with Nx, any project already benefits from [caching](/docs/features/cache-task-results), [task orchestration](/docs/features/run-tasks), and the [project graph](/docs/features/explore-graph). The plugin can help simplify setups and maintenance of TypeScript projects at scale.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -136,13 +19,11 @@ Nx supports the latest version of TypeScript. TypeScript itself only officially
|
||||
|
||||
| Nx Version | TypeScript Version |
|
||||
| -------------- | ------------------ |
|
||||
| 23.x (current) | >= 5.8.0 < 7.1.0 |
|
||||
| 23.x (current) | >= 5.8.0 < 6.1.0 |
|
||||
| 22.x | >= 5.4.2 < 5.10.0 |
|
||||
| 21.x | >= 5.4.2 < 5.10.0 |
|
||||
| 20.x | ~5.4.2 |
|
||||
|
||||
TypeScript 7.0 does not yet provide a programmatic API. To use its CLI while tools that consume the API continue to use TypeScript 6.0, see [Use TypeScript 7.0 alongside TypeScript 6.0](/docs/technologies/typescript/guides/typescript-7).
|
||||
|
||||
## Setting up @nx/js plugin
|
||||
|
||||
### Add to an existing Nx workspace
|
||||
@@ -208,7 +89,7 @@ pnpm create nx-workspace my-org --template nrwl/typescript-template
|
||||
{% /tabitem %}
|
||||
{% /tabs %}
|
||||
|
||||
This creates a monorepo configured with [TypeScript Project References](https://www.typescriptlang.org/docs/handbook/project-references.html) and your package manager's [workspaces](https://docs.npmjs.com/cli/using-npm/workspaces) feature. [Nx automatically maintains the project references](#typescript-project-references-kept-in-sync) as you add and remove projects.
|
||||
This creates a monorepo configured with [TypeScript Project References](https://www.typescriptlang.org/docs/handbook/project-references.html) and your package manager's [workspaces](https://docs.npmjs.com/cli/using-npm/workspaces) feature. [Nx automatically maintains the project references](/docs/features/maintain-typescript-monorepos) as you add and remove projects.
|
||||
|
||||
{% aside type="tip" title="Tutorial" %}
|
||||
For a guided walkthrough, follow the [Learn Nx Tutorial](/docs/getting-started/tutorials/crafting-your-workspace).
|
||||
|
||||
@@ -7,9 +7,7 @@ weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
[Vue](https://vuejs.org/) is a progressive JavaScript framework for building user interfaces. In a Vue monorepo, Nx caches task results and runs only the tasks a change affects.
|
||||
|
||||
The `@nx/vue` plugin adds first-class Vue support to an Nx workspace, so you can scaffold, build, and test apps and libraries.
|
||||
The `@nx/vue` plugin adds first-class [Vue](https://vuejs.org/) support to an Nx workspace, so you can scaffold, build, and test apps and libraries in a Vue monorepo.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@ weight: 2
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
[Nuxt](https://nuxt.com/) is a full stack web framework built on Vue. In a Nuxt monorepo, Nx caches task results and runs only the tasks a change affects.
|
||||
|
||||
The `@nx/nuxt` plugin adds generators, task inference, and caching to your Nuxt projects.
|
||||
The `@nx/nuxt` plugin lets you build and scale a Nuxt monorepo with [Nuxt](https://nuxt.com/), adding generators, task inference, and caching to your projects.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -28,14 +28,7 @@ import {
|
||||
pluginToTechnology,
|
||||
} from './utils/plugin-mappings';
|
||||
|
||||
// Only directories are packages. Stray files at the packages/ root (e.g.
|
||||
// a CLAUDE.md dev-doc) must not be treated as plugins — doing so generates
|
||||
// a bogus `technologies/undefined/<file>/...` route that fails link validation.
|
||||
const PLUGIN_PATHS = readdirSync(join(workspaceRoot, 'packages'), {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name);
|
||||
const PLUGIN_PATHS = readdirSync(join(workspaceRoot, 'packages'));
|
||||
|
||||
type DocEntry = CollectionEntry<'plugin-docs'>;
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"outDir": "out-tsc/playwright",
|
||||
"sourceMap": false,
|
||||
"rootDir": "."
|
||||
"sourceMap": false
|
||||
},
|
||||
"include": ["e2e/**/*.ts", "e2e/**/*.js", "playwright.config.ts"],
|
||||
"exclude": [
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"ignoreDeprecations": "6.0"
|
||||
"strict": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
|
||||
@@ -25,10 +25,8 @@ export function setupModuleFederationTest(): ModuleFederationTestSetup {
|
||||
}
|
||||
|
||||
export function cleanupModuleFederationTest(
|
||||
setup: ModuleFederationTestSetup | undefined
|
||||
setup: ModuleFederationTestSetup
|
||||
): void {
|
||||
cleanupProject();
|
||||
// setup is undefined when setupModuleFederationTest itself failed; don't
|
||||
// let cleanup throw and shadow the real error.
|
||||
process.env.NX_E2E_VERBOSE_LOGGING = setup?.oldVerboseLoggingValue;
|
||||
process.env.NX_E2E_VERBOSE_LOGGING = setup.oldVerboseLoggingValue;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest"],
|
||||
"ignoreDeprecations": "6.0"
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": [],
|
||||
"files": [],
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "out-tsc/spec",
|
||||
"types": ["jest", "node"],
|
||||
"rootDir": "."
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"exclude": ["out-tsc"],
|
||||
"include": [
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest"],
|
||||
"ignoreDeprecations": "6.0"
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": [],
|
||||
"files": [],
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "out-tsc/spec",
|
||||
"types": ["jest", "node"],
|
||||
"rootDir": "."
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"exclude": ["out-tsc"],
|
||||
"include": [
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest"],
|
||||
"ignoreDeprecations": "6.0"
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": [],
|
||||
"files": [],
|
||||
|
||||