Compare commits

...

167 Commits

Author SHA1 Message Date
Jack Hsu 7eccccd817 fix(misc): bump happy-dom, tmp, and form-data to patched versions (#36013)
New workspaces / `nx@23.0.0` ship dependencies with published security
advisories:

- `happy-dom@~9.20.3` (when the happy-dom test environment is selected)
- two **critical** RCE advisories:
[GHSA-37j7-fg3j-429f](https://github.com/advisories/GHSA-37j7-fg3j-429f)
(VM context escape) and
[GHSA-96g7-g7g9-jxw8](https://github.com/advisories/GHSA-96g7-g7g9-jxw8)
(server-side code execution via `<script>`).
- `tmp@0.2.6` - **high**,
[GHSA-7c78-jf6q-g5cm](https://github.com/advisories/GHSA-7c78-jf6q-g5cm)
(path traversal).
- `form-data@4.0.5` (transitive via `axios`) - **high**,
[GHSA-hmw2-7cc7-3qxx](https://github.com/advisories/GHSA-hmw2-7cc7-3qxx)
(CRLF injection).

`tmp` and `form-data` reach generated workspaces because `expand-deps`
pins nx's transitive deps from the monorepo lockfile at publish time.

- `happyDomVersion` bumped `~9.20.3` -> `^20.10.4` in
`packages/vitest/src/utils/versions.ts` (caret matches sibling
`jsdomVersion` so it stays patched within the major).
- `tmp` forced to `~0.2.7` and `form-data` to `^4.0.6` via catalog +
overrides; lockfile re-resolved so the next release pins the patched
versions.

`pnpm audit` reports 0 critical repo-wide; `tmp` and `form-data` are
CLEAN.

N/A

<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/rapid-panther-825e4172)
<!-- polygraph-session-end -->
2026-06-23 10:58:26 -04:00
Jack Hsu 911d259abd docs(nx-cloud): add platform pages for Nx Cloud add-ons (#35857)
## Current Behavior

Resource usage and sandboxing docs lived under guides; there were no
pages for the dedicated compute cluster, Docker layer caching, or the
Docker/npm read-through caches.

## Expected Behavior

Six consolidated platform-feature pages under `features/ci-features`,
grouped in a new "Nx Cloud add-ons" sidebar section (Resource usage,
Sandboxing, Dedicated compute cluster, Docker layer caching, Docker
read-through cache, npm read-through cache). Resource usage moved out of
guides with a redirect, and the launch-templates DinD note now points to
the dedicated compute cluster instead of being enterprise-only.

## Previews:

-
https://deploy-preview-35857--nx-docs.netlify.app/docs/features/ci-features/resource-usage
-
https://deploy-preview-35857--nx-docs.netlify.app/docs/features/ci-features/dedicated-compute-cluster
-
https://deploy-preview-35857--nx-docs.netlify.app/docs/features/ci-features/sandboxing
-
https://deploy-preview-35857--nx-docs.netlify.app/docs/features/ci-features/docker-layer-caching
-
https://deploy-preview-35857--nx-docs.netlify.app/docs/features/ci-features/docker-read-through-cache
-
https://deploy-preview-35857--nx-docs.netlify.app/docs/features/ci-features/npm-read-through-cache
-
https://deploy-preview-35857--nx-docs.netlify.app/docs/reference/nx-cloud/launch-templates#launch-templatestemplate-nameimage

<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/docs-sandbox-resource-usage-f431c8fc)
<!-- polygraph-session-end -->

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
2026-06-02 15:21:23 -04:00
Jack Hsu fdd5e434af docs(nx-dev): promote setting up CI to top-level Getting Started (#35802)
This PR moves the CI portion of the tutorial out to its own top-level
getting started page. The traffic to the tutorial page is low, and this
move surfaces the instructions more prominently.

Preview:
https://deploy-preview-35802--nx-docs.netlify.app/docs/getting-started/setup-ci

Redirects are set up from old CI page to new page.

Also updates the https://nx.dev/docs/guides/nx-cloud/access-tokens page
slightly since the settings have changed since this pages was written.

Closes DOC-503
2026-05-28 10:31:55 -04:00
Jack Hsu 171306d74a fix(core): update tmp to 0.2.6 due to CVE-2026-44705 (#35813)
Bump `tmp` to `0.2.6`. Need exclusion for minimum release age just for
`0.2.6` so we can patch immediately.

See
https://linear.app/nxdev/issue/NXC-4494/patch-vulnerable-tmp-dependency-in-nx

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-05-27 15:03:28 -04:00
Jack Hsu dc849bbd94 fix(core): update brace-expansion and yaml (#35790)
The current versions `yaml@2.8.0` and `brace-expansion@5.0.5` that are
packaged with `nx` have medium vulnerabilities reported.

<img width="793" height="229" alt="image"
src="https://github.com/user-attachments/assets/c53c4a8f-fae7-47f1-9aae-6edd765967c0"
/>

https://npmx.dev/package/nx

This PR updates them to the newest versions without reported
vulnerabilities.

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-05-25 12:51:05 -04:00
polygraph-snapshot-app[bot] d6d0eac310 docs(misc): remove references to the Nx Cloud in-app CNW flow (#35779)
## Current Behavior
We've removed the Nx Cloud in-app CNW flow. This PR removes references
to it from the docs, and directs users to run `npx create-nx-workspace`
from a terminal instead.

Several astro-docs pages link to
`https://cloud.nx.app/create-nx-workspace` (some with framework-specific
subpaths like `/typescript/github`, `/angular/github`, `/react/github`).

## Expected Behavior

These links now point to `https://cloud.nx.app/get-started` or `npx
create-nx-workspace`. Framework-specific subpaths were dropped, and
existing UTM parameters were retained.

Files updated:
- `astro-docs/src/content/docs/features/CI
Features/github-integration.mdoc`
-
`astro-docs/src/content/docs/getting-started/Tutorials/typescript-packages-tutorial.mdoc`
-
`astro-docs/src/content/docs/getting-started/Tutorials/angular-monorepo-tutorial.mdoc`
-
`astro-docs/src/content/docs/getting-started/Tutorials/react-monorepo-tutorial.mdoc`
-
`astro-docs/src/content/docs/getting-started/Tutorials/self-healing-ci-tutorial.mdoc`

## Related Issue(s)

N/A

<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/update-links-to-cnw-1f738cb4)
<!-- polygraph-session-end -->

Co-authored-by: Nicole Oliver <nicole.oliver.42@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:07:12 -04:00
FrozenPandaz f5e74caa1f chore(repo): loosen root webpack pins to allow dedupe with plugin peers 2026-05-22 16:06:37 -04:00
Jason Jean eba3fbab58 feat(js): support pnpm 11.2.2 (#35772)
Repo pins pnpm 10.28.2. @nx/js's typescript inference plugin
  bare-requires 'typescript' without declaring it, relying on Node's
  resolver walking up into the workspace's node_modules. Under pnpm 11's
  enableGlobalVirtualStore, the plugin's real path sits outside the
  workspace tree and resolution fails with MODULE_NOT_FOUND.
  Also, pnpm 11 no longer reads the `pnpm` field in package.json.

  ## Expected Behavior
  - pnpm bumped to 11.2.2 across package.json and CI workflows.
  - @nx/js inference plugin resolves typescript from workspaceRoot —
layout-agnostic across pnpm classic, global virtual store, npm, yarn.
- pnpm.overrides moved into pnpm-workspace.yaml; allowBuilds configured
to preserve prior onlyBuiltDependencies policy. Lockfile regenerated.

  ## Related Issue(s)
  Fixes NXC-4432

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
(cherry picked from commit 8ca1434243)
2026-05-22 14:14:22 -04:00
Jason Jean d2dfc4a65a chore(repo): set pnpm minimumReleaseAge with nx exclusions (#35775)
## Current Behavior

`pnpm install` will happily resolve and install package versions that
were published seconds ago. This is the supply-chain attack window —
when a popular package gets compromised (e.g. the recent `node-ipc`
incident), any CI/dev install during
that window can pick up the malicious version before the registry /
community has a chance to react.

  ## Expected Behavior

`pnpm install` enforces a minimum release age of 1 day (1440 minutes),
so packages published within the last 24 hours are not eligible to be
installed. Nx-published packages (`nx`, `@nx/*`, `@nrwl/*`,
`create-nx-workspace`, `create-nx-plugin`)
are excluded so freshly released Nx packages don't block installs or e2e
flows.

  Configured via `pnpm-workspace.yaml`:

  ```yaml
  minimumReleaseAge: 1440
  minimumReleaseAgeExclude:
    - nx
    - '@nx/*'
    - '@nrwl/*'
    - create-nx-workspace
    - create-nx-plugin
  ```

  ## Related Issue(s)

Refs
[NXC-4466](https://linear.app/nxdev/issue/NXC-4466/set-minimum-release-age-in-ci-with-nx-exclusions)

(cherry picked from commit c96224d104)
2026-05-22 14:12:39 -04:00
Jason Jean 6847d708ef fix(repo): run dotnet restore before macos e2e job (#35774)
## Current Behavior

The macOS e2e job in ci.yml runs `pnpm install` but never restores .NET
packages, so any e2e test that triggers the `@nx/dotnet` plugin's
inferred `build` target (which uses `--no-restore`) fails with
NETSDK1004 (missing `project.assets.json`).

  ## Expected Behavior

Run `dotnet restore nx.sln` after `pnpm install` in the macOS e2e job,
matching what `main-linux` already does.

  ## Related Issue(s)

  N/A

(cherry picked from commit 461aad7f3e)
2026-05-22 14:12:38 -04:00
Jason Jean 7effad4b58 fix(repo): run dotnet restore before publish (#35771)
## Current Behavior

Publish workflow fails when building MsbuildAnalyzer because the
`@nx/dotnet` plugin's inferred `build` target runs
`dotnet build --no-restore --no-dependencies --configuration Release`,
but no restore has happened, so
  `project.assets.json` is missing:

error NETSDK1004: Assets file
'.../packages/dotnet/analyzer/obj/project.assets.json' not found.

  ## Expected Behavior

Run `dotnet restore nx.sln` once in the publish job (right after `pnpm
install`) so all .NET projects have assets files
   before any inferred build runs with `--no-restore`.

  ## Related Issue(s)

  N/A

(cherry picked from commit a48f44f3fe)
2026-05-22 10:46:10 -04:00
polygraph-snapshot-app[bot] 19dbbb0753 fix(angular): only add @oxc-project/runtime on the vitest-analog path (#35734)
The Angular vitest generators added `@oxc-project/runtime` to the user's
`devDependencies` on **both** the vitest-angular and vitest-analog
paths, with a comment claiming `@angular/build`'s rolldown usage emits
external `@oxc-project/runtime/helpers/*` imports. That claim doesn't
match `@angular/build`'s source: its vitest builder sets
`optimizeDeps.noDiscovery: true` plus an in-memory test provider, so no
rolldown pre-bundling against `@angular/*` runs on that path.

Separately, `addVitestAngular`/`addVitestAnalog` silently dropped the
`GeneratorCallback`s returned by `addDependenciesToPackageJson` and
`@nx/vitest`'s `configurationGenerator`. Install still ran in practice
because the parent application/library generators call
`installPackagesTask` separately, but the wiring was incorrect and any
caller that didn't double-call install would lose the post-add hooks.

- `addVitestAngular` no longer adds `@oxc-project/runtime` (it's not
needed on that path).
- `addVitestAnalog` continues to add `@oxc-project/runtime`, with a
corrected comment that accurately attributes the cause.
- Both helpers return proper `GeneratorCallback`s; the application and
library generators chain them into their task lists.
- The matching `update-23-0-0` migration AI-instructions section is
scoped to the vitest-analog path with the corrected explanation.

`@nx/vitest`'s `configurationGenerator` (for `uiFramework: 'angular'`)
registers `@analogjs/vite-plugin-angular`'s `angular()`. In test mode,
analog additionally registers an `angularVitestPlugin` whose `transform`
hook matches `@angular/*` `fesm2022` modules containing `async ` (plus
any `@angular/cdk` file) and calls:

```ts
vite.transformWithOxc(code, id, { target: 'es2016', … })
```

The downlevel is deliberate. The plugin source comments it as
*"downlevels any dependencies that use async/await to support zone.js
testing and tests w/fakeAsync"* — Zone.js relies on monkey-patching
promise scheduling for `fakeAsync` and friends, which it cannot do
against native `async`/`await`, so the plugin lowers them to a form
Zone.js can intercept.

With `target: 'es2016'`, oxc emits the helpers as external
`@oxc-project/runtime/helpers/*` imports (oxc's default `HelperMode =
'Runtime'`). Nothing in the upstream chain
(`@analogjs/vite-plugin-angular`, `@angular/core`, `vite`, `rolldown`)
declares `@oxc-project/runtime` in a way that's resolvable from the
consumer's workspace, so `vite:import-analysis` fails to resolve those
imports unless the dep is added explicitly. This behavior is unchanged
through analog `3.0.0-alpha.54` (latest at time of writing).

- `@angular/build:unit-test` (and `@nx/angular:unit-test` for libraries)
bypasses analog entirely.
- It sets `optimizeDeps.noDiscovery: true` and uses an in-memory test
provider, so no rolldown pre-bundling runs against `@angular/*`.
- No `angularVitestPlugin` is loaded → no `target: 'es2016'` downlevel →
no `@oxc-project/runtime/helpers/*` imports emitted.

- `addVitestAngular`/`addVitestAnalog` return
`Promise<GeneratorCallback>`; the application and library generators
chain them through `runTasksInSerial(...)` so the install-packages and
configuration callbacks actually run through the generator pipeline.
- `@oxc-project/runtime` is added only by `addVitestAnalog`, with the
comment now describing the actual mechanism (analog's
`angularVitestPlugin` + `transformWithOxc({ target: 'es2016' })` for
Zone.js compatibility).
- `update-23-0-0/ai-instructions-for-vite-8.md` section 3 rewritten with
the corrected mechanism, scoped to the vitest-analog path. Detection:
`rg '"@nx/vitest:test"' --type json` + `rg
'@analogjs/vite-plugin-angular' --type ts --type js`.
- e2e: new case in `projects-build-and-test.test.ts` opts into
vitest-angular explicitly (app w/ `--bundler=esbuild`, lib w/
`--buildable`) and runs `nx test` against both. The existing test
exercises vitest-analog implicitly through `app1` (webpack) →
`setGeneratorDefaults` writes `unitTestRunner: vitest-analog` to
`nx.json`, locking subsequent generations to that runner regardless of
per-project defaults.

- Reproduced the failure in an Nx e2e-generated workspace: with
`@oxc-project/runtime` absent, `nx run <lib>:test` fails at
`vite:import-analysis` trying to resolve
`@oxc-project/runtime/helpers/defineProperty` from
`@angular/core/fesm2022/testing.mjs`. The on-disk `testing.mjs` does
**not** contain those imports — they are injected in-memory by analog's
`angularVitestPlugin.transform`. Installing the dep makes the test pass.
- Confirmed the mechanism against analog plugin source in versions
`2.1.3`, `2.5.1`, and `3.0.0-alpha.54` — the `target: 'es2016'`
downlevel is unchanged.
- The new e2e covers the inverse: vitest-angular runs `nx test`
successfully without relying on `@oxc-project/runtime` for the path.

High confidence in the root cause and the path-scoped fix.

Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
(cherry picked from commit 526418996d)
2026-05-22 10:28:37 -04:00
polygraph-snapshot-app[bot] 8fd1febdcf fix(core): allow local plugin subpath imports without custom conditions (#35751)
## Current Behavior

PR #35631 added a "collision guard" in `resolveSubpathFromExports`:
after resolving a subpath with the workspace's custom conditions, it
calls `resolve.exports` a second time with `conditions: []`. If both
calls return the same path, it assumes the match came from a
`default`/`import`/`require` fallback (i.e. a dist artifact) and
hard-fails via `throwUnresolvableLocalPluginError`.

This false-positives on local packages whose `exports` map points all
conditions at source — there's no dist to silently load, but the second
call collides with the first and the loader refuses to resolve.

Real-world example (from `nrwl/ocean`):

```json
"./plugin": {
  "types": "./src/plugin/index.ts",
  "import": "./src/plugin/index.ts",
  "default": "./src/plugin/index.ts"
}
```

`pnpm nx sync:check` fails with:
> the package's 'exports' entry for './plugin' does not declare a
resolvable source-pointing condition recognized by Nx.

## Expected Behavior

- Source-pointing custom condition wins when one is declared and
matches.
- Otherwise, whatever `resolve.exports` returns is used as long as the
file exists on disk.
- The loader only hard-fails when nothing resolves at all (existing
`throwUnresolvableLocalPluginError` path when both source resolution and
`require.resolve` fail).

## Changes

- `packages/nx/src/project-graph/plugins/resolve-plugin.ts`
- Removed the second `resolve.exports` call and the equality check that
returned `null` on collision.
  - Removed the now-unused `getRootTsConfigCustomConditions` import.
- Simplified the subpath branch of `throwUnresolvableLocalPluginError` —
the message no longer instructs users to add a custom condition; it now
just reports that the subpath has no resolvable entry or file on disk.
- `packages/nx/src/project-graph/plugins/resolve-plugin.spec.ts` (new)
- Unit tests covering: custom source condition wins;
types/import/default-only resolves to source (regression for this PR);
guided error when the matched file doesn't exist; guided error when the
subpath has no exports entry.
- `e2e/plugin/src/nx-plugin-ts-solution.test.ts`
- Updated the PR #35631 e2e test "should not load local plugin subpath
imports from dist" — that restriction is intentionally lifted; the test
now verifies a dist-only subpath export loads successfully.

Related: #35631

<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/upnx-aab34-f5a75715)
<!-- polygraph-session-end -->

---------

Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
(cherry picked from commit 51edd56239)
2026-05-22 10:28:24 -04:00
Craigory Coppola eaecdea911 fix(dotnet): include Directory.*.* files in inputs (#35738)
Directory.Build.props and similar files are reported as sandbox
violations if they would be read by the .NET CLI commands

They are considered in inputs

<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #

(cherry picked from commit e3b47e75cf)
2026-05-22 10:28:13 -04:00
Craigory Coppola 92bf264ba4 chore(repo): preserve preset dist ignore in graph jest configs (#35754)
## Current Behavior

`graph/migrate/jest.config.cts` and `graph/client/jest.config.cts` build
their config by spreading `...nxPreset` and then re-declaring
`modulePathIgnorePatterns`:

```js
module.exports = {
  ...nxPreset,
  // ...
  modulePathIgnorePatterns: [
    '/graph/client/src/app/machines/match-media-mock.spec.ts',
  ],
};
```

Object spread is a shallow merge, so this **replaces** the preset's
`modulePathIgnorePatterns` (`['<rootDir>/dist/', '<rootDir>/out-tsc/']`)
rather than extending it. With `dist/` no longer ignored,
`jest-haste-map` crawls each project's own build output. After
`typecheck`/`build` emits declaration files into the project's `dist/`,
the `test` target reads those `.d.ts` files (they match the `ts` module
extension), producing **undeclared-input sandbox violations**.

Observed in a sandbox report for `graph-migrate:test`: 15 unexpected
reads, all `graph/migrate/dist/src/**/*.d.ts`, read by `jest-worker`
processes during the haste-map crawl — even though the only spec
(`machine.spec.ts`) imports none of those files.

Note: configs that use jest's native `preset:` field (e.g.
`packages/devkit`, `packages/gradle`) are unaffected, because jest's
`mergeOptionWithPreset` concatenates `modulePathIgnorePatterns` from the
preset. Only the configs that inline the preset via object spread were
affected.

## Expected Behavior

The preset's `modulePathIgnorePatterns` are preserved, so
`jest-haste-map` keeps ignoring `<rootDir>/dist/` (and
`<rootDir>/out-tsc/`) and never reads the project's own build output
during tests. The fix spreads the preset's patterns ahead of the
project-specific one:

```js
modulePathIgnorePatterns: [
  ...(nxPreset.modulePathIgnorePatterns ?? []),
  '/graph/client/src/app/machines/match-media-mock.spec.ts',
],
```

Verified: both configs now resolve to `['<rootDir>/dist/',
'<rootDir>/out-tsc/', '/graph/client/.../match-media-mock.spec.ts']`,
and `graph-migrate:test` still passes (11/11).

## Related Issue(s)

N/A — surfaced by an Nx Cloud sandbox report.

(cherry picked from commit e1fe753ce9)
2026-05-22 10:27:00 -04:00
Craigory Coppola 22e679c2c0 docs(core): note that task-specific env vars are not loaded in batch mode (#35759)
Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit c4880d3bc8)
2026-05-22 10:26:34 -04:00
Jason Jean 9fa3f96cf3 chore(misc): pin corepack default pnpm to packageManager version (#35765)
Corepack on CI defaults to the latest published pnpm whenever it runs in
a directory without a `packageManager` field. Our e2e tests create temp
workspaces (via `create-nx-workspace`) in `/tmp/...` and run `pnpm
install` there before the field is written. Corepack picks pnpm 11.x,
which fails installs across the e2e matrix.

Corepack reuses the pnpm version activated from the repo's
`packageManager` field (pnpm 10.x today) regardless of the working
directory. Setting `COREPACK_DEFAULT_TO_LATEST=0` at the workflow `env:`
level tells corepack to keep the activated default instead of
auto-upgrading.

Also bumps the cache bust value in `nx.json` so the CI Nx Cache rolls.

N/A — internal CI fix.

(cherry picked from commit e2c8959b90)
2026-05-22 10:25:29 -04:00
Jason Jean a33aaa749a chore(module-federation): re-enable webpack-based react e2e tests (webpack 5.107.1 fix is live) (#35764)
## Current Behavior

PR #35753 skipped 9 webpack-based React Module Federation e2e suites
because webpack 5.107.0 (published 2026-05-20) reorganized its `lib/`
directory and removed `lib/ModuleNotFoundError.js`, which
`@module-federation/enhanced` deep-imports. Every webpack-based MF
build/serve in the affected suites was failing with `Cannot find module
'webpack/lib/ModuleNotFoundError'`.

## Expected Behavior

webpack 5.107.1 (published 2026-05-21) restored the path as a
backward-compat shim — `lib/ModuleNotFoundError.js` now re-exports from
`./errors/ModuleNotFoundError`. Verified locally that all 21
`webpack/lib/*` paths used by `@module-federation/enhanced` 2.4.0 /
2.5.0 now resolve in 5.107.1.

This PR removes the `describe.skip` + TODO + `//
eslint-disable-next-line jest/no-disabled-tests` lines from the 9
affected files, re-enabling:

-
`e2e/react/src/module-federation/core-webpack-basic-host-remote-generation.test.ts`
-
`e2e/react/src/module-federation/core-webpack-basic-playwright.test.ts`
- `e2e/react/src/module-federation/core-webpack-name-and-root.test.ts`
- `e2e/react/src/module-federation/core-webpack-query-params.test.ts`
- `e2e/react/src/module-federation/core-webpack-ssr.test.ts`
- `e2e/react/src/module-federation/dynamic-federation.webpack.test.ts`
- `e2e/react/src/module-federation/federate-module.webpack.test.ts`
-
`e2e/react/src/module-federation/independent-deployability.webpack.test.ts`
- `e2e/react/src/module-federation/misc-rspack-interoperability.test.ts`

## Related Issue(s)

Reverts the skip from #35753. Upstream:
- https://github.com/webpack/webpack/pull/20988 (compat shim, merged)
- https://github.com/webpack/webpack/pull/20989 (webpack 5.107.1
release, merged)

(cherry picked from commit b6858ba197)
2026-05-22 10:25:01 -04:00
Benjamin Staneck 22f3c2e7bc fix(core): detect vscode copilot ai agent (#35757)
(cherry picked from commit c3f1d8afee)
2026-05-22 10:24:48 -04:00
Jason Jean 8ce0683993 fix(js): fall back to npm publish when bun publish fails with auth error (#35756)
## Current Behavior

`nx release publish` detects the workspace package manager and uses it
to run the publish command. In bun workspaces it runs `bun publish`.
However, `bun publish` doesn't support npm's OIDC trusted publishing —
where GitHub Actions exchanges a short-lived OIDC token with the npm
registry so you can publish without storing a static `NPM_TOKEN` secret.
Only `npm publish` (and modern pnpm/yarn) perform that token exchange
automatically.

The result is that a bun workspace configured for OIDC trusted
publishing fails with:

```
bun publish error:
error: missing authentication (run `bunx npm login`)
```

even when the workflow has `id-token: write` set up correctly.

## Expected Behavior

When `bun publish` fails with an authentication-shaped error and `npm`
is available, the executor falls back to `npm publish` to recover. This
lets bun workspaces use OIDC trusted publishing (and provenance, if
requested) transparently — no config changes needed.

Other bun failures (version conflicts, 5xx responses, generic errors)
still surface bun's error directly so we don't hide unrelated problems
behind an unnecessary npm retry.

Implementation:
- The publish step is extracted into a `runPublish(ctx)` helper so the
fallback can re-enter the existing publish + output-handling code with
just `pm` swapped to `'npm'`.
- The fallback is gated by a regex on bun's stderr/stdout: `missing
authentication | bunx npm login | unauthorized | 401`. Misses are soft —
the user gets bun's error as today, not a worse outcome.

## Related Issue(s)

Fixes #

(cherry picked from commit 878b103129)
2026-05-22 10:24:47 -04:00
polygraph-snapshot-app[bot] 480fc80d12 docs(nx-cloud): note Nx 22.1 requirement on manual DTE upload-agent-metrics examples (#35750)
Adds a short `(Requires Nx 22.1 or higher.)` inline next to the `Upload
agent resource metrics` comment in every provider example on the manual
DTE page (GitHub Actions, CircleCI, Azure Pipelines, Bitbucket
Pipelines, GitLab CI, Jenkins).

The note sits with each provider's existing comment rather than as a
top-level callout — this page is about manual distribution, not metrics,
so the version requirement is incidental.

Paired with the corresponding UI change in nrwl/ocean (see linked PR).

<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/mention-nx-version-requirement-for-manual-metric-uploads-4724334b)
<!-- polygraph-session-end -->

---------

Co-authored-by: rarmatei <matei.rar@gmail.com>
(cherry picked from commit 47ac4924aa)
2026-05-22 10:24:47 -04:00
Jason Jean c7582bfce8 chore(module-federation): skip webpack-based e2e tests pending webpack 5.107.0 compat (#35753)
## Current Behavior

All webpack-based React Module Federation e2e tests started failing on
2026-05-20.

Webpack 5.107.0 (published earlier today) reorganized its internal
`lib/` directory into subdirectories — e.g. `lib/ModuleNotFoundError.js`
moved to `lib/errors/ModuleNotFoundError.js`, `lib/DllPlugin.js` moved
to `lib/dll/DllPlugin.js`, etc. `@module-federation/enhanced` (which our
generators wire up for webpack-based host/remote apps) deep-imports
`require('webpack/lib/ModuleNotFoundError')`, which now throws
`MODULE_NOT_FOUND` and aborts every webpack-based MF build/serve in our
react e2e suite.

Sample failure:

```
NX   Cannot find module 'webpack/lib/ModuleNotFoundError'
Require stack:
- node_modules/@module-federation/enhanced/dist/src/lib/sharing/resolveMatchedConfigs.js
- node_modules/@module-federation/enhanced/dist/src/lib/sharing/ConsumeSharedPlugin.js
- ...
- node_modules/@nx/module-federation/src/with-module-federation/webpack/with-module-federation.js
```

This is upstream's bug, not ours:

- webpack/webpack#20985 — closed by webpack maintainer pointing at
module-federation
- module-federation/core#4747 — open issue for module-federation to stop
using private webpack APIs
- webpack/webpack#20988 — adds back a `lib/ModuleNotFoundError` compat
shim (merged, awaiting a webpack patch release)

Even after the shim release lands, other deep imports may still be
broken, so we want to fully decouple our CI from this until both
ecosystems re-sync.

Angular MF goes through the same `@module-federation/enhanced/webpack`
code path and would theoretically fail too, but only react tests were
observed failing in CI. The angular suites are left enabled so we get a
real signal if/when they hit the same issue.

## Expected Behavior

CI passes. Webpack-based react MF e2e suites are temporarily skipped
with `describe.skip` and a TODO comment linking the upstream tracking
issues. Rspack-based MF e2e tests and angular MF e2e tests are untouched
and continue to run.

Skipped suites:

-
`e2e/react/src/module-federation/core-webpack-basic-host-remote-generation.test.ts`
-
`e2e/react/src/module-federation/core-webpack-basic-playwright.test.ts`
- `e2e/react/src/module-federation/core-webpack-name-and-root.test.ts`
- `e2e/react/src/module-federation/core-webpack-query-params.test.ts`
- `e2e/react/src/module-federation/core-webpack-ssr.test.ts`
- `e2e/react/src/module-federation/dynamic-federation.webpack.test.ts`
- `e2e/react/src/module-federation/federate-module.webpack.test.ts`
-
`e2e/react/src/module-federation/independent-deployability.webpack.test.ts`
- `e2e/react/src/module-federation/misc-rspack-interoperability.test.ts`
(both scenarios still build with webpack on one side)

## Related Issue(s)

Tracking upstream:
- https://github.com/webpack/webpack/issues/20985
- https://github.com/module-federation/core/issues/4747
- https://github.com/webpack/webpack/pull/20988

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
(cherry picked from commit a51e340b78)
2026-05-22 10:24:46 -04:00
polygraph-snapshot-app[bot] ba85c77930 chore(repo): disable Roslyn shared compilation (#35724)
Co-authored-by: rarmatei <matei.rar@gmail.com>
(cherry picked from commit 6e6edabb21)
2026-05-22 10:24:45 -04:00
Jack Hsu 1289b017e0 docs(misc): remote cache (#35763)
e.g.
https://deploy-preview-35763--nx-docs.netlify.app/docs/reference/remote-cache-plugins/s3-cache/overview
2026-05-21 14:53:36 -04:00
FrozenPandaz 934850b8e5 fix(rsbuild): lazy-require @rsbuild/core in plugin so spec mocks work after jest.resetModules
The build-outputs tests added in #35707 (cherry-picked here) rely on
mockResolvedValueOnce against the current @rsbuild/core mock between
tests. A top-level import binds loadConfig once at module load, leaving
the binding stale after the spec's jest.resetModules() in afterEach.
Lazy-require matches master and lets the per-test mock take effect.
2026-05-20 11:28:50 -04:00
FrozenPandaz 373e8983a1 chore(repo): realign pnpm-lock.yaml after dropping 23.0.0-beta.15/.16 bumps 2026-05-19 17:51:49 -04:00
FrozenPandaz 640e69a7be chore(linter): write pnpm-workspace.yaml in workspace-rules-project TS solution test
pnpm ignores package.json workspaces, so the TS-solution-setup test was not detected as a workspace when run under pnpm, causing the jest config to fall back to ts-jest instead of @swc/jest.
2026-05-19 17:50:34 -04:00
Jason Jean c25bd9d96e fix(rsbuild): infer build outputs from distPath.root directly (#35707)
## Current Behavior

The `@nx/rsbuild` inferred-plugin computes the build target's `outputs`
by taking `dirname()` of `output.distPath.root`:

```ts
const buildOutputPath = normalizeOutputPath(
  rsbuildConfig?.output?.distPath?.root
    ? dirname(rsbuildConfig?.output.distPath.root)
    : undefined,
  ...
);
```

But `distPath.root` *is* the directory Rsbuild emits the build into, so
`dirname()` points one level too high. A project whose `distPath.root`
resolves to `dist/apps/my-app` gets its `outputs` inferred as
`{workspaceRoot}/dist/apps` — the parent directory, which captures
sibling projects' build artifacts. Nx then caches/restores the whole
`dist/apps` tree as that one project's output.

## Expected Behavior

The inferred `outputs` should be `distPath.root` itself
(`{workspaceRoot}/dist/apps/my-app`).

This PR drops the `dirname()` call so `distPath.root` is used as-is, and
adds `getOutputs` coverage for an unset, a project-relative, and a
workspace-relative `distPath.root` (the existing tests only exercised an
empty config).

## Related Issue(s)

N/A

(cherry picked from commit f17365050a)
2026-05-19 17:50:33 -04:00
Copilot 0d1851288f fix(misc): skip $ escaping in file paths on windows (#35692)
`nx format:write/check` was unconditionally escaping `$` in file paths
(e.g. `_app.chat.$id.tsx` → `_app.chat.\$id.tsx`), causing prettier to
report "No files matching the pattern were found" on Windows.

The `\$` escape exists solely to prevent Unix shells from interpolating
`$var` patterns. On Windows (`cmd.exe`), `$` carries no special meaning,
so the backslash becomes a literal part of the path passed to prettier.

## Change

- **`packages/nx/src/command-line/format/format.ts`** — guard the
`$`-escape behind a `process.platform !== 'win32'` check:

```typescript
// Before (always escaped):
(p) => `"${p.replace(/\$/g, '\\\$')}"`

// After (only escape on non-Windows):
const escaped = process.platform !== 'win32' ? p.replace(/\$/g, '\\\$') : p;
return `"${escaped}"`;
```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: AgentEnder <6933928+AgentEnder@users.noreply.github.com>
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
(cherry picked from commit 8ef4705d27)
2026-05-19 17:50:33 -04:00
Artur 7c1318faba fix(core): handle object form of bin field in getPrettierPath (#35680)
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com>
(cherry picked from commit a1749798eb)
2026-05-19 17:50:32 -04:00
Jason Jean a3acfd6bab fix(core): treat undefined task parallelism as parallel when scheduling (#35736)
## Current Behavior

When deciding whether a task can be scheduled, `TasksSchedule` checked
`task.parallelism === true` in two places:

- `canBeScheduled` — gating a task against already-running parallel
tasks
- `canBatchTaskBeScheduled` — gating a task for batch scheduling

A task whose `parallelism` is `undefined` failed both checks and was
blocked, even though `undefined` is meant to mean "parallel". This was
also inconsistent with the running-tasks check, which uses `parallelism
=== false` — treating `undefined` as parallel-capable.

## Expected Behavior

A task with `parallelism === undefined` is treated as parallel in both
the regular and batch scheduling checks. Both now use `parallelism !==
false`, matching the convention used elsewhere and the documented
default.

## Related Issue(s)

N/A

(cherry picked from commit 05c55890f3)
2026-05-19 17:50:31 -04:00
Benjamin Cabanes 81bdb17992 docs(nx-dev): refresh docs header CTA and star widget styling (#35702)
Swap the visual hierarchy of the docs header: the call-to-action becomes
the primary (black) button labeled "Get started", and the GitHub "Star
us" widget moves to a secondary outlined/muted treatment. Link target,
target/rel, and the existing GTM event remain unchanged.

Fixes DOC-507

(cherry picked from commit 2dd98115d0)
2026-05-19 17:50:30 -04:00
Craigory Coppola c574207825 cleanup(core): drop stale @nrwl dedup TODO in nx report (#35730)
## Current Behavior

`packages/nx/src/command-line/report/report.ts` contains a stale `TODO
(v20)` comment referencing a workaround for hiding `@nrwl/*` packages
when a matching `@nx/*` package was found. We're on v23 — two majors
past the deadline.

The actual workaround was a `packageChangeMap` plus dedup logic inside
`findInstalledPackagesWeCareAbout` that compared `@nrwl/*` and `@nx/*`
versions and suppressed the `@nrwl/*` entry when both matched. That
logic was already removed in #30840 (May 2025). Only the orphaned
comment remained.

## Expected Behavior

The stale comment is removed. No runtime behavior change —
`findInstalledPackagesWeCareAbout` already lists every installed package
from `packagesWeCareAbout` without any `@nrwl/@nx` dedup logic.

Verified `nx report` output is unchanged for workspaces with only
`@nx/*` packages and for workspaces that still have legacy `@nrwl/*`
packages installed (e.g. `@nrwl/nx-cloud` from
`nx-migrations.packageGroup`, or `@nrwl/schematics` from the manual
entry on line 53 — both continue to be reported as before).

## Related Issue(s)

Ref: NXC-4300

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
(cherry picked from commit adfe96a40a)
2026-05-19 17:50:30 -04:00
Leosvel Pérez Espinosa 0d002eceae fix(core): resolve local plugin subpath imports from source (#35631)
Local Nx plugins imported via subpath exports (e.g.
`@scope/pkg/cypress`) cannot be resolved from source. The loader
collapses every subpath onto the project's build-target `main`, ignoring
the imported subpath, so plugins either ship as committed `dist`
artifacts (workaround) or fail to load with an empty entry point.

Additionally, even for non-subpath imports of workspace plugins
symlinked into `node_modules`, Node's resolver picks the built `dist`
artifact via the `default` exports condition (Node doesn't honor
TypeScript `customConditions`), so source edits are ignored until the
plugin is rebuilt.

Subpath imports of local plugins resolve to source via `package.json`
`exports` using the workspace-defined `customConditions`.
Workspace-local plugins (subpath or bare) prefer their source-pointing
condition over the built `dist` so source edits take effect without
rebuilding.

- New `getRootTsConfigCustomConditions` helper reads
`compilerOptions.customConditions` from the root tsconfig via the
TypeScript API, honoring `extends` chains.
- `resolveSubpathFromExports` invokes `resolve.exports` with the
workspace's conditions plus `development` as a backward-compat fallback
for pre-21.5 setups. A second `resolve.exports` call with `conditions:
[]` detects when only `default`/`import`/`require` matched, signaling
"no source-pointing condition" so the loader hard-fails with guidance
instead of silently loading dist.
- Hoists local-source resolution ahead of `require.resolve` in
`getPluginPathAndName` so symlinked workspace packages prefer source
over dist. Default plugins (absolute paths) and external installed
packages skip the local branch to avoid recursing through
`retrieveProjectConfigurationsWithoutPluginInference`.
- Updates `schema-utils` (executors/generators) to use the same
workspace conditions when resolving implementations from source.
- Adds an e2e test covering subpath plugin loading via the exports
condition.

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: leosvelperez <leosvelperez@users.noreply.github.com>
(cherry picked from commit 76d7728d1d)
2026-05-19 17:50:29 -04:00
Leosvel Pérez Espinosa a6f8744726 chore(repo): replace glob with tinyglobby (#35715)
## Current Behavior

Workspace depends on `glob@7.1.4` (EOL) for three usages: two
`tools/workspace-plugin` conformance rules and one release-pipeline
script.

## Expected Behavior

`glob` is dropped from the workspace; the three usages move to
`tinyglobby` (already a catalog dep used in 11+ places, and the
prevailing replacement enforced by `no-restricted-imports` rules
elsewhere in the repo).

## Implementation Details

-
`tools/workspace-plugin/src/conformance-rules/codeblock-language/index.ts`:
removed dead `globSync`/`join`/`relative` imports (rule reads
`fileMapCache.fileMap.projectFileMap` directly).
-
`tools/workspace-plugin/src/conformance-rules/relative-image-imports/index.ts`:
swapped `glob.sync(join(workspaceRoot, 'astro-docs/**/*.mdoc'), { ignore
})` for the idiomatic tinyglobby form (`cwd` + relative pattern +
`absolute: true`). Verified the new call returns the identical set of
501 absolute paths.
- `scripts/cleanup-tsconfig-files.js`: `glob.sync(p)` → `globSync(p)`.
- Dropped `glob` from root and `tools/workspace-plugin` `package.json`;
added `"tinyglobby": "catalog:"` to the latter.
- Lockfile diff is surgical: -6 / +3 lines (two `glob` importer entries
gone, one `tinyglobby` entry added).

### Validation

- `pnpm install` clean.
- `nx run-many -t lint,test,build -p workspace-plugin` — green (38/38
tests).
- `nx conformance` — all 7 rules pass; `image-import-paths` (the
migrated one) scans astro-docs `.mdoc` tree end-to-end.
- Side-by-side comparison: `glob@7.1.4` and `tinyglobby` return the
**identical 501-file set** for the migrated pattern.
- `scripts/cleanup-tsconfig-files.js` smoke run — removes the expected
files.

(cherry picked from commit e6e8fce87c)
2026-05-19 17:50:27 -04:00
Craigory Coppola b58ccd837e fix(core): preserve input order in createNodes plugin results (#35595)
Two structural sources of non-determinism existed inside Nx's
`createNodes`/`createNodesV2` pipeline. Both are invisible to plugin
authors but produce different project graphs across runs on the same
workspace.

`createNodesFromFiles` (in
`packages/nx/src/project-graph/plugins/utils.ts`) — the helper that ~20
first-party plugins use to fan out per-config-file work — runs callbacks
in parallel via `Promise.all(configFiles.map(async (file, idx) => { ...
results.push([file, value]) }))`. Tuples are pushed into the shared
`results` array in the **resolution order of the callbacks**, not the
input order of `configFiles`.

The matched file list arriving at the plugin is sorted (the Rust
`glob_files` impl `par_sort`s, and Rayon's
`par_iter().filter().collect()` preserves order). The downstream merge
(`mergeCreateNodesResultsFromSinglePlugin` → `for (const result of
pluginResults)` → `for (const root in projectNodes)`) walks results in
array / insertion order. So if any plugin returns multiple contributions
for the same project root, the *only* place the order can scramble is
the helper's `Promise.all + .push` race.

A second instance of the same pattern lives in `@nx/eslint`'s
`internalCreateNodesV2`, which mutates `projects[projectRoot] = project`
from inside a `Promise.all`. The `projects` object's key-insertion order
then tracks `eslint.isPathIgnored` / `getProjectUsingESLintConfig`
resolution races and propagates the same non-determinism.

For atomizing plugins, the order of dynamically-generated target names
(`<ciTargetName>--<relativePath>`) leaks into the project graph through
`targets[name]` insertion order, `dependsOn[]`, and
`targetGroups[group][]`. The order is deterministic when the file list
comes from Nx's Rust glob (sorted), but **not** when it comes from a
non-Nx file-discovery layer:

- **`@nx/jest`** (runtime branch, `disableJestRuntime: false`) — uses
`jest.SearchSource.getTestPaths()` which walks via jest-haste-map's
parallel workers; ordering not guaranteed. The `disableJestRuntime:
true` branch was already fine (sorted glob).
- **`@nx/vitest`** and **`@nx/vite`** — both have a
`getTestPathsRelativeToProjectRoot` helper that returns
`vitest.getRelevantTestSpecifications()` directly. Vitest uses
tinyglobby internally, which doesn't sort.

`@nx/cypress`, `@nx/playwright`, `@nx/gradle` (v1 + v2), and
`@nx/eslint`'s atomizer paths all source from sorted Rust glob and
iterate synchronously — they're fine.

Project graph construction is deterministic across runs given a
deterministic input file list. Specifically:

- `createNodesFromFiles` returns `results` and `errors` arrays in
`configFiles` input order, regardless of which callback resolves first.
- `@nx/eslint`'s `projects` map keys are inserted in input order of
`projectRootsByEslintRoots.get(configDir)`.
- Atomized target names from `@nx/jest`, `@nx/vitest`, and `@nx/vite`
are inserted in lexicographic order of relative path.

- **`packages/nx/src/project-graph/plugins/utils.ts`** — settle each
callback into a discriminated tuple `{ kind: 'value' | 'empty' |
'error', ... }` from inside `Promise.all`. `await
Promise.all(arr.map(...))` returns an array indexed by input position,
so a synchronous post-pass over that array bins values and errors in
input order. No change to public API or error semantics.
- **`packages/eslint/src/plugins/plugin.ts`** — each parallel branch
*returns* its contribution (or `null`) instead of mutating the shared
`projects` object. A synchronous post-pass over `orderedProjectRoots`
`Object.assign`s contributions into `projects` in input order.
- **`packages/jest/src/plugins/plugin.ts`** — sort `specs.tests.map(({
path }) => path)` before constructing the `Set` of test paths.
- **`packages/vitest/src/plugins/plugin.ts`** +
**`packages/vite/src/plugins/plugin.ts`** — `.sort()` the relative paths
returned by `getTestPathsRelativeToProjectRoot` before they reach the
atomizer loop.

- `@nx/jest` (`disableJestRuntime: true`) — sources from
`globWithWorkspaceContext` (sorted by Rust glob).
- `@nx/cypress`, `@nx/playwright` — sources from
`globWithWorkspaceContext` / `getFilesInDirectoryUsingContext` (both
deterministic; `get_child_files` is a sequential
`into_iter().filter().collect()` over a sorted file list) and iterates
with `for (const ... of ...)`.
- `@nx/gradle` v1 — `splitConfigFiles` + `forEach` over already-sorted
glob output.
- `@nx/gradle` v2 — synchronous `for...of` over `Array.from(new
Set([...]))`; `Set` iterates in insertion order, source arrays
deterministic.
- `@nx/maven`, `@nx/nuxt`, `@nx/remix`, `@nx/rollup`, `@nx/detox`,
`@nx/dotnet`, `@nx/react/router-plugin`, and the nx-core `project-json`
/ `package-json` / `js` plugins — no parallel-write-to-shared-object
patterns.

Two new regression tests in
`packages/nx/src/project-graph/plugins/utils.spec.ts` force later inputs
to resolve faster (e.g. `file1` waits 30ms, `file2` resolves
immediately) and assert that `results` and `errors` both follow input
order. Existing snapshot tests continue to pass — they had been passing
only coincidentally because trivial sync paths happened to push in input
order; now the guarantee is structural.

For the atomizer sort fixes, existing snapshot tests pass (jest 54/54,
vitest 7/7, vite 22/22, cypress, playwright, eslint). The fixes are pure
ordering — no observable change when test discovery happens to already
be sorted.

<!-- No tracked issue — this came out of an audit of createNodes for
non-determinism. -->

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
(cherry picked from commit e736a6391c)
2026-05-19 14:27:53 -04:00
Jason Jean 7ac33301a0 fix(gradle): pin generated e2e project toolchain to installed JDK (#35703)
The Gradle e2e tests (`e2e/gradle/src/`) generate a project with `gradle
init`. For a `kotlin-application`, `gradle init` bakes a fixed Java
toolchain version (e.g. 21) into the generated build files.

CI machines provision Java via mise (currently Java 24). When the
generated project's pinned toolchain version differs from the installed
JDK, Gradle cannot find a matching local JDK and falls back to
**auto-provisioning** it through the foojay disco API. The e2e tests
then fail whenever foojay is slow or unavailable — e.g. the recent
`Could not HEAD 'https://api.foojay.io/...' Received status code 400` /
`pkg cache is currently be restored` failures, where only the kotlin
tests failed (the groovy template doesn't pin a toolchain).

The generated e2e project pins its Java toolchain to the JDK that is
actually installed on the machine, so Gradle detects it locally and
never needs to download one.

`createGradleProject` now reads `java -version`, derives the installed
major version, and passes it to `gradle init` via `--java-version`. This
removes the dependency on the foojay API from the Gradle e2e suite.

N/A — CI flakiness fix.

(cherry picked from commit b15a359624)
2026-05-19 14:24:57 -04:00
Leosvel Pérez Espinosa 68ee897c93 chore(repo): add multi-version-compliance skill (#35701)
## Current Behavior

The Nx repo has no Claude Code skill for multi-version support
compliance work on first-party plugins. Each fix or audit reapplies the
canonical shape from scratch by reading prior PRs, which is slow and
inconsistent.

## Expected Behavior

A reusable Claude Code skill lives under
`.claude/skills/multi-version-compliance/` and provides:

- **Fix mode**: drive a per-plugin compliance fix from a tracked task
(primary), with discovery fallback when no task exists.
- **Review mode**: code-level review of a compliance PR against the
canonical shape, with scope-drift check vs. the corresponding tracked
task.

## Implementation Details

Files added under `.claude/skills/multi-version-compliance/`:

- `SKILL.md` — entry points, mode workflows, critical rules, findings
doc template.
- `references/canonical-shape.md` — how a compliant plugin looks, plus
the code-level verification rubric used in review mode.
- `references/anti-patterns.md` — 18 numbered anti-patterns with
file:line citations.
- `references/gotchas.md` — edge cases (dist-tags, pnpm catalogs,
ecosystem lockstep, effective floor, cross-plugin coordination).
- `references/examples.md` — reference files, commits, and PRs to grep.

Reference PRs the skill models on: #35587 (`@nx/angular`), #35642
(`@nx/playwright`), #35670 (`@nx/cypress`), #35671 (`@nx/vitest`).

(cherry picked from commit e37ab421c0)
2026-05-19 14:22:53 -04:00
Jason Jean 29aac903b7 chore(repo): update nx to 23.0.0-beta.12 (#35689)
Updating Nx from 23.0.0-beta.11 to 23.0.0-beta.12

(cherry picked from commit dae59384d4)
2026-05-19 14:20:16 -04:00
Leosvel Pérez Espinosa ccb1f87fc7 fix(misc): stop inferring projects: 'self' in dependsOn entries (#35686)
## Current Behavior

The `@nx/cypress`, `@nx/jest`, `@nx/playwright` and `@nx/gradle` plugins
infer atomized CI `dependsOn` entries that include `projects: 'self'`.
That value is the default for `projects` and is no longer supported as
an explicit value.

## Expected Behavior

The plugins infer the same atomized CI `dependsOn` entries without
setting `projects: 'self'`.

(cherry picked from commit 70e2752597)
2026-05-19 14:19:38 -04:00
polygraph-app[bot] 0f6f117edc fix(testing): correct yargs-parser import in getJestProjectsAsync (#35672)
## Current Behavior

`getJestProjectsAsync()` from `@nx/jest` throws `TypeError: yargs is not
a function` whenever a project has an inferred `nx:run-commands` test
target whose command runs `jest`. This affects workspaces using
`@nx/jest/plugin`, which is the default.

## Expected Behavior

`getJestProjectsAsync()` returns the list of jest projects without
error.

## Related Issue(s)

Fixes #35654

## Implementation Details

`packages/jest/src/utils/config/get-jest-projects.ts` imported
`yargs-parser` as a namespace (`import * as yargs from 'yargs-parser'`).
With `esModuleInterop: true`, that compiles to
`tslib.__importStar(require("yargs-parser"))`, which wraps the
CJS-callable export in a non-callable namespace object — so the
subsequent `yargs(match, ...)` call throws.

Switched to a default import (`import yargs from 'yargs-parser'`),
matching the pattern used by every other file in the repo that consumes
`yargs-parser`. Under `esModuleInterop: true` this compiles to
`__importDefault(require("yargs-parser")).default`, which is the
callable parser.

Verified by building `@nx/jest` on master vs. this branch and invoking
`getJestProjectsAsync()` against a synthetic graph with an inferred
`nx:run-commands` jest target — master throws the reported `TypeError`,
this branch returns the project list.

<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/gh-35654-5acce614)
<!-- polygraph-session-end -->

Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
(cherry picked from commit 0a445cac3d)
2026-05-19 14:19:04 -04:00
Jason Jean a23b7be87f fix(linter): only rewrite workspace-package peer deps to workspace:* (#35423)
## Current Behavior

The `@nx/eslint-plugin/dependency-checks` rule's
`peerDepsVersionStrategy: 'workspace'` option unconditionally rewrote
**every** peer dependency to `workspace:*`, including external npm
packages like `react`, `axios`, etc. Running `pnpm install` (or any
equivalent) on the resulting `package.json` failed with
`ERR_PNPM_WORKSPACE_PKG_NOT_FOUND` because those packages do not exist
in the workspace. `eslint --fix` was therefore actively breaking working
workspaces that enabled the strategy.

Two fix branches in
`packages/eslint-plugin/src/rules/dependency-checks.ts` had the bug:

- The `missingDependencies` fix (around L299), which inserted new peer
entries at `workspace:*`.
- The version-mismatch fix (around L370-382), which rewrote any
non-`workspace:*` range to `workspace:*`.

## Expected Behavior

`peerDepsVersionStrategy: 'workspace'` should only produce `workspace:*`
ranges for peer dependencies that are **workspace-published packages**.
External npm packages must retain the normal installed-version logic so
the resulting `package.json` is installable.

This PR:

- Builds a `workspacePackageNames` `Set<string>` once from
`projectGraph.nodes[*].data.metadata.js.packageName`.
- Gates both `peerDepsVersionStrategy === 'workspace'` branches on
`workspacePackageNames.has(packageName)`. Workspace packages continue to
get `workspace:*`; external packages fall through to the existing
installed-version / catalog / root-package-json resolution.

Three existing tests in `dependency-checks.spec.ts` that encoded the
buggy behavior (asserting `workspace:*` for external packages) were
updated to reflect the correct behavior. Two new focused tests were
added to pin down each case explicitly (workspace package →
`workspace:*`; external package → installed range preserved).

## Related Issue(s)

Fixes #35318

Follow-up to #33417, which introduced `peerDepsVersionStrategy`.

(cherry picked from commit f13b8fc260)
2026-05-19 14:18:26 -04:00
Jason Jean e9e447b14e chore(core): remove unused replaceNrwlPackageWithNxPackage devkit utility (#35679)
## Current Behavior

`packages/devkit/src/utils/replace-package.ts` exports
`replaceNrwlPackageWithNxPackage`, a helper introduced to support
migrations that renamed `@nrwl/*` packages to `@nx/*`. Those migrations
ran years ago. The function is no longer imported anywhere in the
codebase — only the file's own spec references it, and it isn't
re-exported from `packages/devkit/index.ts` or
`packages/devkit/internal.ts`.

## Expected Behavior

The dead helper and its spec are deleted. Less surface area to maintain.

## Related Issue(s)

None.

(cherry picked from commit 36132f806b)
2026-05-19 14:18:07 -04:00
Jason Jean 087009e0f4 chore(repo): update nx to 23.0.0-beta.11 (#35681)
Updating Nx from 23.0.0-beta.10 to 23.0.0-beta.11

(cherry picked from commit 772e8be165)
2026-05-19 14:17:21 -04:00
Louie Weng 85f7298a2c fix(gradle): add transitive:true to all tasks (#35677)
<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

Currently our input globs are set without transitive: true, this means
during hashing the input only walks one hop by default — it looks at the
direct dependents, not the chain.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

Add `transitive: true` to all dependent task output files.

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #NXC-4461

(cherry picked from commit 3d62867ea5)
2026-05-19 14:15:37 -04:00
Johan Vrolix 11e96c39c9 chore(core): nx plugin submission @anarchitects/nx-typeorm (#35668)
<!--
_[Please make sure you have read the submission guidelines before
posting an
PR](https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#submit-pr)_

# Community Plugin Submission

Thanks for submitting your Nx Plugin to our community plugins list. Make
sure to follow these steps to ensure that your PR is approved in a
timely manner.

## Plugin Requirements

Before you submit your plugin to be listed in our registry, it needs to
meet the following requirements:
- Run some kind of automated e2e tests in your repository
- Include `@nx/devkit` as a `dependency` in the plugin's `package.json`
- List a `repository.url` in the plugin's `package.json`

i.e.

```
{
  "repository": {
    "type": "git",
    "url": "https://github.com/nrwl/nx.git",
    "directory": "packages/web"
  }
}
```

Note: We reserve the right to remove unmaintained plugins from the
registry. If the plugins become maintained again, they can be
resubmitted to the registry.

## Steps to Submit Your Plugin
- Use the following commit message template: `chore(core): nx plugin
submission [PLUGIN_NAME]`
- Update the `astro-docs/src/content/approved-community-plugins.json`
file with a new entry for your plugin that includes `name`, `url`,
`description`:

Example:

```json
// astro-docs/src/content/approved-community-plugins.json

[{
    "name": "@community/plugin",
    "url": "https://github.com/community/plugin",
    "description": "This plugin provides the following capabilities."
}]
```

Once merged, your plugin will be available when running the `nx list`
command, and will also be available in the Plugin Registry on
[nx.dev](https://nx.dev/docs/plugin-registry)
-->

# Community Plugin Submission

## @anarchitects/nx-typeorm

<!--
Describe what your plugin is and what is its goal or issues it
addresses. If you don't provide a description, we will not merge your
PR.
Is it focused on a technology, tooling or behaviour? Does the plugin
provide generators, executors or graph support?
Do you know who is already using the plugin? Mention who is the author
of the plugin.
-->
Nx plugin for TypeORM integration in Nx backend applications and
libraries. It provides:

- nx add / init setup for minimal TypeORM dependencies.
- bootstrap scaffolding for app runtime datasource wiring and library
infrastructure-persistence templates.
- name-first scaffold generators for TypeORM file creation workflows.
- inferred database targets via createNodesV2.
- thin executors that wrap TypeORM CLI workflows.

---------

Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
(cherry picked from commit fe39d0ae8e)
2026-05-19 14:14:39 -04:00
Jason Jean 20c155f0ff chore(repo): update nx to 23.0.0-beta.10 (#35667)
Updating Nx from 23.0.0-beta.9 to 23.0.0-beta.10

(cherry picked from commit d520730c89)
2026-05-19 14:14:21 -04:00
Leosvel Pérez Espinosa 2930dcac44 fix(linter): improve convert-to-flat-config output fidelity (#35330)
## Current Behavior

Running `@nx/eslint:convert-to-flat-config` against a workspace with
legacy `.eslintrc` configs produces flat configs that don't faithfully
reflect the source, and leaves stale references to the deleted files
behind:

- Every converted config — root and leaves — gets an unconditional `{
ignores: ['**/dist', '**/out-tsc'] }` block prepended, even when the
legacy config never ignored those paths.
- Leaves whose only compat-looking field was a custom `parser` (e.g.
`jsonc-eslint-parser` for `package.json`) get a full `@eslint/eslintrc`
FlatCompat scaffold — `FlatCompat` import, `dirname`, `fileURLToPath`,
`js`, the `const compat = new FlatCompat({...})` block — that's never
referenced.
- Rule option values that embedded legacy filenames (most notably
`@nx/dependency-checks`'s `ignoredFiles`) keep pointing at
`.eslintrc.json` / `.eslintrc.base.json` / `.eslintignore` after those
files are deleted.
- `nx.json` gets the new `eslint.config.<fmt>` entry added to the lint
target and `production` named input, but legacy `.eslintrc.json` /
`.eslintignore` entries stay in place. Other `targetDefaults` inputs and
`namedInputs` are never rewritten.
- `project.json` files aren't touched at all — every `targets[*].inputs`
or `namedInputs[*]` that referenced the deleted files stays stale.
- Configs with `extends: '../../.eslintrc'` (extensionless — ESLint's
JSON-by-convention form) aren't supported: the source file isn't
converted, and if a leaf extends one that was converted elsewhere the
leaf ends up with `...compat.extends('../../.eslintrc')` pointing at
nothing.
- Legacy `ignorePatterns` entries that started with `!` were being
dropped wholesale, destroying real un-ignores like `['dist/**',
'!dist/keep.js']` that flat config still honors.
- `files` / `excludedFiles` arrays with source-side duplicates (e.g.
`package.json`, `./generators.json`, `./executors.json` repeated in the
same override) are emitted with the duplicates intact.

## Expected Behavior

Conversion preserves the semantics and intent of the legacy config,
rewrites everything that referred to the deleted files, and drops noise
that serves no purpose in flat config:

- The implicit `**/dist` / `**/out-tsc` ignore block is no longer added.
If the legacy config didn't ignore those paths, the converted one
doesn't either — migration stays faithful to the source. (`lint-project`
still adds them for fresh scaffolding, where there's no source intent to
preserve.)
- Parser-only overrides emit a clean flat entry with a hoisted static
parser import and no unused FlatCompat scaffold. Leaf configs shed the
dead boilerplate.
- Rule option values that embed `.eslintrc[.base].json` or
`.eslintignore` are rewritten to the flat-config equivalent.
Accidentally collapsed duplicates inside string arrays are deduped.
- `nx.json` is swept generically — every `targetDefaults[*].inputs` and
`namedInputs[*]` gets legacy filenames rewritten, with dedup so the
rewrite doesn't collide with freshly-added entries. `{ fileset }` shapes
are handled; non-path shapes (`runtime`, `env`, `externalDependencies`,
`dependentTasksOutputFiles`, named-input refs) are left untouched.
- Every project's `project.json` receives the same sweep across
`targets[*].inputs` and `namedInputs[*]`.
- Extensionless `.eslintrc` is now a convertible source, and `extends`
paths that point at `../../.eslintrc` (or `.eslintrc.base`) are
rewritten to the generated base config and imported as `baseConfig`.
- Real negated `ignorePatterns` like `!dist/keep.js` survive the
conversion; only the legacy `**/*` / `!**/*` / `node_modules` catch-alls
are dropped.
- `files` / `excludedFiles` arrays are deduped after glob mapping, so
source-side duplicates and glob-normalization collisions collapse.

(cherry picked from commit e84989fec9)
2026-05-19 14:12:38 -04:00
Craigory Coppola 175b6cbb8b fix(core): warn before installing unknown npm packages as preset (#35644)
## Current Behavior

`create-nx-workspace --preset=<name>` silently installs any npm package
matching the name when the preset is not a built-in Nx preset. A user
following a tutorial that suggested `--preset=core` ended up installing
[`core`](https://www.npmjs.com/package/core) — an unrelated ancient
package — without any warning. This is a supply-chain risk: a typo or
malicious preset name could execute untrusted code with no user-visible
signal.

The path through the code:

1. `create-workspace.ts` calls
`getPackageNameFromThirdPartyPreset(preset)`.
2. If the preset isn't in the built-in `Preset` enum, the helper returns
the package name as long as `validateNpmPackage` accepts it.
3. `createPreset` then installs and runs the resolved package — no
confirmation, no warning.

## Expected Behavior

Before installing a third-party preset npm package, surface the npm
package name and ask the user to confirm.

- **Interactive (TTY) mode**: prompt with `enquirer.autocomplete`,
defaulting to **No**, so a reflex `Enter` is safe.
- **`--interactive=false`, CI, or AI-agent contexts**: skip the prompt
(we can't read a TTY) but always emit an `output.warn` describing the
package about to be installed. Automated workflows like
`--preset=@nx-go/nx-go --no-interactive` keep working, but the warning
still appears in logs.

The confirmation runs before sandbox creation — declining doesn't waste
work.

### Out of scope

The original report also suggests validating that the package is
genuinely an Nx plugin. That requires a registry round-trip and a
definition of "is an Nx plugin" (peerDeps on `nx`? keyword? presence of
a preset generator?). The confirmation step alone closes the
silent-install vector; the deeper validation is left as a follow-up.

## Related Issue(s)

Fixes
[NXC-3331](https://linear.app/nxdev/issue/NXC-3331/warn-users-before-installing-unknown-npm-packages-with-preset).

(cherry picked from commit 6d1da6250d)
2026-05-19 14:12:29 -04:00
Jason Jean c01974f27b fix(angular-rspack): exclude eslint config from tailwind v4 source scan (#35663)
## Current Behavior

In `examples-angular-rspack-csr-tailwind:build`, Tailwind v4's automatic
source-detection scanner (`@tailwindcss/oxide`) walks the project root
and reads every file with a known extension (`.html`, `.js`, `.mjs`,
`.cjs`, `.ts`, `.tsx`, `.vue`, …) honoring `.gitignore`.

`eslint.config.mjs` sits at the project root with a scanned extension,
so Tailwind reads it looking for utility class names. The Nx build
target excludes `eslint.config.@(js|cjs|mjs|ts|cts|mts)` from its
inputs, so the read shows up as an undeclared-read sandbox violation:

```
examples/angular-rspack/csr-tailwind/eslint.config.mjs
```

Other root-level files Tailwind also scans (e.g. `rspack.config.js`)
don't trigger violations because they are declared as inputs by their
respective plugins. `eslint.config.mjs` is unique in being both scanned
and excluded.

## Expected Behavior

Tailwind should not scan eslint config. Adding `@source not
"../eslint.config.mjs";` to `src/styles.css` tells Tailwind to skip that
file during its scan. No more sandbox violation; the eslint config
exclusion in the build inputs stays correct (eslint config has no effect
on build output).

## Related Issue(s)

N/A — sandbox-report finding, not a tracked issue.

(cherry picked from commit aa091aefcd)
2026-05-19 14:12:13 -04:00
Jack Hsu 494f4a733b docs(misc): add fix sandbox violations guide (#35693)
This PR adds a guide for fixing sandbox violations. It will be linked
from the UI.

Preview:
https://deploy-preview-35693--nx-docs.netlify.app/docs/guides/nx-cloud/fix-sandbox-violations

## Related Issue(s)
Q-443

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2026-05-15 11:17:11 -04:00
Steven Nance be15c3ef6d docs(nx-dev): link to @nx/owners plugin from code ownership concept page (#35698)
## Current Behavior

The [Code Ownership concept
page](https://nx.dev/docs/concepts/decisions/code-ownership#defining-code-ownership)
only describes the raw GitHub `CODEOWNERS` file. It never points readers
to the `@nx/owners` plugin, even though that plugin's whole purpose is
project-based code ownership.

## Expected Behavior

The "Defining code ownership" section now includes a tip aside linking
to the [`@nx/owners` plugin overview](/docs/reference/owners/overview),
explaining that the plugin lets you define ownership by project (using
`nx run-many` matcher syntax) and compiles it into a valid `CODEOWNERS`
file for GitHub, Bitbucket, or GitLab.

## Related Issue(s)

N/A -- follow-up to an internal discussion about cross-linking the
codeowners plugin from the general ownership page.
2026-05-15 10:21:58 -04:00
polygraph-app[bot] c7fd2fa576 docs(nx-dev): restore powerpack license as noindex reference page (#35690)
## Current Behavior

`/powerpack/license` 301s to `/docs/enterprise` via the `/powerpack/*`
wildcard in `_redirects`. Breaks the link from `@nx/key` (npmjs page).

## Expected Behavior

Add legacy EULA at `astro-docs` `/docs/reference/powerpack-license` with
`noindex,nofollow` head meta and a sidebar entry under Reference. Add
specific 301 from `/powerpack/license` to the new docs URL before the
existing `/powerpack/*` wildcard.


- Old page: https://20.nx.dev/powerpack/license
- New page:
https://deploy-preview-35690--nx-docs.netlify.app/docs/reference/powerpack-license
## Related Issue(s)

Fixes DOC-505

<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/doc-505-powerpack-license-09d8ce8f)
<!-- polygraph-session-end -->

Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
2026-05-15 08:47:53 -04:00
Altan Stalker 8e77b9c07b docs(nx-cloud): simplify resource class specifications in credits pricing (#35685)
Updates the credits pricing reference to show only vCPU counts instead
of full RAM specs. Removes the intermediate resource classes (Medium+,
Large+, Extra large+) to simplify the table. Added a note explaining
that memory is available in approximate 1:4 ratio per vCPU core.

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: StalkAltan <StalkAltan@users.noreply.github.com>
2026-05-15 08:47:53 -04:00
Jack Hsu 3852a1f82d docs(misc): add nx-cloud get sandbox-reports to CLI reference (#35684)
Add Cloud CLI reference section for `nx-cloud get sandbox-reports` with
usage, options, etc.

Fixes DOC-504

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
2026-05-15 08:47:53 -04:00
Jason Jean de9d8a5761 docs(core): explain devkit dependency type and nx exclusion for plugins (#35674)
## Current Behavior

The community-plugin submission criteria in
`extending-nx/publish-plugin.mdoc` state that `@nx/devkit` must be
listed as a `dependency`, but give no rationale. The guide is also
silent on whether to list the `nx` package itself — leading some
submitters to add it as a `peerDependency`, which the first-party
plugins never do.

## Expected Behavior

The publish-plugin guide:

- Explicitly notes that `@nx/devkit` should be a `dependency`, **not** a
`peerDependency`.
- States that the `nx` package itself should **not** be listed as a
dependency or peer dependency.
- Includes a short aside explaining why: `@nx/devkit` has no singleton
state (unlike React/ESLint/Babel ecosystems where peer deps are the
norm), and `nx` is always provided by the user's workspace.

This brings the documented criteria in line with what the first-party
`@nx/*` plugins already do.

## Related Issue(s)

N/A — drive-by docs improvement noticed while reviewing a
community-plugin submission.
2026-05-15 08:47:53 -04:00
FrozenPandaz 4e9ea1bc3f chore(core): refresh stale TUI snapshots on 22.7.x
PR #35640 introduced these tests with snapshots that assumed PR
#35540's Task::new helper, but #35540 is master-only. On 22.7.x the
test setup still produces task1/task2/task3 ids (not app1:build etc.)
and the selection arrow placement differs slightly. Regenerated the
three affected snapshots to match current 22.7.x behavior; tests now
pass cleanly.
2026-05-14 01:55:16 -04:00
FrozenPandaz 472e7d0992 fix(repo): expand "..." spread token in graph typecheck inputs
The spread token's runtime support (#34285) is master-only, so
published nx@23.0.0-beta.9 rejects "..." with "invalid fileset" when
the nx-build e2e clones this branch and runs run-many -t build.
Inline the inferred @nx/js/typescript inputs for the four typecheck
tasks affected by #35458 to unblock the build.
2026-05-14 01:52:45 -04:00
FrozenPandaz d02dc54141 fix(repo): unblock 22.7.x cargo tests and nx-build e2e
- nix crate: add "fs" feature so walker.rs tests can use sys::stat::Mode and unistd::mkfifo
- tui/app.rs: replace master-only Task::new builder with direct struct literal
- packages/nx/project.json: expand the "..." spread token in test inputs into the explicit list @nx/jest would have inferred; the spread token's runtime support (#34285) is only on master, so published nx@23 here rejects it during config parse and breaks the cloned-repo build in nx-build.test.ts
2026-05-13 17:44:16 -04:00
FrozenPandaz d84f424400 fix(devkit): expand @nx/devkit/internal re-exports for cherry-picked v23 deep-import migration
PR #35541's update-deep-imports migration rewrites `@nx/devkit/src/...` deep imports to `@nx/devkit/internal`. On 22.7.x our `internal.ts` only re-exported four symbols, so post-migration call sites for AggregatedLog/loadConfigFile (and namespace imports) failed at runtime. Restore the previously-working state by reverting the over-aggressive deep-path rewrites and instead expanding `internal.ts` with the symbols the migration's INTERNAL_NAMES set actually targets. Also retarget the two jest.mock and four namespace-spy sites at the concrete config-utils module so the production code path (which still imports directly from there) is actually intercepted. Finally, rewrite the two e2e deep-import string-utils call sites to use `names().className` like master does, since e2e tests resolve against the published @nx/devkit with strict exports.
2026-05-13 14:16:30 -04:00
FrozenPandaz ac8187963d fix(repo): revert deep-import rewrites that targeted v23-only @nx/devkit/internal entry 2026-05-12 20:15:31 -04:00
FrozenPandaz 443dee0b22 fix(repo): clear NX_INVOCATION_ROOT_PID in run-native-target to avoid recursion false-positive 2026-05-12 17:18:46 -04:00
FrozenPandaz cd350c1140 fix(testing): update remaining snapshot guide links missed by migration 2026-05-12 16:46:13 -04:00
Jason Jean 7e4bce9178 feat(testing): add migration for Jest 30 snapshot guide link (#35629)
## Current Behavior

Jest 30 enforces that every `.snap` file's first-line guide link points
at the current snapshot-testing docs URL. Snapshot files generated under
earlier Jest versions begin with the legacy short link:

```
// Jest Snapshot v1, https://goo.gl/fbAQLP
```

When users upgrade to Jest 30 (already supported as of `@nx/jest` 21.3 /
22.3), every project that has pre-existing `.snap` files fails at test
setup with:

```
Outdated guide link: The snapshot guide link at the top of this snapshot is outdated.
Please update all snapshots during this upgrade of Jest.
Expected: https://jestjs.io/docs/snapshot-testing
Received: https://goo.gl/fbAQLP
```

There is currently no `nx migrate` step that rewrites these headers, so
users have to do it by hand or run `jest -u` per project.

## Expected Behavior

`nx migrate` runs a new Jest migration (gated on `jest >= 30.0.0`) that
walks every `**/__snapshots__/*.snap` file in the workspace and rewrites
the first-line guide link from `https://goo.gl/fbAQLP` to
`https://jestjs.io/docs/snapshot-testing`. Snapshot bodies and files
that already use the new URL are left untouched.

This PR also brings the 45 outdated `.snap` files inside this repo onto
the new URL so the workspace's own test suites pass under Jest 30.

### What's in this PR

- New migration `update-snapshot-guide-link` registered in
`packages/jest/migrations.json` at `23.0.0-beta.6` with `requires: {
jest: ">=30.0.0" }`.
- Migration implementation, `.md` doc, and unit tests under
`packages/jest/src/migrations/update-23-0-0/`.
- Bulk rewrite of the 45 `.snap` files in this repo that still carried
the legacy link.

## Related Issue(s)

N/A — surfaced while running unit tests against Jest 30 in this
workspace.
2026-05-12 16:45:34 -04:00
FrozenPandaz 43c0c821ba fix(core): cast perf entries to PerformanceMeasure for detail access 2026-05-12 15:34:13 -04:00
Leosvel Pérez Espinosa f1d849ef88 fix(testing): exclude dist and out-tsc from default jest module path scan (#35619)
## Current Behavior

`jest-haste-map` crawls `<rootDir>` to build a module map and indexes
any file matching `moduleFileExtensions`. When build outputs land inside
the project (e.g. `<projectRoot>/dist` or `<projectRoot>/out-tsc`,
common in TS solution setups but possible in any workspace), those
emitted `.js`/`.d.ts` files get stat'd by haste-map. The `@nx/jest`
preset doesn't exclude them, so consumers either see redundant
filesystem work or — when running under the Nx sandbox — get flagged for
unexpected reads from undeclared task inputs.

## Expected Behavior

The `@nx/jest` preset excludes `<rootDir>/dist/` and
`<rootDir>/out-tsc/` from `modulePathIgnorePatterns` by default. Both
directories are conventional project-local build outputs (Nx's TS
solution setup already treats them as the canonical pair to exclude from
tsconfig). Consumers that need to scan those directories can override
the field in their own jest config.

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-05-12 15:26:16 -04:00
polygraph-app[bot] 0cfc6bcd36 fix(core): allow nx mcp to run outside of an Nx workspace (#35655)
## Current Behavior

Running `nx mcp` from a directory that is not part of an Nx workspace
prints the workspace-not-found banner and exits with code 1, **before**
the `mcp` command's handler is ever invoked.

This breaks MCP clients (e.g. Codex CLI) that spawn `npx nx mcp` over
stdio JSON-RPC. The banner is written to **stdout**, corrupting the
JSON-RPC stream, and the process exits before the MCP `initialize`
handshake completes. Clients surface this as:

> MCP startup failed: handshaking with MCP server failed: connection
closed: initialize response

Reproduction:

```
$ cd /tmp/empty-dir
$ npx nx mcp
 NX   The current directory isn't part of an Nx workspace.
 ...
# exit 1, written to stdout
```

## Expected Behavior

`nx mcp` should be able to run outside of an Nx workspace, the same way
`nx init`, `nx configure-ai-agents`, and `nx graph` already can. The
`mcp` command delegates entirely to `nx-mcp@latest` via the package
manager's `dlx`, and `nx-mcp` already handles non-workspace directories
correctly.

## Root Cause

`packages/nx/bin/nx.ts` contains a hard-coded allow-list of commands
that bypass the `!workspace → handleNoWorkspace()` guard. The list
currently includes `new`, `_migrate`, `init`, `configure-ai-agents`, and
`graph && !workspace`. The `mcp` command was added to the command
registry in `nx-commands.ts` but was never added to this allow-list, so
execution hits the workspace check first and exits before reaching
`mcpHandler`.

## Fix

Add `'mcp'` to the allow-list in `packages/nx/bin/nx.ts`. The handler
uses `workspaceRoot` only as the `cwd` for spawning `nx-mcp@latest`, and
`workspaceRoot` already gracefully falls back to `process.cwd()` when no
workspace is detected.

```diff
   process.argv[2] === '_migrate' ||
   process.argv[2] === 'init' ||
   process.argv[2] === 'configure-ai-agents' ||
+  process.argv[2] === 'mcp' ||
   (process.argv[2] === 'graph' && !workspace)
```

## Related Issue(s)

Fixes: discovered via nx-console / Codex CLI MCP integration — `nx mcp`
configured as an MCP server in a non-workspace cwd fails to start.

<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nx-mcp-error-in-non-repo-path-e0a28-a87e5654)
<!-- polygraph-session-end -->

---------

Co-authored-by: Max Kless <maxk@nrwl.io>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-05-12 15:25:52 -04:00
Adam Keenan c2fac5f055 fix(gradle): support Windows file paths (#35184)
<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
Issues with matching on Windows paths because there are hardcoded
forward slashes `/`

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Works on Windows or otherwise

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #34987 

Want to note that while the Unit tests are all passing, I don't have a
way to test this on a unix machine myself. Also, I'm not super familiar
with gradle plugins, how can I "export" my changes here to try in my own
project that uses the plugin?

Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
2026-05-12 15:25:39 -04:00
Jason Jean df0e414420 chore(core): remove dead TUI selection lifecycle helpers (#35649)
## Current Behavior

`TaskSelectionManager::handle_task_status_change` and its only callee
`handle_in_progress_task_finished` remain in
`packages/nx/src/native/tui/components/task_selection_manager.rs`, even
though #35640 rewired the TUI selection lifecycle and no production code
calls them anymore. The methods are kept alive only by a single unit
test (`test_awaiting_pending_task_state`).

## Expected Behavior

The dead methods are deleted. The orphaned test is removed because its
assertions are already covered by `test_selection_state_transitions`,
which exercises the same `AwaitingNextAllocation` entry/exit transitions
directly through `await_next_allocation()` and `next()`.

Lifecycle today:
- Entering `AwaitingNextAllocation` happens in
`tasks_list.rs::handle_standalone_task_finished` via
`selection_manager.lock().await_next_allocation()`.
- Exiting `AwaitingNextAllocation` happens in the draw-time
`perform_initial_in_progress_selection_if_needed` override.

No external (`#[napi]`) callers existed; the methods were Rust-internal
helpers.

Net: 130 deletions, 0 additions. All 14 `task_selection_manager` tests
and all 70 `tasks_list` tests pass.

## Related Issue(s)

Follow-up cleanup from #35640.

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-05-12 15:24:46 -04:00
Leosvel Pérez Espinosa a0f7a52bcb fix(core): keep TUI task selection on the in-progress section (#35640)
## Current Behavior

In the TUI, when running many tasks (e.g. `nx run-many -t test`), the
selection indicator (`>`) ends up on a pending task instead of an
in-progress one and stays there as tasks come and go. As tasks complete
and new ones start, the highlight bounces around unexpectedly between
renders.

## Expected Behavior

- Before any task starts, the selection anchors on the first selectable
entry as a visual indicator (initial placeholder).
- Once an in-progress entry appears, the selection latches onto the
first in-progress task, replacing the placeholder. If the user navigated
away from the placeholder first, their choice is preserved.
- When the selected in-progress task finishes while pending tasks
remain, the selection enters a waiting state — the highlight stays
hidden until the next allocation puts a task into the in-progress
section, at which point it latches on. It does not drop down to a
pending task between allocations.
- An explicit user selection is never overridden by the render loop.

## Implementation Details

Replaces `Option<SelectionEntry>` in `TaskSelectionManager` with a
four-variant `SelectionState`:

- `Empty` — never selected yet; the render-time fallback (anchor on
first selectable) is only allowed from here.
- `InitialPlaceholder(_)` — auto-anchored before any task starts;
replaced by the first in-progress entry once one appears, unless the
user navigates first.
- `Explicit(_)` — user navigation, programmatic `select_task`,
mode-switch restore, etc. Never auto-overridden.
- `AwaitingNextAllocation` — set by `handle_standalone_task_finished`
when the selected in-progress task finishes with pending tasks
remaining. Never falls back to first-available; the render loop only
exits this state when a new in-progress entry appears.

`perform_initial_in_progress_selection_if_needed` runs the state machine
on every draw under a single lock. `update_entries_track_by_*` preserve
the variant across re-sorts, so the placeholder/explicit identity
survives sort cycles.

Navigation from the unselected states (`Empty`/`AwaitingNextAllocation`)
now anchors on the first selectable entry visible in the current
viewport instead of jumping to entry 0.

Batches: standalone in-progress tasks are preferred over batch groups by
the auto-select (they sort first in `entries`). When only batches are
running, the first batch group wins — regardless of its
expanded/collapsed state.

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-05-12 15:24:00 -04:00
Jason Jean fd783ac7d7 chore(repo): update nx to 23.0.0-beta.9 (#35627)
Updating Nx from 23.0.0-beta.8 to 23.0.0-beta.9
2026-05-12 15:23:16 -04:00
Jason Jean 93813e9ef2 fix(core): support skipped batch tasks end-to-end and fix TUI double logs (#35617)
## Current Behavior

When running a batch executor (Gradle or Maven) under the TUI:

1. **Double logs.** Each task's captured `terminalOutput` is written
into the per-task PTY twice — `printTaskTerminalOutput` lazily creates
the PTY with the terminalOutput, then `appendTaskOutput` immediately
writes the same content again, producing repeated text in the pane (e.g.
`> Task :foo:bar UP-TO-DATE> Task :foo:bar UP-TO-DATE`).
2. **Generic failure on dependents.** When one task in the batch fails,
Gradle aborts and the JVM exits non-zero. Every peer that never got to
run is yielded as `success: false` with a generic `Gradlew batch failed`
(or `Maven batch runner exited with code N`) message — both in the
streaming output and in every dependent task's TUI pane. The actual root
failure is hidden in noise. Same applies for Maven.
3. **Run reported as "Cancelled".** Skipped peers were never marked as
completed in the lifecycle, so the in-progress set stayed non-empty and
the run summary printed `Cancelled` even though the failure was real.
4. **Misleading `> nx run X` headers in run-one.** Tasks that never
actually ran still got a header printed in the streaming output,
suggesting they were executed.

## Expected Behavior

1. The captured `terminalOutput` for a batch task is written to its PTY
exactly once.
2. Peers that never ran because a sibling failed are reported with
`status: 'skipped'` and empty terminal output. The actual failed task
keeps its full error in its own pane (✖). Dependents show as ⏭ in the
task list with empty panes — the user navigates to the failed task to
see why.
3. The run summary correctly shows `Ran target build … N/M failed`
rather than `Cancelled`.
4. Skipped tasks are no longer printed in the run-one streaming summary.

## Implementation

This PR changes the batch executor protocol so the **Kotlin batch
runners are the source of truth** for per-task outcomes — the TS
executors are a thin relay instead of inferring missing results.

### Wire protocol

`TaskResult` gains an optional `status?: 'success' | 'failure' |
'skipped'` field. When set, the orchestrator and lifecycle honor it
instead of inferring from `success: boolean`. Existing batch executors
that don't emit `status` are unaffected — the orchestrator falls back to
the boolean.

`NX_RESULT:{json}` lines now carry `status` alongside `success` for
back-compat with older Nx versions.

### Kotlin runners (Gradle and Maven)

- Track requested vs reported task IDs.
- At end-of-batch, walk the requested set and emit an explicit `skipped`
`NX_RESULT` for any task without a finish event (e.g. a peer compilation
failed and the build aborted before this task could be scheduled).
- For Gradle: applied to both `runBuildLauncher` and `runTestLauncher`.
- For Maven: the work-stealing scheduler already tracked
`TaskState.SKIPPED` for tasks removed due to a failed dependency — it
now emits an `NX_RESULT` for each instead of silently dropping them.

### TS executors (`gradle-batch.impl.ts`, `maven-batch.impl.ts`)

Become thin relays:

- Parse `NX_RESULT`, pass `status` through.
- Only fallback path: if the runner crashes (non-zero exit) before
reporting on every task, backfill the missing ones with a generic
failure so Nx doesn't hang.
- The previous `sawFailure`-based inference is gone — it was fragile (a
regression in this PR's CI revealed that Maven's stderr can interleave
concurrent task output and stash one task's `NX_RESULT` inside another's
`terminalOutput` string, defeating the inference).

### Nx core (orchestrator + TUI lifecycle)

- `runBatch`'s `onTaskResults` honors `result.status ?? (success ?
'success' : 'failure')`.
- The double-logs fix is a one-line ordering swap so `appendTaskOutput`
runs before `printTaskTerminalOutput` — the latter then no-ops in the
TUI because the PTY is already populated.
- TUI summary lifecycle clears skipped tasks from `inProgressTasks` on
`setTaskStatus(Skipped)` (so the run summary doesn't say "Cancelled"),
and skips them in `printRunOneSummary` (so we don't print a misleading
`> nx run X` header for tasks that never ran).

### Tests

- `tui-summary-life-cycle.spec.ts`: snapshot test that a `Skipped` task
does not print a `> nx run` header and the run summary reports as a real
failure (not cancelled).
- `gradle-batch.impl.spec.ts` and `maven-batch.impl.spec.ts`:
spawn-mocked tests verify the executor relays `status: 'skipped'` from
the runner unchanged, and backfills as `failure` only when the runner
crashes before reporting.

## Related Issue(s)

Fixes NXC-4439 (Linear) — Show root Gradle failure for dependent tasks
in TUI.

Fixes NXC-4449 (Linear) — TUI shows duplicate terminal output for batch
tasks.

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
2026-05-12 15:19:24 -04:00
Leosvel Pérez Espinosa f7083f02bd fix(testing): handle absolute cypress screenshotsFolder/videosFolder paths (#35624)
## Current Behavior

When a Cypress config produces absolute paths for `screenshotsFolder` or
`videosFolder` (e.g. via `path.resolve(__dirname, ...)` or
`__dirname`-based composition), the inferred Nx outputs don't match
where Cypress actually writes. Caching is broken because Nx scans the
wrong location, and per-spec atomized targets compound the mismatch.

## Expected Behavior

Inferred outputs match where Cypress writes, regardless of whether the
user's config used relative or absolute paths. The atomized `--config`
override is normalized to a project-root-relative form so Cypress (cwd =
project root) writes exactly where Nx declares its outputs.

## Implementation Details

- `getOutputs` rewritten to use `resolve(workspaceRoot, projectRoot)` +
`relative(fullProjectRoot, fullPath)`. A single branch on whether the
relative result starts with `..` chooses between `{projectRoot}/<rel>`
(path inside the project) and `{workspaceRoot}/<rel>` (path outside the
project but inside the workspace). Matches the canonical pattern used in
the playwright plugin and unifies absolute, relative, and `..`-prefixed
inputs.
- New `serializeConfigPath` helper applies the same `resolve`+`relative`
transformation to rewrite absolute folders to project-root-relative form
before appending the per-spec subfolder; used by `getTargetConfig` for
top-level and nested (`e2e.*`, `component.*`) folder overrides.
- Three snapshot tests cover: (1) e2e + atomized e2e-ci with absolute
paths under a `.` project root, (2) component + atomized
component-test-ci with absolute paths under a `.` project root, and (3)
e2e + atomized e2e-ci with a deeper project root and absolute paths
outside the project but inside the workspace (exercises the
`{workspaceRoot}` branch and validates the `--config` override produces
dotted-relative paths).
2026-05-12 15:18:37 -04:00
Leosvel Pérez Espinosa ae1a8419ca chore(repo): root deps housekeeping (#35625)
The root `package.json` declares dependencies that are no longer used
anywhere in the repo (leftovers from feature/site migrations, replaced
libraries, and deprecated tooling).

The root `package.json` only declares deps that are actually used. This
PR removes 27 such entries with no source code or consumer-package
`package.json` changes.

Beyond cleaner manifests, this also:

- **Shrinks the lockfile by ~5,350 lines** (the transitive subtree of
the removed entries), giving faster `pnpm install` and a smaller
`node_modules`.
- **Reduces supply-chain attack surface** — every package we don't
install is one that can't be compromised upstream and pulled into our
builds. Recent ecosystem incidents (`chalk`, `debug`, `is`, etc.
takeovers) all reached projects through transitive deps.

Removed entries by bucket:

- **Docusaurus** (docs migrated to `astro-docs/` Starlight):
`@docusaurus/core`, `@docusaurus/preset-classic`,
`@docusaurus/module-type-aliases`, `@docusaurus/tsconfig`,
`@docusaurus/types`, `@mdx-js/react`, `prism-react-renderer`
- **3D / homepage scene** (superseded homepage iteration): `three`,
`@types/three`, `@react-three/drei`, `@react-three/fiber`,
`@react-spring/three`, `shadergradient`
- **Other unused**: `@notionhq/client`, `@iconify-json/ph`,
`@iconify-json/svg-spinners`, `starlight-typedoc`,
`@monaco-editor/react`, `react-markdown`, `fast-glob` (eslint configs in
`packages/{esbuild,js}` actively ban it in favour of `tinyglobby`),
`cytoscape-popper` (not in `@nx/graph` peer deps), `npm-package-arg`
- **Deprecated `@types/*`** (underlying package ships its own types or
isn't installed): `@types/detect-port`, `@types/marked`,
`@types/cytoscape`, `@types/npm-package-arg`
- **Stale tooling**: `conventional-changelog-cli` (2018-era
release-helpers script, since replaced by `nx nx-release`)

Each entry was verified individually with:
- 0 real-code imports anywhere in the repo (excluding lockfile fixtures
and demo JSON)
- 0 `peerDependencies` declarers across installed `node_modules`
- 0 references in scripts, CI workflows, husky hooks,
`project.json`/`nx.json`

After removal, `pnpm install` is clean (no new unmet peers) and `pnpm nx
prepush` passes locally.

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-05-12 15:18:16 -04:00
Jack Hsu 4d766ff60f docs(node): add Node 26 to compat matrix (#35623)
## Current Behavior

Compat matrix lists Node 24, 22, 20 for Nx 22.x. No mention of Node 26.

## Expected Behavior

Add Node 26 to Nx 22.x row. New aside notes Node 26 is on Current track,
hits LTS Oct 2026, supported today via CI.

## Notes

- There are new deprecation warnings for `module.register` usage, which
will be removed in Node 28. This is fine for workspace that can use the
default Node.js type-stripping (https://github.com/nrwl/nx/pull/35608),
and for other workspaces we need swc/ts-node (or something else) to move
away from the deprecated API before it goes away.
- Also a deprecation warning for `fs.Stats`, but it's not in our code so
it's from a dep or transitive dep.

## Related Issue(s)

NXC-4374
2026-05-12 15:16:28 -04:00
Leosvel Pérez Espinosa 67142f7e35 cleanup(angular): avoid project-graph cache writes inside the angular project during plugin spec (#35620)
## Current Behavior

`packages/angular/src/plugins/plugin.spec.ts` mocks
`nx/src/utils/cache-directory.workspaceDataDirectory` to the relative
string `'tmp/project-graph-cache'`. The spec doesn't `chdir` away from
the project root, so when `@nx/angular/plugin`'s `createNodesV2` calls
`PluginCache.writeToDisk`, the hash file lands at
`packages/angular/tmp/project-graph-cache/angular-<hash>.hash` — inside
the angular project tree. This shows up as a sandbox violation on the
`angular:test` task.

## Expected Behavior

The plugin cache file lands outside the project tree, like in the other
plugin specs (`jest`, `docker`, `react`) that already follow this
pattern. `packages/angular/tmp/` is no longer created during
`angular:test`.

The fix mirrors `packages/jest/src/plugins/plugin.spec.ts`:
`process.chdir(tempFs.tempDir)` in `beforeEach` and restore the original
cwd in `afterEach`. The relative `'tmp/project-graph-cache'` then
resolves under the `tempFs` directory (which is in `os.tmpdir()`), and
`tempFs.cleanup()` already removes it. The explicit `mkdirSync`/`rmSync`
of `'tmp/project-graph-cache'` is redundant — `PluginCache.writeToDisk`
does its own `mkdirSync(dirname(cachePath), { recursive: true })` — and
has been removed.
2026-05-12 15:15:35 -04:00
Leosvel Pérez Espinosa 0c2d0ec445 fix(angular-rspack): keep root-scoped assets out of per-locale i18n emit (#35621)
## Current Behavior

For angular-rspack i18n production builds with `extractLicenses: true`,
the third-party license file is written inside the browser bundle dir at
`dist/browser/3rdpartylicenses.txt` instead of at the application
builder's documented location `dist/3rdpartylicenses.txt`.

The same misbehavior also surfaces sandbox violations on
`examples-angular-rspack-csr-i18n:build` (unexpected reads of
`dist/browser/<locale>/../3rdpartylicenses.txt`).

## Expected Behavior

The license file is written once at `dist/3rdpartylicenses.txt`,
matching Angular CLI's application builder layout. The sandbox report
for `examples-angular-rspack-csr-i18n:build` shows no unexpected reads.

## Implementation Details

`LicenseWebpackPlugin` is configured by angular-rspack with an asset
name that escapes the browser dir, so it lands at `outputPath.base`:

```ts
outputFilename: posix.join(relative(outputPath.browser, outputPath.base), '3rdpartylicenses.txt')
// → '../3rdpartylicenses.txt'
```

`I18nInlinePlugin` re-emits every non-`$localize` asset under each
locale subdirectory. With three locales (`en-GB`, `es-ES`, `fr`) the
license asset becomes three asset names:

```
en-GB/../3rdpartylicenses.txt
es-ES/../3rdpartylicenses.txt
fr/../3rdpartylicenses.txt
```

These are different *path strings* but the `<locale>/..` segments cancel
out, so all three resolve to the same physical file inside
`dist/browser/`. Two consequences:

1. The license file lands at `dist/browser/3rdpartylicenses.txt` (the
collision target) instead of `dist/3rdpartylicenses.txt` (the
LicenseWebpackPlugin's intent — `outputPath.base`).
2. Rspack's `compareBeforeEmit` (default `true`) writes the file once
for the first locale and then opens it `O_RDONLY` to compare contents
for the other two. The sandbox tracker captures the literal syscall
paths (no `..` canonicalization), so a single physical file appears as
one write and two reads under three distinct path strings, and the two
reads are flagged as `unexpectedReads`.

The fix updates `I18nInlinePlugin`'s skip predicate so assets whose path
contains a `..` segment are not duplicated per locale. This mirrors how
Angular CLI's application builder excludes `BuildOutputFileType.Root`
files from i18n inlining; the rspack-asset-name analog is detected via
the `..` segment.
2026-05-12 15:15:00 -04:00
Jason Jean fa5c8b6eef fix(testing): pin jest to ~30.3.0 to avoid jest-runtime 30.4 RN incompat (#35618)
## Current Behavior

`nx test` for any React Native or Expo app crashes immediately:

```
TypeError: this._moduleMocker.clearMocksOnScope is not a function
  at Runtime.resetModules (.../jest-runtime/build/index.js:3782:28)
```

`@nx/jest` defaults to installing `jest@^30.0.2` (a caret range). Today
(2026-05-07) `jest-runtime@30.4.0` was published, which is the first
version to call `_moduleMocker.clearMocksOnScope()`. That method only
exists on `jest-mock@30.x`'s `ModuleMocker`.

React Native's preset (`@react-native/jest-preset`, current latest
0.85.3) hard-pins `jest-environment-node@^29.7.0`, whose env constructor
instantiates a `jest-mock@29` `ModuleMocker`. When `jest-runtime@30.4.0`
calls `clearMocksOnScope` on that 29.x instance, the call is missing →
crash. Same root cause hits both `preset: 'react-native'` and `preset:
'jest-expo'`.

`pnpm-workspace.yaml`'s catalog stays at `^30.0.2`, but the lockfile has
it resolving to 30.0.2 (because the workspace lockfile was written
before 30.4.0 existed); user workspaces don't get that protection. They
re-resolve fresh on first `pnpm install` and land on 30.4.0.

## Expected Behavior

`nx test` keeps working for RN/Expo apps until Meta ships a
Jest-30-aware `@react-native/jest-preset`.

This PR:
- Pins `@nx/jest`'s scaffold defaults from `^30.0.2` → `~30.3.0` for
`jest`, `babel-jest`, and `~30.0.0` for `@types/jest`. New workspaces
get a known-good range.
- Adds `update-23-0-0/pin-jest-30-3-for-rn-compat`, a migration that
walks existing workspaces' root `package.json` and tightens any
`jest`/`babel-jest`/`@types/jest` range that resolves entirely within
major 30 down to the same pinned ranges. Skips ranges that escape major
30 (like `*` or `>=29.0.0`), file-links, and entries already at the pin.
- Lift the pin once `@react-native/jest-preset` bumps
`jest-environment-node` to `^30`. Tracking comment is in
`packages/jest/src/utils/versions.ts`.

## Related Issue(s)

This is a tourniquet: jest 30.5+ may add another `jest-mock@30`-only
call and we'd be back here. Long-term fix is either (a) Meta updating
their preset, or (b) `@nx/jest` writing a self-contained RN-aware jest
config that doesn't rely on `preset: 'react-native'`.

Fixes #

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-05-12 15:14:22 -04:00
Altan Stalker 9021e2ad92 chore(ci): revert assignment rule (#34537)
Will bust cache several times to see whether we can get a repro

---------

Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com>
2026-05-12 15:13:15 -04:00
Craigory Coppola 5d338c2642 fix(devkit): exclude dist from jest module path scan (#35615)
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-05-12 15:12:01 -04:00
Craigory Coppola aac8a789e2 chore(repo): fixup dotnet:lint sb violations (#35612)
## Current Behavior
`dotnet:lint` has violations from json files in subprojects

## Expected Behavior
`dotnet:lint` excludes subprojects

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
2026-05-12 15:10:58 -04:00
Jason Jean a585bffc6f fix(gradle): exclude project-graph from jest module path scan (#35609)
## Current Behavior

`gradle:test` (the Jest target for `packages/gradle`) reads several
files under `packages/gradle/project-graph/build/reports/tests/test/`
(Gradle test report HTML/JS) that are not declared as task inputs. The
sandbox flags them as unexpected reads.

Root cause: jest-haste-map crawls `<rootDir>` to build a module map and
indexes any file matching `moduleFileExtensions` (`ts`, `js`, `html`
from the preset). The `project-graph/` subdirectory is a separate
Kotlin/Gradle sub-project whose `build/` outputs are gitignored — so
they're (correctly) excluded from Nx's `{projectRoot}/**/*` input glob —
but Jest doesn't honor `.gitignore`. The existing
`modulePathIgnorePatterns` covers `<rootDir>/batch-runner/` but not
`<rootDir>/project-graph/`.

Sandbox report:
https://staging.nx.app/runs/PywWp3NK7G/task/gradle%3Atest

## Expected Behavior

Jest does not scan into `packages/gradle/project-graph/`. The
`gradle:test` task no longer produces unexpected reads from that
directory.

## Related Issue(s)

Fixes NXC-4444
2026-05-12 15:10:28 -04:00
Craigory Coppola c514039729 fix(core): enable node's native v8 compile cache support (#35415)
## Current Behavior

Every `nx` invocation re-parses and re-compiles the same JS modules from
disk. Node 22.8+ ships an opt-in V8 cache for compiled bytecode
(`require('module').enableCompileCache()`), but bin/nx.ts doesn't enable
it.

> ⚠️ This is **not the same** as the `v8-compile-cache` npm package that
was previously used in nx and removed in #20454 due to ESM
incompatibility (`Invalid host options` error). The npm package was a
userspace `Module.prototype._compile` monkey-patch and famously broke
when ESM modules were loaded. The Node 22.8 built-in is implemented
inside Node's loader and was designed specifically to support both CJS
and ESM cleanly. It does not have the bug that motivated #20454.

## Expected Behavior

Call `enableCompileCache()` at the top of bin/nx.ts so the cached
bytecode is reused on subsequent runs. The optional chaining (`?.`) plus
try/catch make it a no-op on older Node versions, and the cache itself
is a no-op on the first run — every run after that pays only the
cached-bytecode load instead of full parse+compile.

Cache files live in Node's default location
([`os.tmpdir()/node-compile-cache`](https://nodejs.org/api/module.html#moduleenablecompilecachecachedir))
and Node manages them automatically. Cache entries are keyed on source
mtime+size and Node version, so they invalidate automatically when
source changes or the user upgrades Node.

This also enables the cache in the daemon and plugin workers, which has
shown to be promising for speeding up plugin load times.

## Related Issue(s)

Fixes #

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
2026-05-12 15:09:57 -04:00
Sharon Lougheed 091d838c62 fix(linter): prevent ENOENT crash in getRelativeImportPath for unresolvable paths (#35007)
I saw a bug on a bad import line, but when I fixed it, more lint
errors/warnings popped up in the same file visually. But fixing that
file wasn't enough, because I kept finding even more lint problems in
files without that import error. Ran the lint command and saw an ENOENT
crash. After a small fix, _many_ lint issues emerged. Looks like eslint
doesn't catch errors you throw at it. (I wrote this paragraph myself,
but I worked with _Claude Opus 4.6 Thinking_ for fixing and detailing
the rest of this PR.)

## Current Behavior

When a project uses wildcard tsconfig path aliases (e.g.
`@myorg/mylib/*` → `libs/mylib/src/*`), the `enforce-module-boundaries`
rule's auto-fixer calls `getRelativeImportPath` with the unresolved glob
path (e.g. `libs/mylib/src/*`). The function's `lstatSync` returns
`null` for this path, no file extension resolves it either, and
execution falls through to `readFileSync` — which throws `ENOENT: no
such file or directory`.

Because ESLint does not catch errors thrown inside `fix()` functions
(see
[eslint/eslint#13872](https://github.com/eslint/eslint/issues/13872)),
the impact depends on the context:

- **CLI** (`nx lint` / `eslint .`): the error propagates to
`eslint-helpers.js` where `controller.abort()` kills the **entire lint
run**, suppressing every diagnostic across all files. The ENOENT error
itself is printed, but there is no indication that all other diagnostics
were lost — the user sees an error about one file and reasonably assumes
everything else was checked.
- **IDE extension**: the ESLint language server lints open files
individually with no shared `AbortController`, so only the **crashing
file's** diagnostics are lost. The crash is effectively invisible — no
squiggles, no Problems panel entry, no notification (the error is only
logged to the ESLint Output channel). Every other open file still shows
its lint errors normally, so the linter appears to be working fine.

### Reproduction

Clone
[SharonLougheed/module-boundaries-bug](https://github.com/SharonLougheed/module-boundaries-bug/)
and run `nx lint libA`. The linter crashes with:

```
ENOENT: no such file or directory, open '.../libs/libB/src/lib/*'
Rule: "@nx/enforce-module-boundaries"
```

**0 lint errors are reported**, even though there are 7 real violations
in the library.

## Expected Behavior

`getRelativeImportPath` should return `undefined` when the file path
cannot be resolved, instead of falling through to `readFileSync`. The
callers already handle `undefined` — they skip the auto-fix suggestion
but still report the lint error.

After this fix, the same `nx lint libA` run reports:

```
LibA.ts:3:1  error  Projects cannot be imported by a relative or absolute path  @nx/enforce-module-boundaries
utils.ts     ...4 errors, 3 warnings (no-var, prefer-const, no-explicit-any, no-unused-vars, no-debugger)

✖ 7 problems (4 errors, 3 warnings)
```

### Relationship to #34066

#34066 (merged in v22.5.3 by @JesseZomer) resolves wildcard paths at the
call site in `enforce-module-boundaries.ts`, which fixes the specific
wildcard scenario. This PR adds a defensive guard inside
`getRelativeImportPath` itself, so that _any_ unresolvable path — not
just wildcards — returns `undefined` instead of crashing. Issues #30491
and #16716 describe non-wildcard variants of the same crash that are not
addressed by #34066.

## Related Issue(s)

Fixes #35006
Related: #30491, #16716, #21889, #32190 (closed by #34066)

---------

Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
2026-05-12 15:09:38 -04:00
Leosvel Pérez Espinosa e3c98db922 chore(testing): pin vite resolution on yarn for vite 7 downgrade (#35586)
## Current Behavior

The `cypress-legacy` and `vite` e2e tests downgrade `vite` +
`@vitejs/plugin-react` in `package.json` and then run `install` to
verify backward compatibility with Vite 7. On yarn classic this trips a
linker bug:

```
error Invariant Violation: could not find a copy of vite to link in
    .../node_modules/vitest/node_modules
```

Root cause: `vitest@~4.1.0` declares `vite` as both a regular
`dependency` AND a `peerDependency`. yarn 1's hoisting algorithm fails
when intersecting a top-level `^7.0.0` range with that combo (the bug
does not fire for `^8.0.0`, exact versions, or differently-formatted
ranges like `7.x`). It is not specific to the in-test downgrade — the
same setup fails from a completely empty directory.

`Linux/yarn/20 e2e-cypress` and `Linux/yarn/20 e2e-vite` have been
failing every nightly since the in-test Vite 7 downgrade was introduced
in #34850.

## Expected Behavior

The `cypress-legacy` and `vite` e2e tests pass on yarn classic again
with Vite 7.

The fix adds a yarn-only `resolutions` entry pinning `vite` so yarn
commits to a single version up front and skips the buggy hoisting code
path. npm/pnpm don't have the bug and ignore the field.

## Validation

Verified via a manually-dispatched e2e nightly run on this branch (with
the matrix temporarily narrowed to `Linux/yarn/20 × {e2e-cypress,
e2e-vite}`, the matrices that fail on master):
https://github.com/nrwl/nx/actions/runs/25431401390 — both jobs passed.
2026-05-12 15:08:28 -04:00
Jason Jean 75e349822c fix(core): isolate cache env vars in splitArgs spec (#35584)
The `splitArgs` describe block in
`packages/nx/src/utils/command-line-utils.spec.ts` saves and clears
`NX_BASE`, `NX_HEAD`, and `NX_PARALLEL` so the surrounding shell
environment doesn't bleed into assertions. However, the production code
in `splitArgsIntoNxArgsAndOverrides` also reads four cache-related env
vars as fallbacks for the `skipNxCache` / `skipRemoteCache` defaults:

- `NX_SKIP_NX_CACHE`
- `NX_DISABLE_NX_CACHE`
- `NX_SKIP_REMOTE_CACHE`
- `NX_DISABLE_REMOTE_CACHE`

These are not isolated. When a developer runs the test suite via the
outer `nx` invocation with flags like `--skipNxCache`, nx propagates
them to spawned child processes as `NX_SKIP_NX_CACHE=true`. That env var
leaks into the Jest worker, flips `skipNxCache` to `true`, and breaks
every test in the `splitArgs` block that asserts on the defaults — six
failures in total (`should split nx specific arguments into nxArgs`,
`should default to having a base of main`, `should return configured
base branch from nx.json`, `should return a default base branch if not
configured in nx.json`, `should split projects when it is a string`,
`should set base and head based on environment variables in affected
mode`).

The `splitArgs` specs should pass regardless of which cache-related
flags or env vars are set in the surrounding environment, just like they
already do for `NX_BASE` / `NX_HEAD` / `NX_PARALLEL`.

This PR extends the existing save/clear/restore pattern to cover the
four cache env vars and factors the conditional-restore logic (delete
when originally unset, otherwise reassign — to avoid Node's
`process.env[key] = undefined` coercing to the string `"undefined"`)
into a small `restoreEnv` helper.

N/A

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-05-12 15:08:00 -04:00
Jason Jean 1059584d2f chore(repo): update nx to 23.0.0-beta.8 (#35601)
Updating Nx from 23.0.0-beta.7 to 23.0.0-beta.8
2026-05-12 15:07:27 -04:00
Jason Jean 67ea540639 fix(core): use gethostuuid(3) instead of ioreg on macOS (#35599)
## Current Behavior

`get_machine_id()` is invoked from `connect_to_nx_db()` to derive the
workspace DB filename, so every `nx` invocation hits it. On macOS the
underlying `machine_uid::get()` shells out to `ioreg -rd1 -c
IOPlatformExpertDevice` and parses its output. The fork+exec, dyld,
code-signature verification, and IOKit framework init together cost
**~90ms per call**, and the result is identical for every run on the
same machine.

## Expected Behavior

Use `libc::gethostuuid(3)` on macOS, which is the BSD syscall the same
kernel data is fronted by. It returns the same 16-byte `uuid_t` that
`ioreg` prints — bit-for-bit identical, same hyphenation, same casing —
but in **~10µs** (about 9000× faster) because it skips the subprocess
entirely.

```
ioreg:        398DA1D5-608C-58D6-BA32-FAE0E01A0ED3
gethostuuid:  398DA1D5-608C-58D6-BA32-FAE0E01A0ED3
```

Other platforms are untouched: `machine_uid::get()` already uses fast
direct file/registry reads on Linux and Windows; only macOS was paying
the subprocess tax.

Measured on a hot-cache `run-many --parallel 10` over 5 next.js apps:

|        | median wall time |
| ------ | ---------------- |
| before | 290 ms           |
| after  | 200 ms           |

## Related Issue(s)

Fixes #
2026-05-12 15:06:41 -04:00
Jason Jean 84f0965559 fix(core): skip handleimport miss path when nx key packages are absent (#35596)
## Current Behavior

\`printNxKey()\` runs at the end of every \`nx run-many\` / \`nx run\`
invocation. It calls \`handleImport('@nx/powerpack-license')\` and
\`handleImport('@nx/key')\` to dynamically discover whichever optional
license package the workspace has installed. When neither package is
installed (the common case for OSS users), both calls walk
\`node_modules\` and miss, costing roughly 50 ms per nx command.

## Expected Behavior

Cheaply probe whether either package is resolvable from the workspace
root (\`require.resolve(name, { paths: [workspaceRoot] })\`) before
attempting the dynamic import. The probe is microseconds when the
package is absent, so the wasted ~50 ms goes away.

While here, kick the lookup off at the very top of
\`runCommandForTasks\` so it overlaps with task execution, and split the
log out to the existing late call site so output ordering is unchanged —
the licensee line still lands after task output, never mid-task.

Measured on a hot-cache \`run-many --parallel 10\` over 5 next.js apps:

|        | median wall time |
| ------ | ---------------- |
| before | 350 ms           |
| after  | 310 ms           |

## Related Issue(s)

Fixes #
2026-05-12 15:06:23 -04:00
Jason Jean 40aa20bd86 chore(repo): exclude packages/nx/dist/src/native/*.node from sandbox reads (#35602)
## Current Behavior

Sandbox reads only exclude `packages/nx/src/native/*.node`. The native
`.node` binary is also copied to `packages/nx/dist/src/native/` during
the nx package build, and reads of that dist copy from other tasks show
up as sandbox violations.

## Expected Behavior

Both the source and dist locations of the prebuilt native `.node` binary
are excluded from sandbox read tracking.

## Related Issue(s)

N/A
2026-05-12 15:05:58 -04:00
Jason Jean 0f10b9adb5 fix(maven): widen runCLI timeout for --no-batch maven.test.ts cases (#35589)
## Current Behavior

`e2e-maven:e2e-ci--src/maven.test.ts` is flaking on CI. Example failed
run: https://github.com/nrwl/nx/actions/runs/25398728205

The failure pattern is consistent across the failures we have logs for:

```
FAIL e2e-maven src/maven.test.ts (533.603 s)
  Maven
    ✓ should detect Maven projects (13261 ms)
    ✓ should have proper Maven targets (2190 ms)
    ✕ should build Maven project with dependencies without batch mode (300955 ms)
    ✓ should run tests for Maven project without batch mode (10902 ms)
    ✓ should handle Maven project with complex dependencies (2933 ms)
    ✓ should support targetNamePrefix option (164161 ms)

  Maven › should build Maven project with dependencies without batch mode

    Command timed out after 300s: run app:install --no-batch

    Process output:
    NX   Running target install for project com.example:app and 87 tasks it depends on:
    ...
```

## Root cause

Without `--batch`, Nx expands `app:install` into one task per Maven
lifecycle phase per project (`validate, initialize, generate-sources,
..., install` × 4 projects ≈ 87 tasks), and each task spawns its own
`mvn` JVM that runs the `nx-maven-plugin:apply` and `:record` mojos.

Sampling `Run Command: run app:install --no-batch (Xs)` across the last
few master CI runs:

| Run | Duration |
| --- | --- |
| 25406383967 (passed) | 222.7s |
| 25395898487 (passed) | 261.7s |
| 25393915435 (passed) | 278.9s |
| 25395728023 (passed) | 286.8s |
| 25398728205 (failed) | 300s+ (timeout) |

`runCLI`'s default timeout is `5 * 60 * 1000` ms. On a healthy host the
assertion finishes around 220s. On loaded CI runners the same 87
JVM-spawning tasks routinely climb to 285s+, leaving very little
headroom — any extra noise puts it past 300s. There is no regression in
the underlying graph or in the `cbcd4d552b` / `44ae15eef6` /
`04ec111c88` Maven fixes; those commits are pre-existing on every
passing run as well.

## Expected Behavior

Pass `timeout: 10 * 60 * 1000` to the two `--no-batch` `runCLI` calls in
`e2e/maven/src/maven.test.ts` (the `app:install` case at line 51 and the
`app:mvn-compile` case in `should support targetNamePrefix option` at
line 121). That gives both calls 5 extra minutes of slack, well clear of
the observed 220–290s range, while leaving everything else (including
all four other `e2e-maven` test files, which already use `--batch` and
are fast) unchanged.

This is the smallest possible fix that addresses the actual root cause
(timeout headroom on the slowest run-shape we ship). It is not a
workaround for a regression — there is nothing to revert.

## Why not just speed it up?

A real perf fix is possible (e.g. a single batched `mvn` invocation for
the lifecycle expansion) but is out of scope for a flake fix. This
change only widens the timeout for the two known slow `--no-batch`
calls; everything else is untouched.

## Related Issue(s)

N/A — internal CI flake.
2026-05-12 15:05:40 -04:00
Jason Jean 623cb3d776 chore(repo): use apt mirror+file failover for ubuntu sources (#35600)
## Current Behavior

The `Install system deps` step in `.nx/workflows/agents.yaml` rewrites
`/etc/apt/sources.list` to point exclusively at
`azure.archive.ubuntu.com`, then runs `apt-get update && apt-get
install` for the system packages every Nx Agent needs
(`ca-certificates`, `lsof`, `libvips-dev`, `libglib2.0-dev`,
`libgirepository1.0-dev`, `zip`, `unzip`).

When Azure's Ubuntu mirror is unreachable from the agents — which
started happening today — every pipeline fails on init with:

```
Could not connect to azure.archive.ubuntu.com:80, connection timed out
E: Unable to locate package lsof
E: Unable to locate package libvips-dev
E: Package 'libgirepository1.0-dev' has no installation candidate
```

The original switch to Azure's mirror was made because the canonical
`archive.ubuntu.com` periodically serves a `Packages.gz` that doesn't
match its own `InRelease` metadata while mid-sync. So the previous
design traded one flaky upstream for another with no fallback between
them.

## Expected Behavior

Use apt's native `mirror+file://` failover, configured the same way
GitHub Actions runner-images sets up its hosted Ubuntu runners. A single
`/etc/apt/apt-mirrors.txt` lists mirrors in priority order, and
`sources.list` points at `mirror+file:/etc/apt/apt-mirrors.txt`. Apt
itself handles priority ordering and transparent failover.

The mirror order in this PR (canonical first) was chosen based on what
the diagnostic probes actually showed (see findings below):

```
https://archive.ubuntu.com/ubuntu/        priority:1
https://security.ubuntu.com/ubuntu/       priority:2
http://azure.archive.ubuntu.com/ubuntu/   priority:3
```

Plus a small apt config drop-in
(`/etc/apt/apt.conf.d/80-nx-mirror-failover`) with:

- `Acquire::http::Timeout "5"` / `Acquire::https::Timeout "5"` — cap
each per-fetch stall at 5s instead of the 120s default.
- `Acquire::Retries "0"` — mirror+file already provides
retry-via-failover; apt-level retries on top multiplied the stall when
Azure was consistently dead (60s × dozens of fetched files =
double-digit minutes per agent boot).

## Investigation findings

We probed every mirror from three different vantage points to figure out
what was actually broken:

| Mirror | From your laptop (residential) | From a GitHub-hosted runner
(inside Azure) | From an Nx Agent (GCP) |
| --- | --- | --- | --- |
| `archive.ubuntu.com` (Cloudflare) |  HTTP & HTTPS, sub-second |  | 
HTTP & HTTPS, sub-second |
| `security.ubuntu.com` (Cloudflare) |  HTTP & HTTPS, sub-second |  |
 HTTP & HTTPS, sub-second |
| `azure.archive.ubuntu.com` HTTP (port 80) |  TCP timeout |  HTTP
200, fast |  TCP timeout |
| `azure.archive.ubuntu.com` HTTPS (port 443) |  TCP timeout |  TCP
timeout |  TCP timeout |

Several things fall out of this:

1. **Azure mirror's HTTPS endpoint is broken in multiple regions** —
even from inside Azure (the GitHub-hosted runner) port 443 times out.
This is why we use `http://` for the Azure entry, matching GitHub's
`configure-apt-sources.sh`.
2. **From our agents' GCP egress, both ports are unreachable** to the
Azure mirror — TCP handshake never completes against `52.154.174.208`
(centralus). This is a path-level issue between Google's network and
Microsoft's edge for the geo-DNS region the agents resolve to.
3. **Canonical mirrors are fully healthy from every vantage point**,
including from agents. Both are CDN-fronted by Cloudflare, which is why
response times are consistent across networks.
4. **`azure.archive.ubuntu.com` is documented as publicly accessible**
(Microsoft Q&A confirms) but historically flaky for non-Azure consumers
— multiple Microsoft Q&A threads and a notable 2020 incident where the
entire `pool/` disappeared. Treating it as best-effort rather than
load-bearing matches what GitHub does.

That's why this PR puts Azure last instead of first. With mirror+file
failover, apt tries `archive.ubuntu.com` first, succeeds in
milliseconds, and never has to touch Azure. The 5s timeout and 0 retries
make the worst case (canonical down + Azure has to be tried) bounded.

A "what about ocean?" datapoint: the ocean agents don't apply the Azure
rewrite at all and have been working fine. This PR effectively converges
on that behavior for normal operation while keeping the original
mismatched-metadata-mid-sync escape hatch via Azure-as-fallback.

## Related Issue(s)

N/A — addresses ongoing CI flakiness from Azure mirror reachability
issues.
2026-05-12 15:05:10 -04:00
Craigory Coppola caebbb1ebe chore(repo): provision build toolchain via mise in publish workflow (#35593)
## Current Behavior

The `publish` workflow's matrix builds (Linux/macOS/Windows native
binaries via N-API) install Java, Node.js, and pnpm manually inside each
runner/container. With `@nx/dotnet` now in `nx.json`, these builds also
need .NET to be available before the project graph can be loaded — and
there's no .NET install on any of the matrix entries today, so the
workflow fails at the `pnpm nx run-many --target=build-native` step.

The macOS and `armv7-unknown-linux-gnueabihf` matrix entries already use
`mise-action` and `mise.toml`, but the four Linux *docker* entries
(Debian + Alpine, x64 + arm64) bypass mise entirely and provision tools
through hand-rolled `apt-get` / `apk` / `nodesource` / `npm i -g pnpm`
steps.

## Expected Behavior

- All four Linux docker matrix entries now install `mise` from a
signed/distro source (apt repo at `https://mise.jdx.dev/deb` for Debian,
`apk add mise` from Alpine `community` for Alpine) and provision their
entire toolchain — Node.js, Java, .NET, Maven, corepack — from
`mise.toml`. This drops ~30 lines of bespoke install logic per entry and
keeps versions in lockstep with the non-docker matrix entries, which
already use `mise-action`.
- Windows entries gain `choco install dotnet-9.0-sdk -y` alongside the
existing OpenJDK install (mise's Windows .NET path is broken upstream —
see [jdx/mise#4738](https://github.com/jdx/mise/discussions/4738)).
- The FreeBSD build sets `NX_DOTNET_DISABLE=true` (added to both the
`env:` block and the `cross-platform-actions/action`
`environment_variables` allowlist so the var actually crosses into the
FreeBSD VM) to opt out of the plugin entirely.
- `NODE_VERSION` is now forwarded into `docker run` so containers honor
the workflow's pinned Node version through `mise.toml`'s tera template
instead of falling back to its `24.11.0` default.
- `mise` itself is installed only via signed repositories — no `curl
https://mise.run | sh` — so a hijacked DNS lookup against `mise.run`
cannot drop a malicious script into our publish pipeline.

## Related Issue(s)

N/A — workflow fix triggered by `@nx/dotnet` being added to `nx.json`.
2026-05-12 15:04:46 -04:00
Craigory Coppola 0121307400 fix(dotnet): correct output paths for Web SDK and centralized dist setups (#35398)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-05-12 15:04:06 -04:00
Jack Hsu 4e44f678d3 chore(misc): include blog in auto-deployment (#35582)
It's a separate app now, needs to update as well.
2026-05-12 15:03:17 -04:00
Jason Jean 6c6fe80ec8 fix(core): isolate NX_PARALLEL env var in parallel-related specs (#35579)
## Current Behavior

`readParallelFromArgsAndEnv` reads `process.env.NX_PARALLEL` as a
fallback before the hardcoded `'3'` default. When developers set
`NX_PARALLEL` in their workspace `.env` (or shell), that value bleeds
into the test suite and breaks several specs that assume the default is
`3`:

- `packages/nx/src/utils/command-line-utils.spec.ts` — multiple
`splitArgs` cases plus the `--parallel` defaults.
- `packages/nx/src/command-line/yargs-utils/shared-options.spec.ts` —
`default parallel should be 3`.

CI doesn't set `NX_PARALLEL`, so the failures only show up locally.

## Expected Behavior

The specs should pass regardless of whether `NX_PARALLEL` is set in the
surrounding environment.

Both spec files now save/clear/restore `process.env.NX_PARALLEL` in
their relevant `beforeEach`/`afterEach`, mirroring the pattern already
used for `NX_BASE` and `NX_HEAD`.

## Related Issue(s)

N/A
2026-05-12 15:03:01 -04:00
Jason Jean 5847b3f3b8 fix(core): restore use-legacy-versioning shim for @nx/js@21 ensurePackage path (#35574)
## Current Behavior

In a Nx 21.x workspace, generators that call `ensurePackage(...)` for a
not-yet-installed plugin (e.g. `@nx/webpack` from `@nx/nest:app`,
`@nx/react:app`, `@nx/js:lib`, etc.) can crash with:

```
Cannot find module nx/dist/src/command-line/release/config/use-legacy-versioning.js
```

immediately after the `Fetching @nx/webpack…` line. The bug was
triggered purely by publishing `nx@22.7.0` on 2026-04-24 — no dependency
change in the user's workspace.

Resolution chain:

1. `ensurePackage` runs `installPackageToTmp`, which installs the
requested plugin into a fresh tmp directory and appends that tmp's
`node_modules` to `NODE_PATH`.
2. `@nx/devkit@21.x` declares a peer dep `"nx": ">= 20 <= 22"`.
3. npm 7+ auto-installs missing peers, picking the highest matching
version → `nx@22.7.x` lands in the tmp dir.
4. `@nx/js@21`'s `library.js` does a top-level
`require("nx/src/command-line/release/config/use-legacy-versioning")`.
5. Pre-22.7.0, `nx`'s `package.json` had no `exports` field, so this
require fell through to filesystem lookup and (when the file wasn't in
the tmp's `nx`) Node continued the search to the workspace's `nx@21` and
resolved successfully.
6. `nx@22.7.0` added a new `"./src/*"` exports wildcard that maps to
`./dist/src/*.js`. Resolution now stops at the tmp's `nx@22.7.x` and
tries to load
`dist/src/command-line/release/config/use-legacy-versioning.js` — which
doesn't exist (deleted in v22). MODULE_NOT_FOUND.

## Expected Behavior

`@nx/js@21`'s top-level `require` resolves successfully, and the library
generator's existing legacy-vs-modern release-config branch makes the
correct decision.

## Fix

Restore
`packages/nx/src/command-line/release/config/use-legacy-versioning.ts`
as a deprecated compat shim. The function body matches the 21.x
implementation exactly (env var override, then
`releaseConfig?.version?.useLegacyVersioning`, defaulting to `false`),
so 21.x callers behave identically to before.

The shim is intentionally not imported anywhere inside Nx 22+ — it
exists purely so external 21.x consumers loaded via `ensurePackage` can
resolve the path. A `TODO(v24)` marks when it's safe to remove.

This needs to ship in the 22.7.x line (so the patched `nx@22.7.x` is
what `@nx/devkit@21`'s peer range resolves to). Please cherry-pick to
`22.7.x` for an `nx@22.7.2` patch release.

## Related Issue(s)

Fixes #
2026-05-12 15:01:18 -04:00
Jason Jean da37299531 chore(repo): update nx to 23.0.0-beta.7 (#35565)
Updating Nx from 23.0.0-beta.4 to 23.0.0-beta.7
2026-05-12 15:00:45 -04:00
beeman 54b249ee5c fix(core): update minimatch to 10.2.5 (#35569)
Update the minimatch catalog entry in line with #34660 so catalog
consumers resolve minimatch 10.2.5 and brace-expansion 5.0.5.

This reduces scanner noise for GHSA-7h2j-956f-4vf2 without implying a
practical Nx vulnerability.
2026-05-12 15:00:13 -04:00
Leosvel Pérez Espinosa 8ff77c20e9 chore(testing): split NX_E2E_SKIP_CLEANUP into global/project-scoped vars (#35572)
## Current Behavior

`NX_E2E_SKIP_CLEANUP` is set to `'true'` in every Linux/macOS e2e matrix
entry to gate the build-cache reuse check in `e2e/utils/global-setup.ts`
(skip wiping `e2eCwd` + republishing to verdaccio when `./build` already
exists).

PR #35042 added an early-return to `cleanupProject` in
`e2e/utils/create-project-utils.ts` guarded by the same env var name to
expose a *local* debug opt-in (preserve the per-test tmp project for
inspection). Because CI already had the var set, the new early-return
fires on every CI run, silently disabling the per-test `nx reset` +
`tmpProjPath()` removal that previously kept orphan daemons from
leaking. Jest hangs after all tests pass and the workflow times out at
60 minutes.

## Expected Behavior

Each lifecycle hook is gated by a distinct, scope-specific env var:

- `NX_E2E_SKIP_GLOBAL_CLEANUP` — global-setup.ts (CI sets it).
- `NX_E2E_SKIP_PROJECT_CLEANUP` — cleanupProject (developer-set locally
for debugging only).

Per-test cleanup runs in CI again, jest exits cleanly, and nightly e2e
jobs no longer hit the 60-minute cap.

## Validation

Verified via a manually-dispatched e2e nightly run on this branch (with
the matrix temporarily narrowed to `Linux/{npm,pnpm,yarn}/20 e2e-node`,
the matrix that hung at 60 min on master):
https://github.com/nrwl/nx/actions/runs/25375231501 — all 3 jobs passed
in ~25 minutes each.

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-05-12 14:59:05 -04:00
Justin Mecham a4120c0635 chore(repo): bump @phenomnomnominal/tsquery to ~6.2.0 (#35560)
## Current Behavior

The pnpm workspace catalog pins `@phenomnomnominal/tsquery` to `~6.1.4`,
which declares a TypeScript peer dependency of `^3 || ^4 || ^5`.
Downstream Nx consumers running TypeScript 6 under strict peer-deps
(e.g. via `strict-peer-deps=true`) must add an npm `overrides` entry to
install successfully.

## Expected Behavior

Bump the catalog pin to `~6.2.0`. Version 6.2.0 relaxes the TypeScript
peer dependency to `>3.0.0`, allowing TS6 consumers to install Nx
without the workaround.

The diff between 6.1.4 and 6.2.0 is mechanical:

- A perf optimization (cache `parse.ensure()` result rather than calling
each iteration)
- `esquery` dependency bump from `^1.5.0` to `^1.7.0` (additive features
and bugfixes; no selector-syntax breaking changes between 1.5 and 1.7)
- The peer-dep relaxation itself:
https://github.com/phenomnomnominal/tsquery/pull/103

No tsquery API changes between the two versions.

## Related Issue(s)

No tracking issue — small dependency catalog bump.
2026-05-12 14:58:35 -04:00
Jason Jean 01b542ef8f chore(core): bump ratatui to 0.30 and drop tui-term fork (#35547)
`packages/nx/Cargo.toml` pins `ratatui = "0.29"` and consumes `tui-term`
from a personal git fork (`JamesHenry/tui-term @ 88e3b614…`). The fork
carries two custom commits on top of upstream `tui-term`: a vt100 →
vt100-ctt swap, and a `Modifier::DIM` mapping for dimmed PTY content.
`tui-logger` is also held back at `0.17.2`.

The upstream `a-kenji/tui-term` repo has since merged the dim-modifier
patch (PR #340) and shipped `tui-term 0.3.4` against the new modular
`ratatui-core 0.1` / `ratatui-widgets 0.3` crates that came with
`ratatui 0.30`. This means we no longer need to maintain a tui-term fork
at all — the only remaining reason for it (the dim patch) is upstream.

- `ratatui` bumped to `0.30.0`.
- `tui-term` swapped from the `JamesHenry/tui-term` git dep to crates.io
`tui-term = { version = "0.3.4", default-features = false }`.
- `tui-logger` bumped `0.17.2` → `0.18.2` (which targets `ratatui
^0.30`).
- `vt100-ctt` stays as-is — we still need its `all_contents`,
`all_contents_formatted`, `get_total_content_rows`, and
`Parser::get_raw_output` APIs that aren't in upstream `vt100`. Its
default `tui-term` feature is now disabled so it doesn't drag old
`ratatui 0.29` / `tui-term 0.2` back into the dep tree.
- New `packages/nx/src/native/tui/vt100_adapter.rs` (~95 lines)
implements `tui_term::widget::{Screen, Cell}` for `vt100_ctt::{Screen,
Cell}` via `#[repr(transparent)]` newtypes (orphan-rule workaround). The
two `PseudoTerminal::new(&*screen)` call sites in `terminal_pane.rs` and
`inline_app.rs` now wrap the screen with
`Vt100CttScreen::wrap(&screen)`.
- One unused `Stylize` import dropped from `tasks_list.rs` (ratatui 0.30
made the styling methods inherent).

Resolved dep tree after the bump:

```
ratatui v0.30.0
ratatui-core v0.1.0
ratatui-widgets v0.3.0
ratatui-crossterm v0.1.0
ratatui-macros v0.7.0
tui-logger v0.18.2
tui-term v0.3.4               (crates.io, no fork)
vt100-ctt v0.16.0 (fork)      (kept for scrollback / raw_output APIs)
```

`cargo check` and `cargo clippy --frozen --all-targets` are clean
(warnings only, all pre-existing on master). `cargo test --lib` passes
350/352; the two flaky failures are in `native::watch::watcher::tests`
(filesystem-event timing tests, unrelated and inconsistent across runs).

None — refactor / dependency hygiene.
2026-05-12 14:57:10 -04:00
Jason Jean f2e4c27bce fix(js): include transitive workspace deps in pruned pnpm lockfile (#35532)
## Current Behavior

`@nx/js:prune-lockfile` does not recursively walk workspace→workspace
dependencies. Given an `app → @myorg/lib-a → @myorg/lib-b → lodash`
chain (where `lib-a` and `lib-b` are workspace packages), the executor
produces a pruned `pnpm-lock.yaml` that:

1. **Misses the importer block** for `workspace_modules/@myorg/lib-b`
(the transitive workspace dep).
2. **Misses `lodash`** (lib-b's npm dep) from the `packages:` section.
3. **Keeps `specifier: workspace:*`** for `lib-b` inside `lib-a`'s
importer block, even though `@nx/js:copy-workspace-modules` rewrites
`lib-a/package.json` to `"@myorg/lib-b": "file:../lib-b"`.

The specifier mismatch causes `pnpm install --frozen-lockfile` to fail
in the deployed output:

```
ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with "frozen-lockfile" because
pnpm-lock.yaml is not up to date with workspace_modules/@myorg/lib-a/package.json
  - @myorg/lib-b (lockfile: workspace:*, manifest: file:../lib-b)
```

## Expected Behavior

`@nx/js:prune-lockfile` recursively discovers transitive workspace
dependencies and produces a lockfile that:

1. Contains an `importers` block for every workspace module that
`copy-workspace-modules` writes to disk.
2. Includes the npm dependencies of all transitive workspace modules in
the `packages:` section.
3. Rewrites workspace-package references inside nested importers to the
flat `workspace_modules/` layout (`specifier: file:<rel>` / `version:
link:<rel>`), matching what `copy-workspace-modules` writes to each
package's `package.json`.

`pnpm install --frozen-lockfile` succeeds in the pruned output
directory.

### Implementation

Two coordinated changes in lockfile-side code:

- **`project-graph-pruning.ts`** — `traverseWorkspaceNode` now recurses
into workspace→workspace dependency edges with a `visited` set, so
transitive workspace npm deps reach the pruned graph.
- **`pnpm-parser.ts`** — `stringifyPnpmLockfile` BFS-collects transitive
workspace importers, deep-clones each importer block, and rewrites
workspace-package references to the flat `workspace_modules/` layout.

### Tests

- 3 new unit tests in `pnpm-parser.spec.ts` covering the canonical
chain, dependency cycles, and diamond shapes.
- 1 new e2e test in `e2e/js/src/js-executor-prune-lockfile.test.ts`
exercising the canonical chain end-to-end.
- Verified end-to-end against the reported reproduction repo: `pnpm
install --frozen-lockfile` now succeeds in the pruned output where it
previously failed with `ERR_PNPM_OUTDATED_LOCKFILE`.

### Credit

Diagnosis and original fix sketch by @estevaolucas in #35347 — this PR
carries forward the recursion + specifier rewrite portions in a focused
change. The other concerns from #35347 (devDep/peerDep stripping,
catalog reference resolution in `copy-workspace-modules`) are real but
separable and intentionally left for follow-up issues.

## Related Issue(s)

Fixes #34655

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
2026-05-12 14:54:55 -04:00
Jason Jean c7c56d8631 chore(core): bump detect-port from ^1.5.1 to ^2.1.0 (#35533)
## Current Behavior

`@nx/web`, `@nx/js`, and `@nx/cypress` declare `detect-port: ^1.5.1`.
Per the bulk-dependency-update sweep (NXC-4329), this is due for a major
bump. The queue task initially flagged this as ESM-only — that was stale
info; v2 is actually dual-published.

## Expected Behavior

Bumped to `detect-port@^2.1.0`.

## Validation

`detect-port@2.1.0` ships both ESM and CJS builds via the package.json
exports map:

```jsonc
{
  "type": "module",
  "main": "./dist/commonjs/index.js",
  "exports": {
    ".": {
      "import": { "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" },
      "require": { "types": "./dist/commonjs/index.d.ts", "default": "./dist/commonjs/index.js" }
    }
  },
  "engines": { "node": ">= 16.0.0" }
}
```

The three call sites in this repo:

```ts
// packages/web/src/executors/file-server/file-server.impl.ts
const detectPort = require('detect-port');                  // CJS path

// packages/js/src/executors/verdaccio/verdaccio.impl.ts
import detectPort from 'detect-port';                       // compiles to require → CJS

// packages/cypress/src/utils/start-dev-server.ts
import detectPort from 'detect-port';                       // compiles to require → CJS
```

All three resolve to `./dist/commonjs/index.js` — no code changes
needed. The default-export function shape (`detect(port, callback?) →
Promise<number>`) is unchanged across v1 → v2.

`pnpm nx run-many -t build -p web,js,cypress` — passes.

## Related Issue(s)

Part of [NXC-4329](https://linear.app/nxdev/issue/NXC-4329)
bulk-dependency-update sweep.

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
2026-05-12 14:54:00 -04:00
MaxKless d64c172f04 fix(maven): serialize Maven 4 build state recording (#35555)
## Current Behavior

Maven 4 batch invocations can overlap build-state recording with another
resident invocation on the same adapter. When Maven logs from that path,
the Maven 4 logging context can have a null terminal and throw a
`context.terminal` NPE.

## Expected Behavior

Maven 4 invocation and build-state recording are serialized on the
adapter, so build-state logging uses a valid Maven context. The Maven 4
batch e2e spec also exercises parallel `resources` and `after:resources`
targets.

## Related Issue(s)

N/A
2026-05-12 14:53:47 -04:00
Jason Jean dda7538332 fix(core): unique telemetry user_id; expose workspace_id dimension (#35553) 2026-05-12 14:53:33 -04:00
Jason Jean a5fbb0fd60 chore(repo): bump nx and powerpack packages in workspace-plugin (#35543)
`tools/workspace-plugin` pins `@nx/devkit`, `@nx/js`, and `@nx/plugin`
at `22.7.0-beta.16`, lagging behind the rest of the workspace which is
on `23.0.0-beta.4`. Powerpack packages (`@nx/conformance`, `@nx/key`,
`@nx/powerpack-license`) are still on the `3.x`/`4.x` line.

The workspace plugin uses the same Nx version as the rest of the repo,
and powerpack packages are on the latest stable (`5.0.4`).

- `tools/workspace-plugin/package.json`: `@nx/devkit`, `@nx/js`,
`@nx/plugin` → `23.0.0-beta.4`; `@nx/conformance` → `5.0.4`
- root `package.json`: `@nx/conformance`, `@nx/key`,
`@nx/powerpack-license` → `5.0.4`

N/A
2026-05-12 14:52:34 -04:00
Jason Jean c8bb6d7f6c feat(gradle): stream batch task results to nx as they finish (#35487)
## Current Behavior

The Gradle batch executor (`@nx/gradle:gradle` in batch mode) returns a
`Promise<BatchResults>`. The Kotlin batch runner serializes the entire
result map to a single JSON blob and `println`s it once at the end of
the run. The Node-side executor accumulates stdout chunks and
`JSON.parse`s them after the JVM exits, so Nx only learns about task
outcomes in one burst when the whole batch is done.

The Maven batch executor was migrated to streaming a while ago — it
returns an `AsyncGenerator` and the Kotlin runner emits
`NX_RESULT:{json}` lines as each task finishes — but the Gradle batch
executor was never updated to match.

## Expected Behavior

The Gradle batch executor now mirrors the Maven batch executor's
streaming protocol, with per-task results streamed live during both
**build** and **test** task execution.

### Kotlin runner (`packages/gradle/batch-runner`)

- **`ResultEmitter`** writes `NX_RESULT:{json}` lines to stdout, one per
task, with a thread-safe dedupe set so emission can happen from
build/test listeners without double-reporting.
- **`runBuildLauncher`** emits per build task as `TaskOutputCapture`
detects the next task's `> Task :foo:bar` header, with an end-of-build
flush for the final task.
- **`runTestLauncher`** emits per Nx test task at its class-level
`TestFinishEvent`, with a `TaskFinishEvent` fallback for tasks that
never produced a class event (compile failure, exclusion). Method-level
failures are sticky so a later passing method in the same class can't
mask an earlier failure.
- **`NxBatchRunner.main`** ends with `exitProcess(0)` so lingering
non-daemon threads from the Gradle Tooling API can't keep the JVM alive
after task work completes. The trailing `results.forEach { emit }` loop
now only covers `finalizeTaskResults`-synthesized entries
(excluded/skipped tasks).

### Node executor
(`packages/gradle/src/executors/gradle/gradle-batch.impl.ts`)

- `gradleBatch` is now an `async function*` returning `AsyncGenerator<{
task; result: TaskResult }>`.
- `streamTasksInBatch` spawns the JVM, reads stdout via `readline`, and
**drains `NX_RESULT` lines into an in-memory queue**, yielding from the
queue. Yielding inside the readline loop creates back-pressure — slow
consumers block `yield`, readline pauses, the OS pipe between Java and
Node fills, and Java's `println` blocks on a full pipe. The queue
decouples reading from yielding so back-pressure can no longer deadlock
the JVM.
- Stderr stays inherited so Gradle/JUnit progress flows to the terminal
in real time.
- Tasks the runner never reports get yielded as failed at the end so Nx
never hangs.

### Project graph dependency

`packages/gradle/project.json` adds `:gradle-batch-runner` to
`implicitDependencies`. The gradle package bundles the batch-runner JAR
and references it at runtime via `batchRunnerPath`; without this, `nx
affected` wouldn't pick up gradle when only the runner changed.

### Bug along the way: className format mismatch

`RegexTestParser.kt` records `testClassName` as the **simple** class
name (e.g. `MyTest`), but Gradle's
`JvmTestOperationDescriptor.className` is the **fully qualified** name
(`com.example.MyTest`). The exact-match lookup in the test listener was
failing for every class, so per-class `TestStartEvent`/`TestFinishEvent`
never matched an Nx task — every Nx task fell through to the
`TaskFinishEvent` fallback at the end of the Gradle test task, all
sharing the same emission time and the cumulative shared output buffer.
`resolveNxTaskId` now looks up by FQN first, then by the suffix after
the last `.`, so events match either format.

### Known trade-off

Tests under the same Gradle test task share the captured per-Gradle-task
output for `terminalOutput`. With JUnit `--parallel` the bytes
interleave anyway, and Gradle's `TestLauncher` doesn't expose per-test
stdout segmentation through `setStandardOutput` — getting truly
per-class `terminalOutput` would require either subscribing to
`OperationType.TEST_OUTPUT` (which diverts stdout away from the standard
output stream and didn't reliably fire for some setups in testing) or
recording fully-qualified class names in the project-graph plugin so we
can match `TestOutputEvent` parents precisely. Filed as a follow-up.

The on-the-wire change matches the existing Maven contract
(`run-batch.ts` already special-cases `isAsyncIterator`), so no Nx core
changes are needed.

## Related Issue(s)
2026-05-12 14:51:47 -04:00
Craigory Coppola 86d3104ed5 chore(repo): prevent unit tests from walking the real workspace (#35441)
## Current Behavior

Running `nx:test` (jest) from `packages/nx` produces ~4500 cross-project
sandbox violations. Reads span every plugin's `src/generators/**`,
`src/migrations/**`, `src/executors/**`, schemas, docs, spec files and
`.gitignore`s — essentially the entire monorepo.

Root cause is that tests end up computing the **real** project graph:

- `packages/nx/src/utils/workspace-root.ts` freezes `workspaceRoot` on
first import by walking up from `process.cwd()`. With jest's cwd set to
`packages/nx` and no `NX_WORKSPACE_ROOT_PATH` env var, it resolves to
the real repo root.
- `scripts/patched-jest-resolver.js` did set `NX_WORKSPACE_ROOT_PATH`,
but only when `process.argv[1]` contained `jest-worker` or `argv[3]` had
`:test`. Under `jest --passWithNoTests --detectOpenHandles --forceExit`
with `maxWorkers: 1` (what the Nx test runs use), neither branch
matches, so the env var was never set.
- The existing `@nx/devkit.createProjectGraphAsync` mock in
`scripts/unit-test-setup.js` is specifier-keyed, so relative imports
inside `packages/nx` (e.g. `'../../project-graph/project-graph'`) bypass
it and call the real graph builder.

The smoking gun in the process tree: hundreds of `plugin-worker.ts`
subprocesses and `git remote-https` calls to `nrwl/nx-ai-agents-config`
— both only happen when the real, isolated project graph is computed.

## Expected Behavior

Unit tests never touch the real workspace filesystem or spawn plugin
workers. Three layered fixes:

1. **`scripts/patched-jest-resolver.js`** — always set
`NX_WORKSPACE_ROOT_PATH` to a throwaway `tmp/unit` dir. Runs at resolver
module-load, which is before any `require('nx/...')`, so
`workspace-root.ts` is guaranteed to freeze to the sandbox dir.
2. **`scripts/unit-test-setup.js`** — add
`jest.doMock('nx/src/project-graph/project-graph', …)` that returns an
empty graph for `createProjectGraphAsync`,
`createProjectGraphAndSourceMapsAsync`, and
`buildProjectGraphAndSourceMapsWithoutDaemon`. Jest keys mocks by
resolved absolute path, so the relative imports inside `packages/nx` hit
the same mock.
3. **`scripts/unit-test-setup.js`** — guard mock on
`loadIsolatedNxPlugin`. If any test slips past the graph mocks and tries
to spawn a plugin worker, it throws with an actionable error instead of
silently scanning the monorepo.

`packages/nx/src/utils/workspace-root.spec.ts` directly exercises the
walk-up logic in `workspaceRootInner`, which now short-circuits on the
always-set env var. The suite scopes the var away in
`beforeAll`/`afterAll`.

## Related Issue(s)

This is opened as a draft to validate the hypothesis by re-running the
CI sandbox report and confirming the cross-project violations drop.

Local smoke test: `packages/nx` suites across `workspace-root`,
`run-many`, `affected`, `release/config`, `migrate`, `task-hasher`,
`native-task-hasher-impl`, `package-json/create-nodes`,
`project-json/build-nodes`, `explicit-*-dependencies`,
`normalize-project-nodes`, `show/projects`, and both `isolation/*` specs
— 11 suites, 177 tests, all passing.
2026-05-12 14:51:05 -04:00
polygraph-app[bot] 975b84cb37 docs(nx-cloud): add metric uploader step to manual DTE examples (#35534)
Adds `nx-cloud upload-agent-metrics` to every provider example in the
manual DTE guide, plus a short intro section on why it matters.

- GitHub Actions / Azure / Circle CI — explicit always-run flag
- Bitbucket / GitLab — lives in `after-script:` / `after_script:`
- Jenkins — inside `post { always { } }`

The always-run mechanic is the point: if `start-agent` OOMs, you still
want metrics uploaded so you can see which task killed the agent.

Companion to nrwl/ocean PR which links to this page from the in-product
setup prompt.

<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/add-manual-DTE-resource-metrics-docs-and-link-to-frontend-764c85f2)
<!-- polygraph-session-end -->

---------

Co-authored-by: rarmatei <matei.rar@gmail.com>
2026-05-12 14:50:21 -04:00
Jason Jean eee341265d chore(repo): update nx to 23.0.0-beta.4 (#35528)
Updating Nx from 23.0.0-beta.3 to 23.0.0-beta.4
2026-05-12 14:50:10 -04:00
Jack Hsu bb901a2973 fix(nx-dev): short-circuit bot probes in framer rewrite edge function (#35527)
Some bots are hitting the server, scanning for wordpress paths. This is
resulting in 500 server error, but we should return 404.

This fails in prod (500):

```
 curl -sS -o /dev/null -w "%{http_code}\n" --path-as-is 'https://nx.dev//wp/wp-includes/wlwmanifest.xml'
```

Fixed in preview (404):

```
curl -sS -o /dev/null -w "%{http_code}\n" --path-as-is 'https://deploy-preview-35527--nx-dev.netlify.app//wp/wp-includes/wlwmanifest.xml'
```

## Current Behavior

Bot scanners hit `GET //wp/wp-includes/wlwmanifest.xml` (leading `//`).
`new URL(pathname, framerUrl)` parses `//wp/...` as protocol-relative,
promoting `wp` to upstream host. Fetch fails with DNS lookup error and
the function 500s.

## Expected Behavior

Leading `/+` collapsed before resolving against `framerUrl`. Common
WordPress / exploit probes (`wp-includes`, `wp-admin`, `xmlrpc.php`,
`wlwmanifest`, `.env`, `.git/`) short-circuit to 404.

## Related Issue(s)

DOC-498
2026-05-12 14:49:40 -04:00
Jason Jean 37fb080042 chore(linter): bump ignore from ^5.0.4 to ^7.0.5 (#35508)
## Current Behavior

`@nx/js`, `@nx/next`, and `@nx/react-native` declare `ignore: ^5.0.4`.
The companion packages `@nx/nx` and `@nx/dotnet` are already on `^7.0.5`
(queue file flagged not to re-bump those). Per the
bulk-dependency-update sweep (NXC-4329), the remaining three packages
should align.

## Expected Behavior

Bumped to `ignore@^7.0.5` in all three. ignore@6/7 are API-compatible
with v5 — only changes are dropping Node ≤12 support and internal perf
improvements.

## Validation

The 5 source-level call sites all use the default-import default-export
shape:

```ts
// packages/js/src/utils/generate-globs.ts
// packages/js/src/utils/assets/copy-assets-handler.ts
// packages/js/src/generators/typescript-sync/typescript-sync.ts
// packages/react-native/src/generators/init/lib/add-git-ignore-entry.ts
// packages/next/src/utils/add-gitignore-entry.ts
import ignore from 'ignore';
```

Stable across v5/v6/v7. The `ignore()` constructor + `.add()` +
`.filter()` + `.ignores()` chain is unchanged.

`pnpm nx run-many -t build -p js,react-native,next` — passes.

## Related Issue(s)

Part of [NXC-4329](https://linear.app/nxdev/issue/NXC-4329)
bulk-dependency-update sweep.
2026-05-12 14:49:25 -04:00
Jason Jean 58cb69e076 fix(core): show flaky-task count in run summary (#35491)
## Current Behavior

When Nx's task-history life cycle detects more than one flaky task, the
summary header renders with **two consecutive spaces and no number** in
place of the count, e.g.:

```
> NX  Nx detected  flaky tasks

  myproject:test
  otherproject:e2e
```

The singular case (one flaky task) renders correctly as `Nx detected a
flaky task`.

## Expected Behavior

```
> NX  Nx detected 2 flaky tasks

  myproject:test
  otherproject:e2e
```

## Root Cause

Both `task-history-life-cycle.ts` and the legacy
`task-history-life-cycle-old.ts` had:

```ts
title: `Nx detected ${
  this.flakyTasks.length === 1 ? 'a flaky task' : ' flaky tasks'
}`,
```

The plural branch is a literal `' flaky tasks'` string with a leading
space and **no count interpolation** — so the template renders `Nx
detected ` + `' flaky tasks'` = `Nx detected flaky tasks` (two spaces,
no number).

## Fix

Replace the plural literal with `\`\${this.flakyTasks.length} flaky
tasks\`` so the count appears between the leading space and the word
`flaky`. Singular wording is unchanged.

```ts
title: \`Nx detected \${
  this.flakyTasks.length === 1
    ? 'a flaky task'
    : \`\${this.flakyTasks.length} flaky tasks\`
}\`,
```

Same fix applied symmetrically in both life-cycle files.

## Tests

No existing unit test covers `printFlakyTasksMessage()`'s formatted
output (the surrounding life cycles don't have a `*.spec.ts`). Adding
one would require mocking the task-history daemon channel and life-cycle
hooks — out of scope for a one-line formatting fix. The change is small
enough to verify by inspection of the diff.

## Related Issue(s)

(reported internally; no public issue)
2026-05-12 14:49:10 -04:00
Craigory Coppola e876d8fde0 fix(core): ensure verbose logs go to stderr and daemon logs are properly decorated (#34358)
## Current Behavior
`logger.[x]` doesn't get decorated despite being emitted to the daemon
log, perf logs aren't decorated, etc

## Expected Behavior
daemon logs are decorated

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
2026-05-12 14:48:41 -04:00
Jason Jean 756c81e6b3 chore(repo): update nx to 23.0.0-beta.3 (#35515)
Updating Nx from 23.0.0-beta.2 to 23.0.0-beta.3
2026-05-12 14:48:07 -04:00
Jason Jean 338c59ae45 chore(repo): remove offboarded core team members from README (#35504)
## Current Behavior

The Core Team section of the top-level `README.md` lists 23 people
across 6 markdown tables (5 rows of 4 + 1 final row of 3). Three of
those names are former team members who have offboarded:

- Philip Fulcher (`philipjfulcher`)
- Colum Ferry (`Coly010`)
- Austin Fahsl (`fahslaj`)

## Expected Behavior

Remove the three offboarded entries and reflow the surviving 20 names
into 5 even rows of 4. Original ordering preserved; gaps closed
left-to-right, top-to-bottom.

## Notes

- README only — no other files modified.
- CODEOWNERS uses team handles (`@nrwl/nx-cli-reviewers`), not
individual usernames, so no change needed there.
- 20 surviving members fit cleanly into 5 rows of 4 — no awkward partial
row.
- Source padding regenerated per-row so the raw markdown stays visually
aligned.

## Related Issue(s)

(internal cleanup; no public issue)
2026-05-12 14:47:32 -04:00
Benjamin Cabanes 7738153538 chore(nx-dev): remove '/pricing' from excluded URL rewrite paths (#35467)
Update Framer redirects on nx.dev.
2026-05-12 14:47:17 -04:00
Craigory Coppola 4063e658a9 fix(nx-dev): document nested CLI subcommands beyond two levels (#35519)
## Current Behavior

The CLI docs generator that produces `/docs/reference/nx-commands` only
flattens the top-level command and its direct children. Any subcommand
nested deeper than that is silently dropped from the rendered page, even
though the underlying yargs parser already produces the full nested
tree.

In practice this means commands like `nx show target inputs` and `nx
show target outputs` have no public docs entry — users have no way to
discover their flags (`--check`, `--target`, `--configuration`, etc.)
without running `--help` locally.

## Expected Behavior

The generator walks the full subcommand tree and renders every command.
Three-deep commands like `nx show target inputs` appear as `###`
headings alongside `nx show target`, with their own usage block and
options table.

Verified locally — running the generator before/after this change adds
exactly two new sections (`nx show target inputs`, `nx show target
outputs`) and changes nothing else.

A new e2e test in `astro-docs/e2e/cli-subcommand-formatting.spec.ts`
locks the third-level rendering in so this can't regress silently again.

## Related Issue(s)

Fixes #
2026-05-12 14:47:05 -04:00
Jason Jean 707493b804 fix(maven): skip attached artifacts that fail to materialize in batch record (#35473)
## Current Behavior

Running `nx run-many -t clean` (or any target that wipes `target/`
before `record` runs) in batch mode against a Maven 4 project fails
with:

```
Caused by: java.nio.file.NoSuchFileException: .../target/consumer-<hash>.pom
    at java.nio.file.Files.setLastModifiedTime(...)
    at org.apache.maven.internal.transformation.impl.TransformedArtifact.mayUpdate(...)
    at org.apache.maven.internal.transformation.impl.TransformedArtifact.getFile(...)
    at dev.nx.maven.shared.BuildStateRecorder.captureAttachedArtifacts(BuildStateRecorder.kt:173)
```

In Maven 4, the consumer POM is exposed as a `TransformedArtifact`.
Reading `Artifact.file` invokes `getFile()`, which lazily materializes
the file by touching its `lastModifiedTime`. After `clean` deletes
`target/`, that touch throws and propagates out of
`BuildStateRecorder.captureAttachedArtifacts`, failing the `record` mojo
and the entire build. The existing `consumer-<hash>.pom` filter is dead
code on this path because the throw happens at the property access,
before the filter runs.

## Expected Behavior

`record` is a best-effort metadata recorder. When an attached artifact's
file cannot be resolved — null, missing on disk, or a
`TransformedArtifact` whose materialization throws — we skip that
artifact and continue, the same way the existing
null/exists/consumer-POM guards do. The build succeeds.

The fix resolves `artifact.file` once with `try/catch`, logs the failure
at `debug`, and returns `null` from the mapper. Subsequent
`null`/`exists`/consumer-POM checks run against the local. Behavior is
unchanged when `getFile()` succeeds.

Verified locally with `nx run
e2e-maven:e2e-ci--src/maven-batch-v4.test.ts` — the previously-failing
`should clean multiple projects with run-many in batch mode` test now
passes (no more `NoSuchFileException`).

## Related Issue(s)

None.
2026-05-12 14:45:44 -04:00
Jason Jean 7168981856 chore(linter): bump globals from ^15.9.0 to ^17.0.0 (#35505)
## Current Behavior

`@nx/eslint-plugin` declares `globals: ^15.9.0`. Per the
bulk-dependency-update sweep (NXC-4329), this dependency is due for a
major-version bump.

## Expected Behavior

Bumped to `globals@^17.0.0`. The published v17 package is still CommonJS
(`module.exports = require('./globals.json')`); no ESM migration
required. Engine floor moves to `node >= 18` (which Nx's own floor
already exceeds).

## Validation

The three call sites in `@nx/eslint-plugin` use only stable environment
keys that are unchanged across `15 → 16 → 17`:

```ts
// packages/eslint-plugin/src/flat-configs/javascript.ts
import globals from 'globals';
{ ...globals.browser, ...globals.node }

// packages/eslint-plugin/src/flat-configs/react-base.ts
{ ...globals.browser, ...globals.commonjs, ...globals.es2015, ...globals.jest, ...globals.node }

// packages/eslint-plugin/src/flat-configs/angular.ts
{ ...globals.browser, ...globals.es2015, ...globals.node }
```

`pnpm nx run eslint-plugin:build` — passes.
`pnpm nx run eslint-plugin:test` — passes.

Lockfile delta: 2 lines (specifier + resolved version), no peer-hash
churn.

## Related Issue(s)

Part of [NXC-4329](https://linear.app/nxdev/issue/NXC-4329)
bulk-dependency-update sweep.
2026-05-12 14:44:47 -04:00
Leosvel Pérez Espinosa 262a01b30c chore(repo): fix typecheck failures across e2e and eslint-rules (#35456)
## Current Behavior

Four `typecheck` tasks fail on master:

- `eslint-rules:typecheck` — TS1541 in
`tools/eslint-rules/rules/valid-schema-description.ts`. The type-only
import of `jsonc-eslint-parser` (an ESM-only module) needs a
`resolution-mode` attribute under `module: node16`.
- `e2e-nx:typecheck` — TS6307: `e2e/nx/src/import-utils.ts` is imported
by tests but isn't matched by `tsconfig.spec.json`'s `include` list (the
file is a helper, not a `*.test.ts`).
- `e2e-angular:typecheck` and `e2e-storybook:typecheck` — TS2307: deep
imports into `nx/src/internal-testing-utils/*` (used by
`packages/devkit/internal-testing-utils.ts`,
`packages/workspace/migrations.spec.ts`, and several
`packages/*/src/**/*.spec.ts`) can't be resolved. The catch-all
`typesVersions` entry `src/*` redirects these to
`dist/src/internal-testing-utils/*.d.ts`, which doesn't exist — the
files are excluded from `tsconfig.lib.json` so they're never built into
`dist/`.

## Expected Behavior

All `typecheck` tasks pass. Specifically:

- The ESLint rule's type-only import declares `resolution-mode:
'import'`, satisfying TS1541.
- `e2e/nx/tsconfig.spec.json` includes `src/**/*.ts`, picking up helper
files alongside tests. The redundant `*.test.ts` / `*.spec.ts` glob
variants (no `.tsx`/`.jsx`/`.js` tests in this project, no `.ts` files
outside `src/`) collapse into `["src/**/*.ts", "**/*.d.ts",
"jest.config.ts"]`.
- `packages/nx/package.json` adds a more-specific entry for
`src/internal-testing-utils/*` to both `typesVersions` (so Node10 module
resolution finds the source `.ts` files instead of nonexistent built
declarations) and `exports` (so the `types-versions-exports-sync`
conformance rule stays satisfied). The exports object only needs `types`
and `default` — both pointing to the same `.ts` source — since these
utilities are workspace-internal: `default` is reached at jest runtime
via the resolver's fallback condition list, and `types` covers modern TS
resolution. The published `files` list still excludes the source `.ts`
files, so external consumers are unaffected.
2026-05-12 14:44:03 -04:00
Leosvel Pérez Espinosa 0eef651409 fix(js): reference vitest.config in eslint dep-checks for vitest libs (#35460)
## Current Behavior

When generating an `@nx/js` library with `--unitTestRunner=vitest` and
`--linter=eslint` (and a non-vite bundler such as
`tsc`/`swc`/`rollup`/`esbuild`/`none`), the generated eslint config adds
`{projectRoot}/vite.config.{js,ts,mjs,mts}` to the
`@nx/dependency-checks` `ignoredFiles` list — but the file actually
generated is `vitest.config.mts`, so the ignore pattern never matches.

## Expected Behavior

The eslint config references
`{projectRoot}/vitest.config.{js,ts,mjs,mts}` whenever
`@nx/vitest:configuration` produces a dedicated vitest config (i.e.
bundler is not `vite`). When bundler is `vite`, the existing
`vite.config.{...}` ignore is still correct since the same file holds
both build and test config.

Since #33670 (Nx 22.2), `@nx/vitest:configuration` calls
`shouldUseVitestConfig()` and emits `vitest.config.mts` for
non-framework JS libraries with no existing `vite.config`. The eslint
ignore pattern in `packages/js/src/generators/library/library.ts` was
never updated to match.

## Related Issue(s)

Fixes #35450
2026-05-12 14:43:51 -04:00
Juri 2f530d8281 docs(misc): document agentic nx import flow 2026-05-12 14:43:38 -04:00
Leosvel Pérez Espinosa 4102e9b371 chore(nx-dev): resolve astro-docs:build sandbox violations (#35472)
## Current Behavior

`astro-docs:build` produces a large number of sandbox violations. The
build reads files from packages it doesn't declare as inputs (plugin
schemas, dependent task outputs, source TypeScript via `ts-node`), and
the TypeDoc setup mutates devkit's `dist/` output by writing a modified
`tsconfig.lib.json` into it. The combination produces stale caches and
unexpected reads/writes that make sandboxing reports noisy.

## Expected Behavior

The build only reads files it has declared, and never writes outside its
own outputs.

### Changes

- **Inputs** (`astro-docs/project.json`): declare plugin schemas
(`packages/*/{generators,executors,migrations}.json`,
`packages/*/src/{generators,executors}/**/schema.json`), migration and
docs markdown, plugin `package.json`, and a transitive
`dependentTasksOutputFiles` glob covering
`**/*.{d.ts,json,md,js,cjs,mjs}` so devkit / cnw / nx / dotnet / maven
dist outputs flow correctly through the task dependency chain. Also
declare the workspace `tsconfig.json` as a selective JSON input scoped
to `compilerOptions` so esbuild's ancestor walk-up registers as declared
without making the cache sensitive to references-only changes.
- **TypeDoc** (`astro-docs/src/plugins/utils/typedoc/typedoc.ts`,
`devkit-generation.ts`): write the mutated devkit `tsconfig.lib.json` to
`os.tmpdir()/nx-devkit-docs/packages/devkit/` instead of
`dist/packages/devkit/`. Rewrite the `include` patterns to absolute
paths anchored at `dist/packages/devkit/` so TypeDoc still finds the
declaration files from the relocated tsconfig.
- **CNW subprocess**
(`astro-docs/src/plugins/utils/cnw-subprocess.cjs`): drop the
`ts-node.register(...)` shim and load `create-nx-workspace` from
`dist/packages/create-nx-workspace/...` instead of the TypeScript
source. Aligns the docs generator with what end users actually consume
from the published package.
- **TS project** (`astro-docs/tsconfig.json`): extend `exclude` with
`e2e/` and the `eslint.config.{js,mjs,cjs}` files so the main TypeScript
program no longer pulls in the e2e specs and lint configuration.
- **Tailwind** (`astro-docs/src/styles/global.css`): add `@source not`
directives for `**/*.{spec,test}.*`, `**/eslint.config.*`,
`**/tsconfig*.json`, and `e2e/**`. Tailwind v4's oxide scanner walks
`@source` paths and the project root reading every file with a
recognized extension; without these exclusions it parses test, config,
and tsconfig files looking for class usages they cannot contain. The
exclusions stop reads of these files in the astro-docs project root and
in the symlinked nx-dev/* packages declared via `@source`.
- **Rollup docs path** (`packages/rollup/docs/rollup-examples.md`,
`packages/rollup/src/executors/rollup/schema.json`): move
`rollup-examples.md` from `packages/rollup/src/docs/` to
`packages/rollup/docs/` and update the schema's `examplesFile`
reference. Every other plugin in the workspace already keeps example
markdown under `packages/<pkg>/docs/`; rollup was the lone outlier
(introduced by accident in #14963), and the non-standard path required a
special input glob in `astro-docs` just to cover one file.
2026-05-12 14:42:34 -04:00
Jason Jean 6e47ca24bc fix(gradle): exclude batch-runner from jest haste-map crawl (#35501)
## Current Behavior

Running `nx test gradle` in a sandboxed CI environment produces sandbox
violations: jest is observed reading files that belong to a sibling Nx
project (`:gradle-batch-runner`), e.g.:

- `packages/gradle/batch-runner/build/reports/tests/test/index.html`
- `packages/gradle/batch-runner/build/reports/tests/test/js/report.js`

Root cause: jest's `rootDir` defaults to `packages/gradle/` (the dir of
`jest.config.cts`). With `moduleFileExtensions` including `.html` and
`.js`, `jest-haste-map` walks the entire tree under `rootDir` and reads
matching files to build its module map. `packages/gradle/batch-runner/`
is a separate Nx project whose root happens to be a subdirectory, so its
gradle build output gets pulled into haste-map.

These files are not — and should not be — declared as inputs to
`gradle:test`; they belong to a different project.

## Expected Behavior

`jest-haste-map` skips the `batch-runner/` subtree, so `gradle:test` no
longer reads files owned by the `:gradle-batch-runner` project,
eliminating the sandbox violations.

Fix: add `modulePathIgnorePatterns: ['<rootDir>/batch-runner/']` to
`packages/gradle/jest.config.cts`. `modulePathIgnorePatterns` (vs
`testPathIgnorePatterns`) is the correct knob — it excludes the path
from the haste map entirely so it's never read; `testPathIgnorePatterns`
only filters which files run as tests.

## Related Issue(s)

N/A — surfaced by Nx Cloud sandbox report on `gradle:test`.
2026-05-12 14:42:20 -04:00
Craigory Coppola cdca177ffb chore(repo): declare lazy-loaded packages as implicit deps (#35392)
## Current Behavior

Many Nx plugin packages lazy-load other plugins at runtime via
`ensurePackage()` or the `require('@nx' + '/...')` pattern. These
dependencies weren't declared in `package.json` at all, which meant:

- `pnpm install` in the monorepo happened to find them only via hoisting
from unrelated `devDependencies`.
- Published packages gave no install-time signal about what optional
peers a consumer might want.
- The dependencies were invisible to dependency-graph tooling,
supply-chain audits, and any future semantic-versioning logic.

## Expected Behavior

Every lazy-loaded plugin or tool is declared as `peerDependencies` +
`peerDependenciesMeta: { X: { optional: true } }`, matching the existing
convention in `@nx/angular`, `@nx/angular-rspack`, `@nx/eslint`, etc.
This gives consumers correct install/publish semantics without requiring
them to install peers they don't use.

For two packages — `@nx/workspace` and `@nx/js` — several of their
newly-declared peers transitively reverse-depend on them. Raw
package.json edges would cause `@nx/js:typescript-sync` to produce
circular TypeScript project references (TS6202). Those two packages use
`implicitDependencies: ["!name", …]` in `project.json` to drop the
cyclic graph edges, keeping the task graph and tsc builds cycle-free
without modifying the sync generator itself.

Commits:
1. **workspace + js**: 14 optional peers on `@nx/workspace`, 5 on
`@nx/js`, plus `implicitDependencies` negations in each `project.json`.
2. **Plugin packages**: `@nx/angular`, `@nx/expo`, `@nx/next`,
`@nx/nuxt`, `@nx/react-native`, `@nx/storybook`, `@nx/vite`, `@nx/vue`,
`@nx/web`. Cycles don't form for any of these, so no
`implicitDependencies` negations were needed. `@nx/js:typescript-sync`
populated the corresponding `tsconfig.lib.json` project references,
which are committed alongside the `package.json` changes.

## Related Issue(s)

Fixes #

## Test plan

see:
https://staging.nx.app/runs/m3Otv2Xl7m?sandboxViolations=true&query=%3Atest

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
2026-05-12 14:42:07 -04:00
Jason Jean 3ed6edcf23 chore(repo): update nx to 23.0.0-beta.2 (#35499)
Updating Nx from 23.0.0-beta.1 to 23.0.0-beta.2
2026-05-12 14:41:25 -04:00
Rares Matei b0559f74fc docs(misc): update nx-cloud-workflows references from v5 to v6 (#35498)
Bumps `nrwl/nx-cloud-workflows` references in launch template docs and
agents config from `v5` to `v6`.
2026-05-12 14:39:29 -04:00
Leosvel Pérez Espinosa d5d399002b fix(angular): disable vitest watch by default (#35493)
## Current Behavior

Generated Angular projects using `vitest-angular` create a test target
without an explicit `watch` value. The Angular unit-test builder
defaults watch mode to `true` in TTY environments, so running generated
projects through monorepo workflows such as `nx run-many` can leave test
tasks running instead of exiting.

## Expected Behavior

Generated Angular `vitest-angular` test targets explicitly set `watch:
false`, matching Nx Vitest's default non-watch behavior and preserving
terminating test tasks for `run-many` and affected workflows. Users can
still opt into watch mode with `--watch` or a watch configuration.
2026-05-12 14:39:18 -04:00
Louie Weng e9f0b59d05 chore(gradle): use task graph when traversing task excludes (#35413)
<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

When we calculate the excludes for the gradle executors, we currently
traverse the surface level dependencies and also via the project graph.
We need to cover all transitive dependencies instead.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

Use the task graph to derive excludes instead. This also is easier to
traverse multiple levels of dependencies than using the project graph.

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
2026-05-12 14:39:05 -04:00
MaxKless 7620c1ffe5 fix(core): remove access control header from graph app (#35494)
it's not needed anymore
2026-05-12 14:38:51 -04:00
Leosvel Pérez Espinosa 0591aa5fe3 fix(js): strip glob from inferred outputs before resolving as path (#35463)
## Current Behavior

When a project's build target is inferred via the `@nx/js/typescript`
plugin, its `outputs[0]` is a glob pattern (e.g.
`{projectRoot}/dist/**/*.{js,cjs,mjs,jsx,d.ts,d.cts,d.mts}{,.map}`).
Several executors and utilities pass that value directly to filesystem
APIs:

- `@nx/js:prune-lockfile` and `@nx/js:copy-workspace-modules` crash with
`ENOENT: no such file or directory, lstat '.../dist/**/*.{js,...}'`
because `lstatSync` is called on the literal glob.
- `@nx/web:file-server` returns the glob as the static-serve directory.
- `update-package-json` and `buildable-libs-utils` silently skip
dependent-lib version resolution when `outputs[0]` is a glob
(`existsSync` returns false / try-catch swallows).

## Expected Behavior

The glob portion is stripped back to the last path separator before the
value is used as a filesystem path, recovering the base output directory
(e.g. `apps/foo/dist`). Behavior is unchanged for non-glob output paths.

The `stripGlobToBaseDir` helper that already existed locally in
`@nx/js:node` is extracted to
`packages/js/src/utils/strip-glob-to-base-dir.ts` and reused by all
affected sites. Unit tests cover the helper's contract.

## Related Issue(s)

Fixes #35452
2026-05-12 14:37:54 -04:00
Jack Hsu abcd3b182c fix(react): withSvgr migration preserves other properties (#35484)
The current migration erroneously removes all withReact properties. This
will be copied for the Rspack migration for v23 as well.

Closes NXC-3982
2026-05-12 14:37:43 -04:00
Juri 63481f5776 docs(misc): note sandboxing rolling out to other Nx Cloud plans on June 1 2026-05-12 14:37:32 -04:00
Jason Jean 66f0ffcdc6 chore(core): update misc e2e target info snapshot for tsconfig solution input (#35488)
## Current Behavior

The `Nx Commands show show target human-readable output should render
target info` snapshot test in `e2e/nx/src/misc.test.ts` fails on master.
Recent changes that include the tsconfig solution input for webpack
added a new input entry
(`{"json":"{workspaceRoot}/tsconfig.json","fields":["extends","files","include"]}`)
to the resolved target info, but the snapshot was not updated.

## Expected Behavior

Snapshot reflects the new tsconfig solution input so the e2e test
passes.

## Related Issue(s)

N/A — follow-up to 7a6f796047 / ca7671afd6 which added the tsconfig
solution input to webpack/rollup.
2026-05-12 14:36:17 -04:00
polygraph-app[bot] 7a68bf4f77 fix(bundling): include tsconfig solution input for webpack (#35477)
## Current Behavior

The `@nx/webpack` executor, inferred plugin, and config builder all call
`isUsingTsSolutionSetup()` and let its result influence task outputs
(e.g. `useTsconfigPaths`). However, the root `tsconfig.json` is not part
of the build task's cache inputs — so edits to root `tsconfig.json`
(`extends`, `files`, `include`) don't invalidate webpack task caches and
stale outputs are reused.

The same gap exists in `@nx/node`'s webpack-bundler branch of the
application generator: it calls `addBuildTargetDefaults(tree,
'@nx/webpack:webpack')` without the tsconfig input, even though the
parallel esbuild branch in the same file already passes
`TS_SOLUTION_SETUP_TSCONFIG_INPUT`.

## Expected Behavior

Matches the rollup fix in #35476: the root `tsconfig.json` is included
as a structured input (`{ json: '{workspaceRoot}/tsconfig.json', fields:
['extends', 'files', 'include'] }`) on `@nx/webpack:webpack` task
defaults and in the inferred plugin's build target inputs, so changes to
the relevant fields invalidate caches.

### Changes
- `packages/webpack/src/plugins/plugin.ts` — append
`TS_SOLUTION_SETUP_TSCONFIG_INPUT` to the inferred build target's
`inputs`. Also gate the targets cache on `NX_CACHE_PROJECT_GRAPH`
(mirrors the rollup PR) and update the spec accordingly.
- `packages/webpack/src/generators/configuration/configuration.ts` —
pass `'build', [TS_SOLUTION_SETUP_TSCONFIG_INPUT]` to
`addBuildTargetDefaults`.
- `packages/node/src/generators/application/lib/create-project.ts` —
same on the webpack branch (the esbuild branch already had it).
- `packages/webpack/src/plugins/plugin.spec.ts` — mock spreads
`requireActual` so the constant is real; sets/restores
`NX_CACHE_PROJECT_GRAPH`; snapshot updated to include the new input.

<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/73d1eed2)
<!-- polygraph-session-end -->

Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
2026-05-12 14:35:10 -04:00
Leosvel Pérez Espinosa 8d5c8182e3 fix(bundling): include tsconfig solution input for rollup (#35476)
## Current Behavior

Rollup build target defaults and inferred Rollup build targets do not
include the root `tsconfig.json` fields that TypeScript solution setup
detection reads. Changes to `extends`, `files`, or `include` in
`{workspaceRoot}/tsconfig.json` can therefore cause Rollup tasks to miss
cache invalidation.

## Expected Behavior

Rollup target defaults and inferred Rollup build targets include
`{workspaceRoot}/tsconfig.json` fields `extends`, `files`, and `include`
as task inputs.
2026-05-12 14:34:56 -04:00
Craigory Coppola 1608904e7c fix(core): add provenance check in nx console status path (#35485)
## Current Behavior
nx console's status check is missing a provenance check

## Expected Behavior
provenance is validated before installing latest nx

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
2026-05-12 14:34:30 -04:00
Optischa 159cf9b382 fix(core): bump axios to 1.16.0 for all packages (#35568)
Fix Axios CVE's:
Axios: Authentication Bypass via Prototype Pollution Gadget in
`validateStatus` Merge Strategy -
https://github.com/advisories/GHSA-w9j2-pvgh-6h63
Axios: Incomplete Fix for CVE-2025-62718 — NO_PROXY Protection Bypassed
via RFC 1122 Loopback Subnet (127.0.0.0/8) in Axios 1.15.0 -
https://github.com/advisories/GHSA-pmwg-cvhr-8vh7
Axios: Invisible JSON Response Tampering via Prototype Pollution Gadget
in `parseReviver` - https://github.com/advisories/GHSA-3w6x-2g7m-8v23
Axios has prototype pollution read-side gadgets in HTTP adapter that
allow credential injection and request hijacking -
https://github.com/advisories/GHSA-q8qp-cvcw-x6jj
Axios: Null Byte Injection via Reverse-Encoding in AxiosURLSearchParams
- https://github.com/advisories/GHSA-xhjh-pmcv-23jw
Axios: CRLF Injection in multipart/form-data body via unsanitized
blob.type in formDataToStream -
https://github.com/advisories/GHSA-445q-vr5w-6q77
Axios: no_proxy bypass via IP alias allows SSRF -
https://github.com/advisories/GHSA-m7pr-hjqh-92cm
Axios: unbounded recursion in toFormData causes DoS via deeply nested
request data - https://github.com/advisories/GHSA-62hf-57xw-28j9
Axios' HTTP adapter-streamed uploads bypass maxBodyLength when
maxRedirects: 0 - https://github.com/advisories/GHSA-5c9x-8gcm-mpgx
Axios: HTTP adapter streamed responses bypass maxContentLength -
https://github.com/advisories/GHSA-vf2m-468p-8v99
Axios: Prototype Pollution Gadgets - Response Tampering, Data
Exfiltration, and Request Hijacking -
https://github.com/advisories/GHSA-pf86-5x62-jrwf
Axios: Header Injection via Prototype Pollution -
https://github.com/advisories/GHSA-6chq-wfr3-2hj9
Axios: XSRF Token Cross-Origin Leakage via Prototype Pollution Gadget in
`withXSRFToken` Boolean Coercion -
https://github.com/advisories/GHSA-xx6v-rp6x-q39c

---------

Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
2026-05-12 13:44:50 -04:00
Jack Hsu c47b003a78 chore(misc): a/b testing init Cloud prompt (#35468)
Do A/B test against the previous messaging from March that seems to have
higher success rate.

NXC-4363
2026-05-12 13:39:07 -04:00
Jack Hsu b579c0b669 feat(nx-dev): track docs analytics for code copy, LLM prompt, YouTube (#35526)
## Current Behavior
Astro docs site tracks header nav, search, scroll depth, and 404s. Code
block copies and LLM prompt copies are not tracked. YouTube embeds lack
`enablejsapi=1`, so GA4 enhanced measurement cannot hook the player.

## Expected Behavior
Two new GTM dataLayer events: `code_block_copy` (with derived
alphanumeric `code_id`) and `llm_prompt_copy` (with `prompt_title`).
Both gated by existing production-only check. YouTube iframes get
`enablejsapi=1` patched on the client so GA4 enhanced measurement fires
`video_start` / `video_progress` / `video_complete` natively. GTM
container needs tags wired for the two new event names to forward to
GA4.

## Examples

Copy code block:
<img width="932" height="335" alt="image"
src="https://github.com/user-attachments/assets/e21e8979-0bf2-4e94-a1f6-d832897ad392"
/>

Copy LLM prompt:
<img width="1140" height="250" alt="image"
src="https://github.com/user-attachments/assets/ef4a558c-895d-49ed-8429-ead0997b9fed"
/>

Youtube play/pause/end
<img width="947" height="573" alt="image"
src="https://github.com/user-attachments/assets/dc73cea5-6bb2-4131-a545-45239a80ecc9"
/>


## Related Issue(s)
Fixes DOC-497
2026-05-01 15:53:32 -04:00
Jason Jean 83d98f7813 fix(nextjs): use cached project graph in withNx (#35475)
## Current Behavior

`@nx/next/plugins/with-nx.ts` calls `createProjectGraphAsync()` from
inside `next.config.js` evaluation. This has two negative effects:

1. **Sandbox violations.** Calling `createProjectGraphAsync` inside the
build re-runs every registered Nx plugin's `createNodesV2`. For example,
on `nx-dev:next:build` this generates 562 unexpected reads — including
548 files from `packages/nx/dist/**/*.js` (Nx core internals loaded by
the graph machinery) plus sibling project files like
`nx-dev/nx-dev-e2e/playwright.config.ts`, spec files, and
`eslint.config.mjs` files that are read by `@nx/playwright/plugin` and
`@nx/eslint/plugin` while inferring targets. None of these are real
input dependencies of the Next.js build — they are an implementation
detail of graph creation.

2. **Daemon socket leak (workaround in #34518).** The same call also
opens a daemon client socket that keeps the Node event loop alive. PR
#34518 patched this with `resetDaemonClient: true` after Jest started
hanging in #32880. The socket exists only because we are talking to the
daemon to (re)build the graph at all.

Both problems share a root cause: graph creation is being run inside the
build, when the graph has already been built and cached by the Nx task
runner before `next build` ever starts.

## Expected Behavior

`withNx` reads the already-cached graph instead of rebuilding it.

This matches the pattern used by `@nx/webpack`
(`packages/webpack/src/plugins/nx-webpack-plugin/lib/normalize-options.ts`)
and `@nx/rspack`
(`packages/rspack/src/plugins/utils/plugins/normalize-options.ts`), both
of which call `readCachedProjectGraph()` with the comment _"Since this
is invoked by the executor, the graph has already been created and
cached."_

The early-return guard already in `withNx` (no `NX_TASK_TARGET_TARGET`
env var) ensures we only reach the graph-reading branch when running
inside an Nx task, which is exactly when the cached graph is guaranteed
to exist.

This change:

- Eliminates the 562 sandbox violations on `nx-dev:next:build` (verified
locally by patching `node_modules/@nx/next/plugins/with-nx.js` and
re-running the build).
- Removes the need for `resetDaemonClient: true` since no daemon
connection is opened in the first place — also obviating the original
Jest hang.
- Speeds up `next build` slightly by skipping a full graph re-creation
pass.

## Related Issue(s)

Follow-up to #34518 / #32880 — fixes the underlying cause that the
daemon-reset workaround was treating.
2026-04-28 16:42:20 -04:00
Jason Jean 9934bb5386 fix(core): prevent daemon shutdown from cache-poisoned in-process nx loads (#35482)
## Current Behavior

On every first `nx` command run by a 22.6.x workspace once `22.7.0`
shipped to npm, the daemon spuriously shuts itself down with:

```
[Server] Daemon outdated: NX_VERSION_CHANGED
[Server] Shutting down daemon (no restart)…
Server stopped because: "NX_VERSION_CHANGED"
```

Visible symptoms in the field (#35444): "stuck on Calculating project
graph", `EPIPE` mid-task, and broken CI/CD pipelines for users who
haven't upgraded past `22.6.x`.

### Root cause

The daemon's `handleGetNxConsoleStatus` and
`handleGetConfigureAiAgentsStatus` install `nx@latest` to a temp dir and
`require()` files from that install **into the daemon's own Node
process**. Two `nx` packages now share one process — the workspace's
installed copy and the temp copy.

Inside the temp copy, `setupAiAgentsGenerator(..., inner: true)` calls
`getNxVersion()`, which calls `readModulePackageJson('nx')`, which
calls:

```ts
require.resolve('nx/package.json', { paths: getNxRequirePaths(workspaceRoot) })
```

The calling file lives inside the temp's `nx` package, whose
`package.json` has `name: "nx"` and an `exports` map. Per Node's CJS
resolver, that makes `'nx/package.json'` qualify as a **package
self-reference**, which resolves to the calling package's own
`package.json` — `/tmp/.../nx/package.json` — **ignoring the `paths`
argument**.

`Module._findPath` then writes the result to its process-wide cache, but
builds the cache key from the (workspace-rooted) `paths` argument:

```
Module._pathCache["nx/package.json\0<workspace>/.nx/installation/node_modules\0…"]
    =  /tmp/.../node_modules/nx/package.json
```

20 ms later, the daemon's own watchdog runs `getInstalledNxVersion()`
with the same `paths`, hits the polluted cache entry, and reads back the
`/tmp` `package.json` — version `22.7.0`. Compared against the daemon's
frozen `nxVersion` (`22.6.5`), they differ; `daemonIsOutdated()` returns
`'NX_VERSION_CHANGED'`; the daemon tears itself down.

The bug stayed dormant from `22.6.0` (when the in-process latest-pull
pattern shipped, #34463) until `22.7.0` was published, because while
`latest === installed` the polluted cache value matched the constant. PR
#34111 (which gave `nx`'s `package.json` an `exports` field for the
first time) is what enabled self-reference, without which `paths` would
have been honored and no pollution would have occurred.

## Expected Behavior

The daemon stays alive across `nx` commands. Pulling `nx@latest` and
running the in-process console / AI-agents checks does not poison the
resolver cache, and `getInstalledNxVersion()` keeps returning the
workspace's actual installed version.

### Fix

Replace both `require.resolve('nx/package.json', { paths })` callsites
with a direct filesystem walk over the same
`getNxRequirePaths(workspaceRoot)`. Walking the filesystem bypasses
Node's resolver entirely and is immune to `Module._pathCache` pollution.


**`packages/nx/src/ai/set-up-ai-agents/set-up-ai-agents.ts:getNxVersion`**
— the polluter. Once this ships, every existing `22.6.x` daemon picks up
the fix automatically via its next `nx@latest` pull. The daemon's own
(still-buggy) check then has nothing to misread, and stops dying —
without users having to upgrade their workspace at all.


**`packages/nx/src/daemon/is-nx-version-mismatch.ts:getInstalledNxVersion`**
— the victim. Defensive: if any future code path added to the in-process
latest-pull triggers the same `require.resolve('nx/package.json', {
paths })` pattern, the daemon's verdict on "what version is installed?"
stays correct anyway.

### Verification

Reproduced locally with a `22.6.5` workspace (yarn 4) using the original
unfixed `is-nx-version-mismatch.js`:

- **Before**: daemon shut itself down with `NX_VERSION_CHANGED` on every
first command after `22.7.0` published.
- **After** (fix published as `nx@latest` to a local Verdaccio): daemon
pulled the fixed temp, ran both inner checks (`[NX-CONSOLE]: Console
status check completed`, `[AI-AGENTS]: Agent configuration status
computation completed`), and survived three consecutive `nx` commands.
No `Daemon outdated`, no `Server stopped`, same daemon PID across
commands.

This empirically confirms the `set-up-ai-agents` fix retroactively heals
`22.6.x` users without them touching their workspace.

## Related Issue(s)

Fixes #35444
2026-04-28 16:41:28 -04:00
Craigory Coppola a197001dc8 chore(repo): include native dts header in build-native inputs (#35481) 2026-04-28 16:41:27 -04:00
Jason Jean 37827bdfdc chore(repo): update nx to 23.0.0-beta.1 (#35474)
Updating Nx from 23.0.0-beta.0 to 23.0.0-beta.1
2026-04-28 16:41:26 -04:00
Leosvel Pérez Espinosa 8b8aa98088 fix(core): use require for global to local Nx handoff so Windows drive paths work (#35478) 2026-04-28 13:38:20 -04:00
ShwethaSundar f1a158dbaf fix(release): handle short and full project names in commit scopes (#34219) 2026-04-28 13:38:20 -04:00
Craigory Coppola f9388f3531 fix(core): start TUI event reader synchronously in enter() to prevent stdin race (#35465)
Since the napi v2→v3 migration (#34619) moved Tui::start() out of
enter() and
into an async block, there is a window between enable_raw_mode() and
EventStream::new() during which bytes can sit in the kernel tty buffer
and
later be parsed as keypresses by the reader. Two known sources:

1. Tail bytes of the OSC 11 color-scheme reply that terminal-colorsaurus
   doesn't fully consume (the reply contains '/' separators, e.g.
   `\x1b]11;rgb:RRRR/GGGG/BBBB\x07`, which trigger filter mode and feed
   hex digits as filter text).
2. Leftover input from the analytics prompt (#34144, new in v22.6) that
   uses enquirer/raw-mode and may not drain stdin completely.

Either path manifests as the TUI booting with a bogus filter pre-applied
that hides every task.

start() was moved out because tokio::spawn requires a Tokio runtime
context,
and the sync __init NAPI method runs on the JS main thread without one.
Switching the inner spawn to napi::bindgen_prelude::spawn (which uses
napi's
static runtime and works from any thread — already used elsewhere for
the
same reason) lets enter() call start() synchronously again, so
EventStream
exists before enter() returns and consumes those bytes itself.

https://claude.ai/code/session_01RwrzRzTCZ7k8Uzw2xux6kM

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-28 13:38:20 -04:00
Leosvel Pérez Espinosa b02e138e32 fix(misc): resolve pnpm catalog: refs in version lookups (#35459)
## Current Behavior

`@nx/react:application` (and any path that calls `@nx/vite` or
`@nx/vitest` `ensureDependencies`) crashes when the root `package.json`
declares `vite` via a pnpm catalog alias such as `"vite": "catalog:"` or
`"vite": "catalog:tooling"`:

```
NX   Invalid version. Must be a string. Got type "object".
TypeError ... at new SemVer ... at major ... at ensureDependencies
```

`@nx/storybook`'s `convert-to-inferred` migration is broken in the same
way: `getInstalledPackageVersion` reads dependencies straight from
`package.json` with no catalog resolution, so a literal `catalog:` value
reaches `coerce` and throws via `major(null)` in
`getInstalledPackageVersionInfo`.

## Expected Behavior

- pnpm catalog aliases in `package.json` are resolved through
`pnpm-workspace.yaml` (default and named catalogs) before semver
parsing.
- When the resolved/raw range is not coercible (missing catalog entry,
missing `pnpm-workspace.yaml`, `workspace:*`, `link:`, `file:`, `git:`),
the code falls back to the existing default rather than throwing.
- Existing v4-vs-v6 `@vitejs/plugin-react` selection for plain semver
ranges is preserved.

## Changes

- `@nx/vite` / `@nx/vitest`: `ensureDependencies` now reads `vite` via
`getDependencyVersionFromPackageJson` (catalog-aware) and null-guards
the `coerce` result.
- `@nx/storybook`: `getInstalledPackageVersion` now uses
`getDependencyVersionFromPackageJson`, and
`getInstalledPackageVersionInfo` null-guards `coerce`.
- Tests added in `packages/vite/src/utils/ensure-dependencies.spec.ts`
for named catalog, default catalog, missing catalog entry, and
unparseable ranges (`workspace:*`).

## Related Issue(s)

Fixes #35453
2026-04-28 13:38:20 -04:00
Leosvel Pérez Espinosa 362fb03be8 fix(node): include tsconfig input in node-app esbuild scaffold (#35466)
## Current Behavior

The node app generator with `bundler=esbuild` does not include the
field-scoped `tsconfig.json` input needed for proper cache hashing. This
means builds may return stale cached results when the workspace's
`tsconfig.json` is edited (e.g., changing `extends`, `files`, or
`include` fields).

## Expected Behavior

The node app generator now includes `TS_SOLUTION_SETUP_TSCONFIG_INPUT`
as an input for `@nx/esbuild:esbuild` targets, ensuring cache hashes
properly reflect changes to the workspace root `tsconfig.json`.

Additionally, exports `TS_SOLUTION_SETUP_TSCONFIG_INPUT` from `@nx/js`
so other Nx packages can reuse this constant for their own executors and
plugins.
2026-04-28 13:38:20 -04:00
Leosvel Pérez Espinosa f0cb7a24ba fix(misc): exclude stories and specs from tailwind content scanning (#35470)
## Current Behavior

The default tailwind content glob in `@nx/react/tailwind` and
`@nx/vue/tailwind` helpers, plus their `setup-tailwind` generator
templates, is meant to skip `*.stories.*` and `*.spec.*` files, but the
leading `*` in the `!()` extglob makes the negation a no-op — story and
spec classes end up scanned alongside production code.

## Expected Behavior

Stories and specs are excluded from tailwind content scanning, matching
the intent of the existing pattern.

## Technical details

The current pattern `*!(*.stories|*.spec).{...}` fails because the
leading `*` lets the matcher split the input across `*` and `!()` (e.g.,
`foo.stories` → `*` = `foo.`, `!()` = `stories`, which doesn't end in
`.stories`), so the file matches the glob and gets included. Removing
the leading `*` makes `!(*.stories|*.spec)` apply to the full filename
portion. Affects:

- `packages/react/tailwind.ts` (helper default)
- `packages/vue/tailwind.ts` (helper default)
-
`packages/react/src/generators/setup-tailwind/files/tailwind.config.js__tmpl__`
-
`packages/vue/src/generators/setup-tailwind/files/tailwind.config.js.template`
2026-04-28 13:38:20 -04:00
Craigory Coppola 964a3b67cb fix(core): keep continuous children alive when nx:noop orchestrator completes (#35388) 2026-04-28 13:20:52 -04:00
Jack Hsu 6a368b845b fix(core): surface ./nx --version stderr and force devDeps install (#35469) 2026-04-28 13:20:52 -04:00
Jack Hsu a780ef4a47 docs(nextjs): clarify Vercel root directory and document NEXT_PUBLIC_ cache issue (#35433)
Someone got confused because the build is restoring from cache when the
env var the app uses is different from prod vs local. We should mention
that env vars need to be added as inputs.

Fixes #33331

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
2026-04-28 13:20:52 -04:00
Leosvel Pérez Espinosa 22158cbc61 fix(js): include extended tsconfigs from project references in typecheck inputs (#35457)
## Current Behavior

The `@nx/js/typescript` plugin walks the project reference chain to
collect tsconfig paths as inputs for the inferred `typecheck` target,
but does not follow `extends` chains on those referenced tsconfigs. When
`tsc --build` walks into an external project reference whose
`tsconfig.lib.json` extends a sibling tsconfig (e.g. `./tsconfig.json`),
that extended file is read at compile time but is not declared as an
input — causing sandbox violations and incorrect cache keys for the
consumer's `typecheck` task.

## Expected Behavior

The plugin walks `extends` chains for every tsconfig visited during the
reference chain traversal, emitting `^{projectRoot}/...` patterns for
the extended files alongside the existing reference-chain patterns.
Tasks consuming external project references with `extends` chains now
correctly declare the full set of tsconfig files tsc reads.

## Changes

`getExternalProjectReferenceTsconfigPatterns` now walks `extends` for
each tsconfig visited during the worklist traversal. The walk:

- Reuses the same `visited` set as the reference-chain walk (no
redundant work).
- Reuses the cached parsed tsconfig data (`tsConfigCacheData`) and
`getConfigContext` cache (no extra parsing).
- Pushes extended files onto the same worklist so extends-of-extends and
references-from-extends are covered transitively.
- Skips workspace-root files (no owning project) — those are already
covered by the local project's existing extends walk.

Patterns emit in the same `^{projectRoot}/relPath` form as the rest of
the function, so the existing transitive resolution through the project
graph applies unchanged.

A focused unit test pins the new behavior: a project with an external
ref whose `tsconfig.lib.json` extends a same-project
`tsconfig.shared.json` correctly emits
`^{projectRoot}/tsconfig.shared.json` as an input.
2026-04-28 13:20:52 -04:00
Jason Jean 2f81805f51 fix(core): consider virtual trees in multiGlobWithWorkspaceContext (#35447)
## Current Behavior

`multiGlobWithWorkspaceContext` in
`packages/nx/src/utils/workspace-context.ts` is missing the
`workspaceRoot === '/virtual'` short-circuit that its sibling
`globWithWorkspaceContext` already has (added in #31805).

When the Nx daemon is running and a generator test uses
`createTreeWithEmptyWorkspace` (which sets the root to the `/virtual`
sentinel), `multiGlobWithWorkspaceContext` forwards to the daemon
attached to the real workspace, gets back real paths, and then plugins
(e.g. project-graph-inferring plugins) ENOENT trying to read those paths
at `/virtual/<path>`.

This silently breaks generator test suites for any workspace that has
project-graph-inferring plugins (which is most modern Nx workspaces).

## Expected Behavior

`multiGlobWithWorkspaceContext` should bypass the daemon when called
with `workspaceRoot === '/virtual'`, just like
`globWithWorkspaceContext` does. Generator tests run against the
in-memory virtual tree without any daemon round-trip.

## Related Issue(s)

Fixes #35373

Related: #32588 reported the same symptom via `@nx/cypress` in Sept 2025
and was auto-closed stale; this PR addresses the root cause.

The original `globWithWorkspaceContext` `/virtual` guard was added in
#31805 — this PR mirrors it on the multi-glob path.
2026-04-28 13:20:52 -04:00
Leosvel Pérez Espinosa b512ae6335 chore(repo): resolve sandbox violations on graph typecheck tasks (#35458)
## Current Behavior

Four `typecheck` tasks in the Nx repo — `graph-client`, `graph-migrate`,
`graph-project-details`, `graph-ui-project-details` — trigger sandbox
violations on staging. tsc reads files from `packages/devkit`,
`packages/nx`, and `nx-dev/ui-fence` that are not declared as inputs,
and rebuilds `nx-dev/ui-fence` inside the consumer's sandbox, producing
cross-project writes that cascade down the graph chain.

## Expected Behavior

The four graph typecheck tasks have all required cross-project tsconfigs
declared as inputs and their cross-project source/output reads covered
by task-level dependencies. tsc no longer rebuilds `nx-dev/ui-fence`
inside consumer sandboxes. Sandbox runs come back clean.

## Changes

Three independent causes, addressed together.

1. **`nx-dev/ui-fence` had no upstream `typecheck` task.** Its
`.d.ts`/`tsbuildinfo` outputs weren't being materialized, so consumers
rebuilt it inside their own sandboxes — causing cross-project writes
that cascaded down the graph chain. Added `nx-dev/ui-fence/**` to the
existing `@nx/js/typescript` plugin block in `nx.json`. ui-fence now has
its own task, its outputs flow through `dependentTasksOutputFiles`, and
the cascading writes disappear.

2. **`graph-ui-project-details` declares `implicitDependencies:
["!devkit"]`** to break a project graph cycle, which removes devkit (and
transitively nx) from its nx project graph. The plugin's
`^{projectRoot}/...` patterns walk the project graph, so they never
reach `packages/devkit/tsconfig.lib.json` or
`packages/nx/tsconfig.lib.json` — even though tsc walks into them via
tsconfig project references. Addressed by:
- Adding `packages/devkit/tsconfig.lib.json` as an explicit project
reference in `graph/ui-project-details/tsconfig.lib.json` (with
`nx.sync.ignoredReferences` to keep typescript-sync from stripping it).
- Adding a task-level `dependsOn` on `devkit:build-base` so the task
graph actually has devkit producing outputs.
- Declaring `{workspaceRoot}/packages/devkit/tsconfig.lib.json` and
`{workspaceRoot}/packages/nx/tsconfig.lib.json` as explicit inputs on
the four graph projects' typecheck overrides.

3. **`nx-dev/ui-fence/tsconfig.lib.json` extends `./tsconfig.json`** —
previously the plugin emitted `^{projectRoot}/tsconfig.lib.json` only,
so the extended `tsconfig.json` was an undeclared read. Resolved by
#35457, which makes the plugin walk `extends` chains in external project
references — no per-project workaround needed here.

The four `project.json` overrides use `"..."` spread tokens for `inputs`
to inherit the plugin-inferred input set rather than redeclaring it,
keeping the diff minimal and the intent ("plugin defaults plus these
specific cross-graph-cut tsconfigs") clear.
2026-04-28 13:20:52 -04:00
Leosvel Pérez Espinosa fac25a2062 fix(testing): convert executor-based jest.config.ts and preserve type-only imports (#35286)
## Current Behavior

`nx migrate --run-migrations` crashes on workspaces that use the
`@nx/jest:jest` executor (via `targetDefaults`) instead of
`@nx/jest/plugin`:

```
NX   Failed to run replace-removed-matcher-aliases-v22-3 from @nx/jest. This workspace is NOT up to date!

NX   Jest: Failed to parse the TypeScript config file .../libs/.../jest.config.ts

  ReferenceError: __dirname is not defined in ES module scope
```

The `convert-jest-config-to-cjs` migration (update-22-2-0) is gated on
`@nx/jest/plugin` being registered in `nx.json`, so executor-based
workspaces skip it entirely. Their `jest.config.ts` files (often a mix
of ESM syntax and CJS globals like `__dirname`) never get converted. The
later `replace-removed-matcher-aliases-v22-3` migration then calls
`jest-config.readConfig` on every `jest.config.ts`, which on Node
22+/24+ with native type-stripping reparses the file as ESM and crashes.

Separately, the conversion logic also didn't handle `import type`
declarations — it rewrote them to `const { X } = require('mod')`, which
unnecessarily pulls the module at runtime and drops type references the
IDE/tsc relied on. For types-only specifiers, it could even crash at
runtime.

## Expected Behavior

`convert-jest-config-to-cjs` runs for every `jest.config.ts` whose
project is CommonJS (plugin registration is no longer required), so
executor-based setups are covered. The `type: module` guard still skips
ESM projects.

Type-only imports are preserved:

- `import type { Config } from 'jest'` — left untouched (Node's
type-stripping erases it, so it doesn't force ESM parsing at runtime).
- `import { type Foo, bar } from 'mod'` — split into `import type { Foo
} from 'mod'` plus `const { bar } = require('mod')`.
- Renames (`import { type Foo as JestFoo, run } from 'mod'`) preserved.

## Related Issue(s)

Fixes #34593
2026-04-28 13:20:52 -04:00
Leosvel Pérez Espinosa d8cf2104fb fix(linter): detect root lint target added in same generator run (#35296)
## Current Behavior

When a generator adds a lint target to the root project and then creates
a new non-root project in the same run, the root eslint config is not
split into a base config on that run. Subsequent projects created
afterwards (in the same run or in separate runs) end up wired
incorrectly, and users have to re-run the generator to get the migration
to actually happen.

## Expected Behavior

The root eslint config is split into a base config on the first run
where a non-root project is created alongside a root lint target, so
projects are wired correctly without requiring a second invocation.

## Implementation Notes

`isMigrationToMonorepoNeeded` previously relied on
`createProjectGraphAsync()` to detect the root lint target. The project
graph reflects the filesystem at its last rebuild and misses targets
written to the tree earlier in the same generator run.

The check now reads the tree first via `getProjects(tree)`. The project
graph is only consulted as a fallback when `@nx/eslint/plugin` is
registered, since plugin-inferred targets do not appear on the tree —
preserving the behavior introduced in #23147.

### Known gaps (not addressed)

The symmetric in-flight cases on the inferred branch remain open:

- a root eslint config file written during the same generator run with
`@nx/eslint/plugin` already registered, and
- `@nx/eslint/plugin` registered in `nx.json` during the same run with a
root config already on disk.

Closing them would require reimplementing `@nx/eslint/plugin`'s
`createNodes` pipeline against the tree — the maintenance burden #23147
explicitly avoided. No known user report exercises those paths today.

## Related Issue(s)

Fixes #34531
2026-04-28 13:20:52 -04:00
Leosvel Pérez Espinosa a1b4f7b8e5 fix(core): provide actionable feedback when running migrations and pre-install fails with npm peer dep errors (#33961)
## Current Behavior

When running `nx migrate --run-migrations` in a workspace with `npm` as
the package manager, sometimes the automatic package installation
performed by the command can fail due to peer dependency constraint
violations. In such cases, no actionable feedback is provided to the
user to help resolve the issue.

## Expected Behavior

When running `nx migrate --run-migrations` in a workspace with `npm` as
the package manager, and the automatic package installation performed by
the command fails, Nx should provide actionable feedback to resolve the
issue and to re-run the command while skipping the package installation.

## Related Issue(s)

Fixes #33942
2026-04-28 13:20:52 -04:00
Caleb Ukle 022309d870 docs(nx-cloud): update custom GH app steps for clarity (#35451)
clarify permissions must precede webhook event subscription

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 13:20:52 -04:00
Jason Jean 3142c9d25d chore(module-federation): re-enable webpack module federation e2e suites
Reverts the temporary describe.skip blocks added in #35214. The
@module-federation/enhanced dependency has been bumped (now 2.3.3) and
upstream webpack compatibility is expected to be restored, so we let CI
verify whether the suites pass again.

NXC-4220
2026-04-28 13:20:52 -04:00
Jason Jean eb2fa8ced4 fix(detox): generate valid JSON in .detoxrc for non-expo apps
The detox application generator's .detoxrc.json template left a trailing
comma after the last entry of the apps and configurations blocks when
the optional expo-only entries were not emitted, producing invalid JSON
for react-native apps. Move the comma inside the EJS conditional so it
is only included when the following expo entry is also emitted.
2026-04-28 13:20:52 -04:00
Jack Hsu 5095b4be7d fix(core): exclude hyperfine env vars from daemon env reflection
## Current Behavior
Daemon env reflection sends filtered process.env on first message. Server compares key-by-key; any diff invalidates the project graph cache and forwards env to plugin workers. hyperfine rotates HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET per iteration, so every run busts the graph cache and respawns plugin workers (~170ms overhead).

## Expected Behavior
hyperfine env vars do not affect graph construction. Filter them alongside existing editor, terminal, and CI runner prefixes.
2026-04-28 13:20:52 -04:00
FrozenPandaz f2c0bb67de chore(repo): update nx to 23.0.0-beta.0 2026-04-28 13:20:52 -04:00
Craigory Coppola 48e46004d6 fix(core): prevent spinner flicker when sync applying (#35445) 2026-04-28 13:20:51 -04:00
454 changed files with 18956 additions and 15201 deletions
@@ -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 (P1P29). |
| 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 12 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 12 (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 AF 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 AF 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 ~100500 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 12 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 (AF)
### 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 12 (discovery / read-only)
**Looks like:** `git checkout -b <some-branch>` before the user has approved Phase 3 edits.
**Why wrong:** Phase 12 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 12 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 v8v10 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 v5v8), 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` v5v8.
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).
+12 -3
View File
@@ -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 }}
+11
View File
@@ -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: |
+22 -10
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+60 -54
View File
@@ -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:
+3
View File
@@ -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
+18 -8
View File
@@ -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
-9
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<UseSharedCompilation>false</UseSharedCompilation>
</PropertyGroup>
</Project>
+12 -17
View File
@@ -60,22 +60,17 @@ If you want to file a bug or submit a PR, read up on our [guidelines for contrib
| ![James Henry](https://avatars.githubusercontent.com/u/900523?s=160&v=4) | ![Jon Cammisuli](https://avatars2.githubusercontent.com/u/4332460?s=160) | ![Max Kless](https://avatars.githubusercontent.com/u/34165455?s=160) | ![Juri Strumpflohner](https://avatars1.githubusercontent.com/u/542458?s=160) |
| [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 |
| ------------------------------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| ![Philip Fulcher](https://avatars1.githubusercontent.com/u/1536471?s=160) | ![Caleb Ukle](https://avatars.githubusercontent.com/u/23272162?s=160) | ![Colum Ferry](https://avatars.githubusercontent.com/u/12140467?s=160) | ![Steven Nance](https://avatars.githubusercontent.com/u/1036428?s=160) |
| [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 |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| ![Caleb Ukle](https://avatars.githubusercontent.com/u/23272162?s=160) | ![Steven Nance](https://avatars.githubusercontent.com/u/1036428?s=160) | ![Miroslav Jonaš](https://avatars.githubusercontent.com/u/881612?s=160) | ![Leosvel Pérez Espinosa](https://avatars.githubusercontent.com/u/12051310?s=160) |
| [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 |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| ![Miroslav Jonaš](https://avatars.githubusercontent.com/u/881612?s=160) | ![Leosvel Pérez Espinosa](https://avatars.githubusercontent.com/u/12051310?s=160) | ![Zachary DeRose](https://avatars.githubusercontent.com/u/3788405?s=160) | ![Craigory Coppola](https://avatars.githubusercontent.com/u/6933928?s=160) |
| [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 |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| ![Zachary DeRose](https://avatars.githubusercontent.com/u/3788405?s=160) | ![Craigory Coppola](https://avatars.githubusercontent.com/u/6933928?s=160) | ![Chau Tran](https://avatars.githubusercontent.com/u/25516557?s=160) | ![Nicole Oliver](https://avatars.githubusercontent.com/u/4440385?s=160) |
| [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 |
| -------------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| ![Chau Tran](https://avatars.githubusercontent.com/u/25516557?s=160) | ![Nicole Oliver](https://avatars.githubusercontent.com/u/4440385?s=160) | ![Rares Matei](https://avatars.githubusercontent.com/u/5975076?s=160) | ![Altan Stalker](https://avatars.githubusercontent.com/u/6324206?s=160) |
| [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 |
| ----------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------- |
| ![Josh VanAllen](https://avatars.githubusercontent.com/u/5290334?s=160) | ![Austin Fahsl](https://avatars.githubusercontent.com/u/6913035?s=160) | ![Louie Weng](https://avatars.githubusercontent.com/u/56288712?s=160) |
| [joshvanallen](https://github.com/joshvanallen) | [fahslaj](https://github.com/fahslaj) | [lourw](https://github.com/lourw) |
| Rares Matei | Altan Stalker | Josh VanAllen | Louie Weng |
| --------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------- |
| ![Rares Matei](https://avatars.githubusercontent.com/u/5975076?s=160) | ![Altan Stalker](https://avatars.githubusercontent.com/u/6324206?s=160) | ![Josh VanAllen](https://avatars.githubusercontent.com/u/5290334?s=160) | ![Louie Weng](https://avatars.githubusercontent.com/u/56288712?s=160) |
| [rarmatei](https://github.com/rarmatei) | [StalkAltan](https://github.com/StalkAltan) | [joshvanallen](https://github.com/joshvanallen) | [lourw](https://github.com/lourw) |
+7
View File
@@ -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
+24
View File
@@ -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,
}) => {
+52
View File
@@ -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
View File
@@ -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",
+27
View File
@@ -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
View File
@@ -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:
![Step 5](../../../../assets/enterprise/single-tenant/webhook.png)
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:
![Step 5.1](../../../../assets/enterprise/single-tenant/webhook_events.png)
## Record the app credentials
Once you create the app, keep a note of the Client ID and App ID:
![Step 6](../../../../assets/enterprise/single-tenant/github_custom_app_step_6.avif)
@@ -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.
![Agent resource usage table showing per-agent average and maximum CPU and memory](../../../../assets/guides/nx-cloud/agent-resource-usage-table.png)
### 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.
![Resource usage over time showing memory and CPU charts by task](../../../../assets/guides/nx-cloud/resource-chart-details.png)
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).
![Stacked view showing total resource usage](../../../../assets/guides/nx-cloud/resource-stacked-chart-view.png)
- **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).
![Using the legend to focus on specific tasks](../../../../assets/guides/nx-cloud/resource-chart-legend.png)
- **Timeline scrubber** - jump to a point in time or zoom in on a spike.
![Timeline scrubber for navigating resource usage over time](../../../../assets/guides/nx-cloud/resource-chart-scrubber.jpg)
- **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
![Sandbox analysis tab showing process tree with unexpected reads and writes highlighted](../../../../assets/features/sandboxing-analysis.png)
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.
![View raw sandbox report button](../../../../assets/features/sandboxing-raw-report.png)
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**.
![Nx Cloud settings sidebar showing General settings](../../../../assets/features/sandboxing-settings-sidebar.png)
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.
![Sandboxing enforcement mode setting with Strict, Warning, and Off options](../../../../assets/features/sandboxing-settings.png)
## 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)
@@ -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)
@@ -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 %}
@@ -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)
@@ -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).
![Access Control Settings Page](../../../../assets/nx-cloud/access-control-settings.avif)
{% 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.
![](../../../../assets/nx-cloud/access-control-settings.avif)
{% /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.
![Resource usage summary showing agents with CPU and memory stats](../../../../assets/guides/nx-cloud/agent-resource-usage-table.png)
### 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.
![Resource usage details showing memory and CPU by process](../../../../assets/guides/nx-cloud/resource-chart-details.png)
### 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
![Using the legend to focus on specific tasks](../../../../assets/guides/nx-cloud/resource-chart-legend.png)
- **Timeline scrubber**: Use the scrubber at the bottom to jump to specific points in time or zoom in on peak usage
![Timeline scrubber for navigating resource usage over time](../../../../assets/guides/nx-cloud/resource-chart-scrubber.jpg)
- **View modes**: Switch between "stacked" view (total usage at any time) and "individual" view (each process separately)
![Stacked view showing total resource usage](../../../../assets/guides/nx-cloud/resource-stacked-chart-view.png)
- **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.
![Manual upload interface in the Nx Cloud analysis screen](../../../../assets/guides/nx-cloud/resource-usage-manual-upload.png)
@@ -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).
@@ -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" /%}
@@ -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
![Existing Vercel Project](../../../../../assets/guides/next/next-deploy-vercel-2.png)
## 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));
+18 -14
View File
@@ -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,
};
}
+7 -3
View File
@@ -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 */
+7 -1
View File
@@ -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",
+3 -1
View File
@@ -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');
+10
View File
@@ -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', () => {
+13 -5
View File
@@ -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();
});
}
+26 -4
View File
@@ -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 =
+117 -13
View File
@@ -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`));
});
});
});
+15
View File
@@ -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');
+15 -4
View File
@@ -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');
});
});
+4 -5
View File
@@ -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 -1
View File
@@ -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