Compare commits
167 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7eccccd817 | |||
| 911d259abd | |||
| fdd5e434af | |||
| 171306d74a | |||
| dc849bbd94 | |||
| d6d0eac310 | |||
| f5e74caa1f | |||
| eba3fbab58 | |||
| d2dfc4a65a | |||
| 6847d708ef | |||
| 7effad4b58 | |||
| 19dbbb0753 | |||
| 8fd1febdcf | |||
| eaecdea911 | |||
| 92bf264ba4 | |||
| 22e679c2c0 | |||
| 9fa3f96cf3 | |||
| a33aaa749a | |||
| 22f3c2e7bc | |||
| 8ce0683993 | |||
| 480fc80d12 | |||
| c7582bfce8 | |||
| ba85c77930 | |||
| 1289b017e0 | |||
| 934850b8e5 | |||
| 373e8983a1 | |||
| 640e69a7be | |||
| c25bd9d96e | |||
| 0d1851288f | |||
| 7c1318faba | |||
| a3acfd6bab | |||
| 81bdb17992 | |||
| c574207825 | |||
| 0d002eceae | |||
| a6f8744726 | |||
| b58ccd837e | |||
| 7ac33301a0 | |||
| 68ee897c93 | |||
| 29aac903b7 | |||
| ccb1f87fc7 | |||
| 0f6f117edc | |||
| a23b7be87f | |||
| e9e447b14e | |||
| 087009e0f4 | |||
| 85f7298a2c | |||
| 11e96c39c9 | |||
| 20c155f0ff | |||
| 2930dcac44 | |||
| 175b6cbb8b | |||
| c01974f27b | |||
| 494f4a733b | |||
| be15c3ef6d | |||
| c7fd2fa576 | |||
| 8e77b9c07b | |||
| 3852a1f82d | |||
| de9d8a5761 | |||
| 4e9ea1bc3f | |||
| 472e7d0992 | |||
| d02dc54141 | |||
| d84f424400 | |||
| ac8187963d | |||
| 443dee0b22 | |||
| cd350c1140 | |||
| 7e4bce9178 | |||
| 43c0c821ba | |||
| f1d849ef88 | |||
| 0cfc6bcd36 | |||
| c2fac5f055 | |||
| df0e414420 | |||
| a0f7a52bcb | |||
| fd783ac7d7 | |||
| 93813e9ef2 | |||
| f7083f02bd | |||
| ae1a8419ca | |||
| 4d766ff60f | |||
| 67142f7e35 | |||
| 0c2d0ec445 | |||
| fa5c8b6eef | |||
| 9021e2ad92 | |||
| 5d338c2642 | |||
| aac8a789e2 | |||
| a585bffc6f | |||
| c514039729 | |||
| 091d838c62 | |||
| e3c98db922 | |||
| 75e349822c | |||
| 1059584d2f | |||
| 67ea540639 | |||
| 84f0965559 | |||
| 40aa20bd86 | |||
| 0f10b9adb5 | |||
| 623cb3d776 | |||
| caebbb1ebe | |||
| 0121307400 | |||
| 4e44f678d3 | |||
| 6c6fe80ec8 | |||
| 5847b3f3b8 | |||
| da37299531 | |||
| 54b249ee5c | |||
| 8ff77c20e9 | |||
| a4120c0635 | |||
| 01b542ef8f | |||
| f2e4c27bce | |||
| c7c56d8631 | |||
| d64c172f04 | |||
| dda7538332 | |||
| a5fbb0fd60 | |||
| c8bb6d7f6c | |||
| 86d3104ed5 | |||
| 975b84cb37 | |||
| eee341265d | |||
| bb901a2973 | |||
| 37fb080042 | |||
| 58cb69e076 | |||
| e876d8fde0 | |||
| 756c81e6b3 | |||
| 338c59ae45 | |||
| 7738153538 | |||
| 4063e658a9 | |||
| 707493b804 | |||
| 7168981856 | |||
| 262a01b30c | |||
| 0eef651409 | |||
| 2f530d8281 | |||
| 4102e9b371 | |||
| 6e47ca24bc | |||
| cdca177ffb | |||
| 3ed6edcf23 | |||
| b0559f74fc | |||
| d5d399002b | |||
| e9f0b59d05 | |||
| 7620c1ffe5 | |||
| 0591aa5fe3 | |||
| abcd3b182c | |||
| 63481f5776 | |||
| 66f0ffcdc6 | |||
| 7a68bf4f77 | |||
| 8d5c8182e3 | |||
| 1608904e7c | |||
| 159cf9b382 | |||
| c47b003a78 | |||
| b579c0b669 | |||
| 83d98f7813 | |||
| 9934bb5386 | |||
| a197001dc8 | |||
| 37827bdfdc | |||
| 8b8aa98088 | |||
| f1a158dbaf | |||
| f9388f3531 | |||
| b02e138e32 | |||
| 362fb03be8 | |||
| f0cb7a24ba | |||
| 964a3b67cb | |||
| 6a368b845b | |||
| a780ef4a47 | |||
| 22158cbc61 | |||
| 2f81805f51 | |||
| b512ae6335 | |||
| fac25a2062 | |||
| d8cf2104fb | |||
| a1b4f7b8e5 | |||
| 022309d870 | |||
| 3142c9d25d | |||
| eb2fa8ced4 | |||
| 5095b4be7d | |||
| f2c0bb67de | |||
| 48e46004d6 |
@@ -0,0 +1,399 @@
|
||||
---
|
||||
name: multi-version-compliance
|
||||
description: >
|
||||
Apply or review multi-version support compliance for first-party Nx
|
||||
plugins. Primary entry point: a Linear task ID (NXC-XXXX) from the
|
||||
"Multi-version supported across plugins" milestone — the task carries the
|
||||
resolved support window, findings, and "Needs human decision" items. Falls
|
||||
back to self-discovery when no task exists. Use when asked to "fix
|
||||
multi-version compliance for @nx/X", "do NXC-XXXX", "review this
|
||||
compliance PR", or when working on a branch / PR titled "multi-version
|
||||
support compliance for @nx/X". Covers the canonical shape
|
||||
(assertSupportedPackageVersion, all-generators-enforce-floor.spec.ts,
|
||||
peer dep alignment, requires-gate auditing, user-pin preservation,
|
||||
executor / inferred-plugin feature gating).
|
||||
argument-hint: '[<NXC-XXXX> | @nx/<plugin> | review #<PR>]'
|
||||
allowed-tools: Bash, Read, Edit, Write, Glob, Grep, Agent, mcp__linear-server__get_issue, mcp__linear-server__list_comments, mcp__linear-server__get_milestone, mcp__linear-server__list_issues
|
||||
---
|
||||
|
||||
# Multi-version compliance for Nx plugins
|
||||
|
||||
## What this is
|
||||
|
||||
The `nx migrate --first-party-only` flag lets users upgrade Nx without
|
||||
dragging the managed third-party ecosystems (Angular, Cypress, Playwright,
|
||||
Jest, Vitest, ESLint, etc.) along. For that to be safe, every first-party
|
||||
plugin must keep working across its declared support window — not silently
|
||||
fall through to the latest install constants on older workspaces, not
|
||||
silently break on newer ones.
|
||||
|
||||
**Source-of-truth split:**
|
||||
|
||||
| Source | Owns |
|
||||
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Linear milestone "Multi-version supported across plugins" (project NXC-4072) | What's wrong per plugin, the resolved support window, open human decisions. Per-plugin tasks NXC-4381..NXC-4410 (P1–P29). |
|
||||
| This skill | How to implement the canonical shape, code-level anti-patterns, gotchas, findings doc shape (no-task case). |
|
||||
|
||||
The skill is the gap-closer: it accepts a Linear task, parses it, drives
|
||||
the fix. When no task exists for the plugin, fix mode runs discovery in
|
||||
Phase 1–2 and produces a findings doc that mirrors a Linear task body —
|
||||
so the user can file it as a new task before proceeding.
|
||||
|
||||
**Reference PRs (the canonical shape):**
|
||||
|
||||
- `#35587` — `@nx/angular` — merged. Set the precedent. Introduced
|
||||
`throwForUnsupportedVersion`.
|
||||
- `#35642` — `@nx/playwright` — merged. Generalized the shared helpers
|
||||
into `@nx/devkit/internal`. Established executor / runtime feature-
|
||||
gating.
|
||||
- `#35670` — `@nx/cypress` — merged. Added `excludeGenerators` to the
|
||||
parameterized test helper.
|
||||
- `#35671` — `@nx/vitest` — open at time of writing. Demonstrates
|
||||
"drop phantom peer-range claim" and "declared floor < effective floor"
|
||||
patterns.
|
||||
|
||||
Before citing any PR by number, verify state — these go stale:
|
||||
`gh pr view <N> --repo nrwl/nx --json state`. Verify any unmerged PR's
|
||||
contents via `gh pr diff <N> --repo nrwl/nx`.
|
||||
|
||||
## Entry points
|
||||
|
||||
| Invocation | Mode | Behavior |
|
||||
| ----------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `multi-version-compliance <NXC-XXXX>` | Fix (primary) | Fetch task, surface findings + decisions in Phase 2, wait for user OK before Phase 3 edits. |
|
||||
| `multi-version-compliance` (no arg) | Ask for task ID | Prompt for NXC-XXXX. |
|
||||
| `multi-version-compliance @nx/<plugin>` (bare plugin) | Fix (task lookup) | Look up the per-plugin task in milestone NXC-4072. If found, confirm with user and enter fix mode. If not found, run discovery in Phase 1–2 (rubric against code), present findings, suggest filing as a new task before any edits. |
|
||||
| `multi-version-compliance review #<N>` | Review | Fetch PR, derive Linear task from branch name if possible, compare diff vs. task findings (or run pure code-level review if no task). |
|
||||
|
||||
**Stop-after-Phase-2 (audit-equivalent):** if you want findings without
|
||||
edits, decline to approve at the end of Phase 2. The skill stops, no
|
||||
branch, no commits.
|
||||
|
||||
**On a branch matching `nxc-NNNN` with no explicit arg:** before
|
||||
asking the user, suggest "Use NXC-NNNN?" inferred from the branch name.
|
||||
|
||||
## Linear-fetching protocol
|
||||
|
||||
Before any code-level work in Linear-driven mode, the skill MUST:
|
||||
|
||||
1. **Check Linear MCP availability.** If `mcp__linear-server__get_issue`
|
||||
isn't available (MCP server not installed / not connected), tell the
|
||||
user and fall through to the no-task discovery path (fix mode Phase 1
|
||||
step 2). Don't pretend to fetch.
|
||||
2. **Fetch the task.** `mcp__linear-server__get_issue id="NXC-XXXX"`.
|
||||
If the call errors (invalid ID, network), halt and ask the user to
|
||||
verify the ID.
|
||||
3. **Verify shape.** Confirm:
|
||||
- Title matches `[multi-version][P##] \`@nx/<plugin>\` — multi-version support compliance`(per-plugin) or`[multi-version][W#] ...` (cross-cutting). If the pattern doesn't match, halt and ask the user to confirm this is the right task.
|
||||
- Status. `Done` → ask whether re-audit or follow-up. `Canceled` → halt and ask.
|
||||
4. **Read description sections.** Every per-plugin task has:
|
||||
- **Plugin:** — path, upstream support, peerDep declarations, per-major install map, paired secondaries.
|
||||
- **Needs human decision** — open items blocking implementation.
|
||||
- **Findings** — `(high|medium|low)` items with `[file:line]` and a suggested fix per item.
|
||||
- **Verification checklist** — Sections A (Support window declarations) / B (Generator inputs) / C (Generator outputs) / D (Migrations) / E (Runtime) / F (Out-of-window UX).
|
||||
5. **Fetch comments.** `mcp__linear-server__list_comments issueId="..."`.
|
||||
Audits attached as files / linked uploads may carry additional
|
||||
context.
|
||||
6. **Surface "Needs human decision" as a batch.** Restate every decision
|
||||
item in chat. The user can resolve all, defer some, or override.
|
||||
Block until the user has acknowledged the set — don't proceed silently.
|
||||
7. **Translate findings → code changes.** Map each finding to a canonical
|
||||
pattern in `references/canonical-shape.md`. The Linear task's
|
||||
suggested fix is the authoritative scope; the skill verifies it
|
||||
conforms to the canonical shape and flags any deviation.
|
||||
8. **Run the A–F checklist** against the final code state. The task's
|
||||
checklist is the agreed scope. The skill verifies code-level
|
||||
conformance.
|
||||
|
||||
**Default to the task's resolved support window.** Don't re-derive it
|
||||
from code unless the user explicitly overrides. If the user overrides:
|
||||
restate the new window and confirm before applying.
|
||||
|
||||
**Don't expand scope beyond the task's Findings without asking.** If you
|
||||
spot a new issue mid-fix: stop, present it, ask whether to (a) add it to
|
||||
this PR, (b) defer as a follow-up, or (c) update the Linear task as a
|
||||
comment.
|
||||
|
||||
## Mode workflows
|
||||
|
||||
### Fix mode (primary)
|
||||
|
||||
**Phase 1 — Read.**
|
||||
|
||||
1. If a Linear task ID was provided, fetch it per the Linear-fetching
|
||||
protocol. If only a plugin name was provided, look up the per-plugin
|
||||
task in milestone NXC-4072.
|
||||
2. **No task case.** If no task exists for this plugin: run discovery
|
||||
instead — apply the policy ladder for the support window
|
||||
(Rule 1: upstream LTS for Angular/React/ESLint/Next/Expo; Rule 2:
|
||||
N & N-1; widen to existing supported set if larger), inventory the
|
||||
plugin's code against the A–F rubric, find the effective floor by
|
||||
walking imports, classify all results as new findings. The skill is
|
||||
producing audit-quality output for a plugin that wasn't ticketed.
|
||||
3. If on a branch matching `nxc-NNNN`, read recent commits to understand
|
||||
prior scope decisions.
|
||||
4. Read `references/canonical-shape.md` and `references/anti-patterns.md`.
|
||||
|
||||
**Phase 2 — Align.**
|
||||
|
||||
5. **(task case)** Surface every "Needs human decision" item from the
|
||||
task as a batch. Wait for resolutions.
|
||||
6. **(task case)** Restate the Findings list with severity tags. Confirm
|
||||
scope.
|
||||
7. **(no-task case)** Surface findings discovered from the rubric
|
||||
inventory + decisions the rubric surfaces (floor raise/drop, peer
|
||||
declarations, optional-vs-required peer, one-sided gates, etc.).
|
||||
Suggest filing them as a new Linear task in milestone NXC-4072
|
||||
before proceeding to Phase 3.
|
||||
8. **User OK gate.** Wait for explicit "proceed" before Phase 3.
|
||||
Declining stops the skill — no branch, no edits. (This is the
|
||||
audit-equivalent.)
|
||||
|
||||
**Phase 3 — Implement** (per `canonical-shape.md`).
|
||||
|
||||
9. Branch from `master` if needed using the repo's `nxc-NNNN` convention.
|
||||
10. Order: any shared-helper extension lands first; plugin changes land
|
||||
after. Commit/PR titling defers to the user's conventions.
|
||||
11. For each Finding category, apply the canonical pattern:
|
||||
- Section A → peer ranges + version map + install constants. Every
|
||||
third-party package the plugin **invokes at runtime** (TS import,
|
||||
executor spawning the CLI binary, or inferred-plugin emitting a
|
||||
target with `command: '<bin>'`) gets a peer entry. Default to
|
||||
`optional: true` via `peerDependenciesMeta` for gated surfaces
|
||||
(executor opt-in, inferred plugin gated on config file presence).
|
||||
Non-optional peers are reserved for packages every workspace using
|
||||
the plugin needs.
|
||||
- Section B → generator entry asserts, `keepExistingVersions`,
|
||||
fresh-install branch.
|
||||
- Section C → templates, schema stubs with runtime throws,
|
||||
version-map coverage.
|
||||
- Section D → `requires` gates per package per AND-semantics; split
|
||||
mixed entries; retain intentional pre-floor entries. **Default to
|
||||
bilateral bounds** (`>=N <M`) when writing a cross-major gate.
|
||||
One-sided gates (`<N` with no lower, `>=N` with no upper) need a
|
||||
justified reason (legacy cleanup, undefined source, v0→v1 bridge)
|
||||
— record the reason in the findings doc or as a code comment.
|
||||
- Section E → executor and inferred-plugin feature gates.
|
||||
- Section F → below-floor throw via shared util.
|
||||
- **Cross-cutting:** if the fix changes runtime behavior, update any
|
||||
in-codebase docs (`astro-docs/`, `docs/`, inline `.md`) that
|
||||
describe the changed behavior. Docs that contradict the code are a
|
||||
correctness bug, not a PR-body concern.
|
||||
12. If during implementation you spot something not in the task's
|
||||
Findings: stop, surface it, ask whether to (a) add to this PR, (b)
|
||||
defer as a follow-up, or (c) update the Linear task as a comment.
|
||||
|
||||
**Phase 4 — Tests** (same commit as Phase 3 usually).
|
||||
|
||||
13. Add `all-generators-enforce-floor.spec.ts` — parameterized via
|
||||
`assertGeneratorsEnforceVersionFloor`. This exercises every
|
||||
generator's floor assert and is the high-value spec.
|
||||
14. Footgun: assert calls must be in place in every generator BEFORE
|
||||
running the parameterized spec, or every untouched generator fails
|
||||
and you'll restart.
|
||||
15. Optional: a per-plugin `assert-supported-<pkg>-version.spec.ts`
|
||||
with the 5 canonical cases. The shared `assertSupportedPackageVersion`
|
||||
already has full coverage in devkit, so this is mostly symmetry
|
||||
across the PR series — skip unless the user asks.
|
||||
|
||||
**Phase 5 — Verify locally.**
|
||||
|
||||
16. `npx nx test <plugin> --testPathPattern="all-generators-enforce-floor"`
|
||||
(add `assert-supported-` if you added the optional wrapper spec).
|
||||
17. `npx nx test <plugin> --testPathPattern="<modified-generator>"` per
|
||||
touched generator.
|
||||
18. `npx nx format`.
|
||||
|
||||
**Phase 6 — Hand off.** Code changes complete. The user drives
|
||||
commit/push/PR per their own conventions (loaded globally from
|
||||
`~/.claude/memory/workflow/git/`). This skill does not enforce PR title,
|
||||
body, commit shape, or related-issues format.
|
||||
|
||||
### Review mode
|
||||
|
||||
1. **Fetch PR.** `gh pr view <N> --repo nrwl/nx` and
|
||||
`gh pr diff <N> --repo nrwl/nx`. For a local branch:
|
||||
`git diff master...HEAD`.
|
||||
2. **Derive the Linear task.** Branch name `nxc-NNNN` → `NXC-NNNN`. If
|
||||
no match: ask the user.
|
||||
3. **Fetch the task** (if derivable). Compare diff vs. task Findings:
|
||||
every Finding addressed; nothing extra without justification. Flag
|
||||
scope drift.
|
||||
**If no task and the user has none:** skip task-comparison; run pure
|
||||
code-level review against `canonical-shape.md` and `anti-patterns.md`.
|
||||
4. **Code-level checks.** Run the "Code-level verification (review-mode
|
||||
lens)" section of `canonical-shape.md`. Cross-reference
|
||||
`anti-patterns.md`. For each finding, anchor at `file:line` and cite
|
||||
which reference PR / file demonstrates the correct pattern.
|
||||
**Scope:** code, configs, migrations, and in-codebase docs that claim
|
||||
runtime behavior. NOT PR title / body / commit shape — those defer to
|
||||
the user's PR conventions.
|
||||
5. **Classify each finding.**
|
||||
- **Only two inline categories:** `[blocker]` and `[non-blocker]`. No
|
||||
"open question," "ask," or other inline tags. Questions for the
|
||||
author surface in the closing "Open questions for author" block,
|
||||
drawn from non-blocker findings — list each question once.
|
||||
- **Severity is independent of scope-drift.** A finding can be both a
|
||||
blocker AND not in the Linear task. Flag it as a blocker in the
|
||||
code-level section AND list it under "in PR but not in Linear task"
|
||||
in scope drift. Don't hedge with "in this PR or follow-up?" — if
|
||||
it's a blocker, the answer is "this PR."
|
||||
- **Group related non-blockers.** When multiple non-blockers describe
|
||||
symptoms of one blocker (e.g., five symptoms of a single
|
||||
`version-utils.ts` duplication), list them as sub-bullets under
|
||||
the blocker with "(resolved when §X is fixed)" rather than as N
|
||||
separate top-level non-blockers.
|
||||
- **Be terse on passes.** A section with no findings gets a single
|
||||
summary line ("Pass — all 7 generator entries assert at first
|
||||
statement"), not a per-file enumeration. Detail is reserved for
|
||||
blockers and non-blockers. The reviewer's audience skims for
|
||||
actionable items; passing checks should not eat reading budget.
|
||||
6. **Output.** Markdown checklist of blockers / non-blockers anchored at
|
||||
`file:line`, followed by the structured verdict block from
|
||||
`canonical-shape.md` §"Verdict template". The verdict block is the
|
||||
skimmable index — produce it, don't substitute a free-form prose
|
||||
summary. Do not post via `gh pr review` unless the user explicitly
|
||||
asks.
|
||||
|
||||
## Which references to load (context hygiene)
|
||||
|
||||
| Mode | Required | Optional |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| Fix | `canonical-shape.md`, `anti-patterns.md` | `gotchas.md` (effective floor, ecosystem lockstep, cypress inline tree), `examples.md` (when copying a pattern) |
|
||||
| Review | `anti-patterns.md`, `canonical-shape.md` (especially the "Code-level verification" section) | `gotchas.md` (cross-plugin coordination, lockstep), `examples.md` (when citing) |
|
||||
| "What is compliance?" answer | none | answer from SKILL.md alone |
|
||||
|
||||
References are ~100–500 lines each. Don't pull all of them just because
|
||||
you're invoked. Match the load to the mode.
|
||||
|
||||
## Critical rules (apply in every mode)
|
||||
|
||||
1. **Linear task is the source of truth for scope** (ratified decisions
|
||||
and Findings).
|
||||
- (a) Don't produce a parallel scope document. The task IS the scope.
|
||||
Fix mode runs against the task as input — drift checks, new
|
||||
findings, and decisions feed back to the task (via comments or as
|
||||
deferred items), not into a competing source of truth.
|
||||
- (b) Don't expand a fix beyond the task's Findings without
|
||||
surfacing the new issue.
|
||||
- (c) Don't second-guess the task's resolved support window without
|
||||
an explicit user override.
|
||||
2. **Do not create or duplicate shared helpers.** They live in
|
||||
`@nx/devkit/internal` (`assertSupportedPackageVersion`,
|
||||
`getInstalledPackageVersion`, `getDeclaredPackageVersion`,
|
||||
`throwForUnsupportedVersion`, `normalizeSemver`, `isNonSemverDistTag`)
|
||||
and `@nx/devkit/internal-testing-utils`
|
||||
(`assertGeneratorsEnforceVersionFloor`). Reject any local
|
||||
re-implementation (`cleanVersion`, `getInstalled<Pkg>VersionRuntime`,
|
||||
private `throwBelowFloor`, etc.). See `canonical-shape.md`.
|
||||
3. **Above-ceiling is silent fallthrough.** Do not warn, do not throw,
|
||||
do not branch. Reject `throwAboveWindow`, `warnAboveCeiling`,
|
||||
`versions()` with `switch + throw default:`. The only throw is below
|
||||
the declared floor.
|
||||
4. **`keepExistingVersions: true` is for generators only.** Migration
|
||||
generators (`src/migrations/`) are exempt — their job is to bump.
|
||||
Do not flag missing flags in migration code.
|
||||
5. **Floor assert is the first statement in the function doing the
|
||||
actual work.** Wrapper/internal split (cypress, playwright): in
|
||||
`*Internal`. Single-function generators (angular): in the function
|
||||
itself. Not conditional, not inside an install branch, not after a
|
||||
tree read.
|
||||
6. **Phase 1–2 never writes, never branches.** Discovery, finding
|
||||
classification, and decision-surfacing happen on the current branch
|
||||
with no edits. Any working artifact (e.g., a findings doc for a
|
||||
no-task case, multi-plugin scratch notes) goes in `tmp/` (gitignored)
|
||||
and stays uncommitted. No `TRIAGE-REPORT.md` / `AUDIT.md` at repo
|
||||
root. Branch creation and edits start at Phase 3, after the user OK.
|
||||
7. **PR / commit conventions are out of scope.** Title format, body shape,
|
||||
commit-message structure, related-issues handling, push flags, etc.
|
||||
are governed by the user's global memory (`pr-creation-shorthand.md`,
|
||||
`push-conventions.md`, `explain-before-committing.md`,
|
||||
`chore-not-fix-non-prod.md`). Don't enforce or flag these from this
|
||||
skill — defer to whatever the user's conventions resolve to at PR time.
|
||||
|
||||
## Findings doc template (Phase 2 output, used when no Linear task exists)
|
||||
|
||||
When fix mode hits the no-task case (Phase 1 step 2), produce
|
||||
`tmp/<plugin>-findings.md` shaped to mirror a Linear task body so the
|
||||
user can file it as a new task in milestone NXC-4072 before proceeding
|
||||
to Phase 3.
|
||||
|
||||
For plugins managing multiple primary packages, repeat the install-map
|
||||
/ decisions / findings bullets per primary.
|
||||
|
||||
```md
|
||||
# @nx/<plugin> — multi-version support compliance findings
|
||||
|
||||
> No Linear task in milestone NXC-4072. This doc is filing-ready —
|
||||
> create the task with this body before proceeding to fix.
|
||||
|
||||
## Plugin
|
||||
|
||||
- Path: packages/<plugin>
|
||||
- Upstream support: <official policy if any, else "no formal LTS">
|
||||
- peerDep declarations: <list>
|
||||
- Per-major install (`<file>` branches on installed `<package>` major):
|
||||
- v<N-1>: <constants>
|
||||
- v<N>: <constants> (default)
|
||||
- Paired secondaries: <list of ecosystem-locked siblings>
|
||||
|
||||
## Needs human decision
|
||||
|
||||
1. <decision 1 — e.g., raise floor to vN.0.0 vs keep current>
|
||||
2. <decision 2 — e.g., drop ^1.0.0 from peer (no v1 install lane)>
|
||||
|
||||
## Findings
|
||||
|
||||
- **(high) <one-line summary>** [file:line]
|
||||
_Suggested fix_: <one-line>
|
||||
- **(medium) ...**
|
||||
- **(low) ...**
|
||||
|
||||
## Verification checklist (A–F)
|
||||
|
||||
### A. Support window declarations
|
||||
|
||||
- [ ] peerDep ranges match the support window
|
||||
- [ ] Version map / runtime branching covers every supported major
|
||||
- [ ] Every third-party package the plugin **invokes at runtime** has a peerDep entry. "Invokes" = TS import/`require` OR executor spawns its CLI binary OR inferred plugin emits a target whose `command` invokes its CLI (look for `externalDependencies: ['<pkg>']` in emitted target inputs). Packages the generator installs for the user to consume independently (ESLint plugins loaded by the user's eslintrc, `@types/*`) don't need peer-declaration.
|
||||
- [ ] Peers needed only when a user opts into a specific surface (executor opt-in, inferred plugin gated on config file presence, opt-in preset) are declared **optional** via `peerDependenciesMeta: { "<pkg>": { "optional": true } }`. Required-non-optional peers are reserved for packages every workspace using the plugin needs.
|
||||
|
||||
### B. Generator inputs
|
||||
|
||||
- [ ] Generators don't overwrite installed third-party versions
|
||||
- [ ] `addDependenciesToPackageJson` passes `keepExistingVersions=true` or branches on detected version
|
||||
- [ ] Fresh-install path installs the latest supported version
|
||||
|
||||
### C. Generator outputs
|
||||
|
||||
- [ ] Templates compile and run on every supported version
|
||||
- [ ] Generated `project.json` target shape valid on every major
|
||||
- [ ] Default option values valid on every major
|
||||
- [ ] Version map covers every managed third-party dep — no gaps
|
||||
- [ ] Schema accepts union of options; runtime throws when inapplicable
|
||||
|
||||
### D. Migrations (migrations.json + packageJsonUpdates)
|
||||
|
||||
- [ ] Cross-major `packageJsonUpdates` declare `requires` per bumped package
|
||||
- [ ] `requires` ranges are bilateral (`>=N <M`) by default. One-sided ranges (`<N` with no lower, `>=N` with no upper) are intentional (legacy cleanup, undefined source major, v0→v1 bridge) — flagged in "Needs human decision" or noted in the Findings.
|
||||
- [ ] Every migration declares `requires` against the touched package
|
||||
- [ ] Nx-only migrations have no third-party `requires`
|
||||
- [ ] No silent gap in `packageJsonUpdates` across the support window
|
||||
|
||||
### E. Runtime
|
||||
|
||||
- [ ] Executors branch on installed version where behavior diverges
|
||||
- [ ] Inferred plugin (createNodes/V2) parses configs across every major
|
||||
|
||||
### F. Out-of-window UX
|
||||
|
||||
- [ ] Below-floor: throws via shared util naming package + installed + floor; no silent fall-through
|
||||
|
||||
## Out-of-scope (deferred follow-ups)
|
||||
|
||||
- <e.g., consolidate ... across plugins — separate PR>
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
See "Which references to load" near the top. Don't pull all of them.
|
||||
@@ -0,0 +1,315 @@
|
||||
# Anti-patterns
|
||||
|
||||
Patterns to reject in your own work and flag in reviews. Each entry: what it looks like, why it's wrong, what to do instead, and a reference.
|
||||
|
||||
## 1. Local re-implementation of the shared helpers
|
||||
|
||||
**Looks like:** A new file in the plugin defining any of:
|
||||
|
||||
- `throwBelowFloor` / `throwAboveWindow` / `assertVersion` / `checkMinimumVersion` — duplicates `throwForUnsupportedVersion` / `assertSupportedPackageVersion`.
|
||||
- `function cleanVersion(v) { return clean(v) ?? coerce(v)?.version ?? undefined; }` — duplicates `normalizeSemver`.
|
||||
- `getInstalledRsbuildVersionRuntime` / `getInstalled<X>FromFs` reading `require('<pkg>/package.json')` directly — duplicates `getInstalledPackageVersion`.
|
||||
- Inline `clean(declared) ?? coerce(declared)` chain at a generator entry point — duplicates `getDeclaredPackageVersion`.
|
||||
|
||||
**Why wrong:** The shared helpers in `@nx/devkit/internal` and `@nx/devkit/internal-testing-utils` already exist. Duplicates create drift — one will get the `latest`/`next` handling, the other won't; one will use `getNxRequirePaths()` for pnpm-strict resolution, the other won't.
|
||||
|
||||
**Do instead:** Call `assertSupportedPackageVersion(tree, pkg, floor)` via the per-plugin wrapper (`assertSupportedXVersion`). For executor-side reads: `getInstalledPackageVersion(pkg)`. For tree-side normalization: `getDeclaredPackageVersion(tree, pkg, latestKnown)`. For raw semver cleaning: `normalizeSemver(v)`.
|
||||
|
||||
**Reference:** Compliant — `packages/cypress/src/utils/assert-supported-cypress-version.ts` (7 lines). Concrete anti-pattern — PR `#35676` introduces `function cleanVersion` (`packages/rsbuild/src/utils/versions.ts`) and `getInstalledRsbuildVersionRuntime` reading `require('@rsbuild/core/package.json')`. Both should call the shared helpers instead.
|
||||
|
||||
## 2. Above-ceiling throw or warn
|
||||
|
||||
**Looks like:** `if (major > maxKnown) throw …`, `if (major > maxKnown) logger.warn …`, `versions()` with a `switch + throw default:`, any `warnAboveCeiling` / `throwAboveWindow` helper.
|
||||
|
||||
**Why wrong:** Explicit policy. Above-ceiling falls through silently to `latestVersions`. Throwing breaks users on newer versions of third-party packages, which is the opposite of the initiative's intent. The angular reference implementation does not warn or branch above the highest known major, and every subsequent plugin compliance PR follows that convention.
|
||||
|
||||
**Do instead:** `versionMap[major] ?? latestVersions`. Below-floor is caught by the generator-level assert; the `versions()` function is just a lookup.
|
||||
|
||||
**Reference:** Compliant — `packages/cypress/src/utils/versions.ts` after `#35670` rewrite. Anti-pattern (before fix) — same file before `#35670` had `switch + throw default:`.
|
||||
|
||||
## 3. Hardcoded third-party version in generator body
|
||||
|
||||
**Looks like:**
|
||||
|
||||
```ts
|
||||
addDependenciesToPackageJson(tree, {}, { rspack: '^1.1.10' });
|
||||
```
|
||||
|
||||
in a generator.
|
||||
|
||||
**Why wrong:** Bypasses the `versions(tree)` routing and the install-lane logic. New majors will not be picked up; older workspaces get the wrong version.
|
||||
|
||||
**Do instead:** Route through `versions(tree)` and reference the per-major entry. If you genuinely have a version that's the same across all majors, still put it in the map for consistency.
|
||||
|
||||
## 4. Init generator overwriting pinned versions
|
||||
|
||||
**Looks like:** `addDependenciesToPackageJson(tree, …, …, undefined, options.keepExistingVersions)` where the schema default is `false`. Or no fifth argument at all (defaults to `false`).
|
||||
|
||||
**Known-incomplete reference:** `@nx/angular`'s `init/schema.json` currently has `default: false` and `init.ts` passes `options.keepExistingVersions` directly — PR `#35587` did not fix this. The angular init generator therefore still has this bug. Flagging it in a non-angular compliance PR is correct; fixing it in passing during another angular PR is also appropriate.
|
||||
|
||||
**Why wrong:** Generators bump packages = the user's pinned version is silently overwritten on re-run. Bumping is the job of migrations, not generators.
|
||||
|
||||
**Do instead:** Pass `keepExistingVersions: true` (positional 5th arg) or `options.keepExistingVersions ?? true`. Flip the schema default to `true`.
|
||||
|
||||
**Reference:** Compliant — `packages/cypress/src/generators/init/init.ts` and `init/schema.json` after `#35670`. Anti-pattern — the same files before `#35670` had schema default `false`.
|
||||
|
||||
## 5. `requires` gate on an Nx-only migration
|
||||
|
||||
**Looks like:**
|
||||
|
||||
```json
|
||||
{
|
||||
"update-unit-test-runner-option": {
|
||||
"requires": { "@angular/core": ">=21.0.0" },
|
||||
"description": "Update 'vitest' unit test runner option to 'vitest-analog' in generator defaults."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
when the migration only writes to `nx.json`.
|
||||
|
||||
**Why wrong:** The migration applies regardless of third-party version — it's rewriting an Nx-owned generator default. The gate causes pre-v21 workspaces with the stale default to silently skip the migration and stay broken.
|
||||
|
||||
**Do instead:** Remove the `requires` entry entirely. Nx-only migrations have no third-party gate.
|
||||
|
||||
**Reference:** Anti-pattern (before fix) — `packages/angular/migrations.json` `update-unit-test-runner-option`. Fix — `#35587` removed the gate.
|
||||
|
||||
## 6. Cross-major `packageJsonUpdates` with no `requires`
|
||||
|
||||
**Looks like:**
|
||||
|
||||
```json
|
||||
{
|
||||
"21.3.0": {
|
||||
"packages": {
|
||||
"jest": { "version": "^30.0.0" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
with no `requires` gate, when this is a v29 → v30 bump.
|
||||
|
||||
**Why wrong:** The bump fires for every workspace — including workspaces already on v30 (idempotent best case) or workspaces on v28 or below (which would silently land on v30 without going through any v29 → v30 codemods). Source-major gate ensures the bump only fires for workspaces actually in the source range.
|
||||
|
||||
**Do instead:**
|
||||
|
||||
```json
|
||||
{
|
||||
"21.3.0": {
|
||||
"requires": { "jest": ">=29.0.0 <30.0.0" },
|
||||
"packages": { "jest": { "version": "^30.0.0" } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Reference:** Compliant — `packages/angular/migrations.json` MF entries after `#35587`. Anti-pattern examples on master at time of writing — `@nx/jest` `21.3.0`, `@nx/eslint` `20.7.0`, `@nx/vite` `20.5.0` (verify by inspecting each plugin's `migrations.json` for cross-major `packageJsonUpdates` entries lacking `requires`).
|
||||
|
||||
## 7. Gating ecosystem-locked siblings on the primary's major alone
|
||||
|
||||
**Looks like:** A migration that bumps `@ngrx/store` from v18 to v19 with `requires: { "@angular/core": ">=19.0.0" }` only — no `@ngrx/store` entry.
|
||||
|
||||
**Why wrong:** `@ngrx/store` is independent of `@angular/core` versioning. A workspace can be on `@angular/core: 19` without having `@ngrx/store: 18` (might not use ngrx at all, or might be on v17). Gating on `@angular/core` fires the migration in workspaces where it has nothing to do.
|
||||
|
||||
**Do instead:** Add the sibling to `requires`: `{ "@angular/core": ">=19.0.0", "@ngrx/store": ">=18.0.0 <19.0.0" }`. For Angular ecosystem siblings: `@angular/cli`, `@angular/ssr`, `@angular-devkit/build-angular` (v20+) are peer-locked via `@angular/core` and don't need their own gate. `@ngrx/*`, `@angular-eslint/*`, `zone.js`, `jest-preset-angular` are independent and do.
|
||||
|
||||
**How to verify pairing:** read the sibling package's `peerDependencies` at the version range being bumped from. If it pins the primary's major, the primary's `requires` covers it. If it doesn't, the sibling is independent and needs its own gate.
|
||||
|
||||
## 8. Peer dep claiming a major with no install branch (phantom claim)
|
||||
|
||||
**Looks like:**
|
||||
|
||||
```json
|
||||
{
|
||||
"peerDependencies": {
|
||||
"vitest": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
when `versions.ts` has no v1 entry and no `isVitestV1` branch anywhere.
|
||||
|
||||
**Why wrong:** The plugin advertises support for a version it doesn't honor. v1 workspaces silently fall through to v4 install constants.
|
||||
|
||||
**Do instead:** Drop the unsupported major from the peer. `vitest: "^2.0.0 || ^3.0.0 || ^4.0.0"`. If the support is desired, add the install lane.
|
||||
|
||||
**Reference:** Pattern demonstrated in open PR `#35671` (`@nx/vitest`) — drops `^1.0.0` from the `vitest` peer because there's no v1 install lane in the plugin's `versions.ts`. Inspect via `gh pr diff 35671 --repo nrwl/nx -- packages/vitest/package.json`. Verify state first.
|
||||
|
||||
**Related — drop EOL major (different reasoning, same action):** the major HAS an install lane but is EOL upstream (e.g., Storybook's official policy is "top 3 majors only"; v7 is EOL). Drop it from the peer because it's upstream-unsupported, not because the plugin doesn't honor it. Concrete example: NXC-4406 calls out dropping Storybook v7 from `@nx/storybook`'s peer per Storybook's top-3-majors policy.
|
||||
|
||||
## 8a. PeerDep range wider than the runtime dep pin
|
||||
|
||||
**Looks like:**
|
||||
|
||||
```json
|
||||
{
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typescript-eslint/parser": "^8.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The plugin's own runtime dep pins ^8, but the peer claims ^6/^7/^8.
|
||||
|
||||
**Why wrong:** Distinct from §8 — here the install lane exists (in `dependencies`), but the lane only ships one major. The peer is over-promising relative to what the plugin actually runs against. A workspace on ^6 will satisfy the peer but won't get a compatible runtime once `@typescript-eslint/parser@^8` resolves.
|
||||
|
||||
**Do instead:** Tighten the peer to the actually-supported runtime range, or widen the runtime dep + add the install/branch lanes for the additional majors.
|
||||
|
||||
**Reference:** NXC-4388 (`@nx/eslint-plugin`).
|
||||
|
||||
## 9. Top-level `require()` of an optional peer in an executor
|
||||
|
||||
**Looks like:**
|
||||
|
||||
```ts
|
||||
// at the top of executor.impl.ts
|
||||
const cypress = require('cypress');
|
||||
```
|
||||
|
||||
**Why wrong:** When cypress is absent (not yet installed, peer mismatch, etc.), the executor throws `MODULE_NOT_FOUND` at module load time, before any user-friendly error. Especially bad for deprecated executors that should fail with a deprecation message.
|
||||
|
||||
**Do instead:** `require` inside the function body, after the version detection / clear error.
|
||||
|
||||
## 10. Anything-but-`requires` as substitute for `requires`
|
||||
|
||||
**Looks like (variant A — `incompatibleWith` standing in):**
|
||||
|
||||
```json
|
||||
{
|
||||
"21.0.0-source-bump": {
|
||||
"incompatibleWith": { "@angular-devkit/build-angular": "<21.0.0" },
|
||||
"packages": { "...": { "version": "..." } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
to "gate" a bump to v21+ source workspaces.
|
||||
|
||||
**Looks like (variant B — runtime per-package guard):**
|
||||
|
||||
```ts
|
||||
// inside the migration function body
|
||||
const installed = getInstalledVersion('@typescript-eslint/parser');
|
||||
if (gte(installed, '8.0.0') && lt(installed, '8.13.0')) {
|
||||
// run the migration
|
||||
}
|
||||
return; // otherwise skip
|
||||
```
|
||||
|
||||
with no `requires` block on the migration entry in `migrations.json`.
|
||||
|
||||
**Why wrong:** Neither approach is a source-major gate.
|
||||
|
||||
- `incompatibleWith` blocks running on workspaces that have the matching version — it doesn't gate to a source-major range. A workspace on `@angular-devkit/build-angular: 22.0.0` will still pass the `incompatibleWith` check.
|
||||
- A runtime per-package guard runs the migration _body_ on every workspace and skips internally. The migration record still appears as "executed" to the migrate runner, and any side effects (logging, partial work) leak. The `nx migrate` runner uses `requires` as the source-major filter; bypassing it means the migration isn't filtered at the right layer.
|
||||
|
||||
**Do instead:** `requires: { "<pkg>": ">=N.0.0 <(N+1).0.0" }` — the actual source-major gate at the migration-entry level. Drop the in-body guard once the `requires` is in place.
|
||||
|
||||
**Reference:** Anti-pattern (variant B) — `@nx/eslint` `update-typescript-eslint-v8.13.0` (NXC-4387) has runtime `gte('8.0.0') + lt('8.13.0')` per-package guards but no `requires` block. `@nx/jest` similar with `incompatibleWith` (NXC-4391).
|
||||
|
||||
## 11. Naming a specific plugin in shared helper docstrings
|
||||
|
||||
**Looks like:** A JSDoc in `assert-generators-enforce-version-floor.ts` referencing `migrate-to-cypress-11` as the example use case for `excludeGenerators`.
|
||||
|
||||
**Why wrong:** The helper is shared across plugins. Naming one plugin in its docstring is leaky.
|
||||
|
||||
**Do instead:** Generic phrasing — "generators that must run below the floor by design (e.g., migrators that lift sub-floor workspaces onto a supported version)".
|
||||
|
||||
**Reference:** an early draft of `#35670`'s test helper had the plugin-specific JSDoc; the merged version uses generic phrasing.
|
||||
|
||||
## 12. Both schema `"default": true` AND `options.keepExistingVersions ?? true`
|
||||
|
||||
**Looks like:**
|
||||
|
||||
```json
|
||||
{ "keepExistingVersions": { "default": true } }
|
||||
```
|
||||
|
||||
combined with
|
||||
|
||||
```ts
|
||||
addDependenciesToPackageJson(tree, …, …, undefined, options.keepExistingVersions ?? true);
|
||||
```
|
||||
|
||||
**Why wrong:** Two sources of truth. Either the schema default does the job (and `options.keepExistingVersions` will always be `true`) or the `?? true` fallback handles it (and the schema default is redundant).
|
||||
|
||||
**Do instead:** Pick one. Schema default is sufficient when the call site uses `options.keepExistingVersions` directly. The `?? true` fallback is only needed if the schema can be bypassed (programmatic invocation without schema validation).
|
||||
|
||||
## 13. Manual `RegExp` matching in tests instead of substring `toThrow`
|
||||
|
||||
**Looks like:**
|
||||
|
||||
```ts
|
||||
.rejects.toThrow(new RegExp(`Unsupported version of \\\`${packageName}\\\` detected`));
|
||||
```
|
||||
|
||||
**Why wrong:** Escape bugs. The backtick and the `${}` are easy to get wrong. The shared helper uses substring matching for a reason.
|
||||
|
||||
**Do instead:**
|
||||
|
||||
```ts
|
||||
.rejects.toThrow(`Unsupported version of \`${packageName}\` detected`);
|
||||
```
|
||||
|
||||
**Reference:** see how `assertGeneratorsEnforceVersionFloor` itself does the match in `packages/nx/src/internal-testing-utils/assert-generators-enforce-version-floor.ts` (grep for `Unsupported version of`).
|
||||
|
||||
## 14. Validating only at install sites instead of generator entry
|
||||
|
||||
**Looks like:** A `if (installedVersion < floor) throw …` check guarding only the `addDependenciesToPackageJson` call inside a generator, while the rest of the generator runs unconditionally.
|
||||
|
||||
**Why wrong:** The generator may write configuration or templates incompatible with the sub-floor third-party version before reaching the install branch. The assert must be at the entry point so nothing else runs.
|
||||
|
||||
**Do instead:** `assertSupportedXVersion(tree)` as the first statement in the generator's working function (`*Internal` for plugins with the wrapper/internal split; the function itself for single-function generators). The install branch can then assume the floor is met.
|
||||
|
||||
## 15. Per-major version aliases alongside the bundle map
|
||||
|
||||
**Looks like:**
|
||||
|
||||
```ts
|
||||
export const vitestV4Version = '~4.1.0';
|
||||
export const vitestV3Version = '^3.0.0';
|
||||
export const vitestV2Version = '^2.1.8';
|
||||
export const vitestVersion = vitestV4Version;
|
||||
|
||||
export const vitestV4CoverageV8Version = '~4.1.0';
|
||||
export const vitestV3CoverageV8Version = '^3.0.5';
|
||||
// ...etc
|
||||
|
||||
const versionMap = {
|
||||
3: { vitestVersion: '^3.0.0', vitestCoverageV8Version: '^3.0.5' },
|
||||
4: { vitestVersion: '~4.1.0', vitestCoverageV8Version: '~4.1.0' },
|
||||
};
|
||||
```
|
||||
|
||||
**Why wrong:** The aliases (`vitestV3Version`, `vitestV3CoverageV8Version`, etc.) duplicate the `versionMap` entries. They drift over time — someone bumps the map but forgets the alias (or vice versa), and the plugin starts installing one version via generators and another via tests/runtime. Also: every dropped major (e.g., when raising the floor) becomes three or four delete lines instead of one map entry.
|
||||
|
||||
**Do instead:** Keep the bundle pattern — the per-major `versionMap` is the only place those values live. Stable (cross-version-identical) deps stay as top-level `export const`s; varying deps are accessed via `versions(tree).<key>` or directly from the top-level `latestVersions` bundle.
|
||||
|
||||
**Reference:** Open PR `#35671` initially carried `vitestV2Version` / `vitestV3Version` / `vitestV4Version` aliases. A follow-up commit (`chore(testing): adopt cypress version-resolution pattern in @nx/vitest`) dropped them in favor of the bundle pattern. Inspect via `gh pr view 35671 --repo nrwl/nx --json commits`.
|
||||
|
||||
## 16. Declared floor below the effective floor
|
||||
|
||||
**Looks like:** `peerDependencies` lists `"vitest": "^2.0.0 || ^3.0.0 || ^4.0.0"` and `versions.ts` has a `versionMap` entry for `2`, but somewhere in the plugin's executor / runtime / plugin code there's an import of a third-party API that only exists in v3+:
|
||||
|
||||
```ts
|
||||
// In a runtime helper used by the executor:
|
||||
import { getRelevantTestSpecifications } from 'vitest/node';
|
||||
// ^ This API only exists in vitest >= 3.0.0.
|
||||
```
|
||||
|
||||
**Why wrong:** A workspace on v2 will pass the floor assert (peer + versionMap claim support), then crash at runtime with `getRelevantTestSpecifications is not a function`. The peer is lying.
|
||||
|
||||
**Do instead:** Raise the floor to the lowest major where every called third-party API exists. Drop the now-unsupported entries from `versionMap`, `peerDependencies`, and the per-major version aliases (if any). The `assert-supported-<pkg>-version.spec.ts` sub-floor test now covers the dropped major.
|
||||
|
||||
**Reference:** Open PR `#35671`'s second commit (`fix(testing): drop vitest v2 support from @nx/vitest`) — originally proposed a v2 floor matching the lowest install lane, then raised to v3 after audit caught the `getRelevantTestSpecifications` usage. Inspect via `gh pr view 35671 --repo nrwl/nx --json commits`.
|
||||
|
||||
## 17. Creating a branch during Phase 1–2 (discovery / read-only)
|
||||
|
||||
**Looks like:** `git checkout -b <some-branch>` before the user has approved Phase 3 edits.
|
||||
|
||||
**Why wrong:** Phase 1–2 produces findings, not commits. Creating a branch creates pressure to commit something. Any working artifact (e.g., `tmp/<plugin>-findings.md` for the no-task case) goes in `tmp/` (gitignored) — for the user to read and scope from, not to commit.
|
||||
|
||||
**Do instead:** Run Phase 1–2 on the current branch (typically `master`). Output to `tmp/<plugin>-findings.md` if you wrote one. Branch creation belongs in Phase 3, after explicit user approval to proceed with edits.
|
||||
@@ -0,0 +1,646 @@
|
||||
# Canonical shape
|
||||
|
||||
What a compliant plugin looks like. Don't deviate without a documented reason.
|
||||
|
||||
The first half of this file ("How to write") describes the canonical structure
|
||||
you produce in fix mode. The tail section ("Code-level verification") is the
|
||||
review-mode lens — markers to look for in a diff.
|
||||
|
||||
## Shared helpers (already merged — use, don't duplicate)
|
||||
|
||||
### `@nx/devkit/internal`
|
||||
|
||||
Source: `packages/devkit/src/utils/version-floor.ts` and `packages/devkit/src/utils/installed-version.ts`.
|
||||
|
||||
```ts
|
||||
// version-floor.ts
|
||||
function throwForUnsupportedVersion(
|
||||
packageName: string,
|
||||
installedVersion: string,
|
||||
floor: string
|
||||
): never;
|
||||
|
||||
function assertSupportedPackageVersion(
|
||||
tree: Tree,
|
||||
packageName: string,
|
||||
minSupportedVersion: string
|
||||
): void;
|
||||
```
|
||||
|
||||
```ts
|
||||
// installed-version.ts
|
||||
function getInstalledPackageVersion(packageName: string): string | null;
|
||||
|
||||
function getDeclaredPackageVersion(
|
||||
tree: Tree,
|
||||
packageName: string,
|
||||
latestKnownVersion?: string
|
||||
): string | null;
|
||||
|
||||
const NON_SEMVER_DIST_TAGS = ['latest', 'next'] as const;
|
||||
function isNonSemverDistTag(version: string): version is NonSemverDistTag;
|
||||
function normalizeSemver(version: string): string | null;
|
||||
```
|
||||
|
||||
When to use which:
|
||||
|
||||
| Context | Function |
|
||||
| -------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| Generator entry (assert floor) | `assertSupportedPackageVersion(tree, pkg, floor)` |
|
||||
| Generator-time version branching | `getDeclaredPackageVersion(tree, pkg, latestKnownVersion)` |
|
||||
| Executor / runtime / preset | `getInstalledPackageVersion(pkg)` |
|
||||
| Anywhere | `isNonSemverDistTag`, `normalizeSemver` |
|
||||
| Never | `throwForUnsupportedVersion` directly — it's an implementation detail of the assert |
|
||||
|
||||
### `@nx/devkit/internal-testing-utils`
|
||||
|
||||
Source: `packages/nx/src/internal-testing-utils/assert-generators-enforce-version-floor.ts`.
|
||||
|
||||
```ts
|
||||
function assertGeneratorsEnforceVersionFloor(options: {
|
||||
packageRoot: string;
|
||||
packageName: string;
|
||||
subFloorVersion: string;
|
||||
excludeGenerators?: string[];
|
||||
}): void;
|
||||
```
|
||||
|
||||
Behavior: reads `generators.json` from `packageRoot`, iterates every entry, loads its factory, calls it against a tree with `{ [packageName]: subFloorVersion }` in `package.json`, expects a throw matching `Unsupported version of \`${packageName}\` detected`.
|
||||
|
||||
`excludeGenerators` is only for intentional sub-floor migrators (e.g., `migrate-to-cypress-11`). Comment the reason next to the array.
|
||||
|
||||
## Finding the existing floor (audit input)
|
||||
|
||||
When auditing a plugin you haven't touched before, the floor may not be declared in one place. Check, in order of authority:
|
||||
|
||||
1. **`minSupported<Pkg>Version` constant in `versions.ts`** — if it exists, that's the declared floor.
|
||||
2. **`peerDependencies` lowest range in `package.json`** — what the plugin advertises supporting.
|
||||
3. **Lowest major in `versionMap` / `backwardCompatibleVersions` / `supportedVersions`** — what the plugin has install lanes for.
|
||||
4. **Lowest `packageJsonUpdates` entry that touches the third-party package** — historical evidence of the supported range.
|
||||
5. **Highest API requirement in the plugin's own code (the _effective_ floor).** Grep for every `import` / `require` from the third-party package and identify which APIs are called. Cross-reference each against the third-party's changelog. The plugin's effective floor is the lowest major where **all** called APIs exist. **This trumps the declared floor** — if `versions.ts` claims v2 but the plugin imports an API only available in v3+, the declared floor is wrong.
|
||||
|
||||
These should agree. When they don't, the disagreement is the finding (phantom peer claim, drifted versionMap, declared floor below effective floor).
|
||||
|
||||
**Worked example:** During open PR `#35671`, the audit initially landed on a `v2.0.0` floor (matching the lowest install lane). Then a follow-up commit dropped the floor to `v3.0.0` after noticing the plugin's atomization code calls `getRelevantTestSpecifications`, which is a vitest v3+ API. Lesson: step 5 above is not optional. Always check what APIs the plugin's own runtime code uses — `versions()` having a v2 lane doesn't mean the plugin actually works on v2.
|
||||
|
||||
## The plugin wrapper (one per plugin)
|
||||
|
||||
Path: `packages/<plugin>/src/utils/assert-supported-<pkg>-version.ts`.
|
||||
|
||||
Two canonical shapes:
|
||||
|
||||
### Single-major floor (most plugins)
|
||||
|
||||
```ts
|
||||
import { type Tree } from '@nx/devkit';
|
||||
import { assertSupportedPackageVersion } from '@nx/devkit/internal';
|
||||
import { minSupportedCypressVersion } from './versions';
|
||||
|
||||
export function assertSupportedCypressVersion(tree: Tree): void {
|
||||
assertSupportedPackageVersion(tree, 'cypress', minSupportedCypressVersion);
|
||||
}
|
||||
```
|
||||
|
||||
Reference: `packages/cypress/src/utils/assert-supported-cypress-version.ts`, `packages/playwright/src/utils/assert-supported-playwright-version.ts`.
|
||||
|
||||
### Floor derived from supported-versions list (angular)
|
||||
|
||||
```ts
|
||||
import { type Tree } from '@nx/devkit';
|
||||
import { assertSupportedPackageVersion } from '@nx/devkit/internal';
|
||||
import { supportedVersions } from './backward-compatible-versions';
|
||||
|
||||
const minSupportedAngularMajor = Math.min(...supportedVersions);
|
||||
|
||||
export function assertSupportedAngularVersion(tree: Tree): void {
|
||||
assertSupportedPackageVersion(
|
||||
tree,
|
||||
'@angular/core',
|
||||
`${minSupportedAngularMajor}.0.0`
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Reference: `packages/angular/src/utils/assert-supported-angular-version.ts`.
|
||||
|
||||
Pick the shape that matches whether the plugin already has a `supportedVersions` list (angular does; cypress/playwright/vitest don't).
|
||||
|
||||
### Plugins managing multiple primary packages
|
||||
|
||||
`@nx/jest` manages `jest`, `ts-jest`, `@types/jest`. `@nx/eslint` manages `eslint`, `@typescript-eslint/parser`, `@typescript-eslint/eslint-plugin`, `eslint-config-prettier`. The canonical wrapper signature takes one package; with multiple, decisions are needed:
|
||||
|
||||
- **Gate on the primary only** when the others are peer-locked (e.g., angular's strategy with `@angular/core` covering `@angular/cli`, `@angular/ssr`, etc.). This is sufficient when the siblings' peer-deps tie them to the primary's major.
|
||||
- **Gate on each independently** when the siblings can be installed at any major regardless of the primary (typescript-eslint pair vs eslint; ts-jest vs jest). In that case, the wrapper makes multiple `assertSupportedPackageVersion` calls in sequence:
|
||||
|
||||
```ts
|
||||
export function assertSupportedJestVersion(tree: Tree): void {
|
||||
assertSupportedPackageVersion(tree, 'jest', minSupportedJestVersion);
|
||||
// ts-jest is independent — declare and assert it separately.
|
||||
assertSupportedPackageVersion(tree, 'ts-jest', minSupportedTsJestVersion);
|
||||
}
|
||||
```
|
||||
|
||||
When in doubt: read each sibling's `peerDependencies` block at the version range being supported. If it pins the primary, it's covered by the primary's gate. If it doesn't (or pins something else), it needs its own.
|
||||
|
||||
## Skip writing the install constant when the package is already detected
|
||||
|
||||
Init generators that add the third-party package to `package.json` should NOT overwrite an already-installed minor/patch. The `keepExistingVersions: true` flag handles this at the `addDependenciesToPackageJson` level. But for code paths that compute the version to write (e.g., picking the major-specific value from `versionMap`), the rule is the same: read what's installed first; only write the fresh-install constant when nothing is detected.
|
||||
|
||||
Reference: `packages/cypress/src/generators/init/init.ts` `updateDependencies` — calls `getInstalledCypressVersion(tree)` first, then routes through `versions(tree)` which short-circuits to existing-version paths. The `keepExistingVersions ?? true` flag at the `addDependenciesToPackageJson` call site is the final safety net.
|
||||
|
||||
## The versions module
|
||||
|
||||
Path: `packages/<plugin>/src/utils/versions.ts`.
|
||||
|
||||
### Required exports
|
||||
|
||||
```ts
|
||||
// Plain string, no caret. Used as the floor for assertSupportedPackageVersion.
|
||||
export const minSupportedCypressVersion = '13.0.0';
|
||||
|
||||
// Fresh-install constants — may be HIGHER than minSupported when the feature
|
||||
// surface at the floor is incomplete. Playwright peer is ^1.36.0 but
|
||||
// fresh-install is ^1.37.0 so blob reporter + merge-reports work out of the
|
||||
// box.
|
||||
export const playwrightVersion = '^1.37.0';
|
||||
|
||||
// Optional: feature-gate thresholds for runtime/executor code.
|
||||
export const minPlaywrightVersionForBlobReports = '1.37.0';
|
||||
```
|
||||
|
||||
### Stable deps stay top-level; per-major-varying deps go in the bundle
|
||||
|
||||
A plugin typically manages one primary package whose version map drives several siblings. Deps that vary per major go into a typed bundle; deps that are version-stable stay as plain `export const`s.
|
||||
|
||||
```ts
|
||||
// Stable across all supported majors of the primary → plain exports.
|
||||
export const viteVersion = '^8.0.0';
|
||||
export const jsdomVersion = '^27.1.0';
|
||||
export const vitePluginReactVersion = '^6.0.0';
|
||||
|
||||
// Vary with the primary's major → bundle.
|
||||
export const vitestVersion = '~4.1.0';
|
||||
export const vitestCoverageV8Version = '~4.1.0';
|
||||
export const vitestCoverageIstanbulVersion = '~4.1.0';
|
||||
|
||||
type VitestVersions = {
|
||||
vitestVersion: string;
|
||||
vitestCoverageV8Version: string;
|
||||
vitestCoverageIstanbulVersion: string;
|
||||
};
|
||||
|
||||
// latestVersions reuses the top-level exports so import { vitestVersion }
|
||||
// stays valid for the fresh-install path while versions(tree).vitestVersion
|
||||
// is the route-aware version.
|
||||
const latestVersions: VitestVersions = {
|
||||
vitestVersion,
|
||||
vitestCoverageV8Version,
|
||||
vitestCoverageIstanbulVersion,
|
||||
};
|
||||
|
||||
type CompatVersions = 3;
|
||||
const versionMap: Record<CompatVersions, VitestVersions> = {
|
||||
3: {
|
||||
vitestVersion: '^3.0.0',
|
||||
vitestCoverageV8Version: '^3.0.5',
|
||||
vitestCoverageIstanbulVersion: '^3.0.5',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Do not** keep per-major aliases like `vitestV3Version = '^3.0.0'` alongside the bundle — they duplicate the map entries and drift over time. See `anti-patterns.md` §15.
|
||||
|
||||
### The `versions(tree)` function
|
||||
|
||||
Falls through to latest on unknown majors — no `switch + throw default:`.
|
||||
|
||||
```ts
|
||||
export function versions(tree: Tree): VitestVersions {
|
||||
const installedVitestVersion = getInstalledVitestVersion(tree);
|
||||
if (!installedVitestVersion) {
|
||||
return latestVersions;
|
||||
}
|
||||
const vitestMajorVersion = major(installedVitestVersion);
|
||||
return versionMap[vitestMajorVersion as CompatVersions] ?? latestVersions;
|
||||
}
|
||||
```
|
||||
|
||||
### The `getInstalled<Pkg>Version(tree?)` helper
|
||||
|
||||
Optional `tree` parameter — with tree, reads declared from `package.json` (normalizing `latest`/`next` to the fresh-install constant); without tree, routes through the shared `getInstalledPackageVersion` from `@nx/devkit/internal` (FS resolution via `getNxRequirePaths()`).
|
||||
|
||||
```ts
|
||||
export function getInstalledVitestVersion(tree?: Tree): string | null {
|
||||
if (!tree) {
|
||||
return getInstalledPackageVersion('vitest');
|
||||
}
|
||||
|
||||
const installedVersion = getDependencyVersionFromPackageJson(tree, 'vitest');
|
||||
if (!installedVersion) {
|
||||
return null;
|
||||
}
|
||||
if (installedVersion === 'latest' || installedVersion === 'next') {
|
||||
return clean(vitestVersion) ?? coerce(vitestVersion)?.version ?? null;
|
||||
}
|
||||
return clean(installedVersion) ?? coerce(installedVersion)?.version ?? null;
|
||||
}
|
||||
|
||||
export function getInstalledVitestMajorVersion(tree?: Tree): number | null {
|
||||
const installedVitestVersion = getInstalledVitestVersion(tree);
|
||||
return installedVitestVersion ? major(installedVitestVersion) : null;
|
||||
}
|
||||
```
|
||||
|
||||
This is the cypress/playwright/vitest pattern. Reference: `packages/cypress/src/utils/versions.ts`.
|
||||
|
||||
## Generator entry points
|
||||
|
||||
The assert goes in the function that does the actual work — first statement of the function body, before any other tree access or sub-generator call. (The assert itself reads the tree, of course; the rule is that nothing else in the generator runs against an unsupported version.)
|
||||
|
||||
### Plugins with a `<gen>` / `<gen>Internal` split (cypress, playwright)
|
||||
|
||||
The public wrapper merges defaults and delegates. Assert lives in `*Internal`:
|
||||
|
||||
```ts
|
||||
// Public wrapper — no assert, just default merging.
|
||||
export async function cypressInitGenerator(tree: Tree, options: Schema) {
|
||||
return cypressInitGeneratorInternal(tree, { addPlugin: false, ...options });
|
||||
}
|
||||
|
||||
// Working function — assert is the first statement.
|
||||
export async function cypressInitGeneratorInternal(
|
||||
tree: Tree,
|
||||
options: Schema
|
||||
) {
|
||||
assertSupportedCypressVersion(tree);
|
||||
|
||||
updateProductionFileset(tree);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Reference: `packages/cypress/src/generators/init/init.ts`, `packages/playwright/src/generators/init/init.ts`.
|
||||
|
||||
### Plugins with a single-function generator (angular)
|
||||
|
||||
No wrapper/internal split. The function itself asserts:
|
||||
|
||||
```ts
|
||||
export async function angularInitGenerator(tree: Tree, options: Schema) {
|
||||
assertSupportedAngularVersion(tree);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Reference: `packages/angular/src/generators/init/init.ts`.
|
||||
|
||||
### Double-asserts are established convention, not an edge case
|
||||
|
||||
When `configurationGenerator` calls `initGenerator` internally, both call their respective `assertSupportedXVersion`. This is the angular precedent (29 `generators.json` entries → 58 assert call sites). The assert is idempotent (one tree read + one semver comparison) and the parameterized floor spec treats every entry point as independent — both must throw on sub-floor. Don't refactor away.
|
||||
|
||||
## User-pin preservation
|
||||
|
||||
### `addDependenciesToPackageJson` call sites
|
||||
|
||||
Every call from a generator (NOT a migration) must pass `keepExistingVersions: true` as the fifth positional argument or via the `?? true` pattern.
|
||||
|
||||
```ts
|
||||
// Pattern A — explicit at the call site
|
||||
addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
{ 'eslint-plugin-cypress': pkgVersions.eslintPluginCypressVersion },
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
|
||||
// Pattern B — driven by schema (init generators only)
|
||||
addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
devDependencies,
|
||||
undefined,
|
||||
options.keepExistingVersions ?? true
|
||||
);
|
||||
```
|
||||
|
||||
Reference: `packages/cypress/src/generators/init/init.ts`, `packages/cypress/src/utils/add-linter.ts`, `packages/angular/src/generators/add-linting/lib/add-angular-eslint-dependencies.ts`.
|
||||
|
||||
### init schema
|
||||
|
||||
```json
|
||||
{
|
||||
"keepExistingVersions": {
|
||||
"type": "boolean",
|
||||
"x-priority": "internal",
|
||||
"description": "Keep existing dependencies versions",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reference (on master): `packages/cypress/src/generators/init/schema.json`, `packages/playwright/src/generators/init/schema.json`. (`@nx/vitest` follows the same pattern in its open PR — verify via `gh pr diff 35671`.)
|
||||
|
||||
## Migrations.json gates
|
||||
|
||||
Three categories of migration:
|
||||
|
||||
| Category | Touches | `requires` |
|
||||
| -------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| Nx-only | `nx.json`, executor options, generator defaults | none |
|
||||
| Codemod | source files / config tied to a third-party major | `{ "<pkg>": ">=N <N+1" }` (or open upper bound for legacy-cleanup codemods) |
|
||||
| `packageJsonUpdates` cross-major | bumps `<pkg>` from major N to N+1 | `{ "<pkg>": ">=N.0.0 <(N+1).0.0" }` (source-major gate) |
|
||||
| `packageJsonUpdates` same-major | bumps minor/patch | none required |
|
||||
|
||||
### Sibling packages
|
||||
|
||||
Ecosystem-locked siblings whose peer-on-the-primary covers them: no separate `requires`. Examples in Angular: `@angular/cli`, `@angular/ssr`, `@angular-devkit/build-angular` (from v20+, NOT v19).
|
||||
|
||||
Independent siblings: each needs its own `requires` entry. Examples in Angular: `@ngrx/*`, `@angular-eslint/*`, `zone.js`, `jest-preset-angular`.
|
||||
|
||||
### Reference examples
|
||||
|
||||
- `packages/angular/migrations.json` `20.2.0-module-federation`, `22.2.0`, `22.6.0-module-federation` — Module Federation entries gating on `@module-federation/enhanced` source range. Added in `#35587`.
|
||||
- `@nx/vitest`'s `update-22-1-0` and `update-22-3-2` migrations gating on `vitest: ">=4.0.0"` (Vitest-4-specific AI-instructions) — pattern proposed in open PR `#35671`. Inspect via `gh pr diff 35671 --repo nrwl/nx -- packages/vitest/migrations.json`.
|
||||
- `packages/angular/migrations.json` `update-unit-test-runner-option` — Nx-only migration with the over-gating `@angular/core` `requires` **removed** in `#35587`.
|
||||
|
||||
## Test specs
|
||||
|
||||
### Parameterized floor spec (one per plugin)
|
||||
|
||||
Path: `packages/<plugin>/src/utils/all-generators-enforce-floor.spec.ts`.
|
||||
|
||||
```ts
|
||||
import { assertGeneratorsEnforceVersionFloor } from '@nx/devkit/internal-testing-utils';
|
||||
import { join } from 'node:path';
|
||||
|
||||
describe('@nx/<plugin> generators enforce supported version floor', () => {
|
||||
assertGeneratorsEnforceVersionFloor({
|
||||
packageRoot: join(__dirname, '..', '..'),
|
||||
packageName: '<pkg>',
|
||||
subFloorVersion: '~<floor-minus-one>',
|
||||
// Required only when a generator must run below the floor by design.
|
||||
// excludeGenerators: ['migrate-to-cypress-11'],
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Pick `subFloorVersion` such that `lt(coerce(it).version, floor)` is true. No pre-release identifiers. Reference values used in the repo: `~18.2.0` (angular, v19 floor), `~12.17.0` (cypress, v13 floor), `~1.35.0` (playwright, v1.36 floor).
|
||||
|
||||
### Plugin assert spec (optional, one per plugin)
|
||||
|
||||
Path: `packages/<plugin>/src/utils/assert-supported-<pkg>-version.spec.ts`.
|
||||
|
||||
`assertSupportedPackageVersion` is already fully tested in `@nx/devkit`,
|
||||
so a per-plugin spec mostly re-tests the shared helper. The early
|
||||
compliance PRs (`#35587` angular onward) ship one for symmetry, but it
|
||||
isn't required. If you add one, five canonical cases is the shape used:
|
||||
|
||||
```ts
|
||||
describe('assertSupportedCypressVersion', () => {
|
||||
it('throws when cypress is below the supported floor');
|
||||
it('does not throw when cypress is not installed (fresh-install path)');
|
||||
it('does not throw when cypress is `latest`');
|
||||
it('does not throw when cypress is `next`');
|
||||
it('does not throw when cypress is within the supported window');
|
||||
});
|
||||
```
|
||||
|
||||
Reference: `packages/angular/src/utils/assert-supported-angular-version.spec.ts` (originally landed in `#35587`).
|
||||
|
||||
### Test message matching
|
||||
|
||||
Use substring match on the error message:
|
||||
|
||||
```ts
|
||||
.rejects.toThrow(`Unsupported version of \`${packageName}\` detected`)
|
||||
```
|
||||
|
||||
Not a hand-rolled RegExp (avoid escape bugs). Reference: see how the shared `assertGeneratorsEnforceVersionFloor` itself does the match — search for `Unsupported version of` in `packages/nx/src/internal-testing-utils/assert-generators-enforce-version-floor.ts`.
|
||||
|
||||
## Standardized error format
|
||||
|
||||
```
|
||||
Unsupported version of `<pkg>` detected.
|
||||
|
||||
Installed: <declared-range-as-written-in-package.json>
|
||||
Supported: >= <floor>
|
||||
|
||||
Update `<pkg>` to <floor> or higher.
|
||||
```
|
||||
|
||||
Two notes:
|
||||
|
||||
- The `Installed:` line preserves the **original declared range** (e.g., `~18.2.0`), not the cleaned semver. `assertSupportedPackageVersion` passes `declared` through to `throwForUnsupportedVersion`.
|
||||
- Do not add an "above ceiling" branch to this message. Above-ceiling is silent fallthrough.
|
||||
|
||||
## Peer dep alignment
|
||||
|
||||
### What belongs in `peerDependencies`
|
||||
|
||||
The test: _would the plugin still work if this package were absent from the workspace, with the plugin's code paths unchanged?_
|
||||
|
||||
A package is required-peer if **any** of these is true:
|
||||
|
||||
- The plugin's TypeScript imports / `require`s it (executor, preset, runtime helper).
|
||||
- The plugin's executor spawns its CLI binary (`spawn('cypress')`, etc.).
|
||||
- The plugin's inferred plugin (`createNodes`/`createNodesV2`) **emits a target whose `command` invokes the package's CLI** (e.g., `command: 'rspack build'` → `@rspack/cli` is required). The `externalDependencies: ['<pkg>']` declaration in such targets is itself an admission of the runtime dependency.
|
||||
|
||||
A package is **not** required-peer when:
|
||||
|
||||
- The plugin's generator installs it into the user's workspace for the user to consume independently, and no plugin code (TypeScript, executor binary spawn, or inferred-plugin emitted command) ever invokes it. Example: ESLint plugins written into the user's eslintrc — `@nx/cypress` installs `eslint-plugin-cypress`, but its lint executor uses generic ESLint loading; the cypress plugin is loaded by ESLint per the user's config, not by `@nx/cypress`. Example: `@types/*` packages installed for the user's TS compilation but never imported by plugin code.
|
||||
|
||||
**Ecosystem-signal peer** (Angular's full `@angular/*` peer list) → judgment call, not a compliance requirement. Documents lockstep compatibility but isn't enforced by the multi-version rules.
|
||||
|
||||
### Required vs. optional peer
|
||||
|
||||
Most Nx plugin peers should be **optional** (`peerDependenciesMeta: { "<pkg>": { "optional": true } }`):
|
||||
|
||||
- Required peer: every user of the plugin needs this package, regardless of which surface they use. Example: `@angular-devkit/core`, `rxjs` in `@nx/angular` — every Angular Nx workspace uses them.
|
||||
- **Optional peer (the common case for inferred-plugin / executor surfaces):** the package is only needed when the user opts into a specific surface — an executor they have to write into `project.json`, an inferred plugin gated on the presence of a config file, a preset that auto-injects. Users who don't use that surface shouldn't see an unmet-peer warning. Examples: `@playwright/test` in `@nx/playwright`, `cypress` in `@nx/cypress`, `vitest` / `vite` in `@nx/vitest`, `@angular/build` / `@angular-devkit/build-angular` / `ng-packagr` in `@nx/angular`.
|
||||
|
||||
For `@rspack/cli` / `@rspack/core` in `@nx/rspack`: both surfaces (executor, inferred plugin) are gated — executor opt-in via `project.json`, inferred plugin gated on `rspack.config.{ts,js}` presence. Compliance fix should peer-declare both with `optional: true`.
|
||||
|
||||
Concrete examples:
|
||||
|
||||
| Package | Plugin | Plugin invokes? | Peer? | Optional? |
|
||||
| ----------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------- |
|
||||
| `cypress` | `@nx/cypress` | yes (executor spawns binary; inferred plugin emits `cypress run` commands) | **yes** | optional (executor + inferred plugin are both opt-in) |
|
||||
| `@playwright/test` | `@nx/playwright` | yes (executor + preset + inferred plugin) | **yes** | optional |
|
||||
| `vitest` | `@nx/vitest` | yes (executor + inferred plugin emits `vitest` commands) | **yes** | optional |
|
||||
| `@rspack/cli` | `@nx/rspack` | yes — inferred plugin emits `command: 'rspack build'` (`packages/rspack/src/plugins/plugin.ts:182,196`). Not imported in TS, but invoked via emitted CLI target. | **yes** | optional (both surfaces gated) |
|
||||
| `@angular-devkit/core` | `@nx/angular` | yes (used by every Angular Nx workspace) | **yes** | **not optional** |
|
||||
| `@angular/build` | `@nx/angular` | yes (only when user uses the @angular/build builder) | **yes** | optional |
|
||||
| `eslint-plugin-cypress` | `@nx/cypress` | no (generator writes it into user's eslintrc; ESLint loads it, not the plugin) | **no** | n/a |
|
||||
| `@types/node` | various | no (generator install only; types are build-time) | **no** | n/a |
|
||||
|
||||
### Range / version alignment
|
||||
|
||||
For packages that ARE peer-declared: the range must match the install lanes the code ships. If the code has no `isV1Installed` branch and no v1 entry in `versionMap`, do not list `^1.0.0` in the peer range.
|
||||
|
||||
Reference: open PR `#35671` (`@nx/vitest`) drops `^1.0.0` from the `vitest` peer range because there is no v1 install lane. Inspect via `gh pr diff 35671 --repo nrwl/nx -- packages/vitest/package.json`. Verify state — may have merged or closed since.
|
||||
|
||||
## Executor / runtime feature gating
|
||||
|
||||
Features introduced after the floor must gate at call time on the installed version, not on the floor. Use `getInstalledPackageVersion` + `lt` from `semver`.
|
||||
|
||||
```ts
|
||||
import { getInstalledPackageVersion } from '@nx/devkit/internal';
|
||||
import { lt } from 'semver';
|
||||
import { minPlaywrightVersionForBlobReports } from './versions';
|
||||
|
||||
const installed = getInstalledPackageVersion('@playwright/test');
|
||||
if (installed && lt(installed, minPlaywrightVersionForBlobReports)) {
|
||||
throw new Error(
|
||||
`The "@nx/playwright:merge-reports" executor requires "@playwright/test" version ${minPlaywrightVersionForBlobReports} or greater (the version that introduced the "blob" reporter and the "merge-reports" CLI). You are currently using version ${installed}.`
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Reference: `packages/playwright/src/executors/merge-reports/merge-reports.impl.ts`, `packages/playwright/src/utils/preset.ts`. Both added in `#35642`.
|
||||
|
||||
Two distinct cases:
|
||||
|
||||
- **Auto-injected feature** (preset's auto-blob in CI): skip injection silently when installed < threshold. Only throw when the user explicitly opted in (`generateBlobReports: true`) on an unsupported version.
|
||||
- **Direct invocation** (executor CLI subcommand): throw immediately with a clear "requires >= X.Y.Z (the version that introduced …)" message.
|
||||
|
||||
## File-layout summary
|
||||
|
||||
For plugin `@nx/<plugin>` managing `<pkg>` with floor `X.Y.Z`:
|
||||
|
||||
```
|
||||
packages/<plugin>/
|
||||
src/
|
||||
utils/
|
||||
versions.ts # add minSupportedXVersion
|
||||
assert-supported-<pkg>-version.ts # NEW — 7-line wrapper
|
||||
assert-supported-<pkg>-version.spec.ts # OPTIONAL — 5 cases (mostly re-tests shared helper)
|
||||
all-generators-enforce-floor.spec.ts # NEW — parameterized
|
||||
generators/
|
||||
<each>/
|
||||
<each>.ts # assert as first statement
|
||||
init/
|
||||
schema.json # keepExistingVersions default: true
|
||||
init.ts # keepExistingVersions ?? true
|
||||
executors/ # feature-gate via getInstalledPackageVersion
|
||||
plugins/ # same
|
||||
migrations.json # tighten / remove `requires` gates per audit
|
||||
package.json # align peer; add "semver": "catalog:" if newly used
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Code-level verification (review-mode lens)
|
||||
|
||||
In review mode, walk these markers against the diff.
|
||||
|
||||
Output rules:
|
||||
|
||||
- **Inline categories are `[blocker]` and `[non-blocker]` only.** No "open question," "ask," or other ad-hoc tags. Author-directed questions emerge from non-blocker findings and surface in the closing "Open questions for author" block.
|
||||
- **For each blocker / non-blocker:** anchor at `file:line` and cite which reference PR / file demonstrates the correct pattern. Cross-reference `anti-patterns.md` when the finding matches a numbered pattern.
|
||||
- **Sections without findings get a single summary line**, not a per-file enumeration. `"Pass — all 7 generator entries assert at first statement"` is right; listing seven file:lines is wrong. Reviewer time is spent on actionable items; passing checks should not eat reading budget.
|
||||
- **Produce the verdict block** at the end (see §"Verdict template"). The block is the skimmable index — produce it, don't substitute a free-form summary.
|
||||
|
||||
Scope:
|
||||
|
||||
- **In scope:** the diff's code, configs, schemas, migrations, and **in-codebase documentation that describes runtime behavior** (e.g., `.mdoc` / `.md` files under `astro-docs/` or `docs/` that claim how the plugin behaves). A docs claim that contradicts the code is a correctness issue and belongs here.
|
||||
- **Out of scope:** PR title, PR body shape, commit message format, related-issues section, branch naming. Defer to the user's PR/commit conventions (loaded globally from `~/.claude/memory/workflow/git/`). Don't flag PR/commit shape in this skill's review output.
|
||||
|
||||
## 1. Peer dep & install constants
|
||||
|
||||
- [blocker] `package.json` peer dep range matches the install lanes implemented in `versions.ts`. No phantom version claims. → If `versionMap` has no v1 entry, peer must not list `^1.0.0`. Reference correction: `#35671` (`@nx/vitest`). Anti-pattern: §8.
|
||||
- [blocker] **Declared floor matches the effective floor.** Grep every `import` / `require` from the third-party package in plugin code. If any imported API only exists at version >N, the declared floor must be >=N. Anti-pattern: §16. Reference correction: `#35671` second commit raised vitest from v2 to v3 after catching a `getRelevantTestSpecifications` import (v3+ only).
|
||||
- [blocker] Fresh-install constant exposes the **full feature surface**, not just the peer floor. → Playwright peer stayed `^1.36.0` but fresh-install moved to `^1.37.0` because blob reporter + `merge-reports` CLI both require 1.37. Reference: `#35642` `packages/playwright/src/utils/versions.ts`.
|
||||
- [blocker] No per-major version aliases (`<pkg>V3Version`, `<pkg>V4Version`, etc.) alongside a `versionMap` — pick one source of truth. Anti-pattern: §15. Reference: `#35671`'s third commit dropped these aliases.
|
||||
- [blocker] `versions.ts` exports `minSupportedXVersion = 'X.Y.Z'` as a plain string (no caret, no range markers). The wrapper passes this verbatim to `assertSupportedPackageVersion`.
|
||||
- [blocker] `versionMap[major]` lookup is `versionMap[major] ?? latestVersions`. No `switch + throw default:` or other above-ceiling throw. Anti-pattern: §2. Reference correction: `#35670` (`@nx/cypress` `versions()` rewrite).
|
||||
- [blocker] Every third-party package the **plugin invokes at runtime** has a `peerDependencies` entry. "Invokes" covers: (a) TypeScript `import`/`require`, (b) executor spawning the package's CLI binary, (c) inferred-plugin (`createNodes`/`createNodesV2`) emitting a target whose `command` invokes the package's CLI (the `externalDependencies: ['<pkg>']` field on such targets confirms the dependency). See §"Peer dep alignment" for the full categorization.
|
||||
- **Don't flag** packages the plugin's generator installs into the user's workspace for the user to consume independently, with no plugin codepath invoking them (e.g., ESLint plugins like `eslint-plugin-cypress` that ESLint loads from the user's eslintrc; `@types/*` packages).
|
||||
- Plugins flagged at time of writing for actually-invoked packages without a peer entry: `@nx/webpack`, `@nx/rollup`, `@nx/angular-rspack-compiler` (primary listed under `dependencies`); `@nx/jest`, `@nx/nest`, `@nx/module-federation`, `@nx/react`, `@nx/vue`, `@nx/expo`, `@nx/react-native`, `@nx/node`, `@nx/js` (verify per plugin — TS imports, binary spawns, AND inferred-plugin emitted commands all count).
|
||||
- [blocker] Peers that are only used when the user opts into a specific surface (executor opt-in, inferred plugin gated on config file presence, opt-in preset) are declared **optional** via `peerDependenciesMeta: { "<pkg>": { "optional": true } }`. Pattern is established across reference plugins — `@playwright/test`, `cypress`, `vitest`, `vite`, `@angular/build`, `ng-packagr` are all optional. Required-non-optional peers (`@angular-devkit/core`, `rxjs` in `@nx/angular`) are reserved for packages every workspace using the plugin needs. See §"Required vs. optional peer".
|
||||
- [non-blocker] If the PR introduces `semver` usage in the plugin, `package.json` `dependencies` lists `"semver": "catalog:"`. Reference: `#35642` added it to `packages/playwright/package.json`.
|
||||
|
||||
## 2. Generator entry points
|
||||
|
||||
- [blocker] **Every** entry in `generators.json` has its working function calling `assertSupported<Pkg>Version(tree)` as the **first statement** — before any tree reads, writes, or sub-generator calls. For wrapper/internal-split plugins (cypress, playwright): assert is in `*Internal`. For single-function generators (angular): in the function itself. Anti-pattern: §14.
|
||||
- [blocker] Plugin wrapper file `assert-supported-<pkg>-version.ts` imports `assertSupportedPackageVersion` from `@nx/devkit/internal`. No direct call to `throwForUnsupportedVersion`. No bespoke `throwBelowFloor` / `throwAboveWindow` / `assertVersion` / local `cleanVersion = clean(v) ?? coerce(v)?.version` helpers (use `normalizeSemver` / `getInstalledPackageVersion` / `getDeclaredPackageVersion`). Anti-pattern: §1. Concrete example: PR `#35676` introduces a local `cleanVersion` and `getInstalledRsbuildVersionRuntime` — both already exist as shared helpers.
|
||||
- [blocker] If `all-generators-enforce-floor.spec.ts` uses `excludeGenerators`, each excluded name has a code comment explaining why the generator must run sub-floor (e.g., `migrate-to-cypress-11` lifts v8–v10 workspaces onto v11).
|
||||
- [non-blocker] Double-assert chains (`configurationInternal` calls `initInternal`, both assert) are OK. Idempotent. Don't refactor away.
|
||||
|
||||
## 3. Generator outputs
|
||||
|
||||
- [blocker] Templates the generator writes (project files, configs, schemas) compile and run on every major in the support window. Verify with a quick mental walk: for each template referenced from the generator, identify any per-major-version conditional and confirm it's accurate.
|
||||
- [blocker] Generated `project.json` target shape (executor, options, schema) is valid on every supported major. If the executor's option schema differs across the support window, the generator branches or uses the union shape.
|
||||
- [blocker] Default option values are valid on every supported major. A default that's only valid above a specific major must be conditional.
|
||||
- [blocker] Version map covers every managed third-party dep. If the runtime later branches on a sibling's version (e.g., `@vitest/ui`), the version map must have an entry for that sibling per major — no gaps where the generator picks a constant the runtime then can't reconcile.
|
||||
- [blocker] Generator schema accepts the **union of options across the support window**. Options removed in a newer major still validate at schema level (with description-notice); runtime throws when inapplicable on the installed major. See `gotchas.md` §"Schema-level deprecated-option stubs with runtime throws".
|
||||
|
||||
## 4. `keepExistingVersions` (user-pin preservation)
|
||||
|
||||
- [blocker] Every `addDependenciesToPackageJson` call from a **generator** passes `keepExistingVersions: true` (positionally as the 5th arg) or `options.keepExistingVersions ?? true`.
|
||||
- [blocker] `init/schema.json` has `"keepExistingVersions": { "default": true }`. Not `false`. Not absent. **Known gap:** `@nx/angular`'s init schema currently has `default: false` and was NOT addressed in `#35587` — flagging in a non-angular PR is correct; fixing in passing in an angular PR is also correct. Anti-pattern: §4.
|
||||
- [blocker] Linter / sub-generator helpers (`add-linter.ts`, `add-angular-eslint-dependencies.ts`, equivalents) also pass `true`.
|
||||
- [non-blocker] If both schema `"default": true` AND `options.keepExistingVersions ?? true` are present, that's two sources of truth. Anti-pattern: §12.
|
||||
- **Migration generators are exempt.** Do not flag missing flags in code under `src/migrations/`.
|
||||
|
||||
## 5. `migrations.json` gates
|
||||
|
||||
- [blocker] Every `packageJsonUpdates` entry that bumps across a major version has `requires: { "<pkg>": ">=N.0.0 <(N+1).0.0" }`. Source-major gate, not target. Anti-pattern: §6. Reference: `#35587` Module Federation entries.
|
||||
- **Read the actual range strings; don't tick this by counting split entries.**
|
||||
- [non-blocker / ask author] One-sided gates (`<X` with no lower bound, or `>=Y` with no upper bound) may be intentional or accidental. Legitimate cases: legacy-cleanup codemods that should apply on every source major below the target; a v0→v1 bridge where every v0.x workspace should migrate; bumping a package introduced at vN from `undefined`. Illegitimate cases: a v1→v2 bump expressed as `<2.x` would fire for v0 workspaces too; a `>=N` with no upper bound would fire for future majors. **When you see a one-sided gate, ask the author to confirm intent** — don't auto-flag as blocker.
|
||||
- [blocker] Codemod migrations that only make sense at/above a specific third-party major have a `requires` entry. Open upper bound is intentional when the codemod cleans up legacy flags. Runtime per-package guards (`gte`/`lt` inside the migration body) are NOT a substitute for `requires`.
|
||||
- [blocker] **Nx-only migrations have NO `requires` gate.** A migration that only writes to `nx.json`, executor options, or generator defaults applies regardless of third-party version. Anti-pattern: §5. Reference correction: `#35587` removed the over-gating `@angular/core: >=21.0.0` from `update-unit-test-runner-option`.
|
||||
- [blocker] For independent siblings (Angular: `@ngrx/*`, `@angular-eslint/*`, `zone.js`, `jest-preset-angular`), gating on the primary's major is **not sufficient** — each needs its own `requires` entry. Anti-pattern: §7. Verify pairing by reading the sibling's `peerDependencies` at the version range being bumped from.
|
||||
- [blocker] A single `packageJsonUpdates` entry must not mix mutually-exclusive cross-major bumps under one `requires` (AND-semantics). Split into separate entries each with its own gate. Concrete example: React PR's `22.3.4` entry mixed `react-router 7.12.0` (cross-major) with `react-router-dom 6.30.3` (v6 patch) — must split.
|
||||
- [non-blocker] `incompatibleWith` is not a substitute for `requires`. Anti-pattern: §10. If you see `incompatibleWith` standing in for a source-major gate, ask for a `requires` instead.
|
||||
- [non-blocker] Sibling `packageJsonUpdates` entries within the same block that depend on a peer's post-bump version are fine — tier-1 chaining evaluates against post-bump state. Reference: Storybook 21.2.0 chains on the prior 21.1.0 bump.
|
||||
- [non-blocker] Pre-floor `packageJsonUpdates` entries targeting source majors below the current support floor are intentionally retained for users on older Nx versions. Don't _add_ a bridge entry without explicit decision, and don't _remove_ a legitimately-pre-floor entry mid-audit.
|
||||
|
||||
## 6. Executor / runtime / inferred-plugin feature gating
|
||||
|
||||
- [blocker] Executor code that invokes a CLI subcommand or uses an API introduced after the floor calls `getInstalledPackageVersion('<pkg>')` + `lt(installed, threshold)` from `semver` and throws a clear "requires >= X.Y.Z (the version that introduced …)" message. Reference: `packages/playwright/src/executors/merge-reports/merge-reports.impl.ts` (`#35642`).
|
||||
- [blocker] Preset / config builders that auto-inject feature-version-coupled config skip injection silently when installed < threshold, and only throw when the user **explicitly** opted in on an unsupported version. Reference: `packages/playwright/src/utils/preset.ts` (`#35642` — `generateBlobReports` logic).
|
||||
- [blocker] **Inferred plugins** (`createNodes`/`createNodesV2`) parse configs across every major in the support window. The plugin emits the same target shape regardless of the installed major (or branches if shapes diverge). Don't hardcode helper imports against one major.
|
||||
- [blocker] **Above-ceiling is silent fallthrough.** No warn, no throw, no branch. Anti-pattern: §2.
|
||||
- [non-blocker] Executors don't enforce the plugin floor. Floor enforcement is generator-only. Don't suggest adding an executor-level floor assert unless the user asks.
|
||||
- [non-blocker] `require('<pkg>')` for optional peers should live inside the function body, after version detection. Anti-pattern: §9.
|
||||
|
||||
## 7. Tests
|
||||
|
||||
- [blocker] `all-generators-enforce-floor.spec.ts` exists at `packages/<plugin>/src/utils/all-generators-enforce-floor.spec.ts`, calls `assertGeneratorsEnforceVersionFloor` from `@nx/devkit/internal-testing-utils`. This is the parameterized spec that exercises every generator's floor assert.
|
||||
- [blocker] `subFloorVersion` is a semver range where `lt(coerce(it).version, floor)` is true. No pre-release identifiers. Reference values: `~18.2.0` (angular, v19 floor), `~12.17.0` (cypress, v13 floor), `~1.35.0` (playwright, v1.36 floor).
|
||||
- [non-blocker] Plugins establishing the pattern (`#35587` angular) ship a `assert-supported-<pkg>-version.spec.ts` with the 5 canonical cases (sub-floor / fresh-install / `latest` / `next` / in-range). The underlying `assertSupportedPackageVersion` already has full coverage in `@nx/devkit`, so the per-plugin spec largely re-tests the shared helper. Useful for symmetry across the PR series but not required — don't block on missing.
|
||||
- [non-blocker] Runtime/executor feature-gate throw tests are nice-to-have, not required — reference PRs (`#35587`, `#35642`, `#35670`) do not have them today.
|
||||
- [non-blocker] Error message matching uses substring (`toThrow('Unsupported version of \`<pkg>\` detected')`) instead of hand-rolled `RegExp`. Anti-pattern: §13.
|
||||
- [non-blocker] FS-side helper migrated to `getInstalledPackageVersion`; tree-side helper may stay inline. The two helpers' `null` vs. fallback semantics differ. Reference: `#35670` `packages/cypress/src/utils/versions.ts` rewrite.
|
||||
|
||||
## Open questions to raise (when missing from the PR / Linear task)
|
||||
|
||||
1. **Floor:** deliberate raise from the previous declared peer, or matching the existing peer? If raise: do sub-floor users get a `packageJsonUpdates` bridge or manual bump?
|
||||
2. **Peer-range tightening:** dropping a major because there's no install lane (legitimate, `#35671` pattern) or because tests fail (regression risk — investigate)?
|
||||
3. **`requires` removals on Nx-only migrations:** genuinely Nx-only, or sneaking through a third-party-touching change?
|
||||
4. **Pruned migrations gaps:** if floor is being raised by N+ majors and prior `packageJsonUpdates` entries were removed, do sub-floor users have any auto-bump path? `git log --all -- packages/<plugin>/migrations.json`.
|
||||
5. **Runtime feature gates:** threshold verified against third-party release notes, or guessed?
|
||||
6. **Sibling classification:** ecosystem-locked vs. independent. Read the sibling's `peerDependencies` at the bumped-from range. `@angular-devkit/build-angular` is the gotcha — peer-locked from v20+, NOT v19.
|
||||
7. **Cross-plugin coordination:** if the plugin pins a third-party that another plugin also manages (e.g., `@nx/cypress` pinning vite for cypress v13/v14+; `@nx/vite` supporting vite v5–v8), confirm the windows stay aligned. If `@nx/vite` drops v5, `@nx/cypress` carries an orphaned install lane.
|
||||
|
||||
## Verdict template
|
||||
|
||||
```
|
||||
Blockers: <N>
|
||||
Non-blockers: <N>
|
||||
|
||||
1. Peer dep & install constants: [pass | <findings>]
|
||||
2. Generator entry points: [pass | <findings>]
|
||||
3. Generator outputs: [pass | <findings>]
|
||||
4. keepExistingVersions: [pass | <findings>]
|
||||
5. Migration gates: [pass | <findings>]
|
||||
6. Executor / runtime / inferred-plugin: [pass | <findings>]
|
||||
7. Tests: [pass | <findings>]
|
||||
|
||||
Open questions for author: [list]
|
||||
|
||||
Scope drift vs. Linear task (if applicable):
|
||||
- Findings in task NOT addressed: <list>
|
||||
- Changes in PR NOT in task: <list>
|
||||
```
|
||||
@@ -0,0 +1,115 @@
|
||||
# Examples & references
|
||||
|
||||
Concrete files, commits, and PRs to grep when you need a model.
|
||||
|
||||
## Reference PRs (in order of arrival)
|
||||
|
||||
| PR | Plugin | Branch | State | Why notable |
|
||||
| -------- | ---------------- | ---------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `#35587` | `@nx/angular` | `nxc-4381` | merged | First compliance PR. Established `throwForUnsupportedVersion`, the `assertSupported*Version` wrapper pattern, the `all-generators-enforce-floor.spec.ts` shape, the MF `requires`-gate pattern, the Nx-only-migration over-gate removal pattern. |
|
||||
| `#35642` | `@nx/playwright` | `nxc-4398` | merged | Generalized the helpers into `version-floor.ts`/`installed-version.ts`. Added `assertGeneratorsEnforceVersionFloor` in `internal-testing-utils`. Established executor/runtime feature-gating pattern (blob reporter / `merge-reports`). Demonstrated the "fresh-install constant higher than peer floor" pattern. |
|
||||
| `#35670` | `@nx/cypress` | `nxc-4384` | merged | Established `excludeGenerators` in the shared test helper for intentional sub-floor migrators (`migrate-to-cypress-11`). Demonstrated `versions()` switch-to-fallthrough rewrite. Demonstrated keeping the tree-side inline helper while migrating only the FS side to the shared helper. |
|
||||
| `#35671` | `@nx/vitest` | `nxc-4408` | open at time of writing | Three commits. (1) Establishes "drop phantom peer-range claim" (removes `^1.0.0` from peer); migration `requires` tightening for Vitest-4-only AI-instructions migrations. (2) Raises floor v2 → v3 after audit catches `getRelevantTestSpecifications` import (v3+ API) — establishes the **effective-floor-vs-declared-floor** pattern (see `anti-patterns.md` §16). (3) Adopts the cypress version-resolution pattern (bundle-of-varying-deps `versions(tree)`, `getInstalled<Pkg>Version(tree?)`, no per-major aliases — see `anti-patterns.md` §15). To inspect: `gh pr view 35671 --repo nrwl/nx --json commits` then `gh pr diff 35671`. Verify state — may have merged or closed. |
|
||||
|
||||
Always verify state with `gh pr view <N> --repo nrwl/nx --json state` before citing — this table goes stale.
|
||||
|
||||
## Reference commits (for `git show` inspection)
|
||||
|
||||
When the same change exists as both a pre-squash branch commit AND a merged squash on master, prefer the merged squash — it's the authoritative final state. Pre-squash SHAs are listed because they're easier to read in isolation (smaller diffs) when investigating one specific aspect.
|
||||
|
||||
**Merged on master (authoritative):**
|
||||
|
||||
| SHA | Subject |
|
||||
| ------------ | ---------------------------------------------------------------------------- |
|
||||
| `75578724fa` | `cleanup(core): add throwForUnsupportedVersion util to @nx/devkit/internal` |
|
||||
| `484ce6e5d5` | `fix(angular): multi-version support compliance (#35587)` |
|
||||
| `78f908d015` | `cleanup(angular): adopt shared version-floor helpers` |
|
||||
| `e2ef134645` | `fix(testing): multi-version support compliance for @nx/playwright (#35642)` |
|
||||
| `5d8b1bab7e` | `cleanup(devkit): allow excluding generators from version floor test helper` |
|
||||
| `bc35b484e3` | `fix(testing): multi-version support compliance for @nx/cypress (#35670)` |
|
||||
|
||||
Pre-squash branch SHAs are available via `gh pr view <N> --json commits` even after the branch is deleted; useful when inspecting one specific aspect of a merged PR in isolation. Example:
|
||||
|
||||
```bash
|
||||
gh pr view 35642 --repo nrwl/nx --json commits | jq -r '.commits[] | "\(.oid[:10]) \(.messageHeadline)"'
|
||||
```
|
||||
|
||||
## Shared helpers — current locations
|
||||
|
||||
| File | Exports |
|
||||
| ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `packages/devkit/src/utils/version-floor.ts` | `throwForUnsupportedVersion` (internal-only), `assertSupportedPackageVersion` |
|
||||
| `packages/devkit/src/utils/installed-version.ts` | `getInstalledPackageVersion`, `getDeclaredPackageVersion`, `isNonSemverDistTag`, `normalizeSemver`, `NON_SEMVER_DIST_TAGS` |
|
||||
| `packages/devkit/internal.ts` | re-exports from above (this is `@nx/devkit/internal`) |
|
||||
| `packages/nx/src/internal-testing-utils/assert-generators-enforce-version-floor.ts` | `assertGeneratorsEnforceVersionFloor` |
|
||||
| `packages/devkit/internal-testing-utils.ts` | re-exports `assertGeneratorsEnforceVersionFloor` (this is `@nx/devkit/internal-testing-utils`) |
|
||||
|
||||
## Per-plugin compliant files (grep for the pattern)
|
||||
|
||||
### `@nx/angular` (most extensive — has the `supportedVersions` list pattern)
|
||||
|
||||
- `packages/angular/src/utils/assert-supported-angular-version.ts` — wrapper using `Math.min(...supportedVersions)`
|
||||
- `packages/angular/src/utils/assert-supported-angular-version.spec.ts` — the canonical 5-case spec
|
||||
- `packages/angular/src/utils/all-generators-enforce-floor.spec.ts` — the parameterized floor spec
|
||||
- `packages/angular/src/generators/add-linting/lib/add-angular-eslint-dependencies.ts` — `keepExistingVersions: true` pattern
|
||||
- `packages/angular/migrations.json` — MF `requires` gates and the over-gate removal
|
||||
|
||||
### `@nx/playwright` (the helpers were generalized here)
|
||||
|
||||
- `packages/playwright/src/utils/assert-supported-playwright-version.ts` — wrapper using a `minSupportedPlaywrightVersion` constant
|
||||
- `packages/playwright/src/utils/all-generators-enforce-floor.spec.ts`
|
||||
- `packages/playwright/src/utils/preset.ts` — runtime feature gating (blob reporter)
|
||||
- `packages/playwright/src/executors/merge-reports/merge-reports.impl.ts` — executor feature gating
|
||||
- `packages/playwright/src/utils/versions.ts` — `minSupportedPlaywrightVersion`, `minPlaywrightVersionForBlobReports`, `playwrightVersion = '^1.37.0'` (fresh install higher than peer)
|
||||
- `packages/playwright/src/utils/add-linter.ts` — `keepExistingVersions: true` in linter helper
|
||||
- `packages/playwright/package.json` — peer `^1.36.0` (unchanged), added `"semver": "catalog:"`
|
||||
|
||||
### `@nx/cypress` (excludeGenerators + versions() rewrite)
|
||||
|
||||
- `packages/cypress/src/utils/assert-supported-cypress-version.ts`
|
||||
- `packages/cypress/src/utils/all-generators-enforce-floor.spec.ts` — uses `excludeGenerators: ['migrate-to-cypress-11']` with code comment
|
||||
- `packages/cypress/src/utils/versions.ts` — `versions()` rewritten to `versionMap[major] ?? latestVersions`; `getInstalledCypressVersion` FS-path migrated to shared helper, tree-path kept inline
|
||||
|
||||
## Finding current work-in-progress
|
||||
|
||||
To enumerate all compliance PRs (merged + open) without relying on out-of-tree tracking docs:
|
||||
|
||||
```bash
|
||||
# Open + merged compliance PRs
|
||||
gh pr list --repo nrwl/nx --search "multi-version compliance" --state all --limit 30 \
|
||||
--json number,title,state,headRefName,author
|
||||
|
||||
# Just open ones
|
||||
gh pr list --repo nrwl/nx --search "multi-version compliance" --state open
|
||||
```
|
||||
|
||||
This is the authoritative list. Plugins covered to date can be derived by inspecting which packages each merged PR touched.
|
||||
|
||||
To check which plugins still have known anti-patterns (e.g., phantom peer claims, missing floor assert), grep on master:
|
||||
|
||||
```bash
|
||||
# Plugins WITHOUT an assert-supported-<pkg>-version wrapper
|
||||
for d in packages/*/src/utils; do
|
||||
pkg=$(dirname "$d" | xargs basename)
|
||||
if [ ! -f "$d/assert-supported-$pkg-version.ts" ] && \
|
||||
[ ! -f "$d/assert-supported-${pkg/_/-}-version.ts" ]; then
|
||||
echo "$pkg: no assert-supported wrapper"
|
||||
fi
|
||||
done
|
||||
|
||||
# Plugins missing the parameterized floor spec
|
||||
find packages -name "all-generators-enforce-floor.spec.ts" -not -path "*/dist/*"
|
||||
```
|
||||
|
||||
Cross-reference with the third-party packages each plugin manages (peer deps in `package.json`).
|
||||
|
||||
## How to use these examples
|
||||
|
||||
When auditing a new plugin, before writing anything:
|
||||
|
||||
1. Read the PR body of `#35642` (`@nx/playwright`) — it's the most comprehensive description of the canonical shape.
|
||||
2. Read the four files from `@nx/playwright`: `versions.ts`, `assert-supported-playwright-version.ts`, `all-generators-enforce-floor.spec.ts`, and `preset.ts`. Five minutes.
|
||||
3. If your plugin has a `migrations.json` of any complexity, also read `packages/angular/migrations.json` MF entries and the `update-unit-test-runner-option` entry for the gate patterns.
|
||||
4. If the plugin has runtime feature gates, also read `packages/playwright/src/executors/merge-reports/merge-reports.impl.ts`.
|
||||
|
||||
When reviewing a compliance PR, the diff should look very similar to one of these reference PRs. Differences should be justifiable by the plugin's specifics (different floor, different feature gates, different migration shape) — not by departing from the canonical patterns.
|
||||
@@ -0,0 +1,241 @@
|
||||
# Gotchas & edge cases
|
||||
|
||||
Non-obvious behavior. Load these into your model before auditing or reviewing.
|
||||
|
||||
## `latest` / `next` dist-tags
|
||||
|
||||
When a workspace declares `"<pkg>": "latest"` or `"next"` in its `package.json`:
|
||||
|
||||
- `assertSupportedPackageVersion` no-ops via `isNonSemverDistTag` (NON_SEMVER_DIST_TAGS = `['latest', 'next']`). The floor check is skipped entirely.
|
||||
- `getDeclaredPackageVersion` falls back to the cleaned `latestKnownVersion` argument (if provided) or returns `null`.
|
||||
- `versions(tree)` returns `latestVersions` (the fresh-install path).
|
||||
|
||||
Tests must include `latest` and `next` cases. Both are no-ops; neither throws.
|
||||
|
||||
## pnpm `catalog:` references
|
||||
|
||||
Declared versions may be `"catalog:default"`, `"catalog:typescript"`, etc. (since pnpm 9.5):
|
||||
|
||||
- `getDependencyVersionFromPackageJson` (via the catalog manager in devkit) resolves these before the helper sees them. Don't call `clean`/`coerce` on raw values.
|
||||
- `normalizeSemver` behavior on a raw `catalog:` string is not explicitly tested (open question — verify if you encounter it).
|
||||
|
||||
Reference: PR `#35459` (`fix(misc): resolve pnpm catalog: refs in version lookups`) — landed catalog ref handling.
|
||||
|
||||
## Fresh-install path (package not declared)
|
||||
|
||||
When `<pkg>` is missing from the workspace's `package.json` entirely:
|
||||
|
||||
- `assertSupportedPackageVersion` no-ops.
|
||||
- `versions(tree)` returns `latestVersions`.
|
||||
- The generator proceeds with the fresh-install constant (e.g., `playwrightVersion = '^1.37.0'`).
|
||||
|
||||
This is intentional — the generator is being run on a new workspace or one that's adding this package for the first time.
|
||||
|
||||
## Error message preserves declared range, not cleaned semver
|
||||
|
||||
```
|
||||
Installed: ~18.2.0
|
||||
Supported: >= 19.0.0
|
||||
```
|
||||
|
||||
`Installed:` shows what's in `package.json` verbatim. Don't try to normalize it in the error message — it tells the user exactly what they typed, which helps them find it.
|
||||
|
||||
The argument flow: `assertSupportedPackageVersion` calls `throwForUnsupportedVersion(packageName, declared, minSupportedVersion)` with the raw `declared` value.
|
||||
|
||||
## Cypress's `getCypressVersionFromTree` stays inline
|
||||
|
||||
The shared `getDeclaredPackageVersion` falls back to `latestKnownVersion` when the declared value is `latest`/`next` or missing. Cypress's tree path returns `null` on missing. These semantics differ enough that the helper can't be consolidated without changing behavior.
|
||||
|
||||
The FS path (`getCypressVersionFromFileSystem`) was migrated to `getInstalledPackageVersion` (better resolution for pnpm strict / nested installs). The tree path stayed inline.
|
||||
|
||||
Reference: `packages/cypress/src/utils/versions.ts` after `#35670`.
|
||||
|
||||
## Double-asserts are fine
|
||||
|
||||
When `configurationInternal` calls `initInternal` (or any generator chain), both call their respective `assertSupportedXVersion(tree)`. The assert is idempotent and cheap (one tree read + one semver comparison). Don't refactor away.
|
||||
|
||||
The `assertGeneratorsEnforceVersionFloor` test treats both entry points as separate generators and asserts each throws — which is what we want.
|
||||
|
||||
## Angular ecosystem lockstep — what `@angular/core >=N` covers
|
||||
|
||||
Peer-locked to `@angular/core` (one `requires` on the primary is sufficient):
|
||||
|
||||
- `@angular/cli`
|
||||
- `@angular/ssr`
|
||||
- `@angular-devkit/build-angular` **from v20+** (NOT v19 — `@angular-devkit/build-angular@19` does not peer-on `@angular/core`)
|
||||
- `@angular/material`, `@angular/cdk`, all `@angular/*` framework packages
|
||||
- `@schematics/angular`
|
||||
|
||||
Independent (need their own `requires`):
|
||||
|
||||
- `@ngrx/*`
|
||||
- `@angular-eslint/*`
|
||||
- `zone.js`
|
||||
- `jest-preset-angular`
|
||||
- `karma`, `karma-*`
|
||||
- `protractor` (deprecated)
|
||||
- `tailwindcss` and CSS-tooling siblings
|
||||
|
||||
Always verify pairing at the actual version range being bumped from — `@angular-devkit/build-angular` is the classic gotcha (peers on `@angular/core` in some versions, not others).
|
||||
|
||||
## Pruned migrations leave no trace in `migrations.json`
|
||||
|
||||
Older `packageJsonUpdates` entries (e.g., `12.x` migrations) are removed during normal Nx version cleanup waves. They don't show up in the current `migrations.json` but their absence is meaningful — users on an old floor have no auto-bump path to the new floor.
|
||||
|
||||
Check via:
|
||||
|
||||
```sh
|
||||
git log --all --oneline -p -- packages/<plugin>/migrations.json | head -200
|
||||
git log --all --diff-filter=D --name-only -- packages/<plugin>/migrations.json
|
||||
```
|
||||
|
||||
When raising a floor by N+ majors, decide whether to:
|
||||
|
||||
1. Add a `packageJsonUpdates` entry bridging sub-floor → floor (the user gets auto-bumped on `nx migrate`).
|
||||
2. Leave the gap (the user sees the floor-assert error and has to bump manually).
|
||||
|
||||
The Cypress v12 → v13 gap was left intentionally — users get the assert error and bump manually. Don't add a bridge entry without explicit agreement.
|
||||
|
||||
## Pre-floor `packageJsonUpdates` entries are intentionally retained
|
||||
|
||||
Distinct from the pruned-history case above: a plugin may carry `packageJsonUpdates` entries targeting source majors **below** the current support floor. Example: `@nx/react-native`'s entries `20.3.0` and `21.4.0` target RN versions below the current ~0.79.3 floor. These are intentionally retained for users on older Nx versions that supported older RN.
|
||||
|
||||
Don't _add_ a bridge entry without explicit decision. Don't _remove_ a legitimately-pre-floor entry as part of a compliance pass. The W1/W4 audit window only covers entries that target source majors **inside** the current support window.
|
||||
|
||||
## `subFloorVersion` must satisfy `lt(clean(it), floor)`
|
||||
|
||||
For the parameterized floor spec, pick a value that's actually below the floor after `clean()`. Examples:
|
||||
|
||||
| Floor | Valid `subFloorVersion` | Invalid |
|
||||
| -------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `19.0.0` | `~18.2.0`, `^18.0.0`, `18.2.0` | `~19.0.0-beta.0` (clean strips the pre-release; in some cases this still satisfies `lt`, but it's confusing — avoid pre-release) |
|
||||
| `13.0.0` | `~12.17.0`, `^12.0.0` | `^13.0.0-rc.0` |
|
||||
| `1.36.0` | `~1.35.0`, `^1.35.0` | `1.36.0-beta.5` |
|
||||
|
||||
Use a stable minor-or-patch range below floor. Don't use pre-release identifiers.
|
||||
|
||||
## Declared floor vs. effective floor
|
||||
|
||||
The declared floor (peer dep + `minSupported<Pkg>Version` + `versionMap` lowest entry) is what the plugin advertises. The **effective floor** is the lowest major where every third-party API the plugin's code actually calls is available. When they diverge, the declared floor is lying.
|
||||
|
||||
How this happens: someone bumps the plugin to use a new API (e.g., `getRelevantTestSpecifications` introduced in vitest v3) without raising the floor. The plugin compiles, generators pass tests against the latest install lane, but workspaces on sub-effective-floor versions crash at runtime with `... is not a function`.
|
||||
|
||||
How to detect: in the audit's runtime/executor inventory step, every `import` / `require` from the third-party package goes into a list. Cross-reference each named export against the third-party's release notes / API docs. The effective floor is the highest "introduced in" version across that list.
|
||||
|
||||
How to fix: raise the declared floor to match the effective floor. Drop the now-unsupported entries from `versionMap`, peer, and any per-major aliases. The parameterized floor spec's `subFloorVersion` shifts up accordingly.
|
||||
|
||||
Reference: open PR `#35671` (`@nx/vitest`) — proposed v2 floor initially; raised to v3 in a follow-up commit after spotting `getRelevantTestSpecifications` usage. See `anti-patterns.md` §16.
|
||||
|
||||
## `versions()` fall-through above ceiling, not throw
|
||||
|
||||
Before `#35670`, cypress's `versions()` had a `switch + throw default:`. This is wrong for two reasons:
|
||||
|
||||
1. New majors that don't yet have a `versionMap` entry should silently use `latestVersions` — the plugin hasn't been updated to know about them, but the user should still be able to use them.
|
||||
2. Below-floor is already caught by the generator-level assert. The `versions()` throw is redundant for the in-range/sub-floor case, and wrong for the above-ceiling case.
|
||||
|
||||
The pattern is always:
|
||||
|
||||
```ts
|
||||
return versionMap[major as CompatVersions] ?? latestVersions;
|
||||
```
|
||||
|
||||
## Tier-1 chaining in `packageJsonUpdates`
|
||||
|
||||
Within a single `packageJsonUpdates` entry (single block), if entry A bumps package X and entry B has `requires: { X: ">=N" }` that depends on the post-bump value of X, B's gate evaluates against the post-bump state. This is **deliberate design**, not a bug.
|
||||
|
||||
Concrete example: Storybook's `21.2.0-migrate-storybook-v9` migration is gated on `storybook >=9.0.0` even though the prior state was v8 — the sibling `packageJsonUpdates` `21.1.0` bumps Storybook to v9 first, so the v9 gate evaluates against post-bump state.
|
||||
|
||||
This means you can have one block bump X then chain a sibling bump gated on X's new version, without splitting into separate `packageJsonUpdates` keys.
|
||||
|
||||
## Cross-plugin coordination of shared third-party windows
|
||||
|
||||
Some third-party packages are managed by multiple Nx plugins. Concrete example:
|
||||
|
||||
- `@nx/cypress` pins `vite` v5 (for cypress v13) and v6 (for cypress v14+).
|
||||
- `@nx/vite` supports `vite` v5–v8.
|
||||
|
||||
If `@nx/vite` drops v5 from its supported window, `@nx/cypress`'s v5 pin becomes an orphaned install lane — workspaces using both plugins are now in conflict.
|
||||
|
||||
When raising / lowering a third-party's support window in one plugin, check every other plugin that manages the same package. The Linear milestone tasks call this out per-plugin (e.g., NXC-4384 cypress flags vite coordination with NXC-4407 vite).
|
||||
|
||||
### Sibling declaration consistency
|
||||
|
||||
When the same third-party package appears in multiple plugins, the declaration _kind_ (peerDependencies vs dependencies vs devDependencies) should be consistent unless the plugins genuinely have different roles for the package. Concrete inconsistency on master at time of writing: `@module-federation/enhanced ^2.3.3` is in `dependencies` in `@nx/module-federation` but `peerDependencies` in `@nx/rspack`. Pick one rule per package across the plugin family and document the exception when one plugin must differ.
|
||||
|
||||
## Plugin must own its primary third-party's pin
|
||||
|
||||
A plugin's install constants for its primary third-party must live in the plugin's own `packages/<plugin>/src/utils/versions.ts` — not in another plugin. Cross-plugin imports of install constants create governance drift (the owning plugin can't change the pin without breaking the borrower).
|
||||
|
||||
Example anti-pattern: `@nx/esbuild`'s `esbuild` install constant living in `@nx/js`. Flagged in NXC-4386.
|
||||
|
||||
## Schema-level deprecated-option stubs with runtime throws
|
||||
|
||||
Established Angular pattern (also called out in NXC-4391 jest, NXC-4395 next, NXC-4408 vitest): when an option is deprecated/removed in a newer third-party major but the plugin still supports an older major where it's valid, **retain the option in the schema with a description-notice and throw at runtime when inapplicable to the installed major**.
|
||||
|
||||
This keeps the schema accepting the union of options across the support window. Runtime branches on installed version and throws a clear message if the user passes an option that's only valid on a major they're not running.
|
||||
|
||||
Reference: search the angular generators for `removed in Angular vN` style schema descriptions paired with `assertSupportedAngularVersion`-aware option handling.
|
||||
|
||||
## Known-incomplete plugins
|
||||
|
||||
These were touched by a compliance PR but the work is incomplete. Useful for review and for future PRs.
|
||||
|
||||
- **`@nx/angular` init `keepExistingVersions`**: `packages/angular/src/generators/init/schema.json` has `default: false` and `packages/angular/src/generators/init/init.ts` passes `options.keepExistingVersions` directly (no `?? true`). PR `#35587` fixed `add-linting` but NOT the init generator. Flag in non-angular PRs as a reference to the pattern; fix in passing in any future angular PR. (Verify state on current master before citing.)
|
||||
- **`@nx/jest` peer-dep block missing entirely.** When adopting the floor assert, add `peerDependencies` first declaring `jest` / `ts-jest` / `@types/jest` ranges. Without the peer block, `getDependencyVersionFromPackageJson` for `jest` may return `undefined` on installed workspaces because pnpm catalog refs and certain other patterns rely on the peer being declared.
|
||||
- **Cypress v12→v13 migration gap**: when `#35670` raised the floor to v13, prior v12-cleanup `packageJsonUpdates` entries were already pruned. Decision was to leave it — v12 workspaces see the assert error and bump manually. Reference for the "raise floor, no bridge" pattern.
|
||||
- **`getInstalled<Pkg>Version` consolidation deferred**: each plugin still has its own near-identical helper (the FS-side has been migrated to the shared helper in some plugins, but a full unification across cypress/playwright/vitest/next/expo/angular is pending). Don't bundle that refactor into a compliance PR.
|
||||
|
||||
## `migrate-to-cypress-11` and other intentional sub-floor migrators
|
||||
|
||||
A generator whose purpose is to lift sub-floor workspaces onto a supported version must run on sub-floor workspaces. If it had the floor assert, it could never run.
|
||||
|
||||
For these generators:
|
||||
|
||||
- Do NOT add `assertSupportedXVersion(tree)` to them.
|
||||
- Keep their existing version checks (e.g., `assertMinimumCypressVersion(8)` in `migrate-to-cypress-11`).
|
||||
- Add them to `excludeGenerators` in `all-generators-enforce-floor.spec.ts` with a code comment explaining why.
|
||||
|
||||
There are usually 0 or 1 of these per plugin. Greater than 1 is suspicious — review carefully.
|
||||
|
||||
## `getInstalledPackageVersion` vs. `require('<pkg>/package.json')`
|
||||
|
||||
Bare `require('<pkg>/package.json')` resolves from the plugin's own install location, which in pnpm strict mode or nested installs may not match the workspace's resolved version. `readModulePackageJson` (used by `getInstalledPackageVersion`) goes through `getNxRequirePaths()` for correct workspace-rooted resolution.
|
||||
|
||||
Anywhere you read an installed version at runtime: prefer `getInstalledPackageVersion('<pkg>')`. Don't `require('<pkg>/package.json')`.
|
||||
|
||||
## "Above ceiling" is NOT in the task spec
|
||||
|
||||
Repeating because this gets re-introduced: above-ceiling handling is explicitly out of scope for these compliance tasks. If you find yourself adding it, you've drifted from the spec.
|
||||
|
||||
The behavior we want above the highest known major: silent fall-through to `latestVersions`. The plugin will be updated to add a `versionMap` entry for the new major in a future PR. Until then, the user gets the latest install constants and may run into incompatibilities, which is the existing pre-compliance behavior. We are NOT trying to detect future majors and warn — that's a different feature.
|
||||
|
||||
## Decisions you cannot make alone
|
||||
|
||||
Pause and ask when:
|
||||
|
||||
- **Peer-range drop:** dropping a major from the peer might be a regression if tests pass on that version. Verify whether the absence of an install lane reflects "we never supported it" (legitimate drop) or "we shipped support and quietly broke it" (regression — investigate before dropping).
|
||||
- **Floor raise without a bridging migration:** raising the floor by N+ majors means users on the lowest sub-floor major see the assert error and must manually bump. Confirm with the user: acceptable, or add a `packageJsonUpdates` bridge?
|
||||
- **`requires` removal on a borderline migration:** the diff says the migration is Nx-only (no third-party config touched), but it reads a config file that only exists at certain third-party versions. The third-party dependency is indirect but real. Don't remove the gate without verifying.
|
||||
- **Peer floor and fresh-install constant diverge** (playwright pattern — peer `^1.36.0`, fresh-install `^1.37.0`). Confirm the gap is justified by feature surface (1.37 introduced the blob reporter + merge-reports CLI) and not an oversight.
|
||||
- **Ecosystem-locked vs. independent sibling classification:** before adding or removing a sibling's `requires` entry, read its `peerDependencies` block at the version range being bumped from. `@angular-devkit/build-angular` is the gotcha — only peer-locked to `@angular/core` from v20+.
|
||||
- **Pruned migration gap:** the lowest sub-floor major has no auto-bump path because prior `packageJsonUpdates` entries were removed during cleanup waves. Decide: add a bridge entry, or accept the manual bump? `git log --diff-filter=D -- packages/<plugin>/migrations.json` reveals the gap.
|
||||
- **New plugin doesn't fit the canonical shape** (manages multiple primary packages with different floors, runs partially as a Nx-internal-only plugin, etc.). Ask before improvising — see `canonical-shape.md` §"Plugins managing multiple primary packages" for the established multi-primary pattern.
|
||||
- **Test fails on `latest`/`next` despite the assert being a no-op.** The no-op behavior is intentional, but if the generator downstream of the assert can't handle the unresolved range, that's a real bug — not something to paper over by tightening the assert.
|
||||
|
||||
## Per-plugin decision log
|
||||
|
||||
These were decided once for the reference PRs (#35587, #35642, #35670) — apply them as defaults unless explicitly contradicted by the user for a new plugin:
|
||||
|
||||
- **Executors do NOT enforce the plugin floor.** Generator-only. Executors gate per-feature, not per-floor.
|
||||
- **Above-ceiling: silent fall-through to `latestVersions`.** No warn, no throw, no branch.
|
||||
- **Init generators preserve user pins** via `keepExistingVersions: true` (schema default) and the `?? true` safety net at the call site.
|
||||
- **Skip writing the install constant when the package is already detected** (cypress + angular pattern — preserves the user's installed minor/patch).
|
||||
- **Shared helpers stay in `@nx/devkit/internal`** — not part of the public devkit surface. (The W2 ticket originally proposed adding `throwForUnsupportedVersion` to the public devkit API; the implementation landed under `/internal` instead, matching how other version-related helpers ship.)
|
||||
- **Consolidation of per-plugin `getInstalled<Pkg>Version` helpers is deferred** — don't bundle that refactor into a compliance PR.
|
||||
|
||||
Plugin-specific decisions that may be pending or have settled differently (check the live PR state via `gh pr list --repo nrwl/nx --search "multi-version compliance"`):
|
||||
|
||||
- `@nx/jest` — needs a `peerDependencies` block for `jest`/`ts-jest`/`@types/jest` before the floor assert can rely on `getDependencyVersionFromPackageJson`.
|
||||
- `@nx/eslint` — historically gated on an ESLint v8 EOL decision. If you're touching it, confirm the decision is settled.
|
||||
- `@nx/eslint-plugin` — historically coupled to the eslint v8 decision (typescript-eslint v6/v7 only support eslint v8). Confirm before proceeding.
|
||||
- `@nx/rspack` / `@nx/rsbuild` — there is an open PR (`#35676` at time of writing). Inspect for the local-helper-duplication anti-pattern (`anti-patterns.md` §1).
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Restore cached hash
|
||||
id: cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.3
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: .banner-hash
|
||||
key: banner-content-hash-
|
||||
@@ -64,6 +64,12 @@ jobs:
|
||||
echo "Banner content unchanged"
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
if: steps.compare.outputs.changed == 'true'
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
- name: Trigger Netlify deploys
|
||||
if: steps.compare.outputs.changed == 'true'
|
||||
env:
|
||||
@@ -77,7 +83,10 @@ jobs:
|
||||
echo "Triggering nx-dev deploy..."
|
||||
netlify deploy --trigger --prod -s nx-dev
|
||||
|
||||
echo "Both deploys triggered successfully"
|
||||
echo "Triggering nrwl-blog deploy..."
|
||||
netlify deploy --trigger --prod -s nrwl-blog
|
||||
|
||||
echo "All deploys triggered successfully"
|
||||
|
||||
- name: Save new hash to cache
|
||||
if: steps.compare.outputs.changed == 'true'
|
||||
@@ -86,7 +95,7 @@ jobs:
|
||||
|
||||
- name: Update cache
|
||||
if: steps.compare.outputs.changed == 'true'
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.3
|
||||
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: .banner-hash
|
||||
key: banner-content-hash-${{ github.run_id }}
|
||||
|
||||
@@ -13,6 +13,10 @@ env:
|
||||
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
NX_CLOUD_ENABLE_METRICS_COLLECTION: 'true'
|
||||
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), pulling pnpm 11 and breaking install.
|
||||
COREPACK_DEFAULT_TO_LATEST: '0'
|
||||
|
||||
jobs:
|
||||
main-linux:
|
||||
@@ -81,6 +85,9 @@ jobs:
|
||||
- name: Install project dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Restore .NET analyzer projects
|
||||
run: dotnet restore nx.sln
|
||||
|
||||
- name: Nx Report
|
||||
run:
|
||||
pnpm nx report
|
||||
@@ -309,6 +316,10 @@ jobs:
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm playwright install --with-deps
|
||||
|
||||
- name: Restore .NET packages
|
||||
if: steps.check-changes.outputs.has_changes == 'true'
|
||||
run: dotnet restore nx.sln
|
||||
|
||||
- name: Run E2E Tests for macOS
|
||||
if: steps.check-changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
|
||||
@@ -13,6 +13,10 @@ on:
|
||||
|
||||
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), pulling pnpm 11 and breaking install.
|
||||
COREPACK_DEFAULT_TO_LATEST: '0'
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
@@ -79,10 +83,14 @@ jobs:
|
||||
id: brew-install-python-setuptools
|
||||
run: brew install python-setuptools
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm playwright install --with-deps
|
||||
- name: Install pnpm packages
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright
|
||||
run: pnpm playwright install --with-deps
|
||||
|
||||
- name: Restore .NET packages
|
||||
run: dotnet restore nx.sln
|
||||
|
||||
- name: Homebrew cache directory path
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
@@ -164,10 +172,14 @@ jobs:
|
||||
corepack enable
|
||||
corepack prepare --activate
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm playwright install --with-deps
|
||||
- name: Install pnpm packages
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright
|
||||
run: pnpm playwright install --with-deps
|
||||
|
||||
- name: Restore .NET packages
|
||||
run: dotnet restore nx.sln
|
||||
|
||||
- name: Cleanup
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
@@ -327,7 +339,7 @@ jobs:
|
||||
NX_NATIVE_LOGGING: 'false'
|
||||
NX_E2E_RUN_E2E: 'true'
|
||||
NX_CLOUD_NO_TIMEOUTS: 'true'
|
||||
NX_E2E_SKIP_CLEANUP: 'true'
|
||||
NX_E2E_SKIP_GLOBAL_CLEANUP: 'true'
|
||||
NODE_OPTIONS: --max_old_space_size=8192
|
||||
SELECTED_PM: ${{ matrix.package_manager }}
|
||||
npm_config_registry: http://localhost:4872
|
||||
@@ -352,7 +364,7 @@ jobs:
|
||||
NX_E2E_VERBOSE_LOGGING: 'true'
|
||||
NX_NATIVE_LOGGING: 'false'
|
||||
NX_E2E_RUN_E2E: 'true'
|
||||
NX_E2E_SKIP_CLEANUP: 'true'
|
||||
NX_E2E_SKIP_GLOBAL_CLEANUP: 'true'
|
||||
NODE_OPTIONS: --max_old_space_size=8192
|
||||
SELECTED_PM: 'npm'
|
||||
npm_config_registry: http://localhost:4872
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d # v4.1.0
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 10.28.2
|
||||
version: 11.2.2
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d # v4.1.0
|
||||
with:
|
||||
version: 10.28.2
|
||||
version: 11.2.2
|
||||
|
||||
- name: Use Node.js ${{ matrix.node_version }}
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d # v4.1.0
|
||||
with:
|
||||
version: 10.28.2 # Aligned with root package.json (pnpm/action-setup will helpfully error if out of sync)
|
||||
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
|
||||
|
||||
@@ -22,7 +22,7 @@ env:
|
||||
NX_RUN_GROUP: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
NODE_VERSION: 22.16.0
|
||||
PNPM_VERSION: 10.28.2 # Aligned with root package.json (pnpm/action-setup will helpfully error if out of sync)
|
||||
PNPM_VERSION: 11.2.2 # Aligned with root package.json (pnpm/action-setup will helpfully error if out of sync)
|
||||
NX_GRADLE_PROJECT_GRAPH_TIMEOUT: 600
|
||||
|
||||
jobs:
|
||||
@@ -130,6 +130,7 @@ jobs:
|
||||
- host: windows-latest
|
||||
setup: |-
|
||||
choco install openjdk --version=21.0.0 -y
|
||||
choco install dotnet-9.0-sdk -y
|
||||
rustup target add aarch64-pc-windows-msvc
|
||||
build: |
|
||||
export JAVA_HOME="C:\Program Files\OpenJDK\jdk-21"
|
||||
@@ -148,22 +149,23 @@ jobs:
|
||||
build: |
|
||||
set -e
|
||||
apt-get update
|
||||
apt-get install -y curl ca-certificates git xz-utils gpg
|
||||
|
||||
# Install Java 21
|
||||
apt-get install -y openjdk-21-jdk
|
||||
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
java --version
|
||||
# Install mise from the signed apt repo
|
||||
install -dm 755 /etc/apt/keyrings
|
||||
curl -fsSL https://mise.jdx.dev/gpg-key.pub | gpg --dearmor -o /etc/apt/keyrings/mise-archive-keyring.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/mise-archive-keyring.gpg arch=$(dpkg --print-architecture)] https://mise.jdx.dev/deb stable main" > /etc/apt/sources.list.d/mise.list
|
||||
apt-get update
|
||||
apt-get install -y mise
|
||||
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
apt-get install -y nodejs=22.16.0-1nodesource1
|
||||
# Provision Node, Java, .NET, Maven, corepack from mise.toml
|
||||
cd /build
|
||||
mise trust mise.toml
|
||||
mise install
|
||||
eval "$(mise env -s bash)"
|
||||
|
||||
export PATH="/usr/local/bin:$PATH"
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
npm i -g pnpm@${PNPM_VERSION} --force
|
||||
pnpm --version
|
||||
corepack enable
|
||||
corepack prepare --activate
|
||||
|
||||
pnpm install --frozen-lockfile
|
||||
rustup target add x86_64-unknown-linux-gnu
|
||||
@@ -174,32 +176,32 @@ jobs:
|
||||
build: |
|
||||
bash -c "
|
||||
set -e
|
||||
# mise's core node/java backends don't ship musl binaries and fall back to
|
||||
# compile-from-source on Alpine, which fails. Install via apk + tarball instead.
|
||||
echo 'https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories
|
||||
apk add --no-cache curl xz openjdk21 build-base lld
|
||||
apk add --no-cache curl xz openjdk21 build-base lld dotnet9-sdk
|
||||
|
||||
# Set up Java 21
|
||||
# Java 21
|
||||
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk
|
||||
export PATH=\"\$JAVA_HOME/bin:\$PATH\"
|
||||
java --version
|
||||
|
||||
# .NET 9 SDK (needed by @nx/dotnet plugin)
|
||||
dotnet --version
|
||||
|
||||
# Node.js musl build from unofficial-builds.nodejs.org
|
||||
curl -fsSL https://unofficial-builds.nodejs.org/download/release/v22.16.0/node-v22.16.0-linux-x64-musl.tar.xz -o node.tar.xz
|
||||
tar -xJf node.tar.xz
|
||||
mv node-v22.16.0-linux-x64-musl /usr/local/node
|
||||
|
||||
export PATH=\"/usr/local/node/bin:\$PATH\"
|
||||
node --version
|
||||
|
||||
echo Node: \$(node -v)
|
||||
echo NPM: \$(npm -v)
|
||||
|
||||
# Install PNPM
|
||||
npm i -g pnpm@${PNPM_VERSION} --force
|
||||
pnpm --version
|
||||
npm i -g pnpm@\${PNPM_VERSION} --force
|
||||
|
||||
# Help clang find GCC runtime (crtbeginS.o, libgcc) and use lld for jemalloc build
|
||||
GCC_DIR=\$(dirname \$(find /usr/lib/gcc -name crtbeginS.o | head -1))
|
||||
export CFLAGS=\"\${CFLAGS} -fuse-ld=lld --gcc-install-dir=\${GCC_DIR}\"
|
||||
|
||||
# Install deps and run native build
|
||||
pnpm install --frozen-lockfile
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
pnpm nx run-many --verbose --target=build-native -- --target=x86_64-unknown-linux-musl
|
||||
@@ -221,19 +223,20 @@ jobs:
|
||||
build: |
|
||||
set -e
|
||||
apt-get update
|
||||
apt-get install -y curl ca-certificates git xz-utils gpg
|
||||
|
||||
# Install Java 21
|
||||
apt-get install -y openjdk-21-jdk
|
||||
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
java --version
|
||||
# Install mise from the signed apt repo
|
||||
install -dm 755 /etc/apt/keyrings
|
||||
curl -fsSL https://mise.jdx.dev/gpg-key.pub | gpg --dearmor -o /etc/apt/keyrings/mise-archive-keyring.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/mise-archive-keyring.gpg arch=$(dpkg --print-architecture)] https://mise.jdx.dev/deb stable main" > /etc/apt/sources.list.d/mise.list
|
||||
apt-get update
|
||||
apt-get install -y mise
|
||||
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
apt-get install -y nodejs=22.16.0-1nodesource1
|
||||
|
||||
export PATH="/usr/local/bin:$PATH"
|
||||
node --version
|
||||
npm --version
|
||||
# Provision Node, Java, .NET, Maven, corepack from mise.toml
|
||||
cd /build
|
||||
mise trust mise.toml
|
||||
mise install
|
||||
eval "$(mise env -s bash)"
|
||||
|
||||
# Help clang find GCC runtime (crtbeginS.o, libgcc) and use lld for jemalloc build
|
||||
export CFLAGS="${CFLAGS} -fuse-ld=lld --gcc-toolchain=/usr/aarch64-unknown-linux-gnu"
|
||||
@@ -242,8 +245,8 @@ jobs:
|
||||
# Linux kernels with 4K/16K/64K pages (Asahi, Ampere, Graviton, etc.).
|
||||
export JEMALLOC_SYS_WITH_LG_PAGE=16
|
||||
|
||||
npm i -g pnpm@${PNPM_VERSION} --force
|
||||
pnpm --version
|
||||
corepack enable
|
||||
corepack prepare --activate
|
||||
|
||||
pnpm install --frozen-lockfile
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
@@ -271,26 +274,29 @@ jobs:
|
||||
build: |
|
||||
bash -c "
|
||||
set -e
|
||||
# mise's core node/java backends don't ship musl binaries and fall back to
|
||||
# compile-from-source on Alpine, which fails. Install via apk + tarball instead.
|
||||
echo 'https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories
|
||||
apk add --no-cache curl xz openjdk21 build-base lld
|
||||
apk add --no-cache curl xz openjdk21 build-base lld dotnet9-sdk
|
||||
|
||||
# Set up Java 21
|
||||
# Java 21
|
||||
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk
|
||||
export PATH=\"\$JAVA_HOME/bin:\$PATH\"
|
||||
java --version
|
||||
|
||||
# .NET 9 SDK (needed by @nx/dotnet plugin)
|
||||
dotnet --version
|
||||
|
||||
# Node.js musl build from unofficial-builds.nodejs.org. Container is x64;
|
||||
# rust cross-compiles to aarch64-unknown-linux-musl, so the host node binary
|
||||
# is x64-musl regardless of the build target.
|
||||
curl -fsSL https://unofficial-builds.nodejs.org/download/release/v22.16.0/node-v22.16.0-linux-x64-musl.tar.xz -o node.tar.xz
|
||||
tar -xJf node.tar.xz
|
||||
mv node-v22.16.0-linux-x64-musl /usr/local/node
|
||||
|
||||
export PATH=\"/usr/local/node/bin:\$PATH\"
|
||||
node --version
|
||||
|
||||
echo Node: \$(node -v)
|
||||
echo NPM: \$(npm -v)
|
||||
|
||||
# Install PNPM
|
||||
npm i -g pnpm@${PNPM_VERSION} --force
|
||||
pnpm --version
|
||||
npm i -g pnpm@\${PNPM_VERSION} --force
|
||||
|
||||
# Help clang find GCC runtime (crtbeginS.o, libgcc) and use lld for jemalloc build
|
||||
GCC_DIR=\$(dirname \$(find /aarch64-linux-musl-cross/lib/gcc -name crtbeginS.o | head -1))
|
||||
@@ -300,7 +306,6 @@ jobs:
|
||||
# Linux kernels with 4K/16K/64K pages (Asahi, Ampere, Graviton, etc.).
|
||||
export JEMALLOC_SYS_WITH_LG_PAGE=16
|
||||
|
||||
# Install deps and run native build
|
||||
pnpm install --frozen-lockfile
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
pnpm nx run-many --verbose --target=build-native -- --target=aarch64-unknown-linux-musl
|
||||
@@ -309,6 +314,7 @@ jobs:
|
||||
target: aarch64-pc-windows-msvc
|
||||
setup: |-
|
||||
choco install openjdk --version=21.0.0 -y
|
||||
choco install dotnet-9.0-sdk -y
|
||||
rustup target add aarch64-pc-windows-msvc
|
||||
build: |
|
||||
export JAVA_HOME="C:\Program Files\OpenJDK\jdk-21"
|
||||
@@ -348,11 +354,6 @@ jobs:
|
||||
target/
|
||||
key: ${{ matrix.settings.target }}-cargo-registry
|
||||
|
||||
- uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2.2.1
|
||||
if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' }}
|
||||
with:
|
||||
version: 0.10.0
|
||||
|
||||
- name: Setup toolchain
|
||||
run: ${{ matrix.settings.setup }}
|
||||
if: ${{ matrix.settings.setup }}
|
||||
@@ -387,6 +388,7 @@ jobs:
|
||||
echo "$BUILD_SCRIPT" > "$SCRIPT_FILE"
|
||||
docker run --rm \
|
||||
--user 0:0 \
|
||||
-e NODE_VERSION \
|
||||
-e PNPM_VERSION \
|
||||
-e NX_GRADLE_PROJECT_GRAPH_TIMEOUT \
|
||||
-e NX_VERBOSE_LOGGING \
|
||||
@@ -435,18 +437,19 @@ jobs:
|
||||
PLAYWRIGHT_BROWSERS_PATH: 0
|
||||
NODE_VERSION: 22.16.0
|
||||
NX_GRADLE_DISABLE: 'true'
|
||||
NX_DOTNET_DISABLE: 'true'
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
with:
|
||||
operating_system: freebsd
|
||||
version: '14.0'
|
||||
architecture: x86-64
|
||||
environment_variables: DEBUG RUSTUP_IO_THREADS CI NX_PREFER_TS_NODE PLAYWRIGHT_BROWSERS_PATH NODE_VERSION NX_GRADLE_DISABLE NODE_OPTIONS
|
||||
environment_variables: DEBUG RUSTUP_IO_THREADS CI NX_PREFER_TS_NODE PLAYWRIGHT_BROWSERS_PATH NODE_VERSION NX_GRADLE_DISABLE NX_DOTNET_DISABLE NODE_OPTIONS
|
||||
shell: bash
|
||||
run: |
|
||||
env
|
||||
whoami
|
||||
sudo pkg install -y -f node libnghttp2 www/npm git ca_root_nss
|
||||
sudo npm install --location=global --ignore-scripts pnpm@10.28.2
|
||||
sudo npm install --location=global --ignore-scripts pnpm@11.2.2
|
||||
curl https://sh.rustup.rs -sSf --output rustup.sh
|
||||
sh rustup.sh -y --profile minimal --default-toolchain stable
|
||||
source "$HOME/.cargo/env"
|
||||
@@ -597,6 +600,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Restore .NET packages
|
||||
run: dotnet restore nx.sln
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
|
||||
@@ -107,6 +107,7 @@ node_modules/
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
!/nx.sln
|
||||
*.sw?
|
||||
.specstory/**
|
||||
.cursorindexingignore
|
||||
@@ -143,6 +144,8 @@ test-results
|
||||
# .NET build output
|
||||
/packages/dotnet/analyzer/bin
|
||||
/packages/dotnet/analyzer/obj
|
||||
/packages/dotnet/analyzer.Tests/bin
|
||||
/packages/dotnet/analyzer.Tests/obj
|
||||
/*.deb
|
||||
.nx/polygraph
|
||||
.claude/worktrees
|
||||
|
||||
@@ -11,10 +11,10 @@ common-env-vars: &common-env-vars
|
||||
|
||||
common-init-steps: &common-init-steps
|
||||
- name: Checkout
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/checkout/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/checkout/main.yaml'
|
||||
|
||||
- name: Cache restore
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/cache/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/cache/main.yaml'
|
||||
inputs:
|
||||
key: 'pnpm-lock.yaml'
|
||||
paths: ~/.local/share/pnpm/store
|
||||
@@ -22,7 +22,7 @@ common-init-steps: &common-init-steps
|
||||
|
||||
# reads mise.toml and installs toolchains needed for repo
|
||||
- name: Setup toolchains
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-mise/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-mise/main.yaml'
|
||||
|
||||
- name: Verify toolchain versions
|
||||
script: |
|
||||
@@ -36,11 +36,17 @@ common-init-steps: &common-init-steps
|
||||
|
||||
- name: Install system deps
|
||||
script: |
|
||||
# archive.ubuntu.com periodically serves a Packages.gz that doesn't
|
||||
# match its own InRelease metadata while the canonical mirror is
|
||||
# mid-sync, breaking apt-get update across all agents at once.
|
||||
# Azure's mirror has a better SLA and is what GitHub Actions runners use.
|
||||
sudo sed -i 's|http://archive.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g; s|http://security.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list
|
||||
# apt mirror+file failover: tab-separated; printf preserves \t (YAML heredocs don't).
|
||||
printf 'https://archive.ubuntu.com/ubuntu/\tpriority:1\n' | sudo tee /etc/apt/apt-mirrors.txt > /dev/null
|
||||
printf 'https://security.ubuntu.com/ubuntu/\tpriority:2\n' | sudo tee -a /etc/apt/apt-mirrors.txt > /dev/null
|
||||
printf 'http://azure.archive.ubuntu.com/ubuntu/\tpriority:3\n' | sudo tee -a /etc/apt/apt-mirrors.txt > /dev/null
|
||||
# Retries=0: mirror+file already retries via failover; apt-level retries multiply stall on a dead mirror.
|
||||
sudo tee /etc/apt/apt.conf.d/80-nx-mirror-failover > /dev/null <<'EOF'
|
||||
Acquire::http::Timeout "5";
|
||||
Acquire::https::Timeout "5";
|
||||
Acquire::Retries "0";
|
||||
EOF
|
||||
sudo sed -i 's|http://archive.ubuntu.com/ubuntu|mirror+file:/etc/apt/apt-mirrors.txt|g; s|http://security.ubuntu.com/ubuntu|mirror+file:/etc/apt/apt-mirrors.txt|g' /etc/apt/sources.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ca-certificates lsof libvips-dev libglib2.0-dev libgirepository1.0-dev zip unzip
|
||||
|
||||
@@ -66,6 +72,10 @@ common-init-steps: &common-init-steps
|
||||
./gradlew wrapper
|
||||
./gradlew --version
|
||||
|
||||
- name: Restore .NET analyzer projects
|
||||
script: |
|
||||
dotnet restore nx.sln
|
||||
|
||||
- name: Configure git metadata (needed for lerna smoke tests)
|
||||
script: |
|
||||
git config --global user.email test@test.com
|
||||
|
||||
@@ -107,15 +107,6 @@ assignment-rules:
|
||||
- agent: linux-extra-large
|
||||
parallelism: 6
|
||||
|
||||
# TODO(altan): remove when scheduling issue resolved
|
||||
- projects:
|
||||
- nx-dev
|
||||
targets:
|
||||
- prebuild-banner
|
||||
run-on:
|
||||
- agent: linux-extra-large
|
||||
parallelism: 6
|
||||
|
||||
- targets:
|
||||
- "*"
|
||||
run-on:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
exclude-reads:
|
||||
- packages/nx/src/native/*.node
|
||||
- packages/nx/dist/src/native/*.node
|
||||
- 'dist/target/**'
|
||||
exclude-writes:
|
||||
- '**/.swc/**'
|
||||
|
||||
Generated
+655
-87
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<UseSharedCompilation>false</UseSharedCompilation>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -60,22 +60,17 @@ If you want to file a bug or submit a PR, read up on our [guidelines for contrib
|
||||
|  |  |  |  |
|
||||
| [JamesHenry](https://github.com/JamesHenry) | [cammisuli](https://github.com/cammisuli) | [MaxKless](https://github.com/MaxKless) | [juristr](https://github.com/juristr) |
|
||||
|
||||
| Philip Fulcher | Caleb Ukle | Colum Ferry | Steven Nance |
|
||||
| ------------------------------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
|  |  |  |  |
|
||||
| [philipjfulcher](https://github.com/philipjfulcher) | [barbados-clemens](https://github.com/barbados-clemens) | [Coly010](https://github.com/Coly010) | [llwt](https://github.com/llwt) |
|
||||
| Caleb Ukle | Steven Nance | Miroslav Jonaš | Leosvel Pérez Espinosa |
|
||||
| --------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
||||
|  |  |  |  |
|
||||
| [barbados-clemens](https://github.com/barbados-clemens) | [llwt](https://github.com/llwt) | [meeroslav](https://github.com/meeroslav) | [leosvelperez](https://github.com/leosvelperez) |
|
||||
|
||||
| Miroslav Jonaš | Leosvel Pérez Espinosa | Zachary DeRose | Craigory Coppola |
|
||||
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
|
||||
|  |  |  |  |
|
||||
| [meeroslav](https://github.com/meeroslav) | [leosvelperez](https://github.com/leosvelperez) | [ZackDeRose](https://github.com/ZackDeRose) | [AgentEnder](https://github.com/AgentEnder) |
|
||||
| Zachary DeRose | Craigory Coppola | Chau Tran | Nicole Oliver |
|
||||
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
|  |  |  |  |
|
||||
| [ZackDeRose](https://github.com/ZackDeRose) | [AgentEnder](https://github.com/AgentEnder) | [nartc](https://github.com/nartc) | [nixallover](https://github.com/nixallover) |
|
||||
|
||||
| Chau Tran | Nicole Oliver | Rares Matei | Altan Stalker |
|
||||
| -------------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
|  |  |  |  |
|
||||
| [nartc](https://github.com/nartc) | [nixallover](https://github.com/nixallover) | [rarmatei](https://github.com/rarmatei) | [StalkAltan](https://github.com/StalkAltan) |
|
||||
|
||||
| Josh VanAllen | Austin Fahsl | Louie Weng |
|
||||
| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
|  |  |  |
|
||||
| [joshvanallen](https://github.com/joshvanallen) | [fahslaj](https://github.com/fahslaj) | [lourw](https://github.com/lourw) |
|
||||
| Rares Matei | Altan Stalker | Josh VanAllen | Louie Weng |
|
||||
| --------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
|  |  |  |  |
|
||||
| [rarmatei](https://github.com/rarmatei) | [StalkAltan](https://github.com/StalkAltan) | [joshvanallen](https://github.com/joshvanallen) | [lourw](https://github.com/lourw) |
|
||||
|
||||
@@ -160,3 +160,10 @@ Nx.Headings = NO
|
||||
|
||||
[src/content/docs/troubleshooting/unknown-local-cache.mdoc]
|
||||
Nx.Headings = NO
|
||||
|
||||
# Verbatim legal text - skip Nx voice rules
|
||||
[src/content/docs/reference/powerpack-license.mdoc]
|
||||
BasedOnStyles =
|
||||
Nx.Headings = NO
|
||||
Nx.ProductPossessives = NO
|
||||
Nx.SerialComma = NO
|
||||
|
||||
@@ -41,6 +41,30 @@ export default defineConfig({
|
||||
rehypePlugins: [rehypeTableOptionLinks],
|
||||
},
|
||||
trailingSlash: 'never',
|
||||
redirects: {
|
||||
'/guides/nx-cloud/ci-resource-usage':
|
||||
'/docs/features/ci-features/resource-usage',
|
||||
'/reference/remote-cache-plugins':
|
||||
'/docs/reference/deprecated/self-hosted-cache-packages',
|
||||
'/reference/remote-cache-plugins/s3-cache':
|
||||
'/docs/reference/deprecated/self-hosted-cache-packages',
|
||||
'/reference/remote-cache-plugins/s3-cache/overview':
|
||||
'/docs/reference/deprecated/self-hosted-cache-packages',
|
||||
'/reference/remote-cache-plugins/gcs-cache':
|
||||
'/docs/reference/deprecated/self-hosted-cache-packages',
|
||||
'/reference/remote-cache-plugins/gcs-cache/overview':
|
||||
'/docs/reference/deprecated/self-hosted-cache-packages',
|
||||
'/reference/remote-cache-plugins/azure-cache':
|
||||
'/docs/reference/deprecated/self-hosted-cache-packages',
|
||||
'/reference/remote-cache-plugins/azure-cache/overview':
|
||||
'/docs/reference/deprecated/self-hosted-cache-packages',
|
||||
'/reference/remote-cache-plugins/shared-fs-cache':
|
||||
'/docs/reference/deprecated/self-hosted-cache-packages',
|
||||
'/reference/remote-cache-plugins/shared-fs-cache/overview':
|
||||
'/docs/reference/deprecated/self-hosted-cache-packages',
|
||||
'/reference/remote-cache-plugins/shared-fs-cache/generators':
|
||||
'/docs/reference/deprecated/self-hosted-cache-packages',
|
||||
},
|
||||
// This adapter doesn't support local previews, so only load it on Netlify.
|
||||
adapter: process.env['NETLIFY'] ? netlify() : undefined,
|
||||
integrations: [
|
||||
|
||||
@@ -70,6 +70,35 @@ test.describe('CLI sub-command formatting', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('nested sub-commands beyond two levels are documented', async ({
|
||||
page,
|
||||
}) => {
|
||||
const mainContent = page.getByTestId('main-pane');
|
||||
|
||||
// "nx show target inputs" is a third-level command — it should render as h3
|
||||
// alongside other sub-commands and include its usage block.
|
||||
const showTargetInputsHeading = mainContent.getByRole('heading', {
|
||||
name: 'nx show target inputs',
|
||||
level: 3,
|
||||
exact: true,
|
||||
});
|
||||
await expect(showTargetInputsHeading).toBeVisible();
|
||||
|
||||
const showTargetOutputsHeading = mainContent.getByRole('heading', {
|
||||
name: 'nx show target outputs',
|
||||
level: 3,
|
||||
exact: true,
|
||||
});
|
||||
await expect(showTargetOutputsHeading).toBeVisible();
|
||||
|
||||
const codeBlocks = mainContent.locator('pre code');
|
||||
const allCodeTexts = await codeBlocks.allTextContents();
|
||||
|
||||
expect(
|
||||
allCodeTexts.some((text) => text.includes('nx show target inputs'))
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('release sub-commands use correct heading and usage format', async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[build.environment]
|
||||
NX_GRADLE_DISABLE = "true"
|
||||
NX_MAVEN_DISABLE = "true"
|
||||
NX_DOTNET_DISABLE = "true"
|
||||
|
||||
# Edge functions are auto-discovered from netlify/edge-functions/
|
||||
# Path configuration is in each function's inline `config` export
|
||||
@@ -83,6 +84,57 @@ to = "/docs/technologies/angular/migration/angular"
|
||||
from = "/docs/getting-started/nx-cloud"
|
||||
to = "/docs/features/ci-features"
|
||||
|
||||
# NX-4480: Self-hosted remote cache packages deprecated (CVE-2025-36852)
|
||||
[[redirects]]
|
||||
from = "/docs/reference/remote-cache-plugins"
|
||||
to = "/docs/reference/deprecated/self-hosted-cache-packages"
|
||||
|
||||
[[redirects]]
|
||||
from = "/docs/reference/remote-cache-plugins/s3-cache"
|
||||
to = "/docs/reference/deprecated/self-hosted-cache-packages"
|
||||
|
||||
[[redirects]]
|
||||
from = "/docs/reference/remote-cache-plugins/s3-cache/overview"
|
||||
to = "/docs/reference/deprecated/self-hosted-cache-packages"
|
||||
|
||||
[[redirects]]
|
||||
from = "/docs/reference/remote-cache-plugins/gcs-cache"
|
||||
to = "/docs/reference/deprecated/self-hosted-cache-packages"
|
||||
|
||||
[[redirects]]
|
||||
from = "/docs/reference/remote-cache-plugins/gcs-cache/overview"
|
||||
to = "/docs/reference/deprecated/self-hosted-cache-packages"
|
||||
|
||||
[[redirects]]
|
||||
from = "/docs/reference/remote-cache-plugins/azure-cache"
|
||||
to = "/docs/reference/deprecated/self-hosted-cache-packages"
|
||||
|
||||
[[redirects]]
|
||||
from = "/docs/reference/remote-cache-plugins/azure-cache/overview"
|
||||
to = "/docs/reference/deprecated/self-hosted-cache-packages"
|
||||
|
||||
[[redirects]]
|
||||
from = "/docs/reference/remote-cache-plugins/shared-fs-cache"
|
||||
to = "/docs/reference/deprecated/self-hosted-cache-packages"
|
||||
|
||||
[[redirects]]
|
||||
from = "/docs/reference/remote-cache-plugins/shared-fs-cache/overview"
|
||||
to = "/docs/reference/deprecated/self-hosted-cache-packages"
|
||||
|
||||
[[redirects]]
|
||||
from = "/docs/reference/remote-cache-plugins/shared-fs-cache/generators"
|
||||
to = "/docs/reference/deprecated/self-hosted-cache-packages"
|
||||
|
||||
# DOC-503: CI page moved out of tutorials to top-level Getting Started
|
||||
[[redirects]]
|
||||
from = "/docs/getting-started/tutorials/self-healing-ci-tutorial"
|
||||
to = "/docs/getting-started/setup-ci"
|
||||
|
||||
# Resource usage consolidated into the Nx Cloud add-ons platform feature pages
|
||||
[[redirects]]
|
||||
from = "/docs/guides/nx-cloud/ci-resource-usage"
|
||||
to = "/docs/features/ci-features/resource-usage"
|
||||
|
||||
# Rewrite for base path handling (keeps URL the same)
|
||||
[[redirects]]
|
||||
from = "/docs/*"
|
||||
|
||||
+14
-1
@@ -53,7 +53,20 @@
|
||||
"production",
|
||||
"^production",
|
||||
"{projectRoot}/src/content/banner.json",
|
||||
{ "env": "NX_DEV_URL" }
|
||||
{ "env": "NX_DEV_URL" },
|
||||
"{workspaceRoot}/packages/*/package.json",
|
||||
"{workspaceRoot}/packages/*/{generators,executors,migrations}.json",
|
||||
"{workspaceRoot}/packages/*/src/{generators,executors}/**/schema.json",
|
||||
"{workspaceRoot}/packages/*/src/migrations/**/*.md",
|
||||
"{workspaceRoot}/packages/*/docs/**/*.md",
|
||||
{
|
||||
"json": "{workspaceRoot}/tsconfig.json",
|
||||
"fields": ["compilerOptions"]
|
||||
},
|
||||
{
|
||||
"dependentTasksOutputFiles": "**/*.{d.ts,json,md,js,cjs,mjs}",
|
||||
"transitive": true
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
"{projectRoot}/dist",
|
||||
|
||||
@@ -211,11 +211,38 @@
|
||||
});
|
||||
}
|
||||
|
||||
function deriveCodeId(text) {
|
||||
return (text || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '')
|
||||
.slice(0, 60);
|
||||
}
|
||||
|
||||
function setupCodeBlockCopyTracking() {
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target;
|
||||
if (!(target instanceof Element)) return;
|
||||
const button = target.closest('figure.frame .copy button[data-code]');
|
||||
if (!button) return;
|
||||
// Expressive Code encodes newlines as DEL (0x7F) in the data-code attribute.
|
||||
const code = (button.getAttribute('data-code') || '').replace(
|
||||
/\x7F/g,
|
||||
'\n'
|
||||
);
|
||||
pushGtmEvent('code_block_copy', {
|
||||
event_category: 'docs_interaction',
|
||||
event_label: window.location.pathname,
|
||||
code_id: deriveCodeId(code),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const initializeAnalytics = () => {
|
||||
if (!gtmMeasurementId) return;
|
||||
loadGTM();
|
||||
setupSearchTracking();
|
||||
setupScrollTracking();
|
||||
setupCodeBlockCopyTracking();
|
||||
};
|
||||
|
||||
// Add GTM noscript iframe to body
|
||||
|
||||
+29
-17
@@ -34,6 +34,7 @@ const learnGroups: SidebarItems = [
|
||||
link: 'getting-started/start-with-existing-project',
|
||||
},
|
||||
{ label: 'AI integrations', link: 'getting-started/ai-setup' },
|
||||
{ label: 'CI setup', link: 'getting-started/setup-ci' },
|
||||
{ label: 'Editor setup', link: 'getting-started/editor-setup' },
|
||||
{
|
||||
label: 'Tutorials',
|
||||
@@ -67,10 +68,6 @@ const learnGroups: SidebarItems = [
|
||||
label: 'Reducing boilerplate',
|
||||
link: 'getting-started/tutorials/reducing-configuration-boilerplate',
|
||||
},
|
||||
{
|
||||
label: 'Setting up CI',
|
||||
link: 'getting-started/tutorials/self-healing-ci-tutorial',
|
||||
},
|
||||
{
|
||||
label: 'Gradle monorepo',
|
||||
link: 'getting-started/tutorials/gradle-tutorial',
|
||||
@@ -190,10 +187,6 @@ const learnGroups: SidebarItems = [
|
||||
label: 'Dynamically allocate agents',
|
||||
link: 'features/ci-features/dynamic-agents',
|
||||
},
|
||||
{
|
||||
label: 'CI resource usage',
|
||||
link: 'guides/nx-cloud/ci-resource-usage',
|
||||
},
|
||||
{
|
||||
label: 'Optimize your TTG',
|
||||
link: 'guides/nx-cloud/optimize-your-ttg',
|
||||
@@ -206,17 +199,36 @@ const learnGroups: SidebarItems = [
|
||||
label: 'GitHub integration',
|
||||
link: 'features/ci-features/github-integration',
|
||||
},
|
||||
{
|
||||
label: 'Sandboxing',
|
||||
link: 'features/ci-features/sandboxing',
|
||||
badge: 'New',
|
||||
},
|
||||
{
|
||||
label: 'CIPE affected project graph',
|
||||
link: 'guides/nx-cloud/cipe-affected-project-graph',
|
||||
},
|
||||
{ label: 'Encryption', link: 'guides/nx-cloud/encryption' },
|
||||
{ label: 'Google auth', link: 'guides/nx-cloud/google-auth' },
|
||||
{
|
||||
label: 'Resource usage',
|
||||
link: 'features/ci-features/resource-usage',
|
||||
},
|
||||
{
|
||||
label: 'Dedicated compute cluster',
|
||||
link: 'features/ci-features/dedicated-compute-cluster',
|
||||
},
|
||||
{
|
||||
label: 'Sandboxing',
|
||||
link: 'features/ci-features/sandboxing',
|
||||
},
|
||||
{
|
||||
label: 'Docker layer caching',
|
||||
link: 'features/ci-features/docker-layer-caching',
|
||||
},
|
||||
{
|
||||
label: 'Docker read-through cache',
|
||||
link: 'features/ci-features/docker-read-through-cache',
|
||||
},
|
||||
{
|
||||
label: 'npm read-through cache',
|
||||
link: 'features/ci-features/npm-read-through-cache',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -865,6 +877,10 @@ const knowledgeBaseGroups: SidebarItems = [
|
||||
label: 'Heartbeat and manual shutdown handling',
|
||||
link: 'concepts/ci-concepts/heartbeat-and-manual-shutdown-handling',
|
||||
},
|
||||
{
|
||||
label: 'Fix sandbox violations',
|
||||
link: 'guides/nx-cloud/fix-sandbox-violations',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1208,10 +1224,6 @@ const referenceGroups: SidebarItems = [
|
||||
label: 'Nx Cloud credit pricing',
|
||||
link: 'reference/nx-cloud/credits-pricing',
|
||||
},
|
||||
{
|
||||
label: 'Remote cache plugins',
|
||||
link: 'reference/remote-cache-plugins',
|
||||
},
|
||||
{
|
||||
label: 'Changelog',
|
||||
link: `${resolveNxDevUrl()}/changelog`,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 25 KiB |
@@ -472,14 +472,14 @@ const currentVersion = versions.find((v) => v.current);
|
||||
Contact
|
||||
</a>
|
||||
<a
|
||||
id="header-try-nx-cloud-btn"
|
||||
id="header-get-started-btn"
|
||||
href="https://cloud.nx.app/get-started?utm_source=nx-dev&utm_medium=documentation-header&utm_campaign=try-nx-cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center justify-center px-2.5 py-1.5 text-sm font-medium rounded-md transition no-underline bg-blue-500 dark:bg-sky-500 text-white hover:bg-blue-600 dark:hover:bg-sky-600 shadow-sm"
|
||||
title="Try Nx Cloud for free"
|
||||
class="inline-flex items-center justify-center px-2.5 py-1.5 text-sm font-medium rounded-md transition no-underline bg-zinc-950 dark:bg-zinc-100 text-white dark:text-zinc-950 hover:bg-zinc-900 dark:hover:bg-white shadow-sm"
|
||||
title="Get started"
|
||||
>
|
||||
Try Nx Cloud for free
|
||||
Get started
|
||||
</a>
|
||||
</div>
|
||||
<!-- Social Icons - Hide on screens smaller than 2xl (1536px) -->
|
||||
@@ -505,7 +505,7 @@ const currentVersion = versions.find((v) => v.current);
|
||||
const pricingLink = document.getElementById('header-pricing-link');
|
||||
const enterpriseLink = document.getElementById('header-enterprise-link');
|
||||
const contactBtn = document.getElementById('header-contact-btn');
|
||||
const tryNxCloudBtn = document.getElementById('header-try-nx-cloud-btn');
|
||||
const getStartedBtn = document.getElementById('header-get-started-btn');
|
||||
|
||||
docsHomeLink?.addEventListener('click', () => {
|
||||
sendCustomEventViaGtm(
|
||||
@@ -555,7 +555,7 @@ const currentVersion = versions.find((v) => v.current);
|
||||
);
|
||||
});
|
||||
|
||||
tryNxCloudBtn?.addEventListener('click', () => {
|
||||
getStartedBtn?.addEventListener('click', () => {
|
||||
sendCustomEventViaGtm(
|
||||
'login-click',
|
||||
'header-cta',
|
||||
|
||||
@@ -16,7 +16,7 @@ const id = `llm-prompt-${Math.random().toString(36).slice(2, 9)}`;
|
||||
const promptLines = finalPrompt.split('\n').filter((l) => l.length > 0);
|
||||
---
|
||||
|
||||
<llm-copy-prompt data-content-id={id}>
|
||||
<llm-copy-prompt data-content-id={id} data-prompt-title={title}>
|
||||
<details class="llm-prompt-card">
|
||||
<summary>
|
||||
<div class="llm-prompt-header">
|
||||
@@ -126,6 +126,7 @@ const promptLines = finalPrompt.split('\n').filter((l) => l.length > 0);
|
||||
border: 1px solid var(--sl-color-hairline);
|
||||
border-radius: 0.75rem;
|
||||
background: var(--sl-color-gray-6);
|
||||
padding: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@@ -133,7 +134,6 @@ const promptLines = finalPrompt.split('\n').filter((l) => l.length > 0);
|
||||
list-style: none;
|
||||
list-style-type: none;
|
||||
cursor: pointer;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.llm-prompt-card > summary::-webkit-details-marker {
|
||||
@@ -155,6 +155,8 @@ const promptLines = finalPrompt.split('\n').filter((l) => l.length > 0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--sl-color-hairline);
|
||||
}
|
||||
|
||||
.llm-prompt-icon {
|
||||
@@ -213,6 +215,7 @@ const promptLines = finalPrompt.split('\n').filter((l) => l.length > 0);
|
||||
.llm-prompt-preview {
|
||||
position: relative;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
color: var(--sl-color-gray-3);
|
||||
font-size: var(--sl-text-sm);
|
||||
line-height: 1.5;
|
||||
@@ -240,6 +243,7 @@ const promptLines = finalPrompt.split('\n').filter((l) => l.length > 0);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.llm-prompt-caret-expanded {
|
||||
@@ -262,7 +266,7 @@ const promptLines = finalPrompt.split('\n').filter((l) => l.length > 0);
|
||||
}
|
||||
|
||||
.llm-prompt-body {
|
||||
padding: 0 1.25rem 1rem;
|
||||
padding: 0 1rem 0.5rem;
|
||||
color: var(--sl-color-gray-2);
|
||||
font-size: var(--sl-text-sm);
|
||||
line-height: 1.6;
|
||||
@@ -275,7 +279,7 @@ const promptLines = finalPrompt.split('\n').filter((l) => l.length > 0);
|
||||
.llm-prompt-body :global(ol),
|
||||
.llm-prompt-body :global(ul) {
|
||||
margin: 0 0 0.5rem;
|
||||
padding-left: 1.25rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.llm-prompt-body :global(li) {
|
||||
@@ -284,6 +288,8 @@ const promptLines = finalPrompt.split('\n').filter((l) => l.length > 0);
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { sendCustomEventViaGtm } from '@nx/nx-dev-feature-analytics';
|
||||
|
||||
class LlmCopyPromptElement extends HTMLElement {
|
||||
private button: HTMLButtonElement | null = null;
|
||||
private collapseCaret: HTMLElement | null = null;
|
||||
@@ -335,11 +341,22 @@ const promptLines = finalPrompt.split('\n').filter((l) => l.length > 0);
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.content);
|
||||
this.showCopiedState();
|
||||
this.trackCopy();
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
private trackCopy() {
|
||||
sendCustomEventViaGtm(
|
||||
'llm_prompt_copy',
|
||||
'docs_interaction',
|
||||
window.location.pathname,
|
||||
undefined,
|
||||
{ prompt_title: this.dataset.promptTitle ?? '' }
|
||||
);
|
||||
}
|
||||
|
||||
private showCopiedState() {
|
||||
this.button?.classList.add('copied');
|
||||
const label = this.button?.querySelector('.copy-label');
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
---
|
||||
import { type YouTubeProps, YouTube as Default } from '@nx/nx-dev-ui-common/src/lib/youtube.component'
|
||||
import {
|
||||
type YouTubeProps,
|
||||
YouTube as Default,
|
||||
} from '@nx/nx-dev-ui-common/src/lib/youtube.component';
|
||||
type Props = YouTubeProps;
|
||||
---
|
||||
|
||||
<Default {...Astro.props} />
|
||||
<youtube-jsapi>
|
||||
<Default {...Astro.props} />
|
||||
</youtube-jsapi>
|
||||
|
||||
<script>
|
||||
// Patch iframe src with enablejsapi=1 so GA4 enhanced measurement
|
||||
// (video_start, video_progress, video_complete) can hook into the player.
|
||||
class YouTubeJsapiElement extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const iframe = this.querySelector('iframe');
|
||||
if (!(iframe instanceof HTMLIFrameElement)) return;
|
||||
try {
|
||||
const url = new URL(iframe.src);
|
||||
if (url.searchParams.get('enablejsapi') === '1') return;
|
||||
url.searchParams.set('enablejsapi', '1');
|
||||
iframe.src = url.toString();
|
||||
} catch {
|
||||
// Ignore malformed URLs.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('youtube-jsapi', YouTubeJsapiElement);
|
||||
</script>
|
||||
|
||||
@@ -558,5 +558,10 @@
|
||||
"name": "@frontenderz/backstage-insights",
|
||||
"description": "An Nx plugin to generate a Backstage software catalog from your monorepo, including components, dependencies, and ownership.",
|
||||
"url": "https://github.com/frontenderz/frontenderz-nx-plugins/tree/main/packages/backstage-insights"
|
||||
},
|
||||
{
|
||||
"name": "@anarchitects/nx-typeorm",
|
||||
"description": "Nx plugin for TypeORM integration in Nx backend applications and libraries.",
|
||||
"url": "https://github.com/anarchitects/anarchitecture-plugins/tree/main/packages/typeorm"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -55,7 +55,7 @@ In order to guarantee that cache poisoning will never affect your end users, [sk
|
||||
|
||||
### Do not manually share your local cache
|
||||
|
||||
Nx implicitly trusts the local cache which is stored by default in the `.nx/cache` folder. You can change the location of that folder in the `nx.json` file, so it could be tempting to place it on a network drive and easily share your cache with everyone on the company network. However, by doing this you've voided the guarantee of immutability from your cache. If someone has direct access to the cached files, they could directly poison the cache. Nx will automatically detect if a cache entry has been created in your local cache using a different machine and warn you with an [Unknown Local Cache Error](/docs/troubleshooting/unknown-local-cache). Instead, use Nx Cloud [remote caching](/docs/features/ci-features/remote-cache). If you want share your local cache anyway, you can use the [`@nx/shared-fs-cache`](/docs/reference/remote-cache-plugins/shared-fs-cache) plugin.
|
||||
Nx implicitly trusts the local cache which is stored by default in the `.nx/cache` folder. You can change the location of that folder in the `nx.json` file, so it could be tempting to place it on a network drive and easily share your cache with everyone on the company network. However, by doing this you've voided the guarantee of immutability from your cache. If someone has direct access to the cached files, they could directly poison the cache. Nx will automatically detect if a cache entry has been created in your local cache using a different machine and warn you with an [Unknown Local Cache Error](/docs/troubleshooting/unknown-local-cache). Instead, use Nx Cloud [remote caching](/docs/features/ci-features/remote-cache).
|
||||
|
||||
### Configure end to end encryption
|
||||
|
||||
|
||||
@@ -43,3 +43,7 @@ the `CODEOWNERS` file for that.
|
||||
|
||||
If you want to know more about code ownership on GitHub, please
|
||||
check [the documentation on the `CODEOWNERS` file](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners).
|
||||
|
||||
{% aside type="tip" title="Define ownership by project" %}
|
||||
Maintaining a `CODEOWNERS` file by hand means revisiting it every time a project moves or a new project is added. The [`@nx/owners` plugin](/docs/reference/owners/overview) lets you define code ownership based on projects -- using the same project matcher syntax as [`nx run-many`](/docs/reference/nx-commands#nx-run-many) -- and compiles it into a valid `CODEOWNERS` file for GitHub, Bitbucket, or GitLab.
|
||||
{% /aside %}
|
||||
|
||||
@@ -38,10 +38,20 @@ Configure a webhook and give it a secret:
|
||||
|
||||

|
||||
|
||||
Make sure you subscribe to the "Organization" events:
|
||||
## Configure permissions for the GitHub app
|
||||
|
||||
Configure permissions **before** subscribing to events. GitHub only shows event subscriptions for the permissions you've granted, so the "Organization" event won't appear until you've enabled the corresponding organization permission.
|
||||
|
||||
See the [GitHub App Permissions](/docs/guides/nx-cloud/source-control-integration/github-app-permissions) reference for the full list of required permissions and a detailed breakdown of what each one is used for.
|
||||
|
||||
## Subscribe to webhook events
|
||||
|
||||
Once permissions are set, subscribe to the "Organization" events:
|
||||
|
||||

|
||||
|
||||
## Record the app credentials
|
||||
|
||||
Once you create the app, keep a note of the Client ID and App ID:
|
||||
|
||||

|
||||
@@ -62,10 +72,6 @@ awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' your-key.pem # keep a note of the o
|
||||
|
||||
Save the output of the above, as we'll also use it in a bit.
|
||||
|
||||
## Configure permissions for the GitHub app
|
||||
|
||||
See the [GitHub App Permissions](/docs/guides/nx-cloud/source-control-integration/github-app-permissions) reference for the full list of required permissions and a detailed breakdown of what each one is used for.
|
||||
|
||||
## Connect your Nx Cloud installation
|
||||
|
||||
Provide the following values to your developer productivity engineer so they can help connect Nx Cloud to your custom GitHub app:
|
||||
|
||||
@@ -21,7 +21,8 @@ nx add nx-cfonts
|
||||
Nx provides a utility (`nx list`) that lists both core and community plugins. You can submit your plugin to be added to this list, but it needs to meet a few criteria first:
|
||||
|
||||
- Run some kind of automated e2e tests in your repository
|
||||
- Include `@nx/devkit` as a `dependency` in the plugin's `package.json`
|
||||
- Include `@nx/devkit` as a `dependency` (not a `peerDependency`), so your plugin pins the version it was tested against. `@nx/devkit` itself declares a peer dependency on `nx` spanning its own major plus one before and one after — for example, depending on `@nx/devkit@23` means your plugin works on `nx@22`, `nx@23`, and the upcoming `nx@24`.
|
||||
- Do **not** list `nx` itself as a direct `dependency` or `peerDependency` — the user's workspace already provides it, and `@nx/devkit` handles the version range for you.
|
||||
- List a `repository.url` in the plugin's `package.json`
|
||||
|
||||
```jsonc
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: 'Dedicated Compute Cluster'
|
||||
description: 'Reserve an isolated Nx Cloud compute environment for your organization and unlock Docker-in-Docker, sandboxing, and read-through caches.'
|
||||
keywords:
|
||||
[
|
||||
dedicated compute,
|
||||
single-tenant,
|
||||
docker-in-docker,
|
||||
nx agents,
|
||||
add-on,
|
||||
nx cloud,
|
||||
]
|
||||
sidebar:
|
||||
label: Dedicated compute cluster
|
||||
order: 20
|
||||
badge: new!
|
||||
filter: 'type:Features'
|
||||
---
|
||||
|
||||
A **dedicated compute cluster** reserves an isolated Nx Cloud compute environment for your
|
||||
organization, so your [Nx Agents](/docs/features/ci-features/distribute-task-execution) run in a
|
||||
cluster provisioned just for your org instead of on the shared multi-tenant pool.
|
||||
|
||||
The dedicated cluster lets agents run Docker-in-Docker (DinD) and unlocks additional Nx Cloud
|
||||
add-ons that require isolation in order to run with elevated capabilities.
|
||||
|
||||
{% aside type="note" title="Nx Cloud add-on" %}
|
||||
The **dedicated compute cluster** is an Nx Cloud add-on. Manage it under
|
||||
**Settings > Add-ons** for your organization. Nx Enterprise customers on
|
||||
[single-tenant](/docs/enterprise/single-tenant/overview) deployments already run in a dedicated
|
||||
environment and get these capabilities through their deployment.
|
||||
{% /aside %}
|
||||
|
||||
## What it unlocks
|
||||
|
||||
A dedicated compute cluster unlocks the following:
|
||||
|
||||
- [**Docker-in-Docker**](#docker-in-docker-on-agents) - build and push container images, run
|
||||
Testcontainers, and run any task that needs a Docker daemon, directly on Nx Agents.
|
||||
- [**Sandboxing**](/docs/features/ci-features/sandboxing) - confine each task to its declared
|
||||
[`inputs`](/docs/reference/project-configuration#inputs-and-named-inputs) and [`outputs`](/docs/reference/project-configuration#outputs), and catch any read or write outside them.
|
||||
- [**Docker layer caching**](/docs/features/ci-features/docker-layer-caching) - reuse Docker build
|
||||
layers across CI runs.
|
||||
- [**Docker read-through cache**](/docs/features/ci-features/docker-read-through-cache) - serve
|
||||
repeated image pulls from a cache close to your agents.
|
||||
- [**npm read-through cache**](/docs/features/ci-features/npm-read-through-cache) - serve repeated
|
||||
npm installs from a cache close to your agents.
|
||||
|
||||
DinD is available on every agent in the cluster automatically. The four add-ons are enabled
|
||||
individually once the cluster is active, and are cancelled if you cancel the dedicated compute
|
||||
cluster.
|
||||
|
||||
## Docker-in-Docker on agents
|
||||
|
||||
Every agent in a dedicated compute cluster can run DinD. This lets your tasks build and push
|
||||
container images, run [Testcontainers](https://testcontainers.com), and execute any workflow that
|
||||
needs a Docker daemon, directly on Nx Agents.
|
||||
|
||||
On the shared multi-tenant pool, agents run a fixed set of approved images and cannot run privileged
|
||||
containers. The dedicated cluster lifts that restriction for your organization, so you can run
|
||||
custom agent images and DinD workloads.
|
||||
|
||||
## Enabling dedicated compute cluster
|
||||
|
||||
A dedicated compute cluster is provisioned through your organization settings:
|
||||
|
||||
1. Open **Settings > Add-ons** for your organization.
|
||||
2. On the **Dedicated compute cluster** card, click **Request add-on** and confirm.
|
||||
3. You will be notified via email when the cluster is ready to use.
|
||||
|
||||
Once the cluster is active, the dependent add-ons (sandboxing, Docker layer caching, and the
|
||||
read-through caches) become available to enable on the same page. If you request one of them before
|
||||
the cluster is ready, it is queued and activates automatically when the cluster comes online.
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: 'Docker Layer Caching'
|
||||
description: 'Cache Docker build layers across CI runs on Nx Agents to speed up image builds.'
|
||||
keywords: [docker, layer caching, buildkit, nx agents, nx cloud, add-on]
|
||||
sidebar:
|
||||
label: Docker layer caching
|
||||
order: 21
|
||||
badge: new!
|
||||
filter: 'type:Features'
|
||||
---
|
||||
|
||||
Docker layer caching reuses the intermediate layers produced by `docker build` (the result of each
|
||||
`RUN`, `COPY`, and `ADD` instruction) across CI runs. When a layer's inputs haven't changed, the
|
||||
build pulls it from a registry cache instead of rebuilding it, cutting image build times on
|
||||
[Nx Agents](/docs/features/ci-features/distribute-task-execution).
|
||||
|
||||
## How to enable it
|
||||
|
||||
{% aside type="note" title="Requires a dedicated compute cluster" %}
|
||||
Docker layer caching is an Nx Cloud add-on that runs on a
|
||||
[dedicated compute cluster](/docs/features/ci-features/dedicated-compute-cluster). Request the
|
||||
cluster, then enable this add-on under **Settings > Add-ons**.
|
||||
{% /aside %}
|
||||
|
||||
You can enable Docker layer caching through your organization settings:
|
||||
|
||||
1. Open **Settings > Add-ons** for your organization.
|
||||
2. Under **Dedicated compute cluster** card, find **Docker layer caching** and click **Request add-on** and confirm.
|
||||
|
||||
When the add-on is enabled, Nx Cloud runs a registry cache inside your dedicated cluster and injects
|
||||
its address into every agent as the `NX_DOCKER_CACHE_REGISTRY` environment variable. You point your
|
||||
Docker builds at that registry using BuildKit's `--cache-to` and `--cache-from` flags. Cached layers
|
||||
are written to and read from the in-cluster registry, so they persist across CI runs.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Set up Docker Buildx
|
||||
|
||||
Layer caching requires BuildKit. Add the Buildx setup step to the `init-steps` of the launch
|
||||
template that runs your Docker builds, so it runs before any `docker build` command:
|
||||
|
||||
```yaml
|
||||
- name: Setup Docker Buildx
|
||||
uses: 'nrwl/nx-cloud-workflows/main/workflow-steps/setup-docker-buildx/main.yaml'
|
||||
```
|
||||
|
||||
### 2. Add cache flags to your build commands
|
||||
|
||||
Update your `docker build` commands to export and import layers from the cache registry:
|
||||
|
||||
```bash
|
||||
docker build \
|
||||
--push \
|
||||
-t my-registry.example.com/my-app:1.2.3 \
|
||||
--cache-to type=registry,ref=${NX_DOCKER_CACHE_REGISTRY}/my-app:main,mode=max \
|
||||
--cache-from type=registry,ref=${NX_DOCKER_CACHE_REGISTRY}/my-app:main \
|
||||
.
|
||||
```
|
||||
|
||||
- **`-t my-registry.example.com/my-app:1.2.3`** - your image, tag, and destination registry. Your
|
||||
agents must be authenticated to that registry to push images. That setup is outside the scope of
|
||||
this guide.
|
||||
- **`${NX_DOCKER_CACHE_REGISTRY}`** - provided by Nx Cloud. It points at the in-cluster cache
|
||||
registry, which is separate from your final image registry. Don't push your application images
|
||||
there.
|
||||
- **`/my-app:main`** - the cache reference and tag. Use the `main` tag so subsequent builds reuse
|
||||
the cached layers.
|
||||
- **`--cache-to mode=max`** - exports all layers for maximum reuse. `mode=min` exports fewer layers;
|
||||
see the [Docker registry cache docs](https://docs.docker.com/build/cache/backends/registry).
|
||||
- **`--cache-from`** - imports cached layers when available.
|
||||
|
||||
{% aside type="note" title="Use stable cache image " %}
|
||||
Use the stable tag `main` for `--cache-to` and `--cache-from`. Layers stored under the `main` tag are
|
||||
kept permanently. Other tags are removed periodically, so non-`main` tags won't persist as long-lived cache.
|
||||
|
||||
Use distinct names per cache image (e.g. `my-app:main` and `my-api:main`) so different builds don't
|
||||
overwrite each other's layers.
|
||||
{% /aside %}
|
||||
|
||||
See the [Docker registry cache docs](https://docs.docker.com/build/cache/backends/registry) for more details.
|
||||
|
||||
### 3. Verify it's working
|
||||
|
||||
After the cache is warm, subsequent builds should show:
|
||||
|
||||
- `[CACHED]` markers in the `docker build` logs where layers were reused.
|
||||
- Shorter build times for unchanged layers.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: 'Docker Read-Through Cache'
|
||||
description: 'Serve repeated Docker image pulls from a cache close to your Nx Agents instead of the upstream registry.'
|
||||
keywords:
|
||||
[
|
||||
docker,
|
||||
read-through cache,
|
||||
registry mirror,
|
||||
pull-through,
|
||||
nx agents,
|
||||
nx cloud,
|
||||
add-on,
|
||||
]
|
||||
sidebar:
|
||||
label: Docker read-through cache
|
||||
order: 22
|
||||
badge: new!
|
||||
filter: 'type:Features'
|
||||
---
|
||||
|
||||
The Docker read-through cache puts a registry mirror close to your [Nx Agents](/docs/features/ci-features/distribute-task-execution).
|
||||
The first time an image is pulled, it is fetched from the upstream registry (for example Docker Hub)
|
||||
and stored in the cache. Repeated pulls of the same image are then served from the cache instead of
|
||||
the upstream registry, cutting image download time and reducing dependence on external registries.
|
||||
|
||||
It also protects your organization from Docker registry outages.
|
||||
|
||||
## How to enable it
|
||||
|
||||
{% aside type="note" title="Requires a dedicated compute cluster" %}
|
||||
The Docker read-through cache is an Nx Cloud add-on that runs on a
|
||||
[dedicated compute cluster](/docs/features/ci-features/dedicated-compute-cluster). Request the
|
||||
cluster, then enable this add-on under **Settings > Add-ons**.
|
||||
{% /aside %}
|
||||
|
||||
You can enable Docker read-through cache through your organization settings:
|
||||
|
||||
1. Open **Settings > Add-ons** for your organization.
|
||||
2. Under **Dedicated compute cluster** card, find **Docker read-through cache** and click **Request add-on** and confirm.
|
||||
|
||||
Once enabled, it works automatically. You don't change your Dockerfiles, `docker pull` commands, or
|
||||
image references. Repeated image pulls are served from the cache instead of the upstream registry.
|
||||
@@ -14,7 +14,7 @@ Any CI tool requires tight integration with your existing version control system
|
||||
|
||||
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.
|
||||
|
||||
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. [Create a new Nx workspace](https://cloud.nx.app/create-nx-workspace) to get started.
|
||||
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.
|
||||
|
||||
[Connect your Nx Cloud account to GitHub](/docs/features/ci-features/github-integration#connect-to-github) to use this feature.
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: 'npm Read-Through Cache'
|
||||
description: 'Serve repeated npm installs from a cache close to your Nx Agents instead of the public npm registry.'
|
||||
keywords:
|
||||
[npm, read-through cache, registry proxy, nx agents, nx cloud, add-on, npmrc]
|
||||
sidebar:
|
||||
label: npm read-through cache
|
||||
order: 23
|
||||
badge: new!
|
||||
filter: 'type:Features'
|
||||
---
|
||||
|
||||
The npm read-through cache puts a package registry proxy close to your
|
||||
[Nx Agents](/docs/features/ci-features/distribute-task-execution). The first time a package is
|
||||
requested, it is fetched from the public npm registry and stored in the cache. Repeated installs of
|
||||
the same package are then served from the cache instead of the upstream registry, cutting install
|
||||
time and external network usage. Once the cache is warm, packages often install faster than restoring
|
||||
them from `node_modules` caching.
|
||||
|
||||
It also protects your organization from npm registry outages.
|
||||
|
||||
## How to enable it
|
||||
|
||||
{% aside type="note" title="Requires a dedicated compute cluster" %}
|
||||
The npm read-through cache is an Nx Cloud add-on that runs on a
|
||||
[dedicated compute cluster](/docs/features/ci-features/dedicated-compute-cluster). Request the
|
||||
cluster, then enable this add-on under **Settings > Add-ons**.
|
||||
{% /aside %}
|
||||
|
||||
You can enable npm read-through cache through your organization settings:
|
||||
|
||||
1. Open **Settings > Add-ons** for your organization.
|
||||
2. Under **Dedicated compute cluster** card, find **npm read-through cache** and click **Request add-on** and confirm.
|
||||
|
||||
When the add-on is enabled, Nx Cloud runs a caching proxy in your dedicated cluster that sits in
|
||||
front of the public npm registry. You point your package manager at the proxy through an `.npmrc`
|
||||
file. Installs then flow through the cache.
|
||||
|
||||
The cache works with **npm**, **yarn**, and **pnpm**, since they all read the `registry` setting
|
||||
from `.npmrc`.
|
||||
|
||||
## Scope
|
||||
|
||||
- **Public npm packages only.** The cache proxies the public npm registry.
|
||||
- **Private and scoped packages that require authentication are not cached.** Keep their existing
|
||||
registry entries in your `.npmrc`. Those requests bypass the cache and go straight to your private
|
||||
registry.
|
||||
|
||||
## Configuration
|
||||
|
||||
Point your package manager at the cache by setting the registry in an `.npmrc`. Place it in the
|
||||
project root (simplest) or in the agent's home directory (`~/.npmrc`):
|
||||
|
||||
```ini
|
||||
# .npmrc
|
||||
registry=http://npm:4873/
|
||||
```
|
||||
|
||||
The cache is always reachable from within your cluster at the address above.
|
||||
|
||||
{% aside type="note" title="If you publish packages" %}
|
||||
If your CI publishes npm packages, set a publish registry so `npm publish` targets your real
|
||||
registry instead of the cache. Add `publishConfig.registry` to the package's `package.json`, or
|
||||
pass `npm publish --registry <url>`.
|
||||
{% /aside %}
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
title: 'Resource Usage'
|
||||
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,
|
||||
resource profiling,
|
||||
CPU,
|
||||
memory,
|
||||
out of memory,
|
||||
nx agents,
|
||||
nx cloud,
|
||||
add-on,
|
||||
]
|
||||
sidebar:
|
||||
label: Resource usage
|
||||
order: 14
|
||||
badge: new!
|
||||
filter: 'type:Features'
|
||||
---
|
||||
|
||||
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** for your
|
||||
organization.
|
||||
{% /aside %}
|
||||
|
||||
{% aside type="caution" title="Nx 22.1+ Required" %}
|
||||
Resource usage requires Nx 22.1 or later.
|
||||
{% /aside %}
|
||||
|
||||
## Enabling resource usage
|
||||
|
||||
Enable the add-on under **Settings > Add-ons**, or from the **Enable resource profiling** prompt on
|
||||
the **Analysis** tab of any CI pipeline execution.
|
||||
|
||||
Once the add-on is active:
|
||||
|
||||
- **With [Nx Agents](/docs/features/ci-features/distribute-task-execution)**, metrics are collected
|
||||
and uploaded automatically for every agent and task. There's nothing else to configure.
|
||||
- **With [manual distributed task execution](/docs/guides/nx-cloud/manual-dte)** (your own CI
|
||||
agents), add a single CLI step per agent job to upload metrics. See
|
||||
[Manual DTE](#resource-metrics-with-manual-dte) below.
|
||||
|
||||
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
|
||||
[Self-healing CI](/docs/features/ci-features/self-healing-ci) PR comment when a run hits memory or
|
||||
CPU issues.
|
||||
|
||||
## Viewing resource usage
|
||||
|
||||
Open any CI pipeline execution and go to the **Analysis** tab.
|
||||
|
||||
### 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
|
||||
agent that ran hot.
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
usage. When a task exceeds available memory and is killed, the chart marks the out-of-memory point so
|
||||
you can trace the failure back to the task that caused it.
|
||||
|
||||

|
||||
|
||||
The detail view has a few controls for digging in:
|
||||
|
||||
- **View mode** - switch between **Individual** (each task or process plotted separately) and
|
||||
**Stacked** (total usage at any point in time).
|
||||
|
||||

|
||||
|
||||
- **Reference lines** - toggle the capacity and peak-usage lines on or off.
|
||||
- **Snap to max** - zoom the axis to the peak memory or CPU value.
|
||||
- **Legend** - click items to focus on specific tasks or processes (for example a single
|
||||
`nx build`, the Nx daemon, or CLI overhead).
|
||||
|
||||

|
||||
|
||||
- **Timeline scrubber** - jump to a point in time or zoom in on a spike.
|
||||
|
||||

|
||||
|
||||
- **Download CSV** - export the raw per-process data for deeper analysis.
|
||||
|
||||
## Common use cases
|
||||
|
||||
- **Find memory-hungry tasks** - figure out which project eats the most memory when running in
|
||||
parallel, then lower its parallelism instead of slowing everything down.
|
||||
- **Debug OOM kills** - trace an out-of-memory failure to the exact task that caused it.
|
||||
- **Spot misconfigured tooling** - catch a bundler or build tool pulling in more files than it
|
||||
should.
|
||||
- **Right-size agents** - pick the correct agent resource class when moving to Nx Agents from
|
||||
GitHub Actions or another CI provider.
|
||||
- **Detect memory leaks** - look for tasks where memory keeps climbing over time.
|
||||
- **Compare before and after upgrades** - check whether a dependency upgrade spiked resource usage.
|
||||
|
||||
## Resource metrics with manual DTE
|
||||
|
||||
If you run your own CI agents instead of Nx Agents, Nx Cloud can still collect per-agent CPU and
|
||||
memory metrics. Opt in by adding a single CLI step to each agent job.
|
||||
|
||||
### What to add
|
||||
|
||||
At the end of each agent job, run `npx nx-cloud upload-agent-metrics`. Use your provider's
|
||||
always-run mechanism so the step runs even when the agent is killed mid-task, which is precisely the
|
||||
scenario where the metrics matter most.
|
||||
|
||||
Here's the GitHub Actions step:
|
||||
|
||||
```yaml
|
||||
- name: Upload agent metrics
|
||||
if: always()
|
||||
run: npx nx-cloud upload-agent-metrics
|
||||
env:
|
||||
NX_AGENT_NAME: ${{ matrix.agent }}
|
||||
```
|
||||
|
||||
The `if: always()` condition is important: if an agent is OOM-killed mid-run, the normal step
|
||||
sequence stops, but the upload still needs to happen so you can see which task caused the kill.
|
||||
|
||||
The [Manual DTE guide](/docs/guides/nx-cloud/manual-dte) shows the equivalent step for CircleCI,
|
||||
Azure Pipelines, Bitbucket Pipelines, GitLab CI, and Jenkins.
|
||||
|
||||
## Configuration
|
||||
|
||||
Metric collection is controlled by these environment variables:
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `NX_CLOUD_DISABLE_METRICS_COLLECTION` | Set to `true` to disable CPU and memory metric collection during task execution. |
|
||||
| `NX_CLOUD_METRICS_DIRECTORY` | Directory where Nx writes resource metrics during task execution. |
|
||||
@@ -1,25 +1,27 @@
|
||||
---
|
||||
title: 'Task Sandboxing'
|
||||
description: 'Hermetic task execution with IO tracing to catch undeclared dependencies and ensure correct caching.'
|
||||
keywords: [sandboxing, CI, hermeticity, IO tracing, caching]
|
||||
description: 'Confine each task to its declared inputs and outputs to catch undeclared dependencies and keep caching correct.'
|
||||
keywords: [sandboxing, CI, hermeticity, inputs, outputs, caching]
|
||||
sidebar:
|
||||
label: Sandboxing
|
||||
label: Task sandboxing
|
||||
order: 15
|
||||
badge: new!
|
||||
filter: 'type:Features'
|
||||
---
|
||||
|
||||
Task sandboxing monitors file system access during task execution and flags any reads or writes
|
||||
that fall outside the declared `inputs` and `outputs` in your
|
||||
Task sandboxing confines each task to the files it declares as `inputs` and `outputs` in your
|
||||
[project configuration](/docs/reference/project-configuration)
|
||||
(whether explicit or [inferred](/docs/concepts/inferred-tasks)).
|
||||
It doesn't block access to the rest of the file system, but undeclared dependencies have direct
|
||||
implications on [caching](/docs/features/cache-task-results) correctness, from false cache hits
|
||||
serving stale results to missing output files after a cache restore.
|
||||
Reading a file the task didn't declare, or writing outside its declared outputs, is a sandbox
|
||||
violation.
|
||||
Undeclared dependencies have direct implications on [caching](/docs/features/cache-task-results)
|
||||
correctness, from false cache hits serving stale results to missing output files after a cache
|
||||
restore.
|
||||
|
||||
{% aside type="note" title="Enterprise Feature" %}
|
||||
Sandboxing is available on the [Nx Enterprise plan](https://nx.dev/enterprise).
|
||||
[Reach out to learn more](https://nx.dev/enterprise).
|
||||
{% aside type="note" title="Nx Cloud add-on" %}
|
||||
Sandboxing is an Nx Cloud add-on that runs on a
|
||||
[dedicated compute cluster](/docs/features/ci-features/dedicated-compute-cluster). Request the
|
||||
cluster, then enable sandboxing under **Settings > Add-ons**.
|
||||
{% /aside %}
|
||||
|
||||
{% aside type="caution" title="Nx 22.6+ Required" %}
|
||||
@@ -102,14 +104,10 @@ Include both directories in `outputs` so they can be replayed from cache:
|
||||
|
||||
## How sandboxing works
|
||||
|
||||
Sandboxing runs each task in a monitored environment where all file system reads and writes are
|
||||
tracked.
|
||||
When a task accesses a file outside its declared inputs or writes to a path outside its declared
|
||||
outputs, Nx Cloud flags it.
|
||||
|
||||
Sandboxing runs each task in an isolated environment scoped to its declared `inputs` and `outputs`.
|
||||
You get an audit trail of every file each task touched during execution, warnings when tasks have
|
||||
undeclared dependencies, and confidence that your cache configuration is correct rather than just
|
||||
"working so far."
|
||||
undeclared dependencies, and confidence that your cache configuration is correct rather than only
|
||||
appearing to work.
|
||||
|
||||
In **Warning** mode (recommended when getting started), violations are reported in the Nx Cloud UI
|
||||
but tasks continue to completion.
|
||||
@@ -139,11 +137,30 @@ Files flagged as "unexpected read" or "unexpected write" are the ones not covere
|
||||
|
||||

|
||||
|
||||
To export the raw trace data for further analysis, click **View raw sandbox report** to download
|
||||
To export the raw report data for further analysis, click **View raw sandbox report** to download
|
||||
the JSON report.
|
||||
|
||||

|
||||
|
||||
Once you have identified the violating tasks, follow
|
||||
[Fix sandbox violations](/docs/guides/nx-cloud/fix-sandbox-violations)
|
||||
to download every report on a branch, classify each violation, and update your project configuration in a structured loop.
|
||||
|
||||
## Sandbox violations dashboard
|
||||
|
||||
For an organization-wide view, open **Analytics > Sandbox violations** for your workspace.
|
||||
It summarizes the most recent report for each task over a time window (the last 7 days by default)
|
||||
with two tiles, **Tasks with violations** and **Clean tasks**, and a table of every task showing its
|
||||
count of unexpected reads and writes and when it was last seen.
|
||||
Filter by branch or task to narrow it down.
|
||||
|
||||
The **How to fix these violations** panel offers two paths.
|
||||
**Fix with AI** copies a ready-made prompt for your coding agent that downloads the reports, edits
|
||||
the task config, and validates before stopping.
|
||||
The manual path gives you the equivalent command sequence.
|
||||
Either way, [Fix sandbox violations](/docs/guides/nx-cloud/fix-sandbox-violations) walks through the
|
||||
full loop.
|
||||
|
||||
## Inspecting inputs and outputs
|
||||
|
||||
Check what your tasks currently declare before enabling sandboxing.
|
||||
@@ -222,11 +239,18 @@ and reports discrepancies.
|
||||
|
||||
## Enabling sandboxing
|
||||
|
||||
Sandboxing is available for [Nx Enterprise](https://nx.dev/enterprise) customers on
|
||||
[single-tenant](/docs/enterprise/single-tenant/overview) deployments using
|
||||
Sandboxing requires a
|
||||
[dedicated compute cluster](/docs/features/ci-features/dedicated-compute-cluster) and runs on
|
||||
[Nx Agents](/docs/features/ci-features/distribute-task-execution).
|
||||
It is not supported with [manual distributed task execution](/docs/guides/nx-cloud/manual-dte).
|
||||
Contact your Nx Enterprise representative to enable sandboxing for your deployment.
|
||||
|
||||
1. Request a dedicated compute cluster under **Settings > Add-ons**, if you don't already have one.
|
||||
2. Enable **Sandboxing** on the same page. If the cluster is still being provisioned, sandboxing is
|
||||
queued and activates automatically once the cluster is ready.
|
||||
|
||||
Nx Enterprise [single-tenant](/docs/enterprise/single-tenant/overview) customers already run on a
|
||||
dedicated environment.
|
||||
Contact your Nx representative to turn on sandboxing for your deployment.
|
||||
|
||||
### Excluding paths
|
||||
|
||||
@@ -256,22 +280,15 @@ Patterns use glob syntax relative to the workspace root.
|
||||
|
||||
## Cloud settings
|
||||
|
||||
Enterprise customers with sandboxing enabled can configure the enforcement mode in the Nx Cloud
|
||||
workspace settings under **Settings > General**.
|
||||
Once sandboxing is enabled, configure the enforcement mode in the Nx Cloud workspace settings under
|
||||
**Settings > General**.
|
||||
|
||||

|
||||
|
||||
Three enforcement modes are available:
|
||||
|
||||
- **Strict** — tasks that violate sandbox isolation fail immediately.
|
||||
- **Warning** — tasks complete but violations are reported in the Nx Cloud UI.
|
||||
- **Off** — sandboxing is disabled.
|
||||
- **Strict** - tasks that violate sandbox isolation fail immediately.
|
||||
- **Warning** - tasks complete but violations are reported in the Nx Cloud UI.
|
||||
- **Off** - sandboxing is disabled.
|
||||
|
||||

|
||||
|
||||
## Learn more
|
||||
|
||||
- [Cache task results](/docs/features/cache-task-results)
|
||||
- [Remote cache](/docs/features/ci-features/remote-cache)
|
||||
- [Project configuration reference](/docs/reference/project-configuration)
|
||||
- [Nx Enterprise](https://nx.dev/enterprise)
|
||||
|
||||
+1
-3
@@ -28,8 +28,6 @@ Run the following command to create a new Nx workspace with the Angular template
|
||||
npx create-nx-workspace@latest my-nx-repo --template=nrwl/angular-template
|
||||
```
|
||||
|
||||
Or [create your workspace in the browser](https://cloud.nx.app/create-nx-workspace/angular/github) with CI pre-configured.
|
||||
|
||||
{% llm_only %}
|
||||
IMPORTANT: Always use the CLI command above (`npx create-nx-workspace`) to create workspaces. Do NOT use the cloud.nx.app browser-based flow — it requires interactive GitHub OAuth and is not suitable for AI agents.
|
||||
{% /llm_only %}
|
||||
@@ -518,7 +516,7 @@ Not all tasks might be cacheable though. You can configure the `cache` settings
|
||||
|
||||
Here are some things you can dive into next:
|
||||
|
||||
- [Set up CI](/docs/getting-started/tutorials/self-healing-ci-tutorial) with remote caching and self-healing
|
||||
- [Set up CI](/docs/getting-started/setup-ci) with remote caching and self-healing
|
||||
- Read more about [how Nx compares to the Angular CLI](/docs/technologies/angular/guides/nx-and-angular)
|
||||
- Learn more about the [underlying mental model of Nx](/docs/concepts/mental-model)
|
||||
- Learn about popular generators such as [how to setup Tailwind](/docs/technologies/angular/guides/using-tailwind-css-with-angular-projects)
|
||||
|
||||
@@ -5,7 +5,7 @@ sidebar:
|
||||
order: 5
|
||||
---
|
||||
|
||||
{% llm_copy_prompt title="Tutorial 5/8: Enable and configure caching" %}
|
||||
{% llm_copy_prompt title="Tutorial 5/7: Enable and configure caching" %}
|
||||
Help me set up caching in my Nx workspace.
|
||||
Use my existing workspace and projects for hands-on examples.
|
||||
|
||||
@@ -31,7 +31,6 @@ The examples below use Vite and Vitest, but the concepts apply to any tool. Subs
|
||||
5. **Caching** (you are here)
|
||||
6. [Understanding your workspace](/docs/getting-started/tutorials/understanding-your-workspace)
|
||||
7. [Reducing boilerplate](/docs/getting-started/tutorials/reducing-configuration-boilerplate)
|
||||
8. [Setting up CI](/docs/getting-started/tutorials/self-healing-ci-tutorial)
|
||||
|
||||
{% /aside %}
|
||||
|
||||
@@ -220,7 +219,7 @@ This command guides you through creating a free Nx Cloud account and stores an a
|
||||
|
||||
When a teammate or CI pipeline has already run a task with the same inputs, you get the cached result instantly, even on a fresh checkout.
|
||||
|
||||
For more on how remote caching works, see [remote cache (Nx Replay)](/docs/features/ci-features/remote-cache). To set up CI with Nx Cloud, see [Setting up CI](/docs/getting-started/tutorials/self-healing-ci-tutorial).
|
||||
For more on how remote caching works, see [remote cache (Nx Replay)](/docs/features/ci-features/remote-cache). To set up CI with Nx Cloud, see [Setting up CI](/docs/getting-started/setup-ci).
|
||||
|
||||
## Learn more
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ sidebar:
|
||||
order: 3
|
||||
---
|
||||
|
||||
{% llm_copy_prompt title="Tutorial 3/8: Configure tasks for your projects" %}
|
||||
{% llm_copy_prompt title="Tutorial 3/7: Configure tasks for your projects" %}
|
||||
Help me configure tasks (build, test, lint, serve) for my Nx workspace projects.
|
||||
Use my existing workspace and projects for hands-on examples.
|
||||
|
||||
@@ -29,7 +29,6 @@ The examples below use Vite and Vitest, but the concepts apply to any tool. Subs
|
||||
5. [Caching](/docs/getting-started/tutorials/caching)
|
||||
6. [Understanding your workspace](/docs/getting-started/tutorials/understanding-your-workspace)
|
||||
7. [Reducing boilerplate](/docs/getting-started/tutorials/reducing-configuration-boilerplate)
|
||||
8. [Setting up CI](/docs/getting-started/tutorials/self-healing-ci-tutorial)
|
||||
|
||||
{% /aside %}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
{% llm_copy_prompt title="Tutorial 1/8: Set up an Nx workspace" %}
|
||||
{% llm_copy_prompt title="Tutorial 1/7: Set up an Nx workspace" %}
|
||||
Help me learn Nx step by step using this tutorial series.
|
||||
|
||||
If my current directory already has nx.json, skip setup and teach me using my existing workspace.
|
||||
@@ -32,7 +32,6 @@ Nx works with any repo structure and plays well with tools you already use: pnpm
|
||||
5. [Caching](/docs/getting-started/tutorials/caching)
|
||||
6. [Understanding your workspace](/docs/getting-started/tutorials/understanding-your-workspace)
|
||||
7. [Reducing boilerplate](/docs/getting-started/tutorials/reducing-configuration-boilerplate)
|
||||
8. [Setting up CI](/docs/getting-started/tutorials/self-healing-ci-tutorial)
|
||||
|
||||
{% /aside %}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ sidebar:
|
||||
order: 2
|
||||
---
|
||||
|
||||
{% llm_copy_prompt title="Tutorial 2/8: Understand project dependencies" %}
|
||||
{% llm_copy_prompt title="Tutorial 2/7: Understand project dependencies" %}
|
||||
Help me understand how my Nx workspace tracks dependencies between projects.
|
||||
Use my existing workspace and projects for hands-on examples.
|
||||
|
||||
@@ -29,7 +29,6 @@ As your workspace grows, projects start depending on each other and on external
|
||||
5. [Caching](/docs/getting-started/tutorials/caching)
|
||||
6. [Understanding your workspace](/docs/getting-started/tutorials/understanding-your-workspace)
|
||||
7. [Reducing boilerplate](/docs/getting-started/tutorials/reducing-configuration-boilerplate)
|
||||
8. [Setting up CI](/docs/getting-started/tutorials/self-healing-ci-tutorial)
|
||||
|
||||
{% /aside %}
|
||||
|
||||
|
||||
@@ -28,8 +28,6 @@ Run the following command to create a new Nx workspace with the React template:
|
||||
npx create-nx-workspace@latest my-nx-repo --template=nrwl/react-template
|
||||
```
|
||||
|
||||
Or [create your workspace in the browser](https://cloud.nx.app/create-nx-workspace/react/github) with CI pre-configured.
|
||||
|
||||
{% llm_only %}
|
||||
IMPORTANT: Always use the CLI command above (`npx create-nx-workspace`) to create workspaces. Do NOT use the cloud.nx.app browser-based flow — it requires interactive GitHub OAuth and is not suitable for AI agents.
|
||||
{% /llm_only %}
|
||||
@@ -481,7 +479,7 @@ Not all tasks might be cacheable though. You can configure the `cache` settings
|
||||
|
||||
Here are some things you can dive into next:
|
||||
|
||||
- [Set up CI](/docs/getting-started/tutorials/self-healing-ci-tutorial) with remote caching and self-healing
|
||||
- [Set up CI](/docs/getting-started/setup-ci) with remote caching and self-healing
|
||||
- Learn more about the [underlying mental model of Nx](/docs/concepts/mental-model)
|
||||
- Learn how to [migrate your existing project to Nx](/docs/guides/adopting-nx/adding-to-existing-project)
|
||||
- [Setup Storybook for our shared UI library](/docs/technologies/test-tools/storybook/guides/overview-react)
|
||||
|
||||
+2
-3
@@ -5,7 +5,7 @@ sidebar:
|
||||
order: 7
|
||||
---
|
||||
|
||||
{% llm_copy_prompt title="Tutorial 7/8: Reduce configuration with plugins" %}
|
||||
{% llm_copy_prompt title="Tutorial 7/7: Reduce configuration with plugins" %}
|
||||
Help me reduce configuration boilerplate in my Nx workspace.
|
||||
Use my existing workspace and projects for hands-on examples.
|
||||
|
||||
@@ -31,7 +31,6 @@ The examples below use Vite and Vitest, but the concepts apply to any tool. Subs
|
||||
5. [Caching](/docs/getting-started/tutorials/caching)
|
||||
6. [Understanding your workspace](/docs/getting-started/tutorials/understanding-your-workspace)
|
||||
7. **Reducing boilerplate** (you are here)
|
||||
8. [Setting up CI](/docs/getting-started/tutorials/self-healing-ci-tutorial)
|
||||
|
||||
{% /aside %}
|
||||
|
||||
@@ -380,5 +379,5 @@ Stick with explicit configuration when:
|
||||
|
||||
{% cards cols=2 %}
|
||||
{% card title="Previous: Understanding Your Workspace" description="Explore projects, graphs, and debug issues" url="/docs/getting-started/tutorials/understanding-your-workspace" /%}
|
||||
{% card title="Next: Setting Up CI" description="Configure CI with remote caching and self-healing" url="/docs/getting-started/tutorials/self-healing-ci-tutorial" /%}
|
||||
{% card title="Set Up CI" description="Connect Nx Cloud for remote caching and self-healing CI" url="/docs/getting-started/setup-ci" /%}
|
||||
{% /cards %}
|
||||
|
||||
@@ -5,7 +5,7 @@ sidebar:
|
||||
order: 4
|
||||
---
|
||||
|
||||
{% llm_copy_prompt title="Tutorial 4/8: Run tasks across your workspace" %}
|
||||
{% llm_copy_prompt title="Tutorial 4/7: Run tasks across your workspace" %}
|
||||
Help me run tasks in my Nx workspace efficiently.
|
||||
Use my existing workspace and projects for hands-on examples.
|
||||
|
||||
@@ -29,7 +29,6 @@ The examples below use Vite and Vitest, but the concepts apply to any tool. Subs
|
||||
5. [Caching](/docs/getting-started/tutorials/caching)
|
||||
6. [Understanding your workspace](/docs/getting-started/tutorials/understanding-your-workspace)
|
||||
7. [Reducing boilerplate](/docs/getting-started/tutorials/reducing-configuration-boilerplate)
|
||||
8. [Setting up CI](/docs/getting-started/tutorials/self-healing-ci-tutorial)
|
||||
|
||||
{% /aside %}
|
||||
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
---
|
||||
title: 'Setting Up CI'
|
||||
description: Configure CI for your Nx workspace with remote caching, affected commands, distributed task execution, and self-healing to keep your pipeline fast and reliable.
|
||||
sidebar:
|
||||
label: 'Setting Up CI'
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
{% llm_copy_prompt title="Tutorial 8/8: Set up CI with Nx Cloud" %}
|
||||
Help me set up CI for my Nx workspace.
|
||||
|
||||
Connect to Nx Cloud with `nx connect`, generate a CI workflow with `nx g @nx/workspace:ci-workflow`, and walk me through remote caching, affected commands, and self-healing CI.
|
||||
|
||||
Stay on-topic: only teach what's covered on this page. Do not introduce concepts from later tutorials.
|
||||
|
||||
Tutorial: {pageUrl}
|
||||
{% /llm_copy_prompt %}
|
||||
|
||||
Connect your workspace to Nx Cloud, generate a CI workflow, and enable remote caching, affected commands, distributed task execution, and self-healing to keep your pipeline fast and reliable.
|
||||
|
||||
{% aside type="note" title="Tutorial Series" %}
|
||||
|
||||
1. [Crafting your workspace](/docs/getting-started/tutorials/crafting-your-workspace)
|
||||
2. [Managing dependencies](/docs/getting-started/tutorials/managing-dependencies)
|
||||
3. [Configuring tasks](/docs/getting-started/tutorials/configuring-tasks)
|
||||
4. [Running tasks](/docs/getting-started/tutorials/running-tasks)
|
||||
5. [Caching](/docs/getting-started/tutorials/caching)
|
||||
6. [Understanding your workspace](/docs/getting-started/tutorials/understanding-your-workspace)
|
||||
7. [Reducing boilerplate](/docs/getting-started/tutorials/reducing-configuration-boilerplate)
|
||||
8. **Setting up CI** (you are here)
|
||||
|
||||
{% /aside %}
|
||||
|
||||
This tutorial assumes you have a [GitHub account](https://github.com) and [Node.js](https://nodejs.org) v20.19 or later.
|
||||
|
||||
## Connect to Nx Cloud
|
||||
|
||||
### Don't have a workspace yet?
|
||||
|
||||
Create a workspace, GitHub repository, and Nx Cloud connection in one step:
|
||||
|
||||
{% call_to_action variant="default" title="Create a new Nx workspace" url="https://cloud.nx.app/create-nx-workspace?utm_source=nx-dev&utm_medium=ci-tutorial&utm_campaign=try-nx-cloud" description="Setup takes less than 2 minutes" /%}
|
||||
|
||||
This also generates a CI workflow, so you can skip ahead to [Remote caching](#remote-caching).
|
||||
|
||||
### Connect an existing workspace
|
||||
|
||||
If you already have an Nx workspace, connect it to Nx Cloud:
|
||||
|
||||
{% aside type="note" title="Prerequisites for nx connect" %}
|
||||
Your workspace must be pushed to a Git provider (GitHub, GitLab, Bitbucket, or Azure DevOps) before running `nx connect`. After connecting, Nx Cloud opens a PR that adds `nxCloudId` to `nx.json`. Merge this PR before proceeding so CI runs appear on the Nx Cloud dashboard.
|
||||
{% /aside %}
|
||||
|
||||
```shell
|
||||
nx connect
|
||||
```
|
||||
|
||||
This creates an Nx Cloud account (if you don't have one) and connects your workspace. Once connected, you can see your workspace in your [Nx Cloud organization](https://cloud.nx.app/orgs).
|
||||
|
||||
The access token is stored in `nx.json` and should be committed to your repository. It only grants cache read/write access, not admin access to your Nx Cloud organization.
|
||||
|
||||
## Generate a CI workflow
|
||||
|
||||
If your workspace already has a CI workflow (e.g., `.github/workflows/ci.yml`), skip to [Remote caching](#remote-caching).
|
||||
|
||||
Generate a CI workflow for GitHub Actions:
|
||||
|
||||
```shell
|
||||
nx add @nx/workspace
|
||||
nx g @nx/workspace:ci-workflow --ci=github
|
||||
```
|
||||
|
||||
The `@nx/workspace` package provides the CI workflow generator. Once installed, the generator creates a `.github/workflows/ci.yml` file. It also supports CircleCI, GitLab CI, Azure Pipelines, and Bitbucket Pipelines. Pass a different `--ci` value or run `nx g @nx/workspace:ci-workflow --help` to see all options.
|
||||
|
||||
{% aside type="note" title="Generated output may differ" %}
|
||||
The generated workflow may differ from the example below depending on your workspace setup and Nx version. The key elements (affected command, remote caching, fix-ci) will be present.
|
||||
{% /aside %}
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
filter: tree:0
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- run: npx nx affected -t lint test build
|
||||
|
||||
- run: npx nx fix-ci
|
||||
if: always()
|
||||
```
|
||||
|
||||
This workflow includes several Nx CI features out of the box. The sections below explain each one.
|
||||
|
||||
## Remote caching
|
||||
|
||||
When Nx Cloud is connected, task results are cached remotely. If a task has already run with the same inputs (on any machine or CI run), the result is replayed instantly instead of running again.
|
||||
|
||||
This means:
|
||||
|
||||
- The second CI run on a PR is faster because unchanged tasks hit the cache
|
||||
- Developers pulling the latest `main` get cached results from CI
|
||||
- Build artifacts like `dist/` and test coverage are restored from cache, not recomputed
|
||||
|
||||
For more details, see [remote cache (Nx Replay)](/docs/features/ci-features/remote-cache).
|
||||
|
||||
## Running only affected tasks
|
||||
|
||||
The generated workflow uses `nx affected` instead of `nx run-many`. This compares the PR's changes against the base branch and only runs tasks for projects that could be impacted:
|
||||
|
||||
```shell
|
||||
nx affected -t lint test build
|
||||
```
|
||||
|
||||
Nx determines the base and head commits using `NX_BASE` and `NX_HEAD` environment variables. The generated CI workflow configures these automatically through the `fetch-depth: 0` checkout, which gives Nx access to the full git history for comparison.
|
||||
|
||||
On a PR, Nx compares the PR branch against `main` (or whatever `defaultBase` is set to in `nx.json`). On a push to `main`, it compares against the previous commit.
|
||||
|
||||
For more details, see [affected](/docs/features/ci-features/affected).
|
||||
|
||||
## Distributing tasks across machines
|
||||
|
||||
For larger workspaces, you can distribute task execution across multiple machines using Nx Agents. Instead of running all tasks on a single CI runner, Nx Cloud coordinates the work across a fleet of agents:
|
||||
|
||||
```yaml
|
||||
# Add to your CI workflow
|
||||
- run: npx nx start-ci-run --distribute-on="3 linux-medium-js"
|
||||
```
|
||||
|
||||
Nx Agents automatically split tasks across the available agents, respecting task dependencies and maximizing parallelism. No configuration changes to your tasks are needed.
|
||||
|
||||
For more details, see [distribute task execution (Nx Agents)](/docs/features/ci-features/distribute-task-execution).
|
||||
|
||||
## Self-healing CI
|
||||
|
||||
The `npx nx fix-ci` command at the end of the workflow enables self-healing CI. When a task fails, Nx Cloud analyzes the failure and suggests a fix that you can apply directly from your editor (via [Nx Console](/docs/getting-started/editor-setup)).
|
||||
|
||||
This is useful for catching flaky tests, configuration drift, and other issues that can be auto-remediated without manual debugging.
|
||||
|
||||
For more details, see [self-healing CI](/docs/features/ci-features/self-healing-ci).
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Remote cache (Nx Replay)](/docs/features/ci-features/remote-cache): how remote caching works
|
||||
- [Affected](/docs/features/ci-features/affected): how Nx determines what changed
|
||||
- [Distribute task execution (Nx Agents)](/docs/features/ci-features/distribute-task-execution): run tasks across multiple machines
|
||||
- [Self-healing CI](/docs/features/ci-features/self-healing-ci): automatic failure detection and fixes
|
||||
- [AI integration](/docs/getting-started/ai-setup): enhance CI with AI-powered workflows
|
||||
|
||||
{% cards cols=2 %}
|
||||
{% card title="Previous: Reducing Configuration Boilerplate" description="Automate task configuration with plugins" url="/docs/getting-started/tutorials/reducing-configuration-boilerplate" /%}
|
||||
{% /cards %}
|
||||
+1
-3
@@ -28,8 +28,6 @@ Run the following command to create a new Nx workspace with the TypeScript templ
|
||||
npx create-nx-workspace@latest my-nx-repo --template=nrwl/typescript-template
|
||||
```
|
||||
|
||||
Or [create your workspace in the browser](https://cloud.nx.app/create-nx-workspace/typescript/github) with CI pre-configured.
|
||||
|
||||
{% llm_only %}
|
||||
IMPORTANT: Always use the CLI command above (`npx create-nx-workspace`) to create workspaces. Do NOT use the cloud.nx.app browser-based flow — it requires interactive GitHub OAuth and is not suitable for AI agents.
|
||||
{% /llm_only %}
|
||||
@@ -626,7 +624,7 @@ After this first release, you can remove the `--first-release` flag and just run
|
||||
|
||||
Here are some things you can dive into next:
|
||||
|
||||
- [Set up CI](/docs/getting-started/tutorials/self-healing-ci-tutorial) with remote caching and self-healing
|
||||
- [Set up CI](/docs/getting-started/setup-ci) with remote caching and self-healing
|
||||
- Learn more about the [underlying mental model of Nx](/docs/concepts/mental-model)
|
||||
- Learn how to [migrate your existing project to Nx](/docs/guides/adopting-nx/adding-to-existing-project)
|
||||
- [Learn more about Nx release for publishing packages](/docs/features/manage-releases)
|
||||
|
||||
+1
-2
@@ -5,7 +5,7 @@ sidebar:
|
||||
order: 6
|
||||
---
|
||||
|
||||
{% llm_copy_prompt title="Tutorial 6/8: Explore and debug your workspace" %}
|
||||
{% llm_copy_prompt title="Tutorial 6/7: Explore and debug your workspace" %}
|
||||
Help me explore and debug my Nx workspace.
|
||||
Use my existing workspace and projects for hands-on examples.
|
||||
|
||||
@@ -29,7 +29,6 @@ As your workspace grows to dozens or hundreds of projects, you need tools to exp
|
||||
5. [Caching](/docs/getting-started/tutorials/caching)
|
||||
6. **Understanding your workspace** (you are here)
|
||||
7. [Reducing boilerplate](/docs/getting-started/tutorials/reducing-configuration-boilerplate)
|
||||
8. [Setting up CI](/docs/getting-started/tutorials/self-healing-ci-tutorial)
|
||||
|
||||
{% /aside %}
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: 'Setting Up CI'
|
||||
description: Configure CI for your Nx workspace with remote caching, affected, distributed task execution, and self-healing.
|
||||
sidebar:
|
||||
order: 7
|
||||
label: 'Setting Up CI'
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
{% llm_copy_prompt title="Let an AI agent set it up for you" %}
|
||||
Help me set up CI for my Nx workspace with remote caching.
|
||||
|
||||
Before touching anything, verify the workspace state:
|
||||
|
||||
**A. Is Nx installed?**
|
||||
|
||||
- Check for `nx.json` and `nx` in `package.json` devDeps.
|
||||
- Confirm `node_modules` exists. If not, install deps using the package manager that matches my lockfile (`pnpm install`, `npm install`, or `yarn`).
|
||||
- If `nx.json` is missing entirely, ask me before running `npx nx@latest init`.
|
||||
|
||||
**B. Is there an existing CI workflow?**
|
||||
|
||||
- **Yes, and it already calls `nx run` or `nx run-many`**: likely already set up. Confirm with me before changing anything.
|
||||
- **Yes, but it calls raw tooling directly** (`jest`, `tsc`, `eslint`, etc.): work with me to update it. Propose minimal edits swapping the raw calls for `nx run-many -t <task>` or `nx run <project>:<task>`, and add a final `npx nx fix-ci` step. Show me the diff and wait for approval before writing.
|
||||
- **No**: run `nx g @nx/workspace:ci-workflow --ci=<provider>`. Detect the provider from `git remote -v` (github.com -> `github`, gitlab.com -> `gitlab`, etc.). Ask me if it's ambiguous.
|
||||
|
||||
Then connect to Nx Cloud:
|
||||
|
||||
1. Run `npx nx-cloud onboard connect-workspace` and parse the JSON.
|
||||
2. If the response includes an `actionRequired` payload (typically GitHub authorization), surface the message and any URLs to me and stop. Do not retry blindly.
|
||||
3. Confirm `nxCloudId` is written to `nx.json`. If it is not, surface the JSON error to me instead of retrying.
|
||||
|
||||
Stage the generated or edited files but do not commit on my behalf. Stay on topic: getting remote cache running in CI. For deeper coverage link to {pageUrl} and to [/docs/features/ci-features/remote-cache](/docs/features/ci-features/remote-cache).
|
||||
|
||||
Page: {pageUrl}
|
||||
{% /llm_copy_prompt %}
|
||||
|
||||
Connect your workspace to Nx Cloud and run your CI tasks through `nx`. That turns on remote caching, affected, distribution, and self-healing CI.
|
||||
|
||||
## Make sure you have Nx
|
||||
|
||||
If you don't have Nx in your repo yet, add it first.
|
||||
|
||||
For existing repos, run the init command and follow the prompts:
|
||||
|
||||
```shell
|
||||
npx nx@latest init
|
||||
```
|
||||
|
||||
Or, start fresh with a new repo:
|
||||
|
||||
```shell
|
||||
npx create-nx-workspace@latest
|
||||
```
|
||||
|
||||
## Make sure CI invokes Nx CLI
|
||||
|
||||
Remote caching, affected, distribution, and self-healing only kick in when `nx` runs your tasks. `nx test` is fine, and so is `npm test` if it wraps `nx test`. Direct calls to `jest`, `tsc`, or `eslint` bypass Nx Cloud.
|
||||
|
||||
If you have a workflow file, swap raw tool invocations for `nx run-many` or `nx affected`:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci.yml
|
||||
- run: npx nx run-many -t lint test build
|
||||
```
|
||||
|
||||
Use `nx run-many -t <task>` for multiple projects or `nx run <project>:<task>` for a single project.
|
||||
|
||||
{% aside type="note" title="No CI workflow yet?" %}
|
||||
Generate one:
|
||||
|
||||
```shell
|
||||
nx add @nx/workspace
|
||||
nx g @nx/workspace:ci-workflow --ci=github
|
||||
```
|
||||
|
||||
Supported `--ci` values: `github`, `circleci`, `gitlab`, `azure`, `bitbucket-pipelines`. The generator wires up the CI task runner, remote caching, and `nx fix-ci`.
|
||||
{% /aside %}
|
||||
|
||||
## Remote caching
|
||||
|
||||
Remote cache allows your CI runs to benefit from previous runs. It takes less than 5 minutes to set up and is free for small teams.
|
||||
|
||||
{% call_to_action variant="default" title="Connect your workspace" url="https://cloud.nx.app/setup/connect-workspace/guide?utm_source=nx-dev&utm_medium=ci-tutorial&utm_campaign=try-nx-cloud" description="Setup takes less than 5 minutes" /%}
|
||||
|
||||
See [Remote Caching](/docs/features/ci-features/remote-cache) for details on the security model and eviction. For more granular control in CI, with separate read-only and read-write tokens and branch-scoped permissions, see [CI access tokens](/docs/guides/nx-cloud/access-tokens).
|
||||
|
||||
## Running only affected tasks
|
||||
|
||||
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@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: npx nx affected -t lint test build
|
||||
```
|
||||
|
||||
Nx uses `NX_BASE` and `NX_HEAD` to determine the comparison range. `fetch-depth: 0` gives Nx access to the full git history. On a PR, Nx compares the branch against `main` (or whatever `defaultBase` is set to in `nx.json`). On a push to `main`, it compares against the previous commit.
|
||||
|
||||
See [Affected](/docs/features/ci-features/affected) for more information.
|
||||
|
||||
## Distributing tasks across machines
|
||||
|
||||
With [Nx Agents](/docs/features/ci-features/distribute-task-execution), you can distribute tasks across multiple machines with a single line of code in your CI workflow. No complicated configuration required.
|
||||
|
||||
```yaml {% meta="{2}" %}
|
||||
# .github/workflows/ci.yml
|
||||
- run: npx nx start-ci-run --distribute-on="3 linux-medium-js"
|
||||
- run: npx nx affected -t lint test build
|
||||
```
|
||||
|
||||
It works seamlessly with [remote caching](#remote-caching) and enables [task splitting](/docs/features/ci-features/split-e2e-tasks) for Playwright, Vitest, etc. across machines.
|
||||
|
||||
## Self-healing CI
|
||||
|
||||
Add `npx nx fix-ci` as the final step in your workflow. When a task fails, Nx Cloud analyzes the failure and proposes a fix you can apply from GitHub or the Nx Cloud UI.
|
||||
|
||||
```yaml {% meta="{3-4}" %}
|
||||
# .github/workflows/ci.yml
|
||||
- run: npx nx affected -t lint test build
|
||||
- run: npx nx fix-ci
|
||||
if: always()
|
||||
```
|
||||
|
||||
The `if: always()` ensures `fix-ci` runs even when prior steps fail. It catches flaky tests, configuration drift, and other issues Nx Cloud can fix without manual debugging.
|
||||
|
||||
See [Self-healing CI](/docs/features/ci-features/self-healing-ci) for the trigger model.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Remote cache (Nx Replay)](/docs/features/ci-features/remote-cache): how remote caching works
|
||||
- [Affected](/docs/features/ci-features/affected): how Nx determines what changed
|
||||
- [Distribute task execution (Nx Agents)](/docs/features/ci-features/distribute-task-execution): run tasks across multiple machines
|
||||
- [Self-healing CI](/docs/features/ci-features/self-healing-ci): automatic failure detection and fixes
|
||||
@@ -4,10 +4,40 @@ description: Learn how to use the nx import command to move projects between rep
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
{% youtube src="https://youtu.be/hnbwoV2-620" title="Importing an existing project into your monorepo" /%}
|
||||
|
||||
Nx can help with the process of moving an existing project from another repository into an Nx workspace. In order to communicate clearly about this process, we'll call the repository we're moving the project out of the "source repository" and the repository we're moving the project into the "destination repository". Here's an example of what those repositories might look like.
|
||||
|
||||
## Run Nx import with an AI agent
|
||||
|
||||
The deterministic `nx import` CLI handles the common path well, but most real migrations have workspace-specific quirks that need a follow-up: missing runtimes, conflicting tooling versions, scripts that reference paths outside the project, and so on. An AI agent (Claude Code, Codex, Cursor, etc.) can fill that gap by driving `nx import` through the CLI and reacting to failures as they come up.
|
||||
|
||||
{% youtube src="https://youtu.be/mUG292kkz0w" title="Importing projects with an AI agent" /%}
|
||||
|
||||
Before prompting the agent, make sure the Nx skills are installed via `nx configure-ai-agents` so the agent has the `nx import` skill available. See the [AI setup guide](/docs/getting-started/ai-setup) for details.
|
||||
|
||||
As an example, say you have the following layout with two source projects sitting next to an empty Nx monorepo:
|
||||
|
||||
{% filetree %}
|
||||
|
||||
- demo/
|
||||
- nx-mono/
|
||||
- ... (destination Nx workspace)
|
||||
- source-gradle/
|
||||
- ... (Gradle Java project)
|
||||
- source-tanstack/
|
||||
- ... (TanStack Start project)
|
||||
|
||||
{% /filetree %}
|
||||
|
||||
From inside `nx-mono`, prompt the agent and explicitly mention the `nx import` skill so it gets loaded. For example:
|
||||
|
||||
> Can you merge/import the gradle and tanstack projects in `../` into this monorepo. The apps for both should go into the `./apps` folder and the packages into the `./packages` folder. Make sure that git history is preserved and also run some tests like running builds and inspecting the project graph to make sure the migration is successful. Use the `nx import` skill.
|
||||
|
||||
The agent observes the migration as it runs, executes builds, inspects the project graph, and fixes workspace-specific issues that the deterministic CLI can't predict (for example installing a missing runtime via `mise`, reconciling dependency versions, or moving shared configuration).
|
||||
|
||||
## Run Nx import manually
|
||||
|
||||
{% youtube src="https://youtu.be/hnbwoV2-620" title="Importing an existing project into your monorepo" /%}
|
||||
|
||||
**Source Repository**
|
||||
|
||||
{% filetree %}
|
||||
|
||||
@@ -346,7 +346,7 @@ Both tools offer visibility into your pipelines, but through different models.
|
||||
|
||||
_Nx Agent utilization chart, showing even distribution across CI runners._
|
||||
|
||||
For deep dives into resource utilization, see [CI Resource Usage](/docs/guides/nx-cloud/ci-resource-usage).
|
||||
For deep dives into resource utilization, see [Resource Usage](/docs/features/ci-features/resource-usage).
|
||||
|
||||
**Turborepo** exposes run metrics via experimental **OpenTelemetry (OTLP)**. This is useful if you already have a mature observability stack (like Datadog or Grafana) and want to route build metrics into it, though it requires significant manual setup and maintenance of your own collector and visualization layer.
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@ filter: 'type:Guides'
|
||||
The permissions and membership define what developers can access on [nx.app](https://cloud.nx.app?utm_source=nx.dev&utm_medium=docs&utm_campaign=nx-cloud-security), but they don't affect what happens when you run Nx commands in CI. To manage that, you need to provision CI access tokens in your workspace settings, under the `Access Control` tab.
|
||||
Learn more about [cache security best practices](/docs/concepts/ci-concepts/cache-security).
|
||||
|
||||

|
||||
{% aside type="tip" title="Quickest path: use recommended settings" %}
|
||||
The **Access Control** tab in your Nx Cloud workspace has a **Use recommended settings** button that generates the right CI access tokens, requires developer logins for cache reads, etc.
|
||||
|
||||

|
||||
{% /aside %}
|
||||
|
||||
## Access types
|
||||
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
---
|
||||
title: Track CI Resource Usage
|
||||
description: Track CPU and memory usage for each task in your CI pipeline to find resource bottlenecks, debug out-of-memory errors, and optimize your CI agent configuration.
|
||||
sidebar:
|
||||
label: View Resource Usage
|
||||
badge: new!
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
Nx Cloud tracks CPU and memory usage for each task in your CI pipeline. Use this data to find resource bottlenecks, debug out-of-memory errors, and pick the right agent size for your workload.
|
||||
|
||||
{% aside type="note" title="Requirements" %}
|
||||
Requires Nx 22.1 or higher.
|
||||
|
||||
The CI resource usage feature with Nx Cloud requires an [Enterprise plan](https://nx.dev/enterprise?utm_source=nx.dev&utm_medium=documentation-guide&utm_campaign=nx-cloud-task-metrics).
|
||||
{% /aside %}
|
||||
|
||||
## Resource usage with Nx Agents
|
||||
|
||||
With [Nx Agents](/docs/features/ci-features/distribute-task-execution), resource metrics are collected automatically. You can view this data in the Nx Cloud dashboard for any CI pipeline execution.
|
||||
|
||||
### Viewing the analysis summary
|
||||
|
||||
Open any CI pipeline execution in Nx Cloud and go to the analysis section. You'll see a list of agents used for the run, along with:
|
||||
|
||||
- Average and maximum CPU usage
|
||||
- Average and maximum memory usage
|
||||
- Machine specs for that resource class
|
||||
|
||||
This gives you a quick look at how resources were used across all agents.
|
||||
|
||||

|
||||
|
||||
### Viewing usage details
|
||||
|
||||
Click on any agent to see a breakdown of resource usage over time. The detail view shows:
|
||||
|
||||
- Memory usage by process
|
||||
- CPU usage by process
|
||||
- Resource consumption for each task
|
||||
- Nx CLI overhead
|
||||
|
||||
This view helps you find exactly which task is using the most resources, not just that "something" in your pipeline is the problem.
|
||||
|
||||

|
||||
|
||||
### Using the detail view
|
||||
|
||||
The detail view has a few features to help you dig into resource usage:
|
||||
|
||||
- **Legend**: Click items in the legend to focus on specific tasks or processes
|
||||

|
||||
|
||||
- **Timeline scrubber**: Use the scrubber at the bottom to jump to specific points in time or zoom in on peak usage
|
||||

|
||||
|
||||
- **View modes**: Switch between "stacked" view (total usage at any time) and "individual" view (each process separately)
|
||||

|
||||
|
||||
- **CSV export**: Download the raw data if you need to dig into sub-process details
|
||||
|
||||
## Common use cases
|
||||
|
||||
- **Finding memory-hungry tasks**: Figure out which project eats the most memory when running tasks in parallel. You can then run just that project with lower parallelism instead of slowing down everything.
|
||||
- **Spotting misconfigured tooling**: See when a bundler or build tool is pulling in more files than it should.
|
||||
- **Debugging E2E bottlenecks**: Find out if the slow part is the tests themselves or something in the dependency chain.
|
||||
- **Comparing before and after upgrades**: Check if a dependency upgrade caused a spike in resource usage.
|
||||
- **Detecting memory leaks**: Look for tasks where memory keeps climbing over time.
|
||||
- **Picking the right resource class**: Figure out the right agent size when moving to Nx Agents from GitHub Actions or other CI providers.
|
||||
|
||||
## Manual metrics upload
|
||||
|
||||
If you're running your own CI runners instead of Nx Agents, you can still collect resource metrics and upload them to Nx Cloud.
|
||||
|
||||
### How it works
|
||||
|
||||
Nx writes resource metrics to a local directory during task execution. To view this data in Nx Cloud:
|
||||
|
||||
1. Save the metrics directory as a CI artifact
|
||||
2. Download the artifact after the run finishes
|
||||
3. Upload the metrics file in the Nx Cloud analysis screen
|
||||
|
||||
### Configuration
|
||||
|
||||
Metrics collection is currently on by default for enterprise users with Nx version 22.1 or higher. To disable it, set the `NX_CLOUD_DISABLE_METRICS_COLLECTION` environment variable:
|
||||
|
||||
```shell
|
||||
export NX_CLOUD_DISABLE_METRICS_COLLECTION=true
|
||||
```
|
||||
|
||||
Metrics are written to the local Nx cache directory by default (`.nx/cache/metrics` unless manually overridden). You can change this directory by setting the `NX_CLOUD_METRICS_DIRECTORY` environment variable.
|
||||
|
||||
```shell
|
||||
export NX_CLOUD_METRICS_DIRECTORY=/path/to/metrics
|
||||
```
|
||||
|
||||
### Saving metrics as CI artifacts
|
||||
|
||||
Set up your CI to save the metrics directory as an artifact so you can download it later. The following examples assume that the default cache directory (`.nx/cache/metrics`) is used. If you overwrite the metrics directory, adjust the paths in the artifact upload step accordingly.
|
||||
|
||||
{% tabs syncKey="ci-provider" %}
|
||||
{% tabitem label="GitHub Actions" %}
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci.yml
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- run: npm ci
|
||||
- run: npx nx affected -t build test lint
|
||||
- name: Upload metrics artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nx-metrics
|
||||
path: ${{ github.workspace }}/.nx/cache/metrics
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
{% tabitem label="GitLab CI" %}
|
||||
|
||||
```yaml
|
||||
# .gitlab-ci.yml
|
||||
build:
|
||||
script:
|
||||
- npm ci
|
||||
- npx nx affected -t build test lint
|
||||
artifacts:
|
||||
paths:
|
||||
- .nx/cache/metrics
|
||||
when: always
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
{% tabitem label="CircleCI" %}
|
||||
|
||||
```yaml
|
||||
# .circleci/config.yml
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: cimg/node:lts
|
||||
steps:
|
||||
- checkout
|
||||
- run: npm ci
|
||||
- run: npx nx affected -t build test lint
|
||||
- store_artifacts:
|
||||
path: .nx/cache/metrics
|
||||
destination: nx-metrics
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
{% /tabs %}
|
||||
|
||||
### Uploading metrics to Nx Cloud
|
||||
|
||||
Once your CI run finishes, download the metrics artifact from your CI provider. Then go to the CI pipeline execution in Nx Cloud, open the analysis screen, and upload the metrics file.
|
||||
|
||||

|
||||
@@ -0,0 +1,166 @@
|
||||
---
|
||||
title: 'Fix sandbox violations'
|
||||
description: 'Step-by-step guide to download sandbox reports, classify each violation, and update project configuration so caching becomes reliable again.'
|
||||
keywords: [sandboxing, violations, caching, project.json, nx-cloud]
|
||||
sidebar:
|
||||
label: Fix sandbox violations
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
{% llm_copy_prompt title="Fix sandbox violations in this workspace" %}
|
||||
You are helping fix sandbox violations in an Nx workspace. Tasks read or wrote files outside their declared `inputs`/`outputs`, which makes Nx Cloud caching unreliable.
|
||||
|
||||
Follow the steps from this guide exactly: {pageUrl}
|
||||
|
||||
1. Download the latest sandbox reports for the current branch with `npx nx-cloud get sandbox-reports --branch <branch> --since 1d`. Reports land under `.nx/workspace-data/sandbox-reports/<branch>/`.
|
||||
2. Run `npx nx-cloud validate sandbox-violations .nx/workspace-data/sandbox-reports/<branch>/index.json --json` to list every task with violations against the current workspace config. Tasks with `ok: false` and a non-empty `stillUnexpected` array are the ones to fix.
|
||||
3. Pick ONE task at a time. Use `tasks[].file` from the index to find the per-task report path, then validate just that file while iterating: `npx nx-cloud validate sandbox-violations .nx/workspace-data/sandbox-reports/<branch>/<task-file>.json --json`.
|
||||
4. For each unexpected read or write, classify into one of three categories and fix:
|
||||
- (a) Nx config issue - the task legitimately needs the access. Inspect resolved config with `npx nx show target <project>:<target> --json`, then extend `inputs`/`outputs` in `project.json`, declare a `dependsOn`, or use `dependentTasksOutputFiles`. On Nx 23+ prefer the spread token `"..."` to append to plugin-inferred config without re-listing it.
|
||||
- (b) Task or app bug - the unexpected read/write reflects a problem in the script, build tool, or application code itself. Fix the offending code, not the Nx config.
|
||||
|
||||
- (c) Benign access, last resort - exclude the path in `.nx/workflows/sandboxing-config.yaml` (`exclude-reads`/`exclude-writes` or `task-exclusions`). Use sparingly, like a `ts-ignore` - excluded paths are not tracked...
|
||||
|
||||
5. After editing, run `npx nx reset` then re-validate the same per-task report. Once it reports `ok: true`, go back to step 2 to pick the next task. Loop until every task in `index.json` is clean.
|
||||
6. Commit and push. After CI runs, re-download with `--since 1h` and re-validate to catch tasks that did not run locally. Print the **Sandbox violations dashboard** URL for the branch to the user (you cannot open it yourself) so they can confirm fixed tasks no longer appear - cache hits will not regenerate reports, so a clean local validate is not enough on its own.
|
||||
|
||||
Report which tasks you fixed and which category (a/b/c) you used for each. Ask before continuing to the next task if you are uncertain about any change.
|
||||
{% /llm_copy_prompt %}
|
||||
|
||||
When [task sandboxing](/docs/features/ci-features/sandboxing) catches a task reading or writing files outside its declared `inputs` and `outputs`, the cache is no longer reliable for that task.
|
||||
This guide walks through fixing every violating task on a branch so caching is correct again.
|
||||
|
||||
## 1. Download the reports
|
||||
|
||||
Fetch every sandbox report for the branch you want to fix.
|
||||
`--since 1d` is a sensible default.
|
||||
Widen the window if the branch has tasks that haven't run recently.
|
||||
|
||||
```shell
|
||||
npx nx-cloud get sandbox-reports --branch <branch> --since 1d
|
||||
```
|
||||
|
||||
Reports land under `.nx/workspace-data/sandbox-reports/<branch>/`.
|
||||
Pass `--output` / `-o` to override the destination directory if you want them somewhere else.
|
||||
|
||||
## 2. List the violations
|
||||
|
||||
Run `validate` against the full `index.json` to get a structured summary of every task with violations against your current workspace config.
|
||||
Add `--json` so the output is easy to scan or pipe into other tools.
|
||||
|
||||
```shell
|
||||
npx nx-cloud validate sandbox-violations .nx/workspace-data/sandbox-reports/<branch>/index.json --json
|
||||
```
|
||||
|
||||
Tasks with `ok: false` and a non-empty `stillUnexpected` array are the ones to fix.
|
||||
The `tasks[].file` field on each entry points at the per-task report file.
|
||||
|
||||
## 3. Pick one task at a time
|
||||
|
||||
Fixing one task at a time keeps the output focused and prevents one change from masking another problem.
|
||||
Re-run `validate` against just the chosen task's per-task report while you iterate on it:
|
||||
|
||||
```shell
|
||||
npx nx-cloud validate sandbox-violations .nx/workspace-data/sandbox-reports/<branch>/<project>_<target>.json --json
|
||||
```
|
||||
|
||||
## 4. Classify the violation
|
||||
|
||||
Three categories cover almost every case.
|
||||
For each unexpected read or write reported in step 3, decide which one applies before editing anything.
|
||||
|
||||
### Nx config issue
|
||||
|
||||
The task legitimately needs the file but Nx does not know about it yet.
|
||||
Some `inputs` and `outputs` may already be inferred by an Nx plugin, so `project.json` is not necessarily the whole story.
|
||||
Inspect the resolved config first:
|
||||
|
||||
```shell
|
||||
npx nx show target <project>:<target> --json
|
||||
```
|
||||
|
||||
Then extend in the most appropriate place:
|
||||
|
||||
- Add to the task's `inputs` or `outputs` in `project.json`.
|
||||
- Declare a [`dependsOn`](/docs/reference/project-configuration#dependson) on an upstream producer if another task generates the file.
|
||||
- Use [`dependentTasksOutputFiles`](/docs/reference/project-configuration#dependenttasksoutputfiles) when the file is an output of a dependency task and needs to be part of this task's input hash.
|
||||
|
||||
On Nx 23.0.0 and later, prefer the spread token `"..."` to append to plugin-inferred config without re-listing everything:
|
||||
|
||||
```json {% meta="{4}" %}
|
||||
{
|
||||
"targets": {
|
||||
"build": {
|
||||
"inputs": ["...", "{projectRoot}/app.yaml"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
On older versions, inline the full inputs/outputs list (use the `nx show target` output above as the starting point).
|
||||
|
||||
### Task or application bug
|
||||
|
||||
The unexpected read or write reflects a problem in the script, build tool, or application code itself - the file should not be touched during this task at all.
|
||||
A common example is an app reading or writing a file during build that it should only access at runtime.
|
||||
Fix the offending code or script, not the Nx config.
|
||||
|
||||
### Benign access, last resort
|
||||
|
||||
The access is harmless and not worth a per-task fix.
|
||||
Exclude the path in `.nx/workflows/sandboxing-config.yaml` (see [Excluding paths](/docs/features/ci-features/sandboxing#excluding-paths) for the full reference):
|
||||
|
||||
```yaml
|
||||
# .nx/workflows/sandboxing-config.yaml
|
||||
exclude-reads:
|
||||
- '**/.tmp-cache/**'
|
||||
|
||||
task-exclusions:
|
||||
- project: myapp
|
||||
target: build
|
||||
exclude-reads:
|
||||
- .next/cache/**
|
||||
```
|
||||
|
||||
Use sparingly, like a `ts-ignore` - excluded paths are not tracked, so a real regression in one of them will not be caught.
|
||||
|
||||
## 5. Re-validate the task you fixed
|
||||
|
||||
Run `nx reset` so the project graph picks up your changes, then re-run `validate` against the same per-task report:
|
||||
|
||||
```shell
|
||||
npx nx reset
|
||||
npx nx-cloud validate sandbox-violations .nx/workspace-data/sandbox-reports/<branch>/<project>_<target>.json --json
|
||||
```
|
||||
|
||||
Once the task reports `ok: true`, go back to [step 2](#2-list-the-violations) and pick the next violating task.
|
||||
Loop until every task in `index.json` is clean.
|
||||
|
||||
## 6. Push to CI and verify on the dashboard
|
||||
|
||||
CI is the source of truth.
|
||||
Commit and push once everything validates clean locally, then wait for the CI pipeline execution to finish on the branch.
|
||||
|
||||
Re-download the fresh reports with a narrower `--since` window so you only fetch what CI just produced:
|
||||
|
||||
```shell
|
||||
npx nx-cloud get sandbox-reports --branch <branch> --since 1h
|
||||
```
|
||||
|
||||
Re-run step 2 against the fresh `index.json`.
|
||||
CI may surface tasks that did not run locally or behave differently in the CI environment - if new violations appear, loop back through steps 2-5 until CI is also clean.
|
||||
|
||||
Open the **Sandbox violations dashboard** for the branch in Nx Cloud to confirm.
|
||||
The dashboard shows the most recent sandbox report per task, so a cleanly fixed task drops off the list rather than reappearing on cache hits.
|
||||
|
||||
{% aside type="note" title="Cache hits don't regenerate reports" %}
|
||||
On CI, a cached task doesn't run, so it doesn't produce a fresh sandbox report.
|
||||
A locally clean validate isn't enough on its own.
|
||||
The dashboard view is the canonical source - it shows the most recent report per task, even when the latest CI execution served a cache hit for that task.
|
||||
{% /aside %}
|
||||
|
||||
## Related
|
||||
|
||||
- [Task sandboxing](/docs/features/ci-features/sandboxing)
|
||||
- [Cache task results](/docs/features/cache-task-results)
|
||||
- [Project configuration reference](/docs/reference/project-configuration)
|
||||
@@ -106,6 +106,13 @@ jobs:
|
||||
env:
|
||||
NX_AGENT_NAME: ${{ matrix.agent }}
|
||||
|
||||
# Upload agent resource metrics (Requires Nx 22.1 or higher.) — https://nx.dev/docs/features/ci-features/resource-usage
|
||||
- name: Upload agent metrics
|
||||
if: always()
|
||||
run: npx nx-cloud upload-agent-metrics
|
||||
env:
|
||||
NX_AGENT_NAME: ${{ matrix.agent }}
|
||||
|
||||
# Self-Healing CI: recommend fixes for failures. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
- name: Self-Healing CI
|
||||
run: npx nx fix-ci
|
||||
@@ -160,6 +167,12 @@ jobs:
|
||||
no_output_timeout: 60m
|
||||
environment:
|
||||
NX_AGENT_NAME: << parameters.ordinal >>
|
||||
# Upload agent resource metrics (Requires Nx 22.1 or higher.) — https://nx.dev/docs/features/ci-features/resource-usage
|
||||
- run:
|
||||
command: npx nx-cloud upload-agent-metrics
|
||||
environment:
|
||||
NX_AGENT_NAME: << parameters.ordinal >>
|
||||
when: always
|
||||
# Self-Healing CI: recommend fixes for failures. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
- run:
|
||||
command: npx nx fix-ci
|
||||
@@ -224,6 +237,12 @@ jobs:
|
||||
env:
|
||||
NX_AGENT_NAME: $(System.JobPositionInPhase)
|
||||
|
||||
# Upload agent resource metrics (Requires Nx 22.1 or higher.) — https://nx.dev/docs/features/ci-features/resource-usage
|
||||
- script: npx nx-cloud upload-agent-metrics
|
||||
condition: always()
|
||||
env:
|
||||
NX_AGENT_NAME: $(System.JobPositionInPhase)
|
||||
|
||||
# Self-Healing CI: recommend fixes for failures. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
- script: npx nx fix-ci
|
||||
condition: always()
|
||||
@@ -291,8 +310,10 @@ definitions:
|
||||
- npm ci
|
||||
- npx nx start-agent
|
||||
after-script:
|
||||
# Self-Healing CI: recommend fixes for failures. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
- export NX_AGENT_NAME=$BITBUCKET_STEP_UUID
|
||||
# Upload agent resource metrics (Requires Nx 22.1 or higher.) — https://nx.dev/docs/features/ci-features/resource-usage
|
||||
- npx nx-cloud upload-agent-metrics
|
||||
# Self-Healing CI: recommend fixes for failures. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
- npx nx fix-ci
|
||||
|
||||
pipelines:
|
||||
@@ -345,9 +366,11 @@ image: node:18
|
||||
- yarn install --cache-folder .yarn-cache --prefer-offline --frozen-lockfile
|
||||
- export NX_AGENT_NAME=$CI_JOB_ID
|
||||
- yarn nx start-agent
|
||||
# Self-Healing CI: recommend fixes for failures. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
after_script:
|
||||
- export NX_AGENT_NAME=$CI_JOB_ID
|
||||
# Upload agent resource metrics (Requires Nx 22.1 or higher.) — https://nx.dev/docs/features/ci-features/resource-usage
|
||||
- yarn nx-cloud upload-agent-metrics
|
||||
# Self-Healing CI: recommend fixes for failures. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
- yarn nx fix-ci
|
||||
|
||||
# Creating template for a job running DTE (orchestrator)
|
||||
@@ -466,8 +489,10 @@ pipeline {
|
||||
sh "npx nx start-agent"
|
||||
}
|
||||
post {
|
||||
// Self-Healing CI: recommend fixes for failures. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
always {
|
||||
// Upload agent resource metrics (Requires Nx 22.1 or higher.) — https://nx.dev/docs/features/ci-features/resource-usage
|
||||
sh "npx nx-cloud upload-agent-metrics"
|
||||
// Self-Healing CI: recommend fixes for failures. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
sh "npx nx fix-ci"
|
||||
}
|
||||
}
|
||||
@@ -482,8 +507,10 @@ pipeline {
|
||||
sh "npx nx start-agent"
|
||||
}
|
||||
post {
|
||||
// Self-Healing CI: recommend fixes for failures. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
always {
|
||||
// Upload agent resource metrics (Requires Nx 22.1 or higher.) — https://nx.dev/docs/features/ci-features/resource-usage
|
||||
sh "npx nx-cloud upload-agent-metrics"
|
||||
// Self-Healing CI: recommend fixes for failures. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
sh "npx nx fix-ci"
|
||||
}
|
||||
}
|
||||
@@ -498,8 +525,10 @@ pipeline {
|
||||
sh "npx nx start-agent"
|
||||
}
|
||||
post {
|
||||
// Self-Healing CI: recommend fixes for failures. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
always {
|
||||
// Upload agent resource metrics (Requires Nx 22.1 or higher.) — https://nx.dev/docs/features/ci-features/resource-usage
|
||||
sh "npx nx-cloud upload-agent-metrics"
|
||||
// Self-Healing CI: recommend fixes for failures. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
sh "npx nx fix-ci"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,10 +128,10 @@ Add `.nx/workflows/agents.yaml` to your workspace:
|
||||
// .nx/workflows/agents.yaml
|
||||
common-init-steps: &common-init-steps
|
||||
- name: Checkout
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/checkout/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/checkout/main.yaml'
|
||||
|
||||
- name: Setup toolchains
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-mise/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-mise/main.yaml'
|
||||
|
||||
- name: Install dependencies
|
||||
script: |
|
||||
|
||||
@@ -5,7 +5,7 @@ filter: 'type:Guides'
|
||||
---
|
||||
|
||||
Remote caching shares build results across your team and CI so you don't repeat work.
|
||||
You can use Nx Cloud for a fully managed solution or self-host with one of the available plugins.
|
||||
You can use Nx Cloud for a fully managed solution or build your own self-hosted server using the OpenAPI specification below.
|
||||
|
||||
{% aside type="note" title="Nx Cloud: Managed Remote Cache" %}
|
||||
|
||||
@@ -22,7 +22,7 @@ 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](https://cloud.nx.app/get-started/)
|
||||
{% /aside %}
|
||||
|
||||
{% aside type="note" title="Nx Enterprise" %}
|
||||
@@ -39,37 +39,6 @@ Includes everything from Nx Cloud, plus:
|
||||
|
||||
{% /aside %}
|
||||
|
||||
## Self-hosted cache
|
||||
|
||||
Great for proof of concepts and small teams.
|
||||
|
||||
{% aside type="caution" title="Bucket-based caches are vulnerable to poisoning and often prohibited in organizations" %}
|
||||
|
||||
CREEP (CVE-2025-36852) is a critical vulnerability in bucket-based self-hosted remote caches that allows anyone with PR access to poison production builds. Many organizations are unaware of this security risk. [Learn more](https://nx.dev/blog/creep-vulnerability-build-cache-security)
|
||||
|
||||
All packages below (along with other bucket-based remote cache implementations) are listed in the CVE and are not allowed in many organizations.
|
||||
|
||||
{% /aside %}
|
||||
|
||||
All packages are free but require an activation key. Getting a key is a fully automated, self-service process that happens during package installation. Install any of the following with `nx add`:
|
||||
|
||||
| Package | Storage | Install command |
|
||||
| -------------------------------------------------------------------------------------- | ---------------------------- | ---------------------------- |
|
||||
| [`@nx/s3-cache`](/docs/reference/remote-cache-plugins/s3-cache/overview) | Amazon S3 bucket | `nx add @nx/s3-cache` |
|
||||
| [`@nx/gcs-cache`](/docs/reference/remote-cache-plugins/gcs-cache/overview) | Google Cloud Storage | `nx add @nx/gcs-cache` |
|
||||
| [`@nx/azure-cache`](/docs/reference/remote-cache-plugins/azure-cache/overview) | Azure Blob Storage | `nx add @nx/azure-cache` |
|
||||
| [`@nx/shared-fs-cache`](/docs/reference/remote-cache-plugins/shared-fs-cache/overview) | Shared file system directory | `nx add @nx/shared-fs-cache` |
|
||||
|
||||
The `nx add` command installs the package, configures your workspace, and walks you through generating an activation key.
|
||||
The key is saved to `.nx/key/key.ini` and should be committed to your repository.
|
||||
In CI or public repositories, set the `NX_KEY` environment variable instead.
|
||||
|
||||
If you don't have a key yet, run `nx register` to generate one.
|
||||
If your existing key is expired or invalid, delete `.nx/key/key.ini` and run `nx register` again.
|
||||
In CI, verify that the `NX_KEY` environment variable is set and matches the key in `.nx/key/key.ini`.
|
||||
|
||||
> Why require an activation key? It simply helps us know and support our users. If you prefer not to provide this information, you can also [build your own cache server](#build-your-own-caching-server).
|
||||
|
||||
## Build your own caching server
|
||||
|
||||
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.
|
||||
@@ -232,7 +201,7 @@ To use your custom caching server, set the `NX_SELF_HOSTED_REMOTE_CACHE_SERVER`
|
||||
|
||||
You might have used Nx now-deprecated custom task runners API in these scenarios:
|
||||
|
||||
- To implement custom self-hosted caching: use one of the implementations listed above
|
||||
- To implement custom self-hosted caching, follow the [OpenAPI spec](#open-api-specification), or switch to Nx Cloud
|
||||
- To inject custom behavior before and after running tasks: use our new API with dedicated pre and post hooks
|
||||
|
||||
To learn more about migrating from custom task runners, [please refer to this detailed guide](/docs/reference/deprecated/custom-tasks-runner).
|
||||
|
||||
@@ -76,6 +76,10 @@ 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="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 %}
|
||||
|
||||
### Environment variables for atomized targets
|
||||
|
||||
Atomized targets have their names created dynamically, typically using the file names as a suffix. This makes it difficult to define environment variable files for them.
|
||||
|
||||
@@ -24,7 +24,7 @@ If you are using a custom tasks runner to customize your cache, you have the fol
|
||||
|
||||
Use [Nx Cloud](https://nx.dev/nx-cloud) for your remote cache. This is the safest, lowest-maintenance, most recommended option.
|
||||
|
||||
If you cannot use Nx Cloud, consider an Nx first party plugin for caching: [Amazon S3](/docs/reference/remote-cache-plugins/s3-cache), [Google Cloud](/docs/reference/remote-cache-plugins/gcs-cache), [Azure](/docs/reference/remote-cache-plugins/azure-cache) or a [shared network drive](/docs/reference/remote-cache-plugins/shared-fs-cache). If there is no plugin that supports the service where you want to store the remote cache, [file an issue](https://github.com/nrwl/nx/issues/new).
|
||||
If you cannot use Nx Cloud, [build your own caching server](/docs/guides/tasks--caching/self-hosted-caching#build-your-own-caching-server) using the OpenAPI specification.
|
||||
|
||||
**To learn more about migrating from custom task runners, [please refer to this detailed guide](/docs/reference/deprecated/custom-tasks-runner).**
|
||||
|
||||
@@ -34,6 +34,5 @@ The `NX_REJECT_UNKNOWN_LOCAL_CACHE` environment variable does not work with the
|
||||
|
||||
If you are using `NX_REJECT_UNKNOWN_LOCAL_CACHE` to share your local cache on a network drive, you have a few options moving forward:
|
||||
|
||||
1. Use [Nx Cloud](https://nx.dev/nx-cloud) for your remote cache. This is the safest, lowest-maintenance, most recommended option
|
||||
2. Use the [Nx Enterprise Shared Fs Cache](/docs/reference/remote-cache-plugins/shared-fs-cache) which recognizes artifacts from other machines
|
||||
3. Use another [Nx Enterprise](https://nx.dev/enterprise) plugin for caching on a cloud provider: [Amazon S3](/docs/reference/remote-cache-plugins/s3-cache), [Google Cloud](/docs/reference/remote-cache-plugins/gcs-cache), or [Azure](/docs/reference/remote-cache-plugins/azure-cache)
|
||||
1. Use [Nx Cloud](https://nx.dev/nx-cloud) for your remote cache. This is the safest, lowest-maintenance, most recommended option.
|
||||
2. [Build your own caching server](/docs/guides/tasks--caching/self-hosted-caching#build-your-own-caching-server) using the OpenAPI specification.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: 'Deprecation notice: self-hosted remote cache packages'
|
||||
description: '@nx/s3-cache, @nx/gcs-cache, @nx/azure-cache, and @nx/shared-fs-cache are deprecated due to CVE-2025-36852 (CREEP).'
|
||||
template: splash
|
||||
head:
|
||||
- tag: meta
|
||||
attrs:
|
||||
name: robots
|
||||
content: index, follow
|
||||
---
|
||||
|
||||
`@nx/s3-cache`, `@nx/gcs-cache`, `@nx/azure-cache`, and `@nx/shared-fs-cache` are deprecated as of 2026-05-21.
|
||||
The CREEP vulnerability ([CVE-2025-36852](https://www.cve.org/CVERecord?id=CVE-2025-36852)) affects all four packages.
|
||||
The flaw is in their design and cannot be patched.
|
||||
|
||||
These packages use a single credential that grants read and write access across the entire cache.
|
||||
Nothing in the bucket tracks which branch produced which artifact.
|
||||
|
||||
An attacker can open a PR off `main` with no source changes but a modified CI workflow that builds a malicious artifact.
|
||||
The CI workflow isn't part of the cache key, so the PR hashes to the same key that `main` will hash to.
|
||||
If the PR uploads its artifact first, every later `main` build with that key gets a cache hit on the poisoned artifact and ships it without rebuilding.
|
||||
|
||||
Supply chain attacks against open-source ecosystems are now a near-weekly occurrence, and cache poisoning is a known vector.
|
||||
We have no evidence that these packages have been exploited in the wild, but the design above guarantees that any attempt will succeed.
|
||||
Treat these packages as a live risk and migrate.
|
||||
|
||||
The packages stay on npm so existing builds don't break for now.
|
||||
They will not receive updates or security patches, and may be removed in the future.
|
||||
|
||||
For information on the vulnerability, see [The CREEP vulnerability and build cache security](https://nx.dev/blog/creep-vulnerability-build-cache-security#the-creep-vulnerability).
|
||||
|
||||
## Affected packages
|
||||
|
||||
- `@nx/s3-cache`
|
||||
- `@nx/gcs-cache`
|
||||
- `@nx/azure-cache`
|
||||
- `@nx/shared-fs-cache`
|
||||
|
||||
## Recommended: Migrate to Nx Cloud OR disable remote cache
|
||||
|
||||
[Nx Cloud](https://nx.dev/nx-cloud) is the best solution for remote cache.
|
||||
It includes a free tier for small teams and requires no infrastructure on your side.
|
||||
|
||||
To connect your workspace, see [Connect to Nx Cloud](/docs/getting-started/nx-cloud).
|
||||
If you need on-premises storage, see [Self-hosted caching](/docs/guides/tasks--caching/self-hosted-caching).
|
||||
|
||||
If you cannot use Nx Cloud right now, we recommend disabling remote cache to avoid exploitation of the CREEP vulnerability.
|
||||
|
||||
## Advanced: Build your own
|
||||
|
||||
{% aside type="caution" title="Self-hosting a remote cache is high-risk" %}
|
||||
Implementing a remote cache server yourself means accepting full responsibility for the threat model that CREEP exposed.
|
||||
You must understand cache poisoning, artifact integrity, and access control before you deploy one.
|
||||
A misconfigured implementation reproduces the same vulnerability these deprecated packages had.
|
||||
|
||||
The OpenAPI spec requires a 409 Conflict response when a client tries to write a cache key that already exists.
|
||||
Implementations that allow overwriting existing entries are vulnerable even without a race.
|
||||
Your implementation must enforce 409 on existing keys.
|
||||
|
||||
Most teams should use Nx Cloud instead.
|
||||
{% /aside %}
|
||||
|
||||
If you have the resources to harden and operate a cache server, you can implement the [Nx remote cache OpenAPI specification](/docs/guides/tasks--caching/self-hosted-caching#build-your-own-caching-server).
|
||||
The four deprecated packages will not be updated to match that specification.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Were these packages compromised?
|
||||
|
||||
No. These remote cache packages have not been compromised.
|
||||
The security issue is that bucket-based cache solutions are open to cache poisoning attacks by design.
|
||||
|
||||
### What should I do if I'm using one of these packages today?
|
||||
|
||||
Migrate to Nx Cloud, or implement the OpenAPI specification yourself if you have the resources to harden it.
|
||||
The packages stay on npm but will not be patched.
|
||||
|
||||
### Will these packages receive security patches?
|
||||
|
||||
No. The vulnerability is in the design of the packages, not in a fixable bug.
|
||||
|
||||
### Will the packages be unpublished from npm?
|
||||
|
||||
No. They remain on npm for now so existing builds do not break immediately. They will not receive updates.
|
||||
@@ -28,23 +28,22 @@ Credits represent the computational resources consumed during CI/CD operations.
|
||||
|
||||
#### Docker / Linux AMD64
|
||||
|
||||
| Resource Class | Specifications | Credits/min |
|
||||
| -------------- | ----------------- | ----------- |
|
||||
| Small | 1 vCPU, 2GB RAM | 5 |
|
||||
| Medium | 2 vCPU, 4GB RAM | 10 |
|
||||
| Medium + | 3 vCPU, 6GB RAM | 15 |
|
||||
| Large | 4 vCPU, 8GB RAM | 20 |
|
||||
| Large + | 4 vCPU, 10GB RAM | 30 |
|
||||
| Extra large | 8 vCPU, 16GB RAM | 40 |
|
||||
| Extra large + | 10 vCPU, 20GB RAM | 60 |
|
||||
| Resource Class | Specifications | Credits/min |
|
||||
| -------------- | -------------- | ----------- |
|
||||
| Small | 1 vCPU | 5 |
|
||||
| Medium | 2 vCPU | 10 |
|
||||
| Large | 4 vCPU | 20 |
|
||||
| Extra large | 8 vCPU | 40 |
|
||||
|
||||
#### Docker / Linux ARM64
|
||||
|
||||
| Resource Class | Specifications | Credits/min |
|
||||
| -------------- | ---------------- | ----------- |
|
||||
| Medium | 2 vCPU, 8GB RAM | 13 |
|
||||
| Large | 4 vCPU, 16GB RAM | 26 |
|
||||
| Extra large | 8 vCPU, 32GB RAM | 52 |
|
||||
| Resource Class | Specifications | Credits/min |
|
||||
| -------------- | -------------- | ----------- |
|
||||
| Medium | 2 vCPU | 13 |
|
||||
| Large | 4 vCPU | 26 |
|
||||
| Extra large | 8 vCPU | 52 |
|
||||
|
||||
_Note: Linux resource classes have memory available in an approximate 1:4 ratio per vCPU core._
|
||||
|
||||
#### Docker / Windows
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ common-js-init-steps: &common-js-init-steps
|
||||
# using a reusable step in an external GitHub repo,
|
||||
# this step is provided by Nx Cloud: https://github.com/nrwl/nx-cloud-workflows/tree/main/workflow-steps
|
||||
- name: Checkout
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/checkout/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/checkout/main.yaml'
|
||||
- name: Restore Node Modules Cache
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/cache/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/cache/main.yaml'
|
||||
# the cache step requires configuration via env vars
|
||||
# https://github.com/nrwl/nx-cloud-workflows/tree/main/workflow-steps/cache#options
|
||||
inputs:
|
||||
@@ -32,16 +32,16 @@ common-js-init-steps: &common-js-init-steps
|
||||
# or ~/.local/share/pnpm/store
|
||||
base-branch: 'main'
|
||||
- name: Restore Browser Binary Cache
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/cache/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/cache/main.yaml'
|
||||
inputs:
|
||||
key: 'package-lock.json|yarn.lock|pnpm-lock.yaml|"browsers"'
|
||||
paths: |
|
||||
'~/.cache/Cypress'
|
||||
base-branch: 'main'
|
||||
- name: Install Node Modules
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-node-modules/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-node-modules/main.yaml'
|
||||
- name: Install Browsers (if needed)
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-browsers/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-browsers/main.yaml'
|
||||
# You can also run a custom script to configure various things on the agent machine
|
||||
- name: Run a custom script
|
||||
script: |
|
||||
@@ -55,7 +55,7 @@ common-js-init-steps: &common-js-init-steps
|
||||
|
||||
common-rust-init-steps: &common-rust-init-steps
|
||||
- name: Checkout
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/checkout/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/checkout/main.yaml'
|
||||
|
||||
# add Rust-specific steps
|
||||
- name: Install Rust
|
||||
@@ -156,7 +156,7 @@ launch-templates:
|
||||
image: 'ubuntu22.04-node24.14-v1'
|
||||
init-steps:
|
||||
- name: Checkout
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/checkout/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/checkout/main.yaml'
|
||||
- name: Auth to Registry
|
||||
script: |
|
||||
# we recommend add to the 'user' level npmrc file
|
||||
@@ -165,7 +165,7 @@ launch-templates:
|
||||
npm config set -L user "@myorg:registry" "https://npm.pkg.github.com"
|
||||
npm config set -L user "//npm.pkg.github.com/:_authToken" "${SOME_AUTH_TOKEN}"
|
||||
- name: Install Node Modules
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-node-modules/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-node-modules/main.yaml'
|
||||
```
|
||||
|
||||
Pass `SOME_AUTH_TOKEN` via `--with-env-vars`
|
||||
@@ -192,15 +192,15 @@ launch-templates:
|
||||
image: 'ubuntu22.04-node24.14-v1'
|
||||
init-steps:
|
||||
- name: Checkout
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/checkout/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/checkout/main.yaml'
|
||||
- name: Install mise
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-mise/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-mise/main.yaml'
|
||||
inputs:
|
||||
# you can also define a mise.toml in your repo instead of inline tools
|
||||
tools: |
|
||||
node=21
|
||||
- name: Install Node Modules
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-node-modules/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-node-modules/main.yaml'
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
@@ -218,10 +218,10 @@ launch-templates:
|
||||
image: 'ubuntu22.04-node24.14-v1'
|
||||
init-steps:
|
||||
- name: Checkout
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/checkout/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/checkout/main.yaml'
|
||||
- name: Install Node
|
||||
# note the step is only released as of v4 of the workflow steps
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-node/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-node/main.yaml'
|
||||
inputs:
|
||||
# can omit value if a '.nvmrc' file is within the root of the repo
|
||||
node_version: '21'
|
||||
@@ -246,7 +246,7 @@ launch-templates:
|
||||
image: 'ubuntu22.04-node24.14-v1'
|
||||
init-steps:
|
||||
- name: Checkout
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/checkout/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/checkout/main.yaml'
|
||||
- name: Install nvm
|
||||
script: |
|
||||
# run nvm install script
|
||||
@@ -261,7 +261,7 @@ launch-templates:
|
||||
# confirm that the node version has changed
|
||||
script: node -v
|
||||
- name: Install Node Modules
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-node-modules/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-node-modules/main.yaml'
|
||||
# Continue setup steps as needed
|
||||
```
|
||||
|
||||
@@ -326,11 +326,11 @@ launch-templates:
|
||||
image: 'ubuntu22.04-node24.14-v1'
|
||||
init-steps:
|
||||
- name: Checkout
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/checkout/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/checkout/main.yaml'
|
||||
- name: Install mise
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-mise/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-mise/main.yaml'
|
||||
- name: Install Node Modules
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-node-modules/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-node-modules/main.yaml'
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
@@ -346,16 +346,16 @@ launch-templates:
|
||||
image: 'ubuntu22.04-node24.14-v1'
|
||||
init-steps:
|
||||
- name: Checkout
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/checkout/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/checkout/main.yaml'
|
||||
- name: Install mise
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-mise/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-mise/main.yaml'
|
||||
inputs:
|
||||
tools: |
|
||||
node=22
|
||||
rust=1.90
|
||||
python=3.12
|
||||
- name: Install Node Modules
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-node-modules/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-node-modules/main.yaml'
|
||||
```
|
||||
|
||||
{% aside type="caution" %}
|
||||
@@ -391,7 +391,7 @@ launch-templates:
|
||||
image: 'ubuntu22.04-node24.14-v1'
|
||||
init-steps:
|
||||
- name: Install AWS CLI
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-aws-cli/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-aws-cli/main.yaml'
|
||||
# no additional inputs required, as all configuration is via environment variables via --with-env-vars
|
||||
```
|
||||
|
||||
|
||||
@@ -74,9 +74,13 @@ A launch template's `image` defines the available base software for the agent ma
|
||||
|
||||
{% aside type="tip" title="Looking for Docker in Docker support?" %}
|
||||
|
||||
Docker in Docker support (DinD) is currently limited to Organizations on the enterprise plan.
|
||||
Docker-in-Docker (DinD) runs on a
|
||||
[dedicated compute cluster](/docs/features/ci-features/dedicated-compute-cluster). Every agent in
|
||||
the cluster can run DinD, so your tasks can build and push container images or run Testcontainers.
|
||||
Request the add-on under **Settings > Add-ons**.
|
||||
|
||||
If you're interested in our [Enterprise plan please reach out!](/contact/sales)
|
||||
Nx Enterprise [single-tenant](/docs/enterprise/single-tenant/overview) customers get DinD through
|
||||
their dedicated deployment.
|
||||
{% /aside %}
|
||||
|
||||
```yaml
|
||||
@@ -191,9 +195,9 @@ You can find the [list of Nx Cloud reusable steps here](https://github.com/nrwl/
|
||||
launch-templates:
|
||||
template-one:
|
||||
init-steps:
|
||||
- uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/checkout/main.yaml'
|
||||
- uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/checkout/main.yaml'
|
||||
- name: 'Install Node Modules'
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/install-node-modules/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/install-node-modules/main.yaml'
|
||||
```
|
||||
|
||||
### `launch-templates.<template-name>.init-steps[*].script`
|
||||
@@ -239,7 +243,7 @@ launch-templates:
|
||||
template-one:
|
||||
init-steps:
|
||||
- name: Restore Node Modules Cache
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/cache/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/cache/main.yaml'
|
||||
inputs:
|
||||
# Include patches directories to ensure cache is busted when patches change
|
||||
key: 'package-lock.json|yarn.lock|pnpm-lock.yaml|patches/**|.yarn/patches/**|pnpm-patches/**'
|
||||
@@ -320,7 +324,7 @@ grouped together logically. The below cache steps will also be collapsed togethe
|
||||
- group-name: Restore Cache
|
||||
steps:
|
||||
- name: Restore Node Modules Cache
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/cache/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/cache/main.yaml'
|
||||
inputs:
|
||||
# Include patches directories to ensure cache is busted when patches change
|
||||
key: 'package-lock.json|patches/**|.yarn/patches/**|pnpm-patches/**'
|
||||
@@ -328,7 +332,7 @@ grouped together logically. The below cache steps will also be collapsed togethe
|
||||
~/.npm
|
||||
base-branch: 'main'
|
||||
- name: Restore Browser Binary Cache
|
||||
uses: 'nrwl/nx-cloud-workflows/v5/workflow-steps/cache/main.yaml'
|
||||
uses: 'nrwl/nx-cloud-workflows/v6/workflow-steps/cache/main.yaml'
|
||||
inputs:
|
||||
# Include patches directories to ensure cache is busted when patches change
|
||||
key: 'package-lock.json|patches/**|.yarn/patches/**|pnpm-patches/**|"browsers"'
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
title: Azure Cache
|
||||
sidebar:
|
||||
hidden: true
|
||||
description: Azure Blob Storage cache plugin
|
||||
pagefind: false
|
||||
---
|
||||
|
||||
{% index_page_cards path="reference/remote-cache-plugins/azure-cache" /%}
|
||||
@@ -1,161 +0,0 @@
|
||||
---
|
||||
title: Overview of the Nx Azure Cache Plugin
|
||||
description: The @nx/azure-cache plugin enables you to use Azure Storage to host your remote cache for efficient build caching across your team.
|
||||
sidebar:
|
||||
label: Overview
|
||||
weight: 2.1
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
The `@nx/azure-cache` plugin enables you to self-host your remote cache on [Azure Storage](https://azure.microsoft.com/en-us/products/storage/blobs).
|
||||
|
||||
{% aside type="caution" title="Bucket-based caches are vulnerable to poisoning and often prohibited in organizations" %}
|
||||
|
||||
CREEP (CVE-2025-36852) is a critical vulnerability in bucket-based self-hosted remote caches that allows anyone with PR access to poison production builds. Many organizations are unaware of this security risk. [Learn more](https://nx.dev/blog/creep-vulnerability-build-cache-security)
|
||||
|
||||
`@nx/azure-cache` (along with other bucket-based remote cache implementations) is listed in the CVE, and is not allowed in many organizations.
|
||||
|
||||
{% /aside %}
|
||||
|
||||
{% aside type="tip" title="Nx Cloud: Managed Multi-Tier Remote Cache [Secure]" %}
|
||||
|
||||
Recommended for everyone.
|
||||
|
||||
- [Fully managed multi-tier remote caching with Nx Replay](/docs/features/ci-features/remote-cache)
|
||||
- [Both secure and fast](https://nx.dev/enterprise/security)
|
||||
- Generous free plan
|
||||
|
||||
You'll also get access to advanced CI features:
|
||||
|
||||
- [Automated distribution of tasks across machines with Nx Agents](/docs/features/ci-features/distribute-task-execution)
|
||||
- [Automated splitting of tasks (including e2e tests) with Nx Atomizer](/docs/features/ci-features/split-e2e-tasks)
|
||||
- [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)
|
||||
{% /aside %}
|
||||
|
||||
{% aside type="tip" title="Nx Enterprise [Secure]" %}
|
||||
|
||||
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 %}
|
||||
|
||||
## Set up @nx/azure-cache
|
||||
|
||||
### 1. Install the package
|
||||
|
||||
Run the following command:
|
||||
|
||||
```shell
|
||||
nx add @nx/azure-cache
|
||||
```
|
||||
|
||||
This will add the `@nx/azure-cache` NPM package and automatically configure it for your workspace. As part of this process, you'll be guided to **generate a new activation key**. This is a fully automated process to register your plugin.
|
||||
|
||||
The key will be saved in your repository (`.nx/key/key.ini`) and should be committed so that every developer has access to it. If your repository is public (or in CI), you can also use an environment variable:
|
||||
|
||||
```dotenv
|
||||
// .env
|
||||
NX_KEY=YOUR_ACTIVATION_KEY
|
||||
```
|
||||
|
||||
> Why require an activation key? It simply helps us know and support our users. If you prefer not to provide this information, you can also build your own cache server. [Learn more.](/docs/guides/tasks--caching/self-hosted-caching)
|
||||
|
||||
### 2. Authenticate with Azure
|
||||
|
||||
There are several ways to [authenticate with Azure Storage](https://github.com/Azure/login#login-with-openid-connect-oidc-recommended), but the method recommended by Azure is to use OpenID Connect, like this:
|
||||
|
||||
```yaml
|
||||
// .github/workflows/ci.yml
|
||||
name: CI
|
||||
...
|
||||
permissions:
|
||||
id-token: write
|
||||
...
|
||||
|
||||
jobs:
|
||||
main:
|
||||
env:
|
||||
NX_KEY: ${{ secrets.NX_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
...
|
||||
|
||||
- name: Azure login
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
...
|
||||
|
||||
- run: pnpm exec nx affected -t lint test build
|
||||
```
|
||||
|
||||
You need to set the `AZURE_CLIENT_ID`, `AZURE_TENANT_ID` and `AZURE_SUBSCRIPTION_ID` secrets as defined in the [Azure documentation](https://github.com/Azure/login#login-with-openid-connect-oidc-recommended).
|
||||
|
||||
Note: Any authentication method that [sets up the `DefaultAzureCredentials`](https://learn.microsoft.com/en-us/azure/developer/javascript/sdk/credential-chains#use-defaultazurecredential-for-flexibility) will enable the plugin to work.
|
||||
|
||||
{% aside type="note" title="Custom Azure Endpoint" %}
|
||||
If you are using a custom Azure endpoint, you will need to authenticate by [setting the `AZURE_STORAGE_CONNECTION_STRING` environment variable](https://learn.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string). The `@nx/azure-cache` plugin will detect the environment variable and automatically use it to connect to Azure.
|
||||
{% /aside %}
|
||||
|
||||
### 3. configure the Nx cache to use Azure storage
|
||||
|
||||
Finally, you need to configure your Nx cache in the `nx.json` file. The `container` that you specify needs to already exist - Nx doesn't create it for you.
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
{
|
||||
"azure": {
|
||||
"container": "mycontainer",
|
||||
"accountName": "myaccount",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
| **Property** | **Description** |
|
||||
| --------------- | -------------------------------- |
|
||||
| **container** | The name of the container to use |
|
||||
| **accountName** | The name of blob storage account |
|
||||
|
||||
### Migrating from custom tasks runners
|
||||
|
||||
Many people who are interested in Nx caching plugins have previously used custom task runners. Nx offers a new and simpler extension API designed to meet the same use cases as the now-deprecated custom task runners.
|
||||
|
||||
To learn more about migrating from custom task runners, [please refer to this detailed guide](/docs/reference/deprecated/custom-tasks-runner).
|
||||
|
||||
By default, Nx will try to write and read from the remote cache while running locally. This means that permissions must be set for users who are expected to access the remote cache.
|
||||
|
||||
Nx will only show warnings when the remote cache is not writable. You can disable these warnings by setting `localMode` to `read-only` or `no-cache` in the `nx.json` file.
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
{
|
||||
"azure": {
|
||||
// ...
|
||||
"localMode": "read-only",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The cache mode in CI can also be configured by setting `ciMode` to `read-only` or `no-cache` in the `nx.json` file. Or setting `NX_POWERPACK_CACHE_MODE` to `read-only` or `no-cache` in the CI environment.
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
{
|
||||
"azure": {
|
||||
// ...
|
||||
"ciMode": "read-only",
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
title: GCS Cache
|
||||
sidebar:
|
||||
hidden: true
|
||||
description: Google Cloud Storage cache plugin
|
||||
pagefind: false
|
||||
---
|
||||
|
||||
{% index_page_cards path="reference/remote-cache-plugins/gcs-cache" /%}
|
||||
@@ -1,160 +0,0 @@
|
||||
---
|
||||
title: Overview of the Nx GCS Cache Plugin
|
||||
description: The @nx/gcs-cache plugin enables you to use Google Cloud Storage to host your remote cache for efficient build caching across your team.
|
||||
sidebar:
|
||||
label: Overview
|
||||
weight: 2.1
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
The `@nx/gcs-cache` plugin enables you to self-host your remote cache on [Google Cloud Storage](https://cloud.google.com/storage).
|
||||
|
||||
{% aside type="caution" title="Bucket-based caches are vulnerable to poisoning and often prohibited in organizations" %}
|
||||
|
||||
CREEP (CVE-2025-36852) is a critical vulnerability in bucket-based self-hosted remote caches that allows anyone with PR access to poison production builds. Many organizations are unaware of this security risk. [Learn more](https://nx.dev/blog/creep-vulnerability-build-cache-security)
|
||||
|
||||
`@nx/gcs-cache` (along with other bucket-based remote cache implementations) is listed in the CVE, and is not allowed in many organizations.
|
||||
|
||||
{% /aside %}
|
||||
|
||||
{% aside type="tip" title="Nx Cloud: Managed Multi-Tier Remote Cache [Secure]" %}
|
||||
|
||||
Recommended for everyone.
|
||||
|
||||
- [Fully managed multi-tier remote caching with Nx Replay](/docs/features/ci-features/remote-cache)
|
||||
- [Both secure and fast](https://nx.dev/enterprise/security)
|
||||
- Generous free plan
|
||||
|
||||
You'll also get access to advanced CI features:
|
||||
|
||||
- [Automated distribution of tasks across machines with Nx Agents](/docs/features/ci-features/distribute-task-execution)
|
||||
- [Automated splitting of tasks (including e2e tests) with Nx Atomizer](/docs/features/ci-features/split-e2e-tasks)
|
||||
- [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)
|
||||
{% /aside %}
|
||||
|
||||
{% aside type="tip" title="Nx Enterprise [Secure]" %}
|
||||
|
||||
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 %}
|
||||
|
||||
## Set up @nx/gcs-cache
|
||||
|
||||
### 1. Install the package
|
||||
|
||||
Run the following command:
|
||||
|
||||
```shell
|
||||
nx add @nx/gcs-cache
|
||||
```
|
||||
|
||||
This will add the `@nx/gcs-cache` NPM package and automatically configure it for your workspace. As part of this process, you'll be guided to **generate a new activation key**. This is a fully automated process to register your plugin.
|
||||
|
||||
The key will be saved in your repository (`.nx/key/key.ini`) and should be committed so that every developer has access to it. If your repository is public (or in CI), you can also use an environment variable:
|
||||
|
||||
```dotenv
|
||||
// .env
|
||||
NX_KEY=YOUR_ACTIVATION_KEY
|
||||
```
|
||||
|
||||
> Why require an activation key? It simply helps us know and support our users. If you prefer not to provide this information, you can also build your own cache server. [Learn more.](/docs/guides/tasks--caching/self-hosted-caching)
|
||||
|
||||
### 2. authenticate with google cloud
|
||||
|
||||
There are several ways to [authenticate with Google Cloud Storage](https://github.com/google-github-actions/setup-gcloud#authorization), but the method recommended by Google is to use Workload Identity Federation, like this:
|
||||
|
||||
```yaml
|
||||
// .github/workflows/ci.yml
|
||||
name: CI
|
||||
...
|
||||
permissions:
|
||||
id-token: write
|
||||
...
|
||||
|
||||
jobs:
|
||||
main:
|
||||
env:
|
||||
NX_KEY: ${{ secrets.NX_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
...
|
||||
|
||||
- id: 'auth'
|
||||
name: 'Authenticate to Google Cloud'
|
||||
uses: 'google-github-actions/auth@v2'
|
||||
with:
|
||||
token_format: 'access_token'
|
||||
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
|
||||
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
|
||||
|
||||
- name: 'Set up Cloud SDK'
|
||||
uses: 'google-github-actions/setup-gcloud@v2'
|
||||
with:
|
||||
version: '>= 363.0.0'
|
||||
|
||||
...
|
||||
|
||||
- run: pnpm exec nx affected -t lint test build
|
||||
```
|
||||
|
||||
Note: Any authentication method that [sets up the Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) will enable the plugin to work.
|
||||
|
||||
### 3. configure the Nx cache to use google cloud storage
|
||||
|
||||
Finally, you need to configure your Nx cache in the `nx.json` file. The `bucket` that you specify needs to already exist - Nx doesn't create it for you.
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
{
|
||||
"gcs": {
|
||||
"bucket": "my-bucket",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
| **Property** | **Description** |
|
||||
| ------------ | ----------------------------- |
|
||||
| **bucket** | The name of the bucket to use |
|
||||
|
||||
### Migrating from custom tasks runners
|
||||
|
||||
Many people who are interested in Nx caching plugins have previously used custom task runners. Nx offers a new and simpler extension API designed to meet the same use cases as the now-deprecated custom task runners.
|
||||
|
||||
To learn more about migrating from custom task runners, [please refer to this detailed guide](/docs/reference/deprecated/custom-tasks-runner).
|
||||
|
||||
By default, Nx will try to write and read from the remote cache while running locally. This means that permissions must be set for users who are expected to access the remote cache.
|
||||
|
||||
Nx will only show warnings when the remote cache is not writable. You can disable these warnings by setting `localMode` to `read-only` or `no-cache` in the `nx.json` file.
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
{
|
||||
"gcs": {
|
||||
// ...
|
||||
"localMode": "read-only",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The cache mode in CI can also be configured by setting `ciMode` to `read-only` or `no-cache` in the `nx.json` file. Or by setting `NX_POWERPACK_CACHE_MODE` to `read-only` or `no-cache` in the CI environment.
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
{
|
||||
"gcs": {
|
||||
// ...
|
||||
"ciMode": "read-only",
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
title: Remote Cache Plugins
|
||||
sidebar:
|
||||
hidden: true
|
||||
description: Available remote cache implementations
|
||||
pagefind: false
|
||||
---
|
||||
|
||||
{% index_page_cards path="reference/remote-cache-plugins" /%}
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
title: S3 Cache
|
||||
sidebar:
|
||||
hidden: true
|
||||
description: Amazon S3 cache plugin
|
||||
pagefind: false
|
||||
---
|
||||
|
||||
{% index_page_cards path="reference/remote-cache-plugins/s3-cache" /%}
|
||||
@@ -1,258 +0,0 @@
|
||||
---
|
||||
title: Overview of the Nx S3 Cache Plugin
|
||||
description: The @nx/s3-cache plugin enables you to use an Amazon S3 bucket to host your remote cache for efficient build caching across your team.
|
||||
sidebar:
|
||||
label: Overview
|
||||
weight: 2.1
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
The `@nx/s3-cache` plugin enables you to self-host your remote cache on an [Amazon S3](https://aws.amazon.com/s3) bucket.
|
||||
|
||||
{% aside type="caution" title="Bucket-based caches are vulnerable to poisoning and often prohibited in organizations" %}
|
||||
|
||||
CREEP (CVE-2025-36852) is a critical vulnerability in bucket-based self-hosted remote caches that allows anyone with PR access to poison production builds. Many organizations are unaware of this security risk. [Learn more](https://nx.dev/blog/creep-vulnerability-build-cache-security)
|
||||
|
||||
`@nx/s3-cache` (along with other bucket-based remote cache implementations) is listed in the CVE, and is not allowed in many organizations.
|
||||
|
||||
{% /aside %}
|
||||
|
||||
{% aside type="tip" title="Nx Cloud: Managed Multi-Tier Remote Cache [Secure]" %}
|
||||
|
||||
Recommended for everyone.
|
||||
|
||||
- [Fully managed multi-tier remote caching with Nx Replay](/docs/features/ci-features/remote-cache)
|
||||
- [Both secure and fast](https://nx.dev/enterprise/security)
|
||||
- Generous free plan
|
||||
|
||||
You'll also get access to advanced CI features:
|
||||
|
||||
- [Automated distribution of tasks across machines with Nx Agents](/docs/features/ci-features/distribute-task-execution)
|
||||
- [Automated splitting of tasks (including e2e tests) with Nx Atomizer](/docs/features/ci-features/split-e2e-tasks)
|
||||
- [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)
|
||||
{% /aside %}
|
||||
|
||||
{% aside type="tip" title="Nx Enterprise [Secure]" %}
|
||||
|
||||
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 %}
|
||||
|
||||
## Set up @nx/s3-cache
|
||||
|
||||
### 1. Install the package
|
||||
|
||||
Run the following command:
|
||||
|
||||
```shell
|
||||
nx add @nx/s3-cache
|
||||
```
|
||||
|
||||
This will add the `@nx/s3-cache` NPM package and automatically configure it for your workspace. As part of this process you'll be guided to **generate a new activation key**. This is a fully automated process to register your plugin.
|
||||
|
||||
The key will be saved in your repository (`.nx/key/key.ini`) and should be committed so that every developer has access to it. If your repository is public (or in CI) you can also use an environment variable:
|
||||
|
||||
```dotenv
|
||||
// .env
|
||||
NX_KEY=YOUR_ACTIVATION_KEY
|
||||
```
|
||||
|
||||
If you didn't get an activation key or skipped that step, you can easily generate one at any time by running `nx register` in your terminal.
|
||||
|
||||
> Why require an activation key? It simply helps us know and support our users. If you prefer not to provide this information, you can also build your own cache server. [Learn more.](/docs/guides/tasks--caching/self-hosted-caching)
|
||||
|
||||
### 2. Authenticate with AWS
|
||||
|
||||
There are four different ways to authenticate with AWS. They will be attempted in this order:
|
||||
|
||||
1. Environment variables
|
||||
2. INI config files
|
||||
3. Single sign-on
|
||||
4. `nx.json` settings
|
||||
|
||||
#### Environment variables
|
||||
|
||||
[AWS provides environment variables](https://docs.aws.amazon.com/sdkref/latest/guide/environment-variables.html) that can be used to authenticate:
|
||||
|
||||
| **Environment Variable** | **Description** |
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `AWS_ACCESS_KEY_ID` | The access key for your AWS account. |
|
||||
| `AWS_SECRET_ACCESS_KEY` | The secret key for your AWS account. |
|
||||
| `AWS_SESSION_TOKEN` | The session key for your AWS account. This is only needed when you are using temporary credentials. |
|
||||
| `AWS_CREDENTIAL_EXPIRATION` | The expiration time of the credentials contained in the environment variables described above. This value must be in a format compatible with the [ISO-8601 standard](https://en.wikipedia.org/wiki/ISO_8601) and is only needed when you are using temporary credentials. |
|
||||
|
||||
Both the `AWS_ACCESS_KEY_ID` and the `AWS_SECRET_ACCESS_KEY` environment variables are required to use the environment variable authentication method.
|
||||
|
||||
Here's an example of using OIDC in GitHub Actions to set the environment variables in CI:
|
||||
|
||||
```yaml
|
||||
// .github/workflows/ci.yml
|
||||
name: CI
|
||||
...
|
||||
permissions:
|
||||
id-token: write
|
||||
...
|
||||
|
||||
jobs:
|
||||
main:
|
||||
env:
|
||||
NX_KEY: ${{ secrets.NX_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
...
|
||||
|
||||
- name: 'Configure AWS Credentials'
|
||||
uses: aws-actions/configure-aws-credentials@v4.0.2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::123456789123:role/GhAIBucketUserRole
|
||||
aws-region: us-east-1
|
||||
|
||||
...
|
||||
|
||||
- run: pnpm exec nx affected -t lint test build
|
||||
```
|
||||
|
||||
#### INI config files
|
||||
|
||||
AWS can read your authentication credentials from [shared INI config files](https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html). The files are located at `~/.aws/credentials` and `~/.aws/config`. Both files are expected to be INI formatted with section names corresponding to profiles. Sections in the credentials file are treated as profile names, whereas profile sections in the config file must have the format of `[profile profile-name]`, except for the default profile. Profiles that appear in both files will not be merged, and the version that appears in the credentials file will be given precedence over the profile found in the config file.
|
||||
|
||||
#### Single sign-on
|
||||
|
||||
Nx can read the active access token [created after running `aws sso login`](https://docs.aws.amazon.com/sdkref/latest/guide/understanding-sso.html) then request temporary AWS credentials. You can create the `AwsCredentialIdentityProvider` functions using the inline SSO parameters (`ssoStartUrl`, `ssoAccountId`, `ssoRegion`, `ssoRoleName`) or load them from [AWS SDKs and Tools shared configuration and credentials files](https://docs.aws.amazon.com/credref/latest/refdocs/creds-config-files.html). Profiles in the `credentials` file are given precedence over profiles in the `config` file.
|
||||
|
||||
#### Credentials in `nx.json` file
|
||||
|
||||
Storing your credentials in the `nx.json` file is the least secure of the 4 authentication options, since anyone with read access to your codebase will have access to your AWS credentials.
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
{
|
||||
"s3": {
|
||||
"ssoProfile": "default",
|
||||
"accessKeyId": "MYACCESSKEYID",
|
||||
"secretAccessKey": "MYSECRETACCESSKEY",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
| **Property** | **Description** |
|
||||
| ------------------- | ----------------------------------------------------------------------------- |
|
||||
| **ssoProfile** | The name of the profile to use from your AWS CLI SSO Configuration (optional) |
|
||||
| **endpoint** | The AWS endpoint URL (optional) |
|
||||
| **accessKeyId** | AWS Access Key ID (optional) |
|
||||
| **secretAccessKey** | AWS secret access key (optional) |
|
||||
|
||||
### 3. Configure S3 cache
|
||||
|
||||
Regardless of how you manage your AWS authentication, you need to configure your Nx cache in the `nx.json` file. The `bucket` that you specify needs to already exist - Nx doesn't create it for you.
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
{
|
||||
"s3": {
|
||||
"region": "us-east-1",
|
||||
"bucket": "my-bucket",
|
||||
"encryptionKey": "create-your-own-key",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
| **Property** | **Description** |
|
||||
| ----------------- | --------------------------------------------------------------------------------- |
|
||||
| **region** | The ID of the AWS region to use |
|
||||
| **bucket** | The name of the S3 bucket to use |
|
||||
| **encryptionKey** | Nx encryption key used to encrypt and decrypt artifacts from the cache (optional) |
|
||||
|
||||
#### S3 compatible providers
|
||||
|
||||
To use `@nx/s3-cache` with S3 compatible providers ([MinIO](https://min.io/product/s3-compatibility), [LocalStack](https://www.localstack.cloud), [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces), [Cloudflare](https://www.cloudflare.com/developer-platform/solutions/s3-compatible-object-storage), etc.), `endpoint` will need to be provided. Some providers also need to have `forcePathStyle` set to true in the configuration.
|
||||
|
||||
Below is an example on how to connect to MinIO:
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
{
|
||||
"s3": {
|
||||
"region": "us-east-1",
|
||||
"bucket": "my-bucket",
|
||||
"endpoint": "https://play.min.io",
|
||||
"forcePathStyle": true,
|
||||
"accessKeyId": "abc1234",
|
||||
"secretAccessKey": "4321cba",
|
||||
"disableChecksum": true,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
{% aside type="note" title="Minio and checksum validation" %}
|
||||
If you are using MinIO earlier than `2024-07-04T14-25-45Z` it is recommended to enable `disabledChecksum` else you may trigger aws-sdk checksum errors such as `x-amz-checksum-crc32`.
|
||||
{% /aside %}
|
||||
|
||||
| **Property** | **Description** |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| **region** | The ID of the S3 compatible storage region to use |
|
||||
| **bucket** | The name of the S3 compatible storage bucket to use |
|
||||
| **forcePathStyle** | Changes the way artifacts are uploaded. Usually used for S3 compatible providers (MinIO, LocalStack, etc) |
|
||||
| **endpoint** | The custom endpoint to upload artifacts to. If endpoint is not defined, the default AWS endpoint is used |
|
||||
| **accessKeyId** | AWS Access Key ID (optional if `AWS_ACCESS_KEY_ID` is set in the environment) |
|
||||
| **secretAccessKey** | AWS secret access key (optional if `AWS_SECRET_ACCESS_KEY` is set in the environment) |
|
||||
| **disableChecksum** | This disables AWS' checksum validation for cache entries |
|
||||
| **cacheKeyPrefix** | Prefix added to cache keys |
|
||||
|
||||
By default, Nx will try to write and read from the remote cache while running locally. This means that permissions must be set for users who are expected to access the remote cache.
|
||||
|
||||
Nx will only show warnings when the remote cache is not writable. You can disable these warnings by setting `localMode` to `read-only` or `no-cache` in the `nx.json` file.
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
{
|
||||
"s3": {
|
||||
"region": "us-east-1",
|
||||
"bucket": "my-bucket",
|
||||
"localMode": "read-only",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The cache mode in CI can also be configured by setting `ciMode` to `read-only` or `no-cache` in the `nx.json` file. Or by setting `NX_POWERPACK_CACHE_MODE` to `read-only` or `no-cache` in the CI environment.
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
{
|
||||
"s3": {
|
||||
"region": "us-east-1",
|
||||
"bucket": "my-bucket",
|
||||
"ciMode": "read-only",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
# Cache key prefix
|
||||
|
||||
The `cacheKeyPrefix` setting enables you to add a prefix to your cache keys, making it possible to store multiple projects or environments within the same storage bucket.
|
||||
|
||||
```jsonc
|
||||
// nx.json
|
||||
{
|
||||
"s3": {
|
||||
"cacheKeyPrefix": "designSystem",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Migrating from custom tasks runners
|
||||
|
||||
Many people who are interested in Nx caching plugins have previously used custom task runners. Nx offers a new and simpler extension API designed to meet the same use cases as the now-deprecated custom task runners.
|
||||
|
||||
To learn more about migrating from custom task runners, [please refer to this detailed guide](/docs/reference/deprecated/custom-tasks-runner).
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
---
|
||||
title: '@nx/shared-fs-cache - Generators'
|
||||
description: Generator for the shared filesystem cache plugin
|
||||
sidebar:
|
||||
label: Generators
|
||||
weight: .5
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
## `init`
|
||||
|
||||
Initialize the shared-fs-cache plugin
|
||||
|
||||
**Usage:**
|
||||
|
||||
```shell
|
||||
nx g @nx/shared-fs-cache:init
|
||||
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
title: Shared FS Cache
|
||||
sidebar:
|
||||
hidden: true
|
||||
description: Shared filesystem cache plugin
|
||||
pagefind: false
|
||||
---
|
||||
|
||||
{% index_page_cards path="reference/remote-cache-plugins/shared-fs-cache" /%}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
---
|
||||
title: Overview of the Nx Shared File System Cache Plugin
|
||||
description: The @nx/shared-fs-cache plugin enables you to use a shared file system directory to host your remote cache for efficient build caching across your team.
|
||||
sidebar:
|
||||
label: Overview
|
||||
weight: 2.1
|
||||
filter: 'type:References'
|
||||
---
|
||||
|
||||
The `@nx/shared-fs-cache` plugin enables you to host your remote cache on a shared file system directory. While you're responsible for implementing the actual directory sharing mechanism, the plugin configures Nx to read from both your local cache and the shared directory.
|
||||
|
||||
{% aside type="tip" title="Free managed remote cache with Nx Cloud" %}
|
||||
|
||||
Note, you can get started for free with a **fully managed remote caching powered by Nx Cloud**. It comes with a generous Hobby plan that is enough for most small teams. [Learn more here](https://nx.dev/nx-cloud).
|
||||
|
||||
If you are an enterprise and **data privacy and security is a concern**, [reach out for an Enterprise trial](https://nx.dev/enterprise/trial). It is fully SOC 2 type 1 and 2 compliant and comes with single-tenant, dedicated EU region hosting as well as on-premise.
|
||||
|
||||
**Are you an OSS project?** Nx Cloud is free for OSS. [Reach out here](https://nx.dev/pricing#oss).
|
||||
|
||||
{% /aside %}
|
||||
|
||||
{% aside type="caution" title="Bucket-based caches are vulnerable to poisoning and often prohibited in organizations" %}
|
||||
|
||||
CREEP (CVE-2025-36852) is a critical vulnerability in bucket-based self-hosted remote caches. It lets attackers with PR access poison production builds via a race condition during artifact creation—before security checks can catch it. [Learn more](https://nx.dev/blog/cve-2025-36852-critical-cache-poisoning-vulnerability-creep)
|
||||
|
||||
{% /aside %}
|
||||
|
||||
## Set up @nx/shared-fs-cache
|
||||
|
||||
### 1. Install the package
|
||||
|
||||
Run the following command:
|
||||
|
||||
```shell
|
||||
nx add @nx/shared-fs-cache
|
||||
```
|
||||
|
||||
This will add the `@nx/shared-fs-cache` NPM package and automatically configure it for your workspace. As part of this process, you'll be guided to **generate a new activation key**. This is a fully automated process to register your plugin.
|
||||
|
||||
The key will be saved in your repository (`.nx/key/key.ini`) and should be committed so that every developer has access to it. If your repository is public (or in CI), you can also use an environment variable:
|
||||
|
||||
```dotenv
|
||||
// .env
|
||||
NX_KEY=YOUR_ACTIVATION_KEY
|
||||
```
|
||||
|
||||
> Why require an activation key? It simply helps us know and support our users. If you prefer not to provide this information, you can also build your own cache server. [Learn more.](/docs/guides/tasks--caching/self-hosted-caching)
|
||||
|
||||
### 2. configure the cache directory
|
||||
|
||||
The `@nx/shared-fs-cache` plugin treats your local cache directory as if it is also a remote cache directory. The local cache directory can be set using `cacheDirectory` in the `nx.json` file or the `NX_CACHE_DIRECTORY` environment variable. The default local cache directory is `.nx/cache`.
|
||||
|
||||
### 3. share the cache directory
|
||||
|
||||
The `@nx/shared-fs-cache` plugin does not actually share the cache directory across your organization. You are responsible for enabling the actual sharing mechanism. If you want Nx to handle the sharing, use [Nx Replay](/docs/features/ci-features/remote-cache) instead. Your shared file system directory might be a directory that is saved and restored by a CI provider or it could be a shared network drive.
|
||||
@@ -48,6 +48,7 @@ The following environment variables are ones that you can set to change the beha
|
||||
| ------------------------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `NX_ADD_PLUGINS` | boolean | If set to `false`, Nx will not add plugins to infer tasks. This is `true` by default. |
|
||||
| `NX_CACHE_PROJECT_GRAPH` | boolean | If set to `false`, disables the project graph cache. Most useful when developing a plugin that modifies the project graph. |
|
||||
| `NX_COMPILE_CACHE` | boolean | If set to `false`, disables Node's built-in V8 bytecode compile cache for Nx processes (CLI, daemon, plugin workers). The cache is enabled by default on Node 22.8+ and speeds up repeated invocations by reusing compiled bytecode. Has no effect on older Node versions. |
|
||||
| `NX_DAEMON_SOCKET_DIR` | string | Alias of `NX_SOCKET_DIR`, used only when `NX_SOCKET_DIR` is not set. Despite the name, it controls all Nx socket placements, not just the daemon socket. Prefer `NX_SOCKET_DIR` in new setups. |
|
||||
| `NX_FORCE_REUSE_CACHED_GRAPH` | boolean | If set to `true`, Nx will reuse an existing cached project graph when available and skip recomputing it. Useful in short-lived CI steps that run immediately after a step which already computed the graph. |
|
||||
| `NX_FORMAT_SORT_TSCONFIG_PATHS` | boolean | If set to `true`, generators will sort the TypeScript path mappings in the root tsconfig file. |
|
||||
@@ -115,8 +116,8 @@ Similar to the Nx CLI, Nx Cloud also uses the `NX_VERBOSE_LOGGING` environment v
|
||||
| Property | Type | Description |
|
||||
| ------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `NX_BRANCH` | string | The current branch name. For most CI providers, `nx-cloud` determines this automatically. Must be set to the PR number for GitHub, BitBucket and GitLab integrations to work properly. |
|
||||
| `NX_CLOUD_DISABLE_METRICS_COLLECTION` | boolean | Disables collection of CPU and memory metrics during task execution (enabled by default for enterprise users on Nx 22.1+). See [Task Resource Usage](/docs/guides/nx-cloud/ci-resource-usage). |
|
||||
| `NX_CLOUD_METRICS_DIRECTORY` | string | Directory where Nx writes resource metrics during task execution. See [Task Resource Usage](/docs/guides/nx-cloud/ci-resource-usage). |
|
||||
| `NX_CLOUD_DISABLE_METRICS_COLLECTION` | boolean | Disables collection of CPU and memory metrics during task execution (enabled by default for enterprise users on Nx 22.1+). See [Resource Usage](/docs/features/ci-features/resource-usage). |
|
||||
| `NX_CLOUD_METRICS_DIRECTORY` | string | Directory where Nx writes resource metrics during task execution. See [Resource Usage](/docs/features/ci-features/resource-usage). |
|
||||
| `NX_CI_EXECUTION_ID` | string | A unique identifier for the current CI run or job. For most CI providers, `nx-cloud` determines this automatically. The value on the main job must match the value on all agents. |
|
||||
| `NX_CI_EXECUTION_ENV` | string | Used when you have multiple main jobs (e.g., running CI on both Linux and Windows). The main job with this env variable will connect to agents with the same env name. |
|
||||
| `NX_AGENT_LAUNCH_TEMPLATE` | string | Should only be used when running agents with Manual DTE. Attaches a launch template type to your agents to leverage assignment rules for task distribution. |
|
||||
|
||||
@@ -635,6 +635,44 @@ Remove temporary marker files created by `start-ci-run` if accidentally run loca
|
||||
npx nx-cloud cleanup
|
||||
```
|
||||
|
||||
### `nx-cloud get sandbox-reports`
|
||||
|
||||
Download sandbox reports for tasks that ran on a branch. Reports capture unexpected file reads and writes detected by the Nx Cloud task sandbox and are written to disk as JSON files for inspection or post-processing.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```shell
|
||||
npx nx-cloud get sandbox-reports [options]
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
| Option | Type | Description | Default |
|
||||
| ------------------- | ------- | -------------------------------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| `--branch` | string | Branch to query | current git branch |
|
||||
| `--fallback-branch` | string | For each task without a report on `--branch`, fall back to this branch's latest report | |
|
||||
| `--since` | string | Time window. Units: `h`, `d`, `w`, `y` (e.g. `1h`, `7d`, `4w`, `1y`) | `7d` |
|
||||
| `--output`, `-o` | string | Output directory | `./.nx/workspace-data/sandbox-reports/<branch>/` |
|
||||
| `--include-clean` | boolean | Include reports with zero unexpected reads/writes | `false` |
|
||||
| `--concurrency` | number | Parallel downloads | `20` |
|
||||
| `--force` | boolean | Overwrite existing files instead of skipping them | `false` |
|
||||
| `--json` | boolean | Print a machine-readable summary on stdout | `false` |
|
||||
| `--help`, `-h` | boolean | Show command help and exit | |
|
||||
|
||||
#### Examples
|
||||
|
||||
Download reports for a feature branch over the last hour:
|
||||
|
||||
```shell
|
||||
npx nx-cloud get sandbox-reports --branch 11249 --since 1h
|
||||
```
|
||||
|
||||
Fall back to `main` for any task that has no report on the feature branch:
|
||||
|
||||
```shell
|
||||
npx nx-cloud get sandbox-reports --branch my-feature --fallback-branch main
|
||||
```
|
||||
|
||||
## Getting help
|
||||
|
||||
You can get help for any command by adding the `--help` flag:
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: Nx Powerpack End User License Agreement
|
||||
description: Legacy Nx Powerpack EULA, preserved for reference.
|
||||
sidebar:
|
||||
label: Powerpack License
|
||||
filter: 'type:References'
|
||||
template: splash
|
||||
head:
|
||||
- tag: meta
|
||||
attrs:
|
||||
name: robots
|
||||
content: noindex, nofollow
|
||||
---
|
||||
|
||||
Last Updated: September 18th, 2024
|
||||
|
||||
This Nx End User License Agreement (together with your associated Order Information, the "Agreement") governs your use of our Nx Powerpack, a suite of paid extensions for Nx (the "Software"). To make this Agreement easier to read, the terms "Nx," "we," and "us" refers to Narwhal Technologies, Inc., and the term "you" refers to you and any organization that you are acting on behalf of in signing up for a subscription to the Software. If you are an individual acting on behalf of an entity, you represent and warrant that you have the authority to enter into this Agreement on behalf of that entity and to legally bind that entity. If you do not accept the terms of this Agreement, then you must not use the Software.
|
||||
|
||||
## 1. Definitions
|
||||
|
||||
(a) "Licensed Volume" means the limits, volume or other conditions of permitted use for the Software as set forth in the Order Information, including any limits on the number of Authorized Users or number of workspaces.
|
||||
|
||||
(b) "Nx IP" means the Software, algorithms, technology, databases, tools, know-how or processes used to provide or deliver the Software or any related services, and its documentation ("Documentation"), all improvements, modifications or derivative works of the foregoing (regardless of authorship), and all intellectual property rights ("IPR") in any of the foregoing.
|
||||
|
||||
(c) "Order Information" means (i) certain terms associated with your subscription to Use the Software, as communicated to you via our pricing page available at https://cloud.nx.app/powerpack/purchase ("Pricing Page"), as may be updated from time to time or (ii) as otherwise set forth in a written order form or purchase order signed by you and Nx ("PO").
|
||||
|
||||
## 2. License
|
||||
|
||||
(a) License. Subject to the terms and conditions of this Agreement (including receipt of the License Key), Nx hereby grants you a worldwide, non-exclusive, non-transferable (except in compliance with Section 12), non-sublicensable license to download and install the Software on premises owned or controlled by you, and run the Software solely for your internal business purposes (the "Purpose") during the Term in accordance with the Documentation and subject to the Licensed Volume. You have the right to permit your employees or contributors ("Authorized Users") to use the Software on your behalf for the Purpose in accordance with this Agreement; provided, however, that you will remain fully and directly liable to Nx for any and all use of the Software by Authorized Users as if such use was by you yourself under this Agreement. Nothing in this Agreement will operate to grant you any right, title or interest, whether by implication, estoppel or otherwise, in or to the Nx IP, other than as expressly set forth herein. As between Nx and you, Nx will exclusively own all right, title and interest in and to the Nx IP.
|
||||
|
||||
(b) Use Restrictions. You will not at any time, directly or indirectly, and will not permit any person or entity (collectively, "Person") (including, without limitation, your Authorized Users) to: (i) copy, modify or create derivative works of the Software or Documentation, in whole or in part; (ii) reverse engineer, disassemble, decompile, decode or otherwise attempt to derive or gain improper access to any software component of the Software, in whole or in part; (iii) frame, mirror, sell, resell, rent or lease the use of the Software, License Key or Documentation to any other Person, or otherwise use or allow any Person to use the Software, License Key or Documentation for any purpose other than for your benefit for the Purpose in accordance with this Agreement; (iv) create any script or other automated tool that attempts to create multiple License Keys; (v) use the Software or License Key in any infringing or unlawful manner; or (vi) use the Software, Documentation or any other Confidential Information of Nx for competitive analysis or benchmarking purposes, or to otherwise develop, commercialize, license or sell any product, service or technology that could, directly or indirectly, compete with the Nx IP.
|
||||
|
||||
(c) Authorized Equipment. You will bear the sole responsibility for obtaining and maintaining the hardware and any computer systems, networks, telecommunications systems, Internet access, third party services or any other materials required to meet the minimum technical and operational requirements required to operate the Software.
|
||||
|
||||
## 3. License Key; Fees and Payment
|
||||
|
||||
To use the Software, you are required to purchase a license key via the Pricing Page or PO ("License Key"). You are liable for any actions or inactions performed under your License Key. You will pay Nx all fees set forth in your Order Information ("Fees") on the payment dates specified in your Order Information. All Fees are non-refundable. Nx reserves the right to change the Fees and Licensed Volume and to institute new Fees and revised limits of the Licensed Volume upon 30 days' prior notice to you. Unless otherwise specified in the Order Information, Fees will be paid by the approved credit card that you designate when you sign up to use the Software. You hereby authorize us to initiate all payment transactions for Fees from your approved credit card when such Fees are due, if applicable. Any and all Fees that are not paid to Nx when due will accrue interest at a rate of 1.5% per month, or the maximum rate permitted by law, whichever is greater. In the event of a conflict between this Agreement and the Order Information, the Order Information will control and govern. All Fees do not include any sales, use, value added or other applicable taxes, payment of which will be your sole responsibility (excluding any taxes based on Nx's net income).
|
||||
|
||||
## 4. Confidential Information
|
||||
|
||||
"Confidential Information" means any information that one party (the "Disclosing Party") provides to the other party (the "Receiving Party") in connection with this Agreement, whether orally or in writing, that is designated as confidential or that reasonably should be considered to be confidential given the nature of the information and/or the circumstances of disclosure. Confidential Information will not include any information that: (i) is or becomes generally known to the public through no fault or breach of this Agreement by the Receiving Party; (ii) is rightfully known by the Receiving Party at the time of disclosure without an obligation of confidentiality; (iii) is independently developed by the Receiving Party without access to or use of any Confidential Information of the Disclosing Party that can be evidenced in writing; or (iv) is rightfully obtained by the Receiving Party from a third-party without restriction on use or disclosure. For clarity, the Software and the Documentation will be deemed Confidential Information of Nx. The Receiving Party will not use or disclose any Confidential Information of the Disclosing Party except as necessary to perform its obligations or exercise its rights under this Agreement. The Receiving Party may disclose Confidential Information of the Disclosing Party only: (A) to those of its employees, contractors, agents and advisors who have a bona fide need to know such Confidential Information to perform under this Agreement and who are bound by written agreements with use and nondisclosure restrictions at least as protective of the Confidential Information as those set forth in this Agreement, or (B) as such disclosure may be required by the order or requirement of a court, administrative agency or other governmental body, subject to the Receiving Party providing to the Disclosing Party reasonable written notice to allow the Disclosing Party to seek a protective order or otherwise contest the disclosure.
|
||||
|
||||
## 5. Policies; Support
|
||||
|
||||
You hereby acknowledge that you have reviewed and agreed to the Nx Privacy Policy at https://cloud.nx.app/privacy. Such policy is hereby incorporated into and is hereby deemed a part of this Agreement, binding upon you and you Authorized Users with respect to your and their use of the Software in connection with this Agreement. As part of your subscription to the Software, Nx will provide reasonable support in connection with the Software in accordance with the support terms set forth in your Order Information.
|
||||
|
||||
## 6. Feedback
|
||||
|
||||
From time-to-time you or your Authorized Users may provide Nx with feedback with regard to the Software. You, on behalf of yourself and your Authorized Users, hereby grant Nx a perpetual, irrevocable, royalty-free and fully-paid up license to use and exploit all such feedback in connection with Nx's business purposes.
|
||||
|
||||
## 7. Indemnification
|
||||
|
||||
(a) Nx Indemnification. Nx will defend and pay all damages finally awarded against you pursuant to a final, valid and binding judgment or order, or a final settlement agreement with respect to any claim, suit or proceeding brought by a third party against you arising from the Software's infringement of such third-party's IPR. The foregoing obligation will not apply if the underlying third-party claim arises from (i) your breach of this Agreement, negligence, willful misconduct or fraud; (ii) modifications to the Software by anyone other than Nx; or (iii) combinations of the Software of with software, data or materials not provided by Nx. If Nx reasonably believes the Software (or any component) could infringe any third party's IPR, Nx may, at its sole option and expense: (A) procure the right for you to continue using the Software (or any infringing component) to make it non-infringing without materially reducing its functionality; or (B) replace the Software (or any infringing component) with a non-infringing alternative that is functionally equivalent in all material respects. If the foregoing remedies are not available to Nx on commercially reasonable terms, then Nx may terminate your use of the Software upon notice to you.
|
||||
|
||||
(b) Your Indemnification. You will defend and pay all damages finally awarded against Nx pursuant to a final, valid and binding judgment or order or a final settlement agreement with respect to any claim, suit or proceeding brought by a third party against Nx arising from any breach of the restrictions set forth in Section 2(b).
|
||||
|
||||
(c) Indemnification Procedures. The party seeking defense and indemnity (the "Indemnified Party") will promptly notify the other party (the "Indemnifying Party") of any and all such claims and will reasonably cooperate with the Indemnifying Party with the defense and/or settlement thereof. The Indemnifying Party will have the sole right to conduct the defense of any claim for which the Indemnifying Party is responsible hereunder (provided that the Indemnifying Party may not settle any claim without the Indemnified Party's prior written approval unless the settlement unconditionally releases the Indemnified Party from all liability, does not require any admission by the Indemnified Party, and does not place restrictions upon the Indemnified Party's business). The Indemnified Party may participate in the defense or settlement of any such claim at its own expense and with its own choice of counsel or, if the Indemnifying Party refuses to fulfill its obligation of defense, the Indemnified Party may defend itself and seek reimbursement from the Indemnifying Party.
|
||||
|
||||
## 8. Disclaimers
|
||||
|
||||
THE SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND NX MAKES NO WARRANTIES OR REPRESENTATIONS TO YOU, YOUR AUTHORIZED USERS OR TO ANY OTHER PERSON REGARDING THE SOFTWARE. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, NX HEREBY DISCLAIMS (a) ALL WARRANTIES AND REPRESENTATIONS, WHETHER EXPRESS OR IMPLIED AND (b) ANY WARRANTY THAT USE OF THE SOFTWARE WILL BE ERROR-FREE.
|
||||
|
||||
## 9. Limitations of Liability
|
||||
|
||||
EXCEPT FOR A PARTY'S GROSS NEGLIGENCE, WILLFUL MISCONDUCT OR FRAUD, IN NO EVENT WILL (a) EITHER PARTY BE LIABLE TO THE OTHER PARTY FOR ANY INCIDENTAL, SPECIAL, EXEMPLARY, PUNITIVE OR CONSEQUENTIAL DAMAGES, LOSS OF INCOME, DATA, PROFITS, REVENUE OR BUSINESS INTERRUPTION, OR THE COST OF SUBSTITUTE SERVICES OR OTHER ECONOMIC LOSS, ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT, WHETHER SUCH LIABILITY ARISES FROM ANY CLAIM BASED ON CONTRACT, WARRANTY, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, AND WHETHER OR NOT SUCH PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSS OR DAMAGE AND (b) NX'S TOTAL LIABILITY TO YOU, YOUR AUTHORIZED USERS OR ANY OTHER PERSON IN CONNECTION WITH THIS AGREEMENT OR THE PROVISION OF THE SOFTWARE EXCEED THE FEES ACTUALLY PAID BY YOU TO NX IN THE 12 MONTH PERIOD PRECEDING THE ACTION GIVING RISE TO SUCH LIABILITY.
|
||||
|
||||
## 10. Term and Termination
|
||||
|
||||
(a) Term; Termination. The term of this Agreement will begin on the effective date in the Order Information, and will expire at the end of the initial term specified in the Order Information (the "Initial Term"). Following the Initial Term, this Agreement will automatically renew for successive one-month terms (the Initial Term, together with any renewal term, the "Term"), unless Nx or you provides the other with at least twenty (20) days' written notice of its intent not to renew prior to the end of the then-current term. Either party may terminate this Agreement, effective on written notice to the other party, if the other party materially breaches this Agreement, and such breach: (A) is incapable of cure; or (B) being capable of cure, remains uncured thirty (30) days after the non-breaching party provides the breaching party with written notice of such breach.
|
||||
|
||||
(b) Effect of Termination; Survival. Upon termination of this Agreement, your right to use the Software will immediately terminate and you shall cease using the Software, including for example, by permanently removing the Software dependency. This Section 10(b) and Sections 2(b), 3, 4, 5 (first sentence), 6 - 9 and 12 survive any termination of this Agreement.
|
||||
|
||||
## 11. Trademarks
|
||||
|
||||
You hereby grant Nx a limited, non-exclusive, royalty-free license to use and display your name, designated trademarks and associated logos ("Your Marks") during the Term in connection with Nx's marketing and promotional efforts for its products and services, including by publicly naming you as a customer of Nx. Nx will conform to and observe the trademark standards as you prescribe from time to time. All goodwill generated by Nx's use of Your Marks inures to your benefit.
|
||||
|
||||
## 12. Miscellaneous
|
||||
|
||||
This Agreement, together with the Order Information, is the complete and exclusive agreement between the parties with respect to its subject matter and supersedes all prior or contemporaneous agreements, communications and understandings, both written and oral, with respect to its subject matter, including any prior terms. This Agreement may be amended or modified only by a written document assented by duly authorized representatives of the parties. Nx may perform an audit of your use of the Software once per year in connection with your compliance with this Agreement, including if you are exceeding the Licensed Volume. Nx may provide notices to you by posting them on our website, by providing electronic notification via the Software, or by email to the address associated with your account. You may provide notices to us via email at powerpack-support@nrwl.io. All notices are effective upon posting or when delivered. Except as otherwise set forth herein, either party's failure to enforce any provision of this Agreement will not constitute a waiver of future enforcement of that or any other provision. No waiver of any provision of this Agreement will be effective unless it is in writing and signed by the party granting the waiver. If any provision of this Agreement is held invalid, illegal or unenforceable, that provision will be enforced to the maximum extent permitted by law, and the remaining provisions of this Agreement will remain in full force and effect. This Agreement will be governed by and construed in accordance with the laws of the State of California without giving effect to any principles of conflict of laws that would lead to the application of the laws of another jurisdiction. Any legal action or proceeding arising under this Agreement will be brought exclusively in the federal or state courts located in the Northern District of California and the parties irrevocably consent to the personal jurisdiction and venue therein. Nx may freely assign its rights and obligations under this Agreement. You may not assign or transfer this Agreement, by operation of law or otherwise, without Nx's prior written consent; provided, however, that you may assign your rights or delegate your obligations, in whole or in part, without such consent, to (i) one or more of your affiliates, or (ii) a third party that succeeds to all or substantially all of your business and assets relating to the subject matter of this Agreement, whether by sale, merger, operation of law or otherwise. Any attempt to assign or transfer this Agreement without such consent will be void. Subject to the foregoing, this Agreement is binding upon and will inure to the benefit of each of the parties and their respective successors and permitted assigns. Unless otherwise expressly provided, no provisions of this Agreement are intended or will be construed to confer upon or give to any person or entity, other than the parties, any rights, remedies or other benefits under or by reason of this Agreement.
|
||||
@@ -19,11 +19,15 @@ The Nx policy is to support the LTS versions (i.e. actively maintained even numb
|
||||
Other versions of Node.js **may** still work without issue for these versions of Nx. Those include versions which are already EOL, or odd version numbers (e.g. 23), which Node.js actively discourages using in production.
|
||||
{% /aside %}
|
||||
|
||||
| Nx Version | Node Version |
|
||||
| -------------- | ------------------------ |
|
||||
| 22.x (current) | 24.x, ^22.12.0, ^20.19.0 |
|
||||
| 21.x | 24.x, ^22.12.0, ^20.19.0 |
|
||||
| 20.x | 22.x, 20.x, 18.x |
|
||||
{% aside type="note" title="Node 26" %}
|
||||
Node 26 is on the Current release track and enters LTS in October 2026. Nx tests against it in CI and supports it today.
|
||||
{% /aside %}
|
||||
|
||||
| Nx Version | Node Version |
|
||||
| -------------- | ------------------------------ |
|
||||
| 22.x (current) | 26.x, 24.x, ^22.12.0, ^20.19.0 |
|
||||
| 21.x | 24.x, ^22.12.0, ^20.19.0 |
|
||||
| 20.x | 22.x, 20.x, 18.x |
|
||||
|
||||
We intentionally do not include an `"engines"` field in the `package.json` file for Nx in order to allow for user flexibility, but this table should be considered the official compatibility matrix.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Your Next.js application should already be ready for deployment to Vercel.
|
||||
|
||||
### New Vercel project
|
||||
|
||||
1. If you are "importing" your Nx workspace's repository for the first time, make sure you do _not_ choose a root directory as part of the repo selection process (therefore leaving it to be the root of the full repo/workspace)
|
||||
1. If you are "importing" your Nx workspace's repository for the first time, make sure you leave the **Root Directory** field **empty**. Do not set it to `./` or any subdirectory. Vercel will use the full repository root, which is required for Nx to work correctly.
|
||||
2. Ensure the Next.js "Framework Preset" is selected
|
||||
3. Expand the "Build and Output Settings" and toggle the override switch for the build command. For example, for an application named `tuskdesk` the value will look like this:
|
||||
|
||||
@@ -38,6 +38,45 @@ When everything is updated appropriately, for our `tuskdesk` example we would se
|
||||
|
||||

|
||||
|
||||
## Handling `NEXT_PUBLIC_` environment variables with Nx cache
|
||||
|
||||
Next.js bakes `NEXT_PUBLIC_*` environment variables into the static bundle at **build time**. If Nx (or Nx Cloud) has a cached build from a previous run, for example from your local development environment, it will restore that cached output rather than running `next build` again. This means the cached bundle may contain development values for your `NEXT_PUBLIC_*` variables even when Vercel has the correct production values configured.
|
||||
|
||||
The proper solution is to include your `NEXT_PUBLIC_*` environment variables in the `inputs` of your build target. This tells Nx to treat a change in those variable values as a cache miss, ensuring a fresh build is triggered whenever they differ.
|
||||
|
||||
In your application's `project.json`, extend the build target inputs:
|
||||
|
||||
```json
|
||||
{
|
||||
"targets": {
|
||||
"build": {
|
||||
"inputs": [
|
||||
"default",
|
||||
"^production",
|
||||
{ "env": "NEXT_PUBLIC_API_URL" },
|
||||
{ "env": "NEXT_PUBLIC_SUPABASE_URL" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add one `{ "env": "VARIABLE_NAME" }` entry for each `NEXT_PUBLIC_*` variable your application uses. When the value of any listed variable differs from the cached build, Nx will invalidate the cache and rebuild.
|
||||
|
||||
{% aside type="note" title="Preserving plugin-inferred inputs" %}
|
||||
The `@nx/next` plugin automatically infers certain inputs for Next.js build targets. When you override the `inputs` array, make sure to include `"default"` and `"^production"` so those defaults are preserved. If you are unsure what inputs your build target currently has, run `nx show project <app-name>` to inspect the effective configuration.
|
||||
{% /aside %}
|
||||
|
||||
If you need a quick workaround before configuring inputs, you can pass `--skip-nx-cache` to bypass the cache entirely:
|
||||
|
||||
```shell
|
||||
npx nx build tuskdesk --prod --skip-nx-cache
|
||||
```
|
||||
|
||||
Note that this disables all Nx caching for that run, eliminating the performance benefit. The `inputs` approach above is preferred for ongoing deployments.
|
||||
|
||||
See [Nx Inputs documentation](/docs/reference/inputs#environment-variables) for more details.
|
||||
|
||||
## Skipping build if the application is not affected
|
||||
|
||||
One of the core features of Nx is to run code quality checks and builds only for projects that are affected by recent code changes. We can use [Vercel's ignored build step feature](https://vercel.com/docs/platform/projects#ignored-build-step) to only build our application if it is affected.
|
||||
|
||||
@@ -8,17 +8,6 @@ const workspaceRoot = process.cwd();
|
||||
// Set environment variable for documentation generation
|
||||
process.env.NX_GENERATE_DOCS_PROCESS = 'true';
|
||||
|
||||
// Register ts-node to handle TypeScript files
|
||||
require('ts-node').register({
|
||||
project: join(workspaceRoot, 'tsconfig.base.json'),
|
||||
transpileOnly: true,
|
||||
compilerOptions: {
|
||||
module: 'commonjs',
|
||||
},
|
||||
});
|
||||
|
||||
// TypeScript paths are now handled by pnpm workspaces, no need for tsconfig-paths
|
||||
|
||||
// Inline command parser functions
|
||||
const YargsTypes = ['array', 'count', 'string', 'boolean', 'number'];
|
||||
|
||||
@@ -88,11 +77,11 @@ async function parseCommand(name, command) {
|
||||
|
||||
async function runCnwParser() {
|
||||
try {
|
||||
// Import create-nx-workspace command
|
||||
// Import create-nx-workspace command from compiled dist
|
||||
const { commandsObject } = importFresh(
|
||||
join(
|
||||
workspaceRoot,
|
||||
'packages/create-nx-workspace/bin/create-nx-workspace'
|
||||
'dist/packages/create-nx-workspace/bin/create-nx-workspace.js'
|
||||
)
|
||||
);
|
||||
|
||||
@@ -103,11 +92,11 @@ async function runCnwParser() {
|
||||
commands['$0']
|
||||
);
|
||||
|
||||
// Import Preset enum to get all preset values
|
||||
// Import Preset enum to get all preset values from compiled dist
|
||||
const { Preset } = importFresh(
|
||||
join(
|
||||
workspaceRoot,
|
||||
'packages/create-nx-workspace/src/utils/preset/preset'
|
||||
'dist/packages/create-nx-workspace/src/utils/preset/preset.js'
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ export async function loadDevkitPackage(
|
||||
const { logger, renderMarkdown } = context;
|
||||
logger.info('Loading DevKit documentation');
|
||||
|
||||
const { defaultTypedocOptions, outDir, buildDir } = setupTypeDoc(logger);
|
||||
const { defaultTypedocOptions, outDir, generatedTsconfigPath } =
|
||||
setupTypeDoc(logger);
|
||||
const entries: CollectionEntry<'nx-reference-packages'>[] = [];
|
||||
|
||||
logger.info('Generating devkit docs to dir...');
|
||||
@@ -33,7 +34,7 @@ export async function loadDevkitPackage(
|
||||
{
|
||||
...defaultTypedocOptions,
|
||||
entryPoints: [devkitEntryPoint],
|
||||
tsconfig: join(buildDir, 'tsconfig.lib.json'),
|
||||
tsconfig: generatedTsconfigPath,
|
||||
out: outDir,
|
||||
excludePrivate: true,
|
||||
publicPath: '/docs/reference/devkit/',
|
||||
@@ -56,7 +57,7 @@ export async function loadDevkitPackage(
|
||||
{
|
||||
...defaultTypedocOptions,
|
||||
entryPoints: [ngcliEntryPoint],
|
||||
tsconfig: join(buildDir, 'tsconfig.lib.json'),
|
||||
tsconfig: generatedTsconfigPath,
|
||||
out: join(outDir, 'ngcli_adapter'),
|
||||
publicPath: '/docs/reference/devkit/ngcli_adapter/',
|
||||
},
|
||||
|
||||
@@ -110,23 +110,29 @@ function flattenCommands(
|
||||
): FlattenedCommand[] {
|
||||
const allCommands: FlattenedCommand[] = [];
|
||||
|
||||
for (const [cmdName, cmd] of Object.entries(commands)) {
|
||||
allCommands.push({ fullName: cmdName, cmd });
|
||||
const visit = (
|
||||
cmd: ParsedCliCommand,
|
||||
fullName: string,
|
||||
parentOptions?: ParsedCliCommand['options']
|
||||
) => {
|
||||
allCommands.push({ fullName, cmd, parentOptions });
|
||||
|
||||
if (cmd.subcommands) {
|
||||
for (const sub of cmd.subcommands) {
|
||||
// For $0 (default command), use parent name; otherwise, combine parent and sub name
|
||||
const subName =
|
||||
sub.command?.startsWith('$0') || sub.name === '$0'
|
||||
? cmdName
|
||||
: `${cmdName} ${sub.name}`;
|
||||
allCommands.push({
|
||||
fullName: subName,
|
||||
cmd: sub,
|
||||
parentOptions: cmd.options,
|
||||
});
|
||||
}
|
||||
if (!cmd.subcommands) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const sub of cmd.subcommands) {
|
||||
// For $0 (default command), use parent name; otherwise, combine parent and sub name
|
||||
const subName =
|
||||
sub.command?.startsWith('$0') || sub.name === '$0'
|
||||
? fullName
|
||||
: `${fullName} ${sub.name}`;
|
||||
visit(sub, subName, cmd.options ?? []);
|
||||
}
|
||||
};
|
||||
|
||||
for (const [cmdName, cmd] of Object.entries(commands)) {
|
||||
visit(cmd, cmdName);
|
||||
}
|
||||
|
||||
return allCommands.sort((a, b) => a.fullName.localeCompare(b.fullName));
|
||||
|
||||
@@ -32,12 +32,12 @@ export const directoryToCategoryMap: Record<string, string> = {
|
||||
export function setupTypeDoc(logger: LoaderContext['logger']) {
|
||||
const tempDir = join(tmpdir(), `nx-devkit-docs`);
|
||||
const projectRoot = process.cwd();
|
||||
const buildDir = join(workspaceRoot, 'dist', 'packages', 'devkit');
|
||||
const tsconfigDir = join(tempDir, 'packages', 'devkit');
|
||||
const generatedTsconfigPath = join(tsconfigDir, 'tsconfig.lib.json');
|
||||
const outDir = join(tempDir, 'docs', 'generated', 'devkit');
|
||||
|
||||
mkdirSync(buildDir, { recursive: true });
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
mkdirSync(join(tempDir, 'packages', 'devkit'), { recursive: true });
|
||||
mkdirSync(tsconfigDir, { recursive: true });
|
||||
|
||||
const devkitPath = join(workspaceRoot, 'packages', 'devkit');
|
||||
const tsconfigLibPath = join(devkitPath, 'tsconfig.lib.json');
|
||||
@@ -53,23 +53,20 @@ export function setupTypeDoc(logger: LoaderContext['logger']) {
|
||||
);
|
||||
}
|
||||
|
||||
cpSync(tsconfigLibPath, join(buildDir, 'tsconfig.lib.json'));
|
||||
cpSync(tsconfigLibPath, generatedTsconfigPath);
|
||||
if (existsSync(tsconfigPath)) {
|
||||
cpSync(tsconfigPath, join(tempDir, 'packages', 'devkit', 'tsconfig.json'));
|
||||
cpSync(tsconfigPath, join(tsconfigDir, 'tsconfig.json'));
|
||||
}
|
||||
if (existsSync(tsconfigBasePath)) {
|
||||
cpSync(tsconfigBasePath, join(tempDir, 'tsconfig.base.json'));
|
||||
}
|
||||
|
||||
let tsconfigContent = readFileSync(
|
||||
join(buildDir, 'tsconfig.lib.json'),
|
||||
'utf-8'
|
||||
);
|
||||
let tsconfigContent = readFileSync(generatedTsconfigPath, 'utf-8');
|
||||
const tsconfigObj = JSON.parse(tsconfigContent);
|
||||
|
||||
// remap to generated tsconfig to resolve correct local packages
|
||||
if (tsconfigObj.extends === '../../tsconfig.base.json') {
|
||||
tsconfigObj.extends = join(tempDir, 'packages', 'devkit', 'tsconfig.json');
|
||||
tsconfigObj.extends = join(tsconfigDir, 'tsconfig.json');
|
||||
}
|
||||
|
||||
tsconfigObj.compilerOptions = tsconfigObj.compilerOptions || {};
|
||||
@@ -98,11 +95,18 @@ export function setupTypeDoc(logger: LoaderContext['logger']) {
|
||||
'node_modules/@types/jest/**',
|
||||
];
|
||||
|
||||
writeFileSync(
|
||||
join(buildDir, 'tsconfig.lib.json'),
|
||||
JSON.stringify(tsconfigObj, null, 2)
|
||||
// The tsconfig now lives in tempDir but it operates on devkit's compiled
|
||||
// dist (entry point is dist/packages/devkit/index.d.ts). Resolve include
|
||||
// patterns to absolute paths anchored at the dist directory so TypeDoc
|
||||
// picks up the .d.ts files instead of looking for sources next to the temp
|
||||
// tsconfig.
|
||||
const distDevkitDir = join(workspaceRoot, 'dist', 'packages', 'devkit');
|
||||
tsconfigObj.include = (tsconfigObj.include || ['**/*.ts']).map(
|
||||
(pattern: string) => join(distDevkitDir, pattern)
|
||||
);
|
||||
|
||||
writeFileSync(generatedTsconfigPath, JSON.stringify(tsconfigObj, null, 2));
|
||||
|
||||
rmSync(outDir, { recursive: true, force: true });
|
||||
|
||||
const defaultTypedocOptions: Partial<TypeDocOptions> & {
|
||||
@@ -127,7 +131,7 @@ export function setupTypeDoc(logger: LoaderContext['logger']) {
|
||||
return {
|
||||
projectRoot,
|
||||
outDir,
|
||||
buildDir,
|
||||
generatedTsconfigPath,
|
||||
defaultTypedocOptions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
@import 'tailwindcss/theme.css' layer(theme);
|
||||
@import 'tailwindcss/utilities.css' layer(utilities);
|
||||
|
||||
@source '../../node_modules/@nx/nx-dev-feature-analytics';
|
||||
@source '../../node_modules/@nx/nx-dev-ui-markdoc';
|
||||
@source '../../node_modules/@nx/nx-dev-ui-common';
|
||||
@source '../../node_modules/@nx/nx-dev-feature-analytics/src';
|
||||
@source '../../node_modules/@nx/nx-dev-ui-markdoc/src';
|
||||
@source '../../node_modules/@nx/nx-dev-ui-common/src';
|
||||
|
||||
@source not "../../e2e";
|
||||
@source not "../../**/eslint.config.*";
|
||||
@source not "../../node_modules/@nx/nx-dev-ui-markdoc/src/**/*.{spec,test}.*";
|
||||
|
||||
/* Custom styles for Nx documentation */
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"e2e",
|
||||
"eslint.config.js",
|
||||
"eslint.config.mjs",
|
||||
"eslint.config.cjs"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react",
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
uniq,
|
||||
updateFile,
|
||||
} from '@nx/e2e-utils';
|
||||
import { classify } from '@nx/devkit/src/utils/string-utils';
|
||||
import { names } from '@nx/devkit';
|
||||
|
||||
const classify = (s: string) => names(s).className;
|
||||
|
||||
describe('Move Angular Project', () => {
|
||||
let proj: string;
|
||||
|
||||
@@ -130,6 +130,26 @@ describe('Angular Projects - Build and Test', () => {
|
||||
}
|
||||
}, 1000000);
|
||||
|
||||
it('should successfully generate and run tests for vitest-angular', async () => {
|
||||
// Workspace default unitTestRunner is vitest-analog (set when app1
|
||||
// was generated with --bundler=webpack via setGeneratorDefaults
|
||||
// during projects-setup), so opt into vitest-angular explicitly.
|
||||
// - App: --bundler=esbuild required (uses @angular/build:unit-test).
|
||||
// - Lib: --buildable required (uses @nx/angular:unit-test against
|
||||
// the built output).
|
||||
const app = uniq('vitest-angular-app');
|
||||
runCLI(
|
||||
`generate @nx/angular:app ${app} --bundler=esbuild --unitTestRunner=vitest-angular --no-interactive`
|
||||
);
|
||||
|
||||
const lib = uniq('vitest-angular-lib');
|
||||
runCLI(
|
||||
`generate @nx/angular:lib ${lib} --buildable --unitTestRunner=vitest-angular --no-interactive`
|
||||
);
|
||||
|
||||
runCLI(`run-many --target test --projects=${app},${lib}`);
|
||||
}, 1000000);
|
||||
|
||||
it('should successfully work with playwright for e2e tests', async () => {
|
||||
const app = uniq('app');
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
cleanupProject,
|
||||
getPackageManagerCommand,
|
||||
getSelectedPackageManager,
|
||||
killPort,
|
||||
newProject,
|
||||
runCLI,
|
||||
@@ -58,10 +59,19 @@ describe('Cypress E2E Test runner (legacy)', () => {
|
||||
);
|
||||
// Cypress CT (@cypress/vite-dev-server) does not support Vite 8 yet.
|
||||
// Downgrade the workspace to Vite 7 before configuring Cypress CT.
|
||||
const isYarn = getSelectedPackageManager() === 'yarn';
|
||||
updateJson('package.json', (json) => {
|
||||
json.devDependencies ??= {};
|
||||
json.devDependencies['vite'] = '^7.0.0';
|
||||
json.devDependencies['@vitejs/plugin-react'] = '^4.2.0';
|
||||
// Yarn classic's linker bombs ("could not find a copy of vite to link
|
||||
// in node_modules/vitest/node_modules") when intersecting a
|
||||
// top-level `^7.0.0` range with vitest's vite dep+peer combo. Pin
|
||||
// vite via `resolutions` so yarn commits to a single version up
|
||||
// front and skips the buggy hoisting path.
|
||||
if (isYarn) {
|
||||
json.resolutions = { ...(json.resolutions ?? {}), vite: '^7.0.0' };
|
||||
}
|
||||
return json;
|
||||
});
|
||||
runCommand(getPackageManagerCommand().install);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
checkFilesMatchingPatternExist,
|
||||
cleanupProject,
|
||||
newProject,
|
||||
removeFile,
|
||||
runCLI,
|
||||
tmpProjPath,
|
||||
uniq,
|
||||
@@ -374,6 +375,89 @@ describe('.NET Plugin - Advanced MSBuild Features', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Directory.Build.* Inputs', () => {
|
||||
beforeAll(() => {
|
||||
createDotNetProject({
|
||||
name: 'DirBuildInputsApp',
|
||||
type: 'console',
|
||||
});
|
||||
|
||||
// Workspace-root Directory.Build.props — exists.
|
||||
updateFile(
|
||||
'Directory.Build.props',
|
||||
`<Project>
|
||||
<PropertyGroup>
|
||||
</PropertyGroup>
|
||||
</Project>`
|
||||
);
|
||||
|
||||
// Project-level Directory.Build.targets — also exists, at a different ancestor.
|
||||
updateFile(
|
||||
'DirBuildInputsApp/Directory.Build.targets',
|
||||
`<Project>
|
||||
</Project>`
|
||||
);
|
||||
|
||||
// Workspace-root Directory.Packages.props — Central Package Management.
|
||||
updateFile(
|
||||
'Directory.Packages.props',
|
||||
`<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
</Project>`
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// The mere presence of a workspace-root Directory.Packages.props enables
|
||||
// Central Package Management workspace-wide, which makes `dotnet restore`
|
||||
// fail (NU1008) for every other project that pins versions inline. Remove
|
||||
// the files this block wrote so later blocks (which restore/build other
|
||||
// projects) aren't poisoned by leaked state.
|
||||
removeFile('Directory.Packages.props');
|
||||
removeFile('DirBuildInputsApp/Directory.Build.targets');
|
||||
});
|
||||
|
||||
it('should declare only existing Directory.* files as inputs', () => {
|
||||
const projectDetails = runCLI(`show project DirBuildInputsApp --json`);
|
||||
const details = JSON.parse(projectDetails);
|
||||
|
||||
const buildInputs = details.targets.build.inputs as unknown[];
|
||||
|
||||
// The closest ancestor that defines each filename is declared as an input.
|
||||
expect(buildInputs).toContain('{workspaceRoot}/Directory.Build.props');
|
||||
expect(buildInputs).toContain(
|
||||
'{workspaceRoot}/DirBuildInputsApp/Directory.Build.targets'
|
||||
);
|
||||
expect(buildInputs).toContain('{workspaceRoot}/Directory.Packages.props');
|
||||
|
||||
// Files that do NOT exist anywhere must not be declared — that was the point
|
||||
// of moving from the always-declare design to exists-only inputs.
|
||||
expect(buildInputs).not.toContain('{workspaceRoot}/Directory.Build.rsp');
|
||||
expect(buildInputs).not.toContain(
|
||||
'{workspaceRoot}/Directory.Solution.props'
|
||||
);
|
||||
expect(buildInputs).not.toContain(
|
||||
'{workspaceRoot}/Directory.Solution.targets'
|
||||
);
|
||||
|
||||
// Cacheable targets other than build (publish here) get the same inputs.
|
||||
const publishInputs = details.targets.publish.inputs as unknown[];
|
||||
expect(publishInputs).toContain('{workspaceRoot}/Directory.Build.props');
|
||||
expect(publishInputs).toContain(
|
||||
'{workspaceRoot}/DirBuildInputsApp/Directory.Build.targets'
|
||||
);
|
||||
expect(publishInputs).toContain(
|
||||
'{workspaceRoot}/Directory.Packages.props'
|
||||
);
|
||||
|
||||
// Targets without a declared inputs array (e.g. restore) are left untouched
|
||||
// so we don't accidentally narrow Nx's default-input fallback.
|
||||
expect(details.targets.restore.inputs).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Publish with Artifacts', () => {
|
||||
beforeAll(() => {
|
||||
createDotNetProject({
|
||||
@@ -390,6 +474,12 @@ describe('.NET Plugin - Advanced MSBuild Features', () => {
|
||||
</PropertyGroup>
|
||||
</Project>`
|
||||
);
|
||||
|
||||
// Restore after switching to the artifacts layout so the artifacts/obj
|
||||
// assets file exists for the `--no-restore` build below. The other
|
||||
// artifacts blocks in this file do the same; relying on a prior block to
|
||||
// have left UseArtifactsOutput enabled is brittle (test ordering).
|
||||
runCLI('run-many -t restore');
|
||||
});
|
||||
|
||||
it('should use artifacts path for publish output', () => {
|
||||
|
||||
@@ -37,13 +37,15 @@ describe('Gradle Plugin V1', () => {
|
||||
afterAll(() => cleanupProject());
|
||||
|
||||
it('should build without batch mode', () => {
|
||||
const projects = runCLI(`show projects`);
|
||||
const projects = runCLI(`show projects`, {});
|
||||
expect(projects).toContain('app');
|
||||
expect(projects).toContain('list');
|
||||
expect(projects).toContain('utilities');
|
||||
expect(projects).toContain(gradleProjectName);
|
||||
|
||||
const buildOutput = runCLI('build app --no-batch', { verbose: true });
|
||||
const buildOutput = runCLI('build app --no-batch', {
|
||||
verbose: true,
|
||||
});
|
||||
expect(buildOutput).toContain('nx run list:build');
|
||||
expect(buildOutput).toContain(':list:classes');
|
||||
expect(buildOutput).toContain('nx run utilities:build');
|
||||
@@ -94,7 +96,9 @@ dependencies {
|
||||
}
|
||||
);
|
||||
|
||||
let buildOutput = runCLI('build app2 --no-batch', { verbose: true });
|
||||
let buildOutput = runCLI('build app2 --no-batch', {
|
||||
verbose: true,
|
||||
});
|
||||
// app2 depends on app
|
||||
expect(buildOutput).toContain('nx run app:build');
|
||||
expect(buildOutput).toContain(':app:classes');
|
||||
@@ -120,8 +124,12 @@ dependencies {
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
runCLI('run app:test-ci--MessageUtilsTest', { verbose: true });
|
||||
runCLI('run list:test-ci--LinkedListTest', { verbose: true });
|
||||
runCLI('run app:test-ci--MessageUtilsTest', {
|
||||
verbose: true,
|
||||
});
|
||||
runCLI('run list:test-ci--LinkedListTest', {
|
||||
verbose: true,
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import { readFileSync } from 'fs';
|
||||
import { createFileSync, writeFileSync } from 'fs-extra';
|
||||
import { join, resolve } from 'path';
|
||||
|
||||
const kotlinVersion = '2.1.20';
|
||||
// Kotlin 2.2+ is required to target JVM 24 bytecode; older versions cap at
|
||||
// JVM 23 and cause an inconsistent-JVM-target build failure on JDK 24.
|
||||
const kotlinVersion = '2.3.21';
|
||||
|
||||
export function createGradleProject(
|
||||
projectName: string,
|
||||
@@ -18,7 +20,10 @@ export function createGradleProject(
|
||||
packageName: string = 'gradleProject',
|
||||
addProjectJsonNamePrefix: string = ''
|
||||
) {
|
||||
e2eConsoleLogger(`Using java version: ${execSync('java -version')}`);
|
||||
// `java -version` prints to stderr, so redirect it to capture the output.
|
||||
const javaVersionOutput = execSync('java -version 2>&1').toString();
|
||||
e2eConsoleLogger(`Using java version: ${javaVersionOutput}`);
|
||||
const javaMajorVersion = parseJavaMajorVersion(javaVersionOutput);
|
||||
const gradleCommand = isWindows()
|
||||
? resolve(`${__dirname}/../../../../gradlew.bat`)
|
||||
: resolve(`${__dirname}/../../../../gradlew`);
|
||||
@@ -35,14 +40,18 @@ export function createGradleProject(
|
||||
);
|
||||
e2eConsoleLogger(
|
||||
runCommand(
|
||||
`${gradleCommand} init --type ${type}-application --dsl ${type} --project-name ${projectName} --package ${packageName} --no-incubating --split-project --overwrite`,
|
||||
// Pin the generated project's Java toolchain to the JDK that is
|
||||
// actually installed. Without this, `gradle init` defaults to a fixed
|
||||
// version (e.g. 21); if that differs from the installed JDK, Gradle
|
||||
// falls back to downloading it via the foojay API, which makes the
|
||||
// tests fail whenever foojay is unavailable.
|
||||
`${gradleCommand} init --type ${type}-application --dsl ${type} --project-name ${projectName} --package ${packageName} --java-version ${javaMajorVersion} --no-incubating --split-project --overwrite`,
|
||||
{
|
||||
cwd,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Update Kotlin version to 2.0.21 after project creation
|
||||
if (type === 'kotlin') {
|
||||
updateKotlinVersion(cwd, type);
|
||||
}
|
||||
@@ -90,6 +99,19 @@ export function createGradleProject(
|
||||
);
|
||||
}
|
||||
|
||||
function parseJavaMajorVersion(javaVersionOutput: string): string {
|
||||
// Matches both modern (`24.0.2`) and legacy (`1.8.0_392`) version strings.
|
||||
const match = javaVersionOutput.match(/version "(\d+)(?:\.(\d+))?/);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Could not determine Java major version from: ${javaVersionOutput}`
|
||||
);
|
||||
}
|
||||
// Legacy versions report as `1.8` etc., where the real major is the second
|
||||
// segment; modern versions report the major directly.
|
||||
return match[1] === '1' && match[2] ? match[2] : match[1];
|
||||
}
|
||||
|
||||
function addLocalPluginManagement(filePath: string) {
|
||||
let content = readFileSync(filePath).toString();
|
||||
content =
|
||||
|
||||
@@ -9,6 +9,38 @@ import {
|
||||
runCommand,
|
||||
tmpProjPath,
|
||||
} from '@nx/e2e-utils';
|
||||
import { cpSync, mkdtempSync, rmSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
const installCmd = {
|
||||
pnpm: 'pnpm install --frozen-lockfile',
|
||||
yarn: 'yarn install --frozen-lockfile',
|
||||
npm: 'npm ci',
|
||||
} as const;
|
||||
|
||||
// Copy the pruned dist to a fresh tmp dir outside the e2e workspace and run
|
||||
// install there. This isolates the install from the surrounding workspace
|
||||
// (pnpm-workspace.yaml, parent node_modules) so the assertion is really
|
||||
// "the pruned dist is self-sufficient" — i.e. the actual deployment contract.
|
||||
function installPrunedDist(
|
||||
packageManager: 'pnpm' | 'yarn' | 'npm',
|
||||
distPath: string
|
||||
) {
|
||||
const installDir = mkdtempSync(join(tmpdir(), 'prune-lockfile-install-'));
|
||||
try {
|
||||
cpSync(distPath, installDir, { recursive: true });
|
||||
// failOnError: true — runCommand silently swallows non-zero exits by
|
||||
// default (see e2e/utils/command-utils.ts), which would let a broken
|
||||
// pruned lockfile pass this assertion. We want a real failure.
|
||||
runCommand(installCmd[packageManager], {
|
||||
cwd: installDir,
|
||||
failOnError: true,
|
||||
});
|
||||
} finally {
|
||||
rmSync(installDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe('js:prune-lockfile executor', () => {
|
||||
describe.each([
|
||||
@@ -18,7 +50,7 @@ describe('js:prune-lockfile executor', () => {
|
||||
])(
|
||||
'package manager %s',
|
||||
(packageManager: 'pnpm' | 'yarn' | 'npm', lockfile) => {
|
||||
let scope;
|
||||
let scope: string;
|
||||
|
||||
beforeAll(() => {
|
||||
scope = newProject({
|
||||
@@ -31,7 +63,7 @@ describe('js:prune-lockfile executor', () => {
|
||||
cleanupProject();
|
||||
});
|
||||
|
||||
it('should prune lockfile with workspace module', () => {
|
||||
it('should produce installable pruned output with a workspace module', () => {
|
||||
const nodeapp = uniq('nodeapp');
|
||||
const nodelib = uniq('nodelib');
|
||||
|
||||
@@ -49,9 +81,11 @@ describe('js:prune-lockfile executor', () => {
|
||||
};
|
||||
json.nx.targets['prune-lockfile'] = {
|
||||
executor: '@nx/js:prune-lockfile',
|
||||
options: {
|
||||
buildTarget: 'build',
|
||||
},
|
||||
options: { buildTarget: 'build' },
|
||||
};
|
||||
json.nx.targets['copy-workspace-modules'] = {
|
||||
executor: '@nx/js:copy-workspace-modules',
|
||||
options: { buildTarget: 'build' },
|
||||
};
|
||||
return json;
|
||||
});
|
||||
@@ -59,11 +93,80 @@ describe('js:prune-lockfile executor', () => {
|
||||
|
||||
runCLI(`build ${nodeapp}`);
|
||||
runCLI(`prune-lockfile ${nodeapp}`);
|
||||
runCLI(`copy-workspace-modules ${nodeapp}`);
|
||||
|
||||
checkFilesExist(`${nodeapp}/dist/${lockfile}`);
|
||||
installPrunedDist(packageManager, tmpProjPath(`${nodeapp}/dist`));
|
||||
});
|
||||
|
||||
// app -> lib-a -> lib-b with lib-b having an npm dep. The pruned lockfile
|
||||
// must include lib-b as an importer and pull in its npm deps; otherwise
|
||||
// the dist install fails. Regression for #34655 (originally hit on pnpm
|
||||
// with `workspace:*`; this asserts the install contract on every PM).
|
||||
it('should produce installable pruned output with a transitive workspace dep', () => {
|
||||
const nodeapp = uniq('nodeapp');
|
||||
const liba = uniq('liba');
|
||||
const libb = uniq('libb');
|
||||
|
||||
runCLI(
|
||||
`generate @nx/node:app ${nodeapp} --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/js:lib ${liba} --bundler=tsc --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/js:lib ${libb} --bundler=tsc --linter=eslint --unitTestRunner=jest`
|
||||
);
|
||||
|
||||
// pnpm uses workspace:* for sibling workspace refs; yarn classic and
|
||||
// npm don't support that protocol, so use file:../ for those.
|
||||
const ref = (name: string) =>
|
||||
packageManager === 'pnpm' ? 'workspace:*' : `file:../${name}`;
|
||||
|
||||
updateJson(`${liba}/package.json`, (json) => {
|
||||
json.dependencies = {
|
||||
...json.dependencies,
|
||||
[`@${scope}/${libb}`]: ref(libb),
|
||||
};
|
||||
return json;
|
||||
});
|
||||
updateJson(`${libb}/package.json`, (json) => {
|
||||
json.dependencies = {
|
||||
...json.dependencies,
|
||||
lodash: '^4.17.21',
|
||||
};
|
||||
return json;
|
||||
});
|
||||
updateJson(`${nodeapp}/package.json`, (json) => {
|
||||
json.dependencies = {
|
||||
...json.dependencies,
|
||||
[`@${scope}/${liba}`]: ref(liba),
|
||||
};
|
||||
json.nx.targets['prune-lockfile'] = {
|
||||
executor: '@nx/js:prune-lockfile',
|
||||
options: { buildTarget: 'build' },
|
||||
};
|
||||
json.nx.targets['copy-workspace-modules'] = {
|
||||
executor: '@nx/js:copy-workspace-modules',
|
||||
options: { buildTarget: 'build' },
|
||||
};
|
||||
return json;
|
||||
});
|
||||
runCommand(`${packageManager} install`);
|
||||
|
||||
runCLI(`build ${nodeapp}`);
|
||||
runCLI(`prune-lockfile ${nodeapp}`);
|
||||
runCLI(`copy-workspace-modules ${nodeapp}`);
|
||||
|
||||
installPrunedDist(packageManager, tmpProjPath(`${nodeapp}/dist`));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Plain semver workspace refs (e.g. "*" or "0.0.1") are an npm/yarn-classic
|
||||
// thing — pnpm requires the `workspace:` protocol — so this scenario stays
|
||||
// npm-only. The executor must recognize the sibling and rewrite the dep
|
||||
// to point at workspace_modules. Regression test for #33523.
|
||||
describe('package manager npm (plain semver workspace dependency)', () => {
|
||||
let scope: string;
|
||||
|
||||
@@ -78,11 +181,7 @@ describe('js:prune-lockfile executor', () => {
|
||||
cleanupProject();
|
||||
});
|
||||
|
||||
// npm workspaces reference sibling packages with plain semver (e.g. "*" or
|
||||
// "0.0.1") rather than a `workspace:`/`file:`/`link:` protocol prefix. The
|
||||
// executor must still recognize these as workspace modules and rewrite
|
||||
// them to point at `workspace_modules`. Regression test for #33523.
|
||||
it('should rewrite dependency to workspace_modules when version is plain semver', () => {
|
||||
it('should rewrite dependency to workspace_modules and install cleanly', () => {
|
||||
const nodeapp = uniq('nodeapp');
|
||||
const nodelib = uniq('nodelib');
|
||||
|
||||
@@ -100,9 +199,11 @@ describe('js:prune-lockfile executor', () => {
|
||||
};
|
||||
json.nx.targets['prune-lockfile'] = {
|
||||
executor: '@nx/js:prune-lockfile',
|
||||
options: {
|
||||
buildTarget: 'build',
|
||||
},
|
||||
options: { buildTarget: 'build' },
|
||||
};
|
||||
json.nx.targets['copy-workspace-modules'] = {
|
||||
executor: '@nx/js:copy-workspace-modules',
|
||||
options: { buildTarget: 'build' },
|
||||
};
|
||||
return json;
|
||||
});
|
||||
@@ -110,6 +211,7 @@ describe('js:prune-lockfile executor', () => {
|
||||
|
||||
runCLI(`build ${nodeapp}`);
|
||||
runCLI(`prune-lockfile ${nodeapp}`);
|
||||
runCLI(`copy-workspace-modules ${nodeapp}`);
|
||||
|
||||
checkFilesExist(`${nodeapp}/dist/package-lock.json`);
|
||||
const prunedPackageJson = JSON.parse(
|
||||
@@ -118,6 +220,8 @@ describe('js:prune-lockfile executor', () => {
|
||||
expect(prunedPackageJson.dependencies[`@${scope}/${nodelib}`]).toBe(
|
||||
`file:./workspace_modules/@${scope}/${nodelib}`
|
||||
);
|
||||
|
||||
installPrunedDist('npm', tmpProjPath(`${nodeapp}/dist`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,21 @@ wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-w
|
||||
);
|
||||
});
|
||||
|
||||
it('should record build state while running Maven 4 resource phases in parallel', () => {
|
||||
const output = runCLI(
|
||||
'run-many -t resources,after:resources --parallel=3 --skip-nx-cache',
|
||||
{
|
||||
env: { NX_BATCH_MODE: 'true', NX_VERBOSE_LOGGING: 'true' },
|
||||
}
|
||||
);
|
||||
|
||||
expect(output).toContain('Successfully ran targets');
|
||||
expect(output).toContain('resources');
|
||||
expect(output).toContain('after:resources');
|
||||
expect(output).not.toContain('context.terminal');
|
||||
expect(output).not.toContain('Terminal.writer()');
|
||||
});
|
||||
|
||||
it('should install successfully after restoring cached package outputs', () => {
|
||||
// Step 1: Clean target directories to simulate a clean CI checkout
|
||||
runCLI('run-many -t clean');
|
||||
|
||||
@@ -47,8 +47,15 @@ describe('Maven', () => {
|
||||
});
|
||||
|
||||
it('should build Maven project with dependencies without batch mode', () => {
|
||||
// Build app which depends on lib, which depends on utils
|
||||
let buildOutput = runCLI('run app:install --no-batch', { verbose: true });
|
||||
// Build app which depends on lib, which depends on utils.
|
||||
// Without --batch, Nx fans `app:install` out into one task per Maven
|
||||
// lifecycle phase per project (~87 tasks total), each spawning its own
|
||||
// mvn JVM. On loaded CI hosts this routinely runs 220-290s, so the
|
||||
// default 5-minute runCLI timeout is too tight; bump it to 10 minutes.
|
||||
let buildOutput = runCLI('run app:install --no-batch', {
|
||||
verbose: true,
|
||||
timeout: 10 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Should build dependencies first
|
||||
expect(buildOutput).toContain('BUILD SUCCESS');
|
||||
@@ -117,8 +124,12 @@ describe('Maven', () => {
|
||||
expect(output).toContain('- mvn-package:');
|
||||
expect(output).toContain('- mvn-install-ci:');
|
||||
|
||||
// Verify prefixed target works
|
||||
const buildOutput = runCLI('run app:mvn-compile --no-batch');
|
||||
// Verify prefixed target works. Same lifecycle-fan-out concern as the
|
||||
// app:install case above — give it a 10-minute timeout so CI load
|
||||
// doesn't push us past the default 5 minutes.
|
||||
const buildOutput = runCLI('run app:mvn-compile --no-batch', {
|
||||
timeout: 10 * 60 * 1000,
|
||||
});
|
||||
expect(buildOutput).toContain('BUILD SUCCESS');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { capitalize } from '@nx/devkit/src/utils/string-utils';
|
||||
import { joinPathFragments } from '@nx/devkit';
|
||||
import { joinPathFragments, names } from '@nx/devkit';
|
||||
import {
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
@@ -199,9 +198,9 @@ describe('@nx/next (legacy)', () => {
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const TestComponent = dynamic(
|
||||
() => import('@${proj}/${nextLib}').then(d => d.${capitalize(
|
||||
nextLib
|
||||
)})
|
||||
() => import('@${proj}/${nextLib}').then(d => d.${
|
||||
names(nextLib).className
|
||||
})
|
||||
);
|
||||
${content.replace(
|
||||
`</h2>`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`Extra Nx Misc Tests task graph inputs should correctly expand default task inputs 1`] = `
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user