Document practical patterns and caveats for keeping createNodesV2 plugins
fast: prefer the batched v2 API, cache results to disk with a content hash,
hoist shared work out of the per-file loop, load configs in parallel, keep
globs narrow and output deterministic, and avoid per-file process spawning.
Also covers daemon/cache dev overrides and diagnosing slow graph creation
with NX_PERF_LOGGING and nx report.
Generated with [Linear](https://linear.app/nxdev/issue/DOC-516/document-performant-createnodes-v2-plugin-patterns#agent-session-07d2f00a)
Co-authored-by: linear-code[bot] <222613912+linear-code[bot]@users.noreply.github.com>
## Current Behavior
Workspaces reference the `dev.nx.gradle.project-graph` Gradle plugin at
version `0.1.21`.
## Expected Behavior
Bump the plugin to `0.1.22`. This release carries the deterministic
project-configuration-hash fix from #35972 (sorting
`options.includeDependsOnTasks` so the `ProjectConfiguration` hash no
longer drifts between JVM runs).
The bump updates the version constant, the plugin's `build.gradle.kts`,
and adds a migration (`change-plugin-version-0-1-22`, gated at
`23.0.0-rc.2`) that updates existing workspaces' version catalogs and
`build.gradle(.kts)` files on `nx migrate`.
> Note: the published `0.1.22` plugin artifact must include #35972
before this is released.
## Related Issue(s)
Follow-up to #35972 (the source fix).
## Current Behavior
The `@nx/gradle` project-graph plugin infers a
`dependentTasksOutputFiles: "**/*.bin"` input for any task that depends
on a Java/Kotlin compile task. That happens because the compile tasks
declare their incremental-compilation state as task outputs, and the
plugin consolidates dependent-output file extensions into globs:
- `compileJava` / `compileTestJava` →
`build/tmp/<task>/previous-compilation-data.bin`
- `compileKotlin` / `compileTestKotlin` →
`build/kotlin/<task>/cacheable` and `…/classpath-snapshot` (`*.bin`)
These `.bin` files are the compilers' incremental bookkeeping —
rewritten on **every** compile and embedding **machine-specific absolute
paths**. No downstream task consumes them. As a result, every consuming
task (`test`, `jar`, `classes`, `testClasses`, `javadoc`,
`validatePlugins`, …) gets a hash input that changes on every build and
never matches across machines/agents, so those tasks miss the cache
constantly.
## Expected Behavior
The `.bin` incremental-compilation state is excluded from the inferred
`dependentTasksOutputFiles` inputs. Consuming tasks still hash the real
`.class`/`.jar` outputs, so cache correctness is preserved while the
spurious churn is removed. A unit test asserts a dependent task's `.bin`
output never produces a `**/*.bin` input (while `.class` still does).
Note: this changes the Gradle plugin source, so it takes effect once
shipped via a `dev.nx.gradle.project-graph` version bump.
## Related Issue(s)
Found during a Gradle project-graph hash-drift investigation; no
separate GitHub issue.
## Current Behavior
The daily **NPM Audit** workflow (`.github/workflows/npm-audit.yml`,
which runs `pnpm dlx audit-ci --critical`) is failing on a critical
advisory:
-
**[GHSA-w7jw-789q-3m8p](https://github.com/advisories/GHSA-w7jw-789q-3m8p)**
— `shell-quote`'s `quote()` does not escape newlines in object `.op`
values.
- Vulnerable range: `>= 1.1.0, <= 1.8.3`; patched in `1.8.4`.
- The lockfile resolved `shell-quote@1.8.3`, pulled in transitively
(primarily via `webpack-dev-server > launch-editor > shell-quote`, also
`react-devtools-core`).
Failing run: https://github.com/nrwl/nx/actions/runs/27386736287
## Expected Behavior
`shell-quote` is pinned to the patched `^1.8.4` via a pnpm override, so
all consumers resolve the safe version and the audit passes with
`critical: 0`.
Verified locally with the exact CI command:
```
pnpm dlx audit-ci --critical --report-type summary
→ "critical": 0 → Passed pnpm security audit.
```
`shell-quote@1.8.4` (published 2026-05-22) clears the repo's
`minimumReleaseAge` gate.
## Related Issue(s)
Fixes the failing scheduled NPM Audit workflow.
## Current Behavior
The `@nx/gradle` project-graph plugin produces a non-deterministic
project configuration. For each target it emits
`options.includeDependsOnTasks` in the iteration order of a set that is
populated by walking Gradle's internal dependency containers
(`findProviderBasedDependencies` → lifecycle/input-property providers).
That iteration order follows JVM identity hashcodes, so it changes on
every fresh JVM.
Nx hashes a target's `options` verbatim (JSON string, unsorted) into the
project's `ProjectConfiguration` hash component. As a result, the
`ProjectConfiguration` hash drifts between otherwise-identical runs, and
tasks miss the cache on rerun even though nothing changed.
Reproduced by running the report task in 5 fresh JVMs: the
`gradle-project-graph` project produced 5 distinct hashed
configurations, differing only in `options.includeDependsOnTasks`
ordering.
## Expected Behavior
The project configuration is deterministic: identical inputs produce the
same `ProjectConfiguration` hash across runs, so task caching works on
reruns. Sorting `includeDependsOnTasks` collapses all 5 runs to a single
stable value.
A regression test (`processTask emits includeDependsOnTasks in sorted
order`) guards against reintroducing the unsorted ordering.
## Related Issue(s)
Caught via Nx Cloud task comparison (project configuration hash drift on
rerun); no separate GitHub issue.
## Current Behavior
The workspace pins nx `23.0.0-rc.0`.
## Expected Behavior
The workspace is upgraded to nx `23.0.0-rc.1` via `nx migrate`. This
single release-candidate step is **dependency-only** — `nx migrate`
generated no `migrations.json`, so there are no code migrations to run.
Only `package.json` (`nx` + `@nx/*` → rc.1) and `pnpm-lock.yaml` change.
## Related Issue(s)
N/A — routine version bump. Part of a coordinated 5-repo migration to nx
23.0.0-rc.1.
<!-- polygraph-session-start -->
---
[View session information
↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/Migrate-Nx-default-repos-to-23.0.0-rc.1-403697eb)
<!-- polygraph-session-end -->
## Current Behavior
When a package manager's minimum-release-age (cooldown) policy is
active, `nx migrate` resolves dist-tags through per-package-manager
emulation, and degrading a too-new tag produced inconsistent, often
nonsensical results. `nx migrate next` could resolve to an internal
`pr.*` preview build or a years-old release candidate; the npm path
diverged from what npm itself installs; and yarn errored outright. Each
of npm, pnpm, yarn, and bun carried its own tag-degrade implementation.
## Expected Behavior
Dist-tag degrade is unified into a single rule across all four package
managers. When the resolved tag target is within the cooldown window,
the candidate pool is every version at or below it that is stable, in
the target's own prerelease channel, or on a lower rung of an explicit
channel ladder (`alpha < beta < rc`). That pool is ordered, and the
first compliant version wins:
- Prereleases of the target's exact `major.minor.patch` come first — own
channel, then lower ladder rungs.
- Then everything else, ordered by most recently published.
So `latest` (or any stable tag) lands on the newest compliant stable. A
prerelease tag keeps a compliant prerelease of the release it points at:
every same-line release candidate is exhausted first, and a blocked
`rc.0` with no older rc sibling falls to a same-line `beta.x` rather
than skipping back to the previous stable. A newer-published cross-line
backport can't outrank the same-line beta, and the degrade never climbs
the ladder upward. Channels with no place on the ladder (such as the
internal `pr` builds, or `canary`) stay walled off entirely. `next` is
deliberately left off the ladder: it is pre-rc in some ecosystems
(Angular) but a rolling dev snapshot in others — exactly the class of
build a degrade must never land on. The channel is derived generically
from a version's prerelease identifier, so it works for any package's
naming convention.
## Implementation Details
A shared `degradeTagToCompliant` helper replaces npm's `<=tagTarget`
recursion, pnpm's same-major degrade (and its deprecation tie-break
machinery), yarn's latest-only walk-down, and bun's channel walk. It
builds the pool, orders it (same-line own-channel, then same-line
lower-rung, then by publish date), and returns the first version that
passes the caller's maturity test; each package manager supplies only
that test and its own violation shape. The helper guards against
non-semver dist-tag targets and registry version entries
(`semver.compare` previously threw, and the migrate consumer swallows
unknown errors into a real-install fallback).
The cleanup also drops the now-dead `latestTagDegrade` behavior axis and
the redundant pnpm 10.20 behavior row, along with the tests that pinned
the old version-specific behavior. The npm policy-reader specs are
isolated from the host's real `~/.npmrc` and reuse the real `.npmrc`
parser (`parseNpmrcContent`) instead of a hand-copied mirror, and npm's
tag-path ENOVERSIONS branch is now covered.
<!-- polygraph-session-start -->
---
[View session information
↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/fix-nx-migrate-min-release-99acf946)
<!-- polygraph-session-end -->
---------
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
## Current Behavior
- The `update-all-repos` script fails to find its config file. Since the
build moved off the workspace-root `dist/` (#35915), `CONFIG_FILE` in
`update-repo.ts` and `setup-repos.ts` resolves to
`tools/tools/update-repos/config/repos.json`, which does not exist.
- `nx migrate` runs with the default (`latest`) migrate CLI version.
- Migrations run without the `--agentic` flag.
## Expected Behavior
- `CONFIG_FILE` resolves relative to the package root (two levels up
from the compiled `dist/src` output), so the scripts find
`tools/update-repos/config/repos.json` again.
- Both migrate invocations run with `NX_MIGRATE_CLI_VERSION=next`, so
the next version of the migrate CLI drives the flow. The env var is
passed through the spawn environment because commands are wrapped in
`mise exec --`, which would treat a `VAR=x` prefix as a program name.
- `--agentic` is passed to `nx migrate --run-migrations`.
## Related Issue(s)
N/A — tooling fix found while running the update-all-repos script.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/Fix-update-all-repos-tooling-script-199f7e9b)
<!-- polygraph-session-end -->
## Current Behavior
Nx Cloud distributed-execution workers each open their own connection to
the Nx daemon and send their full (filtered) process environment with
messages like `GET_ESTIMATED_TASK_TIMINGS`. Per-worker Nx Cloud vars
(`NX_CLOUD_WORKER_ID`, `NX_CLOUD_WORKER_INDEX`, `NX_CLOUD_EXECUTION_ID`,
`NX_CLOUD_ORCHESTRATOR_SOCKET`) are not excluded from the env the daemon
reflects, so they differ from the env the daemon was started with — and
differ between workers. Every worker that talks to the daemon rewrites
the daemon env and invalidates the project-graph cache, forcing a full
graph recompute on each worker message (`Graph recompute necessary due
to env variable refresh`). The daemon also does not log which keys
changed, so the churn is hard to diagnose.
## Expected Behavior
`NX_CLOUD_`-prefixed vars are excluded from the env reflected by the
daemon (they cannot affect the project graph), so connecting workers no
longer trigger graph recomputes. The daemon also logs the changed env
keys when a refresh does occur, to make future env-driven recompute
churn diagnosable.
## Related Issue(s)
Surfaced via staging DTE agent logs (no GitHub issue).
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nx-cloud-dte-debug-aee8e394)
<!-- polygraph-session-end -->
## Current Behavior
The workspace is on nx `23.0.0-beta.24`.
## Expected Behavior
The workspace is migrated to nx `23.0.0-rc.0`. All `nx` / `@nx/*`
packages are bumped to `23.0.0-rc.0` (and the catalog `webpack` entry
moves 5.105.2 → 5.107.2). The three generated migrations (`@nx/web`,
`@nx/react`, `@nx/angular` internal-subpath-import rewrites) ran and
made **no** changes — this repo doesn't use those deep `src/*` imports.
Dependency-only migration; no source code modified.
## Related Issue(s)
N/A — routine version migration.
---
Part of a coordinated nx `23.0.0-rc.0` migration across nrwl repos (see
linked PRs). Draft.
<!-- polygraph-session-start -->
---
[View session information
↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/Coordinated-Nx-23.0.0-rc.0-Migration-Across-nrwl-Repositories-39dcf816)
<!-- polygraph-session-end -->
## Current Behavior
When AI agents run Nx inside a network-restricted sandbox (e.g. Claude
Code's sandboxed Bash), analytics requests to
`https://www.google-analytics.com/g/collect` are blocked because the
hostname is not on the sandbox's allowlist. Depending on the agent's
configuration, this either silently drops the events or surfaces a
confusing permission prompt asking why running tests wants to reach
`google-analytics.com`.
## Expected Behavior
`nx configure-ai-agents` (via `setupAiAgentsGenerator`) now adds
`www.google-analytics.com` to `sandbox.network.allowedDomains` in
`.claude/settings.json`, alongside the existing marketplace/plugin
configuration it already manages. Analytics requests during sandboxed
CLI runs go through without prompting.
- Existing user-defined allowed domains are preserved; the entry is
appended idempotently (no duplicates on re-run).
- The domain lives in a shared constant
(`packages/nx/src/ai/constants.ts`) noted to stay in sync with
`GA_ENDPOINT` in `packages/nx/src/native/telemetry/constants.rs`.
- Existing workspaces pick this up through the regular `nx
configure-ai-agents --check` drift detection.
## Related Issue(s)
Internal:
[NXC-4091](https://linear.app/nxdev/issue/NXC-4091/allow-analytics-requests-during-sandboxed-cli-runs)
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The `migrate-to-vitest-4` migration defers `vitest.workspace.*` handling
entirely to the AI step, which inlines the project globs verbatim into a
root `vitest.config.*` under `test.projects`. After the upgrade,
packages that run tests through a package.json `vitest` script without a
local config fail with "No projects were found": Vitest 4 discovers the
new root config by walking up from the package directory and resolves
the `test.projects` globs relative to that directory. Workspaces
migrated without the AI step get no workspace-file handling at all.
Reported while testing the Nx 23 betas; reproduction:
https://github.com/juristr/nx23-vitest-issue-repro.
## Expected Behavior
The migration handles workspace files deterministically and produces a
working setup on its own:
- Static `vitest.workspace.*` files (the Nx-generated shape) are inlined
into the root `vitest.config.*` under `test.projects` and deleted. Only
dynamic shapes are forwarded to the AI step.
- Packages that run vitest via a package.json script and have no local
config get a minimal `vitest.config.*` generated, so their tests keep
running from the package directory and `@nx/vitest` infers their test
target.
- When the inlined globs match both a `vite.config.*` and a
`vitest.config.*` in the same directory resolving to the same project
name, the `vite.config.*` file is excluded with a negative glob. This
matches vitest's own vitest-over-vite preference and avoids the "Project
name ... is not unique" startup error that the copied globs previously
produced on root-level runs. Cases that can't be determined statically
are forwarded to the AI step as advisories.
Anything the migration cannot do safely (dynamic workspace files,
configs it can't merge into) is still deferred to the AI step with
accurate context, and the instructions teach the agent the same rules
for the files it inlines itself.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/troubleshoot-migrate-to-vitest-4-issue-ba08225a)
<!-- polygraph-session-end -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The `nx migrate` flow emits no analytics, so there is no visibility into
how migrations are invoked, where they fail, or how features like
multi-major and agentic runs are used.
## Expected Behavior
The migrate flow now reports Google Analytics events (gated by the
`nx.json` analytics opt-in) across the generate and run phases:
invocation and flags, interactive prompt choices, completion (resolved
include, fetch method, multi-major decision), run lifecycle with
migration counts, and structured errors (phase code, error name, and an
nx-relative throw location).
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4508-de945c44)
<!-- polygraph-session-end -->
---------
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The generated GitLab CI workflow (`nx generate @nx/workspace:ci-workflow
--ci=gitlab`) fails on new repositories with errors like:
fatal: ambiguous argument 'HEAD~1': unknown revision or path not in the
working tree.
This happens because:
1. GitLab's default shallow clone doesn't have full git history
2. `CI_COMMIT_BEFORE_SHA` is always
`0000000000000000000000000000000000000000` for merge requests and first
commits
3. Environment variables `NX_HEAD` and `NX_BASE` weren't exported, so Nx
couldn't read them
## Expected Behavior
The GitLab CI workflow should work correctly on:
- New repositories with no previous commits
- Merge request pipelines
- Main branch push pipelines
## Related Issue(s)
Fixes https://linear.app/nxdev/issue/NXC-3707
## Changes Made
1. **Added `GIT_DEPTH: 0`** - Fetches full git history instead of
shallow clone
2. **Changed `only` branch to use template variable** - Uses `<%=
mainBranch %>` instead of hardcoded `main`
3. **Fixed `NX_BASE` and `NX_HEAD` exports** - Added `export` keyword
and improved fallback logic:
- Uses `CI_MERGE_REQUEST_DIFF_BASE_SHA` for merge requests
- Falls back to `HEAD~1` for regular commits
- Falls back to `HEAD` for initial commits (no parent)
## Testing
Verified on real GitLab CI at `gitlab.com/AgentEnder/nx-gitlab-ci-test`:
- ✅ Main branch push pipeline passed
- ✅ Merge request pipeline passed
## Current Behavior
Inferred (`createNodes`) targets that set an `executor` but omit
`continuous` force project-graph normalization (`normalizeTarget`) to
read the executor's `schema.json` — resolved from `dist` via
`executors.json` — solely to infer the `continuous` flag. In this
workspace that surfaces as Nx Cloud sandbox violations (e.g.
`playwright:test` reading
`packages/playwright/dist/src/executors/merge-reports/schema.json`).
## Expected Behavior
The affected `createNodes` targets declare `continuous` explicitly, so
normalization short-circuits the `!('continuous' in target)` guard and
never reads the executor schema:
- `@nx/playwright` — `merge-reports`
- `@nx/docker` — `release-publish`
- `@nx/expo` — `install`, `prebuild`, `build`
- `@nx/react-native` — `sync-deps`
The `@nx/web:file-server` targets in storybook/vite/webpack/rspack/nuxt
already declare `continuous: true`, which is why they were never
affected.
## Related Issue(s)
N/A — Nx Cloud sandbox violation cleanup.
## Current Behavior
The v23.0.0 `migrate-create-nodes-v2-to-create-nodes` migration (shipped
in 22 plugins) rewrites **only import/export named bindings** of
`createNodesV2` to `createNodes` — it never touches value references in
the file body.
So a lone `import { createNodesV2 }` (or one deduped against an existing
`createNodes`) is renamed to `import { createNodes }`, but any value
usage of `createNodesV2` is left dangling:
```ts
import { createNodesV2 } from '@nx/js/typescript'; // → renamed to createNodes
addPlugin(graph, '@nx/js/typescript', createNodesV2, {}); // ← left as-is → TS2304: Cannot find name 'createNodesV2'
```
The existing specs only ever asserted on import lines, never on a file
that *uses* `createNodesV2` as a value, so the gap went unnoticed.
(Found while running the migration against a real workspace.)
## Expected Behavior
When the migration renames a local `createNodesV2` import binding, it
now also renames in-file **value references** to `createNodes`, so the
file still compiles. The rename is AST-scoped and conservative — it
skips:
- property accesses (`x.createNodesV2`) and qualified type names
- object-literal keys
- declaration names that shadow the import
- strings and comments (never `Identifier` nodes)
and expands a shorthand property (`{ createNodesV2 }` → `{
createNodesV2: createNodes }`) to preserve the key. Aliased imports (`{
createNodesV2 as cn }`) and re-exports keep their local name, so they
never trigger a usage rewrite.
Applied to all 22 plugin copies of the migration; regression tests added
for the value-usage cases (21 specs; `@nx/gradle`'s multi-specifier spec
keeps its existing structure and the shared logic is covered by the
others).
## Related Issue(s)
N/A — follow-up hardening of the v23 migration.
<!-- polygraph-session-start -->
---
[View session information
↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/humble-koala-715f4ad1)
<!-- polygraph-session-end -->
---------
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Before we added AI assistance from the cloud sandboxing dashboard we had
added a skill to the nx repo to deal with it, and that skill is getting
reached for instead of the cloud prompt which is hurting dogfooding.
## Expected Behavior
The skill is removed so we are dogfooding the cloud prompt better
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
The `@nx/webpack` v23 migration adds `webpack`, `webpack-cli`, and
`webpack-dev-server` to **`dependencies`**. They're declared with
`"alwaysAddToPackageJson": true`, and a boolean `true` defaults to
`dependencies` (see `migrate.ts` — `alwaysAddToPackageJson` resolves to
`'dependencies'` when truthy-but-not-a-string).
These are build-time tools, so they don't belong in runtime
`dependencies` — and it's inconsistent with `@nx/webpack` itself, which
workspaces keep in `devDependencies`.
## Expected Behavior
The migration adds them to **`devDependencies`** by declaring
`"alwaysAddToPackageJson": "devDependencies"` (a string value routes to
the named section — this is already a supported, tested form, e.g.
`migrate.spec.ts`). Packages that already exist in `dependencies` are
left in place by the migration runner, so this only governs new
additions.
## Related Issue(s)
N/A
<!-- polygraph-session-start -->
---
[View session information
↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/humble-koala-715f4ad1)
<!-- polygraph-session-end -->
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
## Current Behavior
The nx-dev docs app runs on Next.js 14.2.35, which is end-of-life.
## Expected Behavior
nx-dev runs on the latest Next 16 (16.2.7). React stays at 18.3.1
(peer-accepted). App-router course page `params` are now async, and the
unused `eslint-config-next` dependency is dropped (`next lint` is
removed in v16; nx-dev lints via a flat config).
The only affected pages are the courses:
https://deploy-preview-35923--nx-dev.netlify.app/courses
## Related Issue(s)
DOC-518
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/upgrade-next-to-v16-1eae0db6)
<!-- polygraph-session-end -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The workspace is on nx 23.0.0-beta.24.
## Expected Behavior
Migrated to nx 23.0.0-beta.25 via `nx migrate`. All 22 `@nx/*` packages
bumped beta.24→beta.25, the 3 internal-subpath-rewrite migrations were
run (no source changes), and the shared webpack catalog version was
bumped 5.105.2→5.107.2.
## Related Issue(s)
N/A — routine nx version migration.
Part of a coordinated multi-repo nx 23.0.0-beta.25 upgrade.
<!-- polygraph-session-start -->
---
[View session information
↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/humble-koala-715f4ad1)
<!-- polygraph-session-end -->
---------
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
## Current Behavior
The migration config flag that opts a package into `nx migrate
--include`
(optional migrations) is named `supportsOptionalUpdates`. It is read
from a
package's `nx-migrations` / `ng-update` config and threaded through the
migrate
command. The name says "updates", but the feature it gates is optional
**migrations**, so the name is misleading.
## Expected Behavior
The flag is renamed to `supportsOptionalMigrations` everywhere it is
defined and
consumed:
- The `NxMigrationsConfiguration` / `NxPackageJson` type field and the
`readNxMigrateConfig` parsing in `packages/nx/src/utils/package-json.ts`
- The `--include` gate and related plumbing in
`packages/nx/src/command-line/migrate/migrate.ts`
- The `"supportsOptionalMigrations": true` flag in every first-party
plugin's
`package.json`
- The associated unit tests
This is a purely internal rename — the flag is both defined and consumed
inside
the nx repo, so no backwards-compat shim is required. Behavior is
unchanged.
## Related Issue(s)
N/A — internal naming cleanup.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The single test in `e2e/release/src/custom-registries.test.ts` ("should
respect registry configuration for each package") intermittently fails
with a jest timeout:
```
thrown: "Exceeded timeout of 1000000 ms for a test."
```
The test runs ~13 sequential `nx release publish --dry-run` commands,
most of which target intentionally-unreachable registry hostnames (e.g.
`publish-config-registry.com`, `scoped-registry.com`,
`ignored-registry.com`). Under npm 11 (Node 24+), `npm publish
--dry-run` performs a network preflight to the configured registry. When
that host doesn't resolve, npm's default retry backoff
(`fetch-retry-mintimeout=10000` + `fetch-retry-maxtimeout=60000`) sleeps
~70s per command before giving up. Across all the unreachable-registry
calls this adds up to ~900s of pure idle waiting, pushing the whole test
past its `1_000_000` ms budget. Because it sits right on the boundary,
the test is flaky — small timing variations tip it over.
## Expected Behavior
The test completes well within its timeout. npm still retries registry
requests (preserving resilience for the real verdaccio reads in the
second half of the test), but the retry backoff is capped so the wasted
sleep per unreachable call drops from ~70s to ~1.1s.
This is done by adding the following to both `.npmrc` blocks the test
writes:
```
fetch-retry-mintimeout=100
fetch-retry-maxtimeout=1000
```
The dry-run output and all assertions are unchanged — `npm publish
--dry-run` exits 0 with identical output regardless of registry
reachability; only the idle backoff time is reduced.
## Related Issue(s)
N/A — test stabilization (flaky e2e fix).
## Current Behavior
Following the local-dist migration (#35900), several projects were still
writing build or typecheck output to the shared workspace-root `dist/`:
- **Five packages' spec configs** — `@nx/angular-rspack`,
`@nx/angular-rspack-compiler`, `@nx/dotnet`, `@nx/maven`, and `nx` —
kept their `tsconfig.spec.json` `outDir` pointing at
`dist/packages/<name>/spec` even though their lib output had been
migrated to a local `dist`.
- **`tools/workspace-plugin`** built to the shared
`dist/workspace-plugin` (its conformance rules are loaded from the built
output).
- **The graph client** bundled to `dist/apps/graph` and emitted its
typecheck declarations to `dist/graph/client`; the graph libs wrote
spec/storybook typecheck output under `dist/out-tsc`.
- **nx-dev** lib/spec tsconfigs pointed their `outDir` at
`dist/out-tsc/...`.
Separately, astro-docs documentation generation resolved each plugin's
`schema.json` from its **built** `dist` (the migrated
`generators.json`/`executors.json` refs point at `./dist/src/...`).
Reading another project's build output tripped the task sandbox with
undeclared `dist/**/schema.json` reads.
## Expected Behavior
Each project builds to its own local directory, leaving the
workspace-root `dist/` alone:
- The five spec configs now emit to local `dist/spec`.
- `tools/workspace-plugin` builds to `tools/workspace-plugin/dist`; the
seven conformance rule paths in `nx.json` are updated, and
`main`/`typings` are repointed into `dist` so the `@nx/js/typescript`
plugin still infers its build target.
- The graph client bundles to `graph/client/dist` (the `nx` package's
`assets.json` input is updated to match); its typecheck output goes to a
local `out-tsc` so the emitted declarations stay out of the copied
bundle dir. Graph lib spec/storybook output moves to local `dist`.
- nx-dev lib/spec outputs move to local `dist` / `dist/spec`
(preventative — these were vestigial as no target runs `tsc` on most
nx-dev libs today).
astro-docs now reads plugin `schema.json` from source (the verbatim
copy), which also matches `astro-docs:build`'s already-declared
`packages/*/src/.../schema.json` inputs — removing the sandbox violation
without weakening cache correctness.
Validation: `nx build workspace-plugin` + `nx conformance` pass from the
new path; the graph client builds and copies into
`packages/nx/dist/src/core/graph` with no declaration leakage; `nx build
astro-docs` is green (753 pages, no schema-resolution errors); affected
graph and nx-dev `typecheck`/`lint`/`test` pass.
## Related Issue(s)
Follow-up to #35900 (local-dist build migration). No separate issue.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The last four publishable packages still on the old layout —
`@nx/maven`, `@nx/dotnet`, `@nx/angular-rspack`, and
`@nx/angular-rspack-compiler` — build with classic `module: commonjs` /
`moduleResolution: node` and inherit `baseUrl`. They were never on the
main local-dist migration board, so they're the only first-party
packages left on classic resolution. This blocks moving the workspace to
a single compiler (tsgo / TS7), which hard-errors on both classic
`node10` resolution (`TS5108`) and `baseUrl` (`TS5102`).
## Expected Behavior
All four packages build with `nodenext` module resolution and expose the
`@nx/nx-source` export condition, matching the already-migrated packages
— so every first-party package is now on `nodenext`. `rootDir: src` is
intentionally kept (rather than `.`) so the
generators/executors/migrations manifest paths and the `versions.ts`
self-reference don't need rewriting; it's functionally identical for the
compiler.
Two nodenext-specific fixes were required:
- **`@nx/angular-rspack-compiler`**: `@angular/compiler-cli` ships
`"type": "module"` typings whose extensionless `export *` chains don't
resolve under nodenext, so `CompilerHost` / `readConfiguration` come
back as "no exported member". They're replaced with minimal local type
declarations. The package is now only referenced via a dynamic runtime
import, so it's added to the dependency-checks `ignoredDependencies`.
- **`@nx/angular-rspack`**: the render and routes-extractor workers
loaded the rspack-built CommonJS server bundle via `await
import(serverBundlePath)`. Under `commonjs` that downleveled to
`require()`, which exposes the bundle's named exports
(`ɵSERVER_CONTEXT`, `ɵgetRoutesFromAngularRouterConfig`); under
`nodenext` it stays a true ESM import and those resolve as `undefined`,
breaking the angular-rspack SSR/SSG example builds. They now load the
bundle with `require()`.
Validated with `nx affected -t build,lint,test` — 21 projects / 121
tasks green, including all 14 `@nx/angular-rspack` example apps.
The actual tsgo compiler flip (re-add `@typescript/native-preview`, drop
the base `baseUrl`, set `strict: false`, set `compiler: "tsgo"` on the
`@nx/js/typescript` plugin) is a separate follow-up.
## Related Issue(s)
Implements Linear NXC-4538 (auto-linked via the branch name). Follow-up:
NXC-4539 enables tsgo workspace-wide. No GitHub issue to close.
## Current Behavior
The React Native ecosystem plugins (`@nx/detox`, `@nx/expo`,
`@nx/react-native`) and `@nx/remix` were not compliant with the
multi-version support initiative (`nx migrate --first-party-only`). None
of them enforced a supported-version floor on their generators or
preserved user-pinned versions, and:
- **`@nx/detox`** pinned a single `detox` version with no floor; the
`22.0.0` migration bundled a cross-major `@config-plugins/detox` bump
together with same-major bumps, ungated.
- **`@nx/expo`** had SDK 53/54 lanes but not the current SDK 55; several
SDK-specific migrations ran unconditionally; `expo` was not declared as
a peer.
- **`@nx/react-native`** was hardcoded to a single, upstream-Unsupported
RN `0.79.3` — no version map, no floor, `react-native` undeclared as a
peer.
- **`@nx/remix`** generators overwrote installed `react` / `vite` /
`@remix-run/*` versions and had no floor enforcement.
## Expected Behavior
Each plugin now follows the canonical multi-version compliance shape
(`assertSupported<Pkg>Version` on every generator entry point, user-pin
preservation via `keepExistingVersions`, source-major-gated migrations,
and a parameterized floor spec):
- **`@nx/detox`** — v20-only floor (`detox` is v20-only upstream; v19
has been unmaintained since 2022). The `22.0.0` migration is split so
the cross-major `@config-plugins/detox` bump is gated on `expo >=53
<54`, while the `detox`/`jest-dom` bumps stay ungated for bare React
Native + Detox workspaces.
- **`@nx/expo`** — adds the **SDK 55** lane (RN `0.83.6`, React `19.2`)
as the new default with `isExpoV55` detection (53/54 lanes retained);
declares `expo` as a peer; floor at SDK 53; SDK-specific migrations
gated with `requires`.
- **`@nx/react-native`** — per-minor version map for the Active line
(`0.83`/`0.84`/`0.85`, default `0.85`) routed through `versions(tree)`;
declares `react-native` as a peer; floor at `0.83.0`; the
`remove-deprecated-deps` migration gated on `react-native >=0.76 <0.79`.
- **`@nx/remix`** — stays Remix v2 (React Router v7 remains in
`@nx/react`) and documents the split; floor at `@remix-run/dev >=2.0.0`;
generators preserve installed `react`/`vite`/`@remix-run/*` versions
instead of overwriting them.
A shared test utility in `@nx/devkit/internal-testing-utils`
(`assertGeneratorsEnforceVersionFloor`) was extended to resolve
generators declared via `implementation` as well as `factory`, so
`@nx/remix` can use the shared floor spec.
Supported-version docs for `@nx/expo`, `@nx/react-native`, and
`@nx/remix` are updated.
## Related Issue(s)
Tracked in Linear (not GitHub Issues): NXC-4385, NXC-4389, NXC-4400,
NXC-4402.
## Current Behavior
The publish workflow's **Build FreeBSD** job fails with the VM's OOM
killer killing the build (`/usr/bin/ssh` exit code 137):
```
swap_pager: out of swap space
kernel: pid ... (node) was killed: failed to reclaim memory
kernel: pid ... (cargo) was killed: failed to reclaim memory
```
The FreeBSD bindings build runs inside a QEMU VM via
`cross-platform-actions/action`, which defaults to **6G** of memory on a
Linux host. The job dies ~90s in — during project-graph computation (the
main `nx` process plus isolated plugin workers), before the native
(cargo) build even ramps up. That working set outgrew 6G, so the VM ran
out of memory and swap.
## Expected Behavior
The VM is allocated **12G** (the `ubuntu-latest` runner has ~16G,
leaving ~4G for QEMU + the host). Graph computation and the native build
complete without exhausting VM memory. This is a pure infra knob — no
change to nx behavior. `cpu_count` is left at the default.
## Related Issue(s)
N/A — CI fix.
## Current Behavior
The first-party Nx plugins still ship migration code (and
`packageJsonUpdates`) targeting Nx **v20 and earlier**. With v23 on the
way, those migrations are dead weight in the published packages and
their `migrations.json` manifests.
## Expected Behavior
All migrations prior to **v21** are removed across the first-party
plugins via the `@nx/workspace-plugin:remove-migrations` generator
(`--v=21`), keeping the two most recent prior majors (v21, v22) plus v23
— consistent with prior major-release prep (#30839 removed `< v19`,
#32904 removed `< v20`).
`packages/nx` and `packages/angular` are intentionally preserved (`nx`
is needed for `nx repair`; `angular` keeps its migrations until LTS
support is dropped).
Because the recent move to local dist builds (#35900) means every
plugin's `migrations.json` now references built (`./dist/...`) paths,
the generator can no longer auto-delete the corresponding source files.
The orphaned pre-v21 migration source directories were removed manually
as part of this change.
## Related Issue(s)
N/A — release preparation for v23.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
`nx release --group=<group>` walks the full commit range since the last
matching tag and resolves each commit's conventional-commit scope
against the **entire** project graph (`findMatchingProjects` over
`projectGraph.nodes`). If a scope is ambiguous against *any* projects in
the workspace, Nx throws unconditionally — even when those projects
aren't in the release group currently being processed.
In workspaces with slash-style subpath project names (e.g. a meta
package `@scope/cui` plus subpath packages `@scope/cui/forms`,
`@scope/cui/select`, …), a single commit with a short-form scope like
`fix(cui): …` permanently blocks every release group whose tag-range
includes it — including unrelated groups that don't contain any of the
matching projects.
Closes#35744.
## Expected Behavior
The ambiguity check should be scoped to the active release group's
projects. A scope that only collides with projects *outside* the group
should fall through to the file-affectedness path that's already used
when a commit has no scope at all.
## Fix
In `getCommitsRelevantToProjects`, `projects: string[]` (already in the
function signature as the active release group's projects, materialized
as `projectSet` at the top) is now applied to the per-pattern ambiguity
check and to the `scopedProjects` set:
1. **Per-pattern check filters to the group.** `inGroupMatches =
perPatternMatches.filter(p => projectSet.has(p))`. Throw only when
`inGroupMatches.length > 1` — ambiguity *within* the group is still a
real error.
2. **`scopedProjects` is intersected with the group.** Cross-group
matches no longer bleed into `isProjectScopedCommit` downstream.
When the filtered set is empty or singleton, the commit is treated
identically to a commit with no scope for this group's processing —
`isProjectScopedCommit` falls back to `false` for projects whose
`affectedGraph` includes them via files, matching the existing
scope-less behavior.
## Tests
`packages/nx/src/command-line/release/utils/shared.spec.ts`:
- **Existing test for in-group ambiguity** (`should throw when commit
scope matches multiple projects (ambiguous scope)`) — converted from a
`try { … } catch (err) { expect(…) }` pattern (which silently passed if
no throw happened) to `await expect(…).rejects.toThrow(…)`. Existing
behavior preserved: throws when `@foo/graph` + `@bar/graph` are both in
the active group with scope `graph`.
- **New test for cross-group ambiguity** — active group `['lib-a']`,
scope `graph` matches `@foo/graph` + `@bar/graph` (both outside the
group). No throw. Commit is included via file-affectedness,
`isProjectScopedCommit: false`.
- **New test for partial cross-group ambiguity** — active group
`['@foo/graph']`, scope `graph` matches `@foo/graph` (in group) +
`@bar/graph` (out). Filtered to one match. No throw, treated as scoped
to `@foo/graph` (`isProjectScopedCommit: true`).
Also extended `createMockCommit` to accept an explicit `scope` argument
so tests actually exercise the scope-parsing path. The existing
ambiguity test relied on `feat(graph): …` in the commit *message*, but
`commit.scope` itself was hardcoded to `''`, so `scopePatterns` was
always empty and the throw never fired in the test — the assertion lived
in a catch block that was never reached. The conversion to
`rejects.toThrow` plus the explicit `scope` argument makes the original
test actually verify what its name says.
## Verification
```
pnpm exec nx test nx --testPathPatterns=shared.spec
# Tests: 38 passed, 38 total
pnpm exec nx test nx --testPathPatterns="release"
# Tests: 1 skipped, 503 passed, 504 total
pnpm exec nx lint nx
# 0 errors (2 pre-existing warnings, unrelated)
```
Minimal real-world repro repo:
https://github.com/jmclellan-crexi/nx-release-scope-ambiguity-repro
## PR Checklist
- [x] Bug-fix only — no API surface changes
- [x] Tests added for new behavior + existing test converted to assert
what it claims
- [x] Backwards compatible — in-group ambiguity still throws; scope-less
commits unchanged; unambiguous scopes unchanged
- [x] No documentation changes needed (behavior is now closer to what
the existing docs imply)
---------
Co-authored-by: Justin McLellan <jmclellan@beast>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Current Behavior
`nx migrate --include` only applies when migrating Nx itself; for any
other target the option is rejected, and eligibility is derived from
Nx-specific version math.
## Expected Behavior
`--include` now applies to any package whose `nx-migrations` /
`ng-update` config declares `supportsOptionalUpdates: true`, read from
the package metadata via the shared fetcher (registry-first, install
fallback). It accepts `required` (the target package and the related
packages it ships with), `optional` (the optional dependency updates
those packages recommend), or `all` (default). The `--interactive` /
`x-prompt` confirmation flow is deprecated in favor of `--include`
(removal slated for Nx v24; no hard error yet), and
`supportsOptionalUpdates: true` is set on the first-party packages.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4517-37fe1336)
<!-- polygraph-session-end -->
## Current Behavior
During `nx migrate`, the up-front **"Enable the agentic flow?"** prompt
rendered every choice's description at once (via enquirer's built-in
per-choice `hint`), producing a wall of muted text beside the options.
## Expected Behavior
Each choice's description is shown **only when that option is focused**
— as a single dimmed line in the prompt footer that updates as you arrow
through the list. This mirrors the focused-description pattern already
used in `configure-ai-agents`.
Implementation: the per-choice blurb moved from enquirer's `hint` field
(which auto-renders on every row) to a plain `description` field that
enquirer ignores, and a `footer` function surfaces
`this.focused.description`.
## Related Issue(s)
N/A
## Current Behavior
The repository still contains a `.circleci/config.yml` file. It is a
leftover transition stub from when Nx migrated its own CI from CircleCI
to GitHub Actions — its only job (`main-linux`) does nothing but echo a
message:
> "We are in the process of transitioning from Circle CI to GitHub
Actions. For details about your build results, consult github actions
build logs."
The migration to GitHub Actions is long complete, so the file serves no
purpose.
## Expected Behavior
The dead `.circleci/config.yml` is removed. Nx's own CI continues to run
on GitHub Actions, unaffected.
Note: this only removes the nx repo's *own* CircleCI pipeline. CircleCI
as a supported provider in the `ci-workflow` generators
(`@nx/workspace`, `@nx/gradle`, `@nx/maven`, `@nx/dotnet`) and the Nx
Cloud setup docs are product features and remain untouched.
## Related Issue(s)
N/A
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/remove-circle-config-a3a01061)
<!-- polygraph-session-end -->
## Current Behavior
The remaining 14 publishable packages still build to the shared
workspace output `dist/packages/<name>` and use `commonjs`/`node` module
resolution. This is the last set of packages on the old layout — `nx`,
`devkit`, `js`, `web`, `node`, `webpack`, etc. were already migrated.
## Expected Behavior
Each of the remaining packages now builds to its own local
`packages/<name>/dist` directory with `nodenext` module resolution and
an `exports` map using the custom `@nx/nx-source` condition (so
in-workspace consumers resolve to source, while published/runtime
consumers resolve to built `./dist`). This matches the already-migrated
packages.
Packages migrated (one commit each):
- **Tier 0:** `@nx/react`, `@nx/esbuild`, `@nx/express`, `@nx/vue`,
`@nx/plugin`, `create-nx-workspace`, `@nx/angular`
- **Tier 1:** `@nx/react-native`, `@nx/next`, `@nx/remix`, `@nx/detox`,
`@nx/expo`, `@nx/nuxt`, `create-nx-plugin`
Notes:
- Kept the `./src/*` wildcard exports (the `@nx/nest` pattern) so no
consumer imports needed editing; the optional `./internal` lockdown is
deferred to a follow-up.
- Converted `ensurePackage` + `await import('@nx/...')` pairs to typed
`require()` (ESM dynamic import ignores `Module._initPaths` under
`nodenext`) and replaced `require('../../package.json')` self-references
with the dynamic `require(join('@nx/<name>', 'package.json'))` form.
- `@nx/angular` keeps its dual build: `tsc` (`build-base`) + ng-packagr
(`build-ng`); `ng-package.json` `dest`, both tsconfig `outDir`s, and the
publish `packageRoot` were relocated to `packages/angular/dist`.
- `create-nx-workspace`/`create-nx-plugin` were missing from the
migration board; tracked as NXC-4515 / NXC-4516.
Validation: `nx run-many -t build,lint` is green across all 14 packages
(including angular's real ng-packagr build and
`@nx/nx-plugin-checks`/`@nx/dependency-checks`). **Still needs CI
validation:** full e2e and the `nx release`/publish path (notably
angular's ng-packagr `packageRoot`).
## Breaking Changes
This is a breaking change because only `/internal` can be imported on packages now rather than importing from `src`. There is a migration to take care of this when necessary though.
## Related Issue(s)
Linear: NXC-3580, NXC-3582, NXC-3583, NXC-3584, NXC-3585, NXC-3587,
NXC-3589, NXC-3590, NXC-3596, NXC-3597, NXC-3600, NXC-3601, NXC-4515,
NXC-4516
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
When resolving package versions, `nx migrate` does not account for the
workspace package manager's minimum release age (cooldown) configuration
- npm `min-release-age`, pnpm `minimumReleaseAge`, yarn
`npmMinimalAgeGate`, and bun `minimumReleaseAge`. It can resolve to
versions newer than the package manager would actually allow to be
installed.
## Expected Behavior
`nx migrate` now honors the active package manager's minimum release age
policy, resolving to the newest version that satisfies the configured
cooldown. Registry-based resolution can be disabled with
`migrate.useRegistryResolution: false` in `nx.json` or the
`NX_MIGRATE_USE_REGISTRY_RESOLUTION` environment variable (the legacy
`NX_MIGRATE_SKIP_REGISTRY_FETCH` remains supported), in which case
versions are resolved through a package-manager install.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4499-7e1e10b2)
<!-- polygraph-session-end -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The Nx Cloud [Launch
Templates](https://nx.dev/docs/reference/nx-cloud/launch-templates#launch-templatestemplate-nameresource-class)
reference page lists `+` variant resource classes
(`docker_linux_amd64/medium+`, `docker_linux_amd64/large+`,
`docker_linux_amd64/extra_large+`) in the
`launch-templates.<template-name>.resource-class` section.
## Expected Behavior
The `+` variant resource classes are removed from the list. Only the
base resource classes remain.
## Related Issue(s)
Fixes DOC-515
## Current Behavior
The `createNodesV2` → `createNodes` rename migrations added in #35893
registered the `update-23-0-0-migrate-create-nodes-v2-import` migration
for **@nx/remix** and **@nx/nuxt** with a `./dist/src/migrations/...`
implementation path.
Both are flat-build packages (`main: ./index.js`) whose files are
published at the package root, not under a `dist/` directory. As a
result, `nx migrate` fails when it reaches these migrations:
```
NX Could not resolve implementation for migration
"update-23-0-0-migrate-create-nodes-v2-import" from .../node_modules/@nx/remix/migrations.json
```
## Expected Behavior
The migration resolves and runs. The implementation/documentation paths
for the flat-build packages point to `./src/migrations/...`, matching
the published package layout — consistent with the other flat-build
plugins (angular, next, expo, react-native, react), which already use
the `./src/...` prefix.
Audited all 23 plugins touched by #35893: only `@nx/remix` and
`@nx/nuxt` were mismatched. Dist-build packages (webpack, vite, jest,
etc.) correctly keep the `./dist/src/...` prefix.
## Related Issue(s)
Follow-up fix to #35893.
…ssets plugin
CreateNodesV2 and TargetConfiguration are types but were imported as
values. The @nx/workspace-plugin package is type: module, so under
Node's native TypeScript stripping (Node >= 22.18) those names are left
as runtime imports. @nx/devkit is CommonJS with no such runtime exports,
so the plugin fails to load -- surfacing on the FreeBSD publish job as
the misleading "imported again after being required. Status = 0" Node
error. Marking them import type lets native strip erase them so the
plugin loads on any Node.
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
- The `@nx/react-native` library build (`tsc --build tsconfig.lib.json`)
fails with **TS2742**. The exported `addJest` helper has no explicit
return type, so TypeScript infers `GeneratorCallback` and can only name
it through a non-portable, pnpm-hashed `@nx/devkit` path when emitting
the declaration file.
- The `rewrite-rollup-internal-subpath-imports` and
`rewrite-webpack-internal-subpath-imports` migrations are scheduled to
run at `23.0.0-beta.25`.
## Expected Behavior
- `addJest` is explicitly annotated as `Promise<GeneratorCallback>`
(imported from `@nx/devkit`), giving `tsc` a portable name to emit. The
library now builds cleanly.
- Both internal-subpath-import migrations are retargeted to
`23.0.0-beta.24` to align with the rest of the `23.0.0` migration batch.
## Related Issue(s)
N/A
## Current Behavior
<!-- This is the behavior we have today -->
- The `@nx/dotnet` plugin is labeled **experimental** in two user-facing
places: the docs site introduction page (a `caution` aside) and the
published npm README (generated from `readme-template.md`).
- The package exposes the plugin via **two** specifiers: the bare
`@nx/dotnet` and the `@nx/dotnet/plugin` subpath. Both resolve to the
same plugin implementation, which is redundant and confusing.
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
- The experimental banner/warning is removed from both the docs page and
the npm README. (Factual asides — .NET SDK compatibility, minimum Nx
version — are kept.)
- The `@nx/dotnet/plugin` subpath export is removed. The plugin is
registered solely via the bare `@nx/dotnet` specifier (already what `nx
add @nx/dotnet` / the init generator writes).
- A migration rewrites any existing `nx.json` plugin entries from
`@nx/dotnet/plugin` to `@nx/dotnet`, handling both the string and object
(`{ plugin, options }`) registration forms and de-duplicating if both
paths are present.
### Implementation notes
- Removed the `./plugin` entry from the package `exports` map.
- Inlined the plugin re-export into `src/index.ts`, converted the
internal plugin module to named exports, and deleted the now-redundant
`src/plugin.ts` shim. The bare `@nx/dotnet` public surface is unchanged.
- Added the `update-23-0-0-migrate-dotnet-plugin-path` migration with
unit tests (6 cases, all passing).
**BREAKING CHANGE:** the `@nx/dotnet/plugin` entry point has been
removed; use `@nx/dotnet`. The included migration updates `nx.json`
automatically.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
N/A
---------
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
## Current Behavior
When the Node.js runtime supports native TypeScript type stripping (Node
22.18+ LTS, 23.6+, or 22.6–22.17 with `--experimental-strip-types`), Nx
defers to native stripping and skips registering a transpiler entirely.
The `preferNodeStripTypes` gate is evaluated at module load and
short-circuits *before* the swc-vs-ts-node decision, where
`NX_PREFER_TS_NODE` is consulted. As a result, setting
`NX_PREFER_TS_NODE=true` has **no effect** on modern Node — native
stripping wins, even though the user explicitly asked for ts-node. This
is a problem for workspaces using constructs native stripping can't
handle the way ts-node would (e.g. relying on full type-aware
transpilation rather than stripping).
## Expected Behavior
Setting `NX_PREFER_TS_NODE=true` now opts out of native type stripping,
so the existing ts-node code path is used as intended. The opt-out is
added to the authoritative `preferNodeStripTypes` gate, keeping
`isNativeStripPreferred()` and `loadTsFile` aligned. Behavior is
unchanged when the flag is unset.
Added `isNativeStripPreferred` test coverage across the runtime-support
/ env-flag combinations.
## Related Issue(s)
N/A
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
#35386 renamed the canonical plugin API to drop the `V2` suffix — both
the exported `createNodesV2` **value** in first-party plugins (now
`createNodes`) and the `*V2` **types** in `@nx/devkit` (`CreateNodesV2`,
`CreateNodesContextV2`, `CreateNodesResultV2`, `CreateNodesFunctionV2`,
`NxPluginV2`). The old names are kept as deprecated aliases, so existing
code keeps compiling — but there is no migration to move workspaces onto
the canonical names, and `@nx/angular/plugin` / `@nx/vitest` only
re-exported `createNodesV2` from their public entry points (the rename
was incomplete there).
## Expected Behavior
- **Per-plugin value migration**
(`migrate-create-nodes-v2-to-create-nodes`): every first-party plugin
that publicly exposes `createNodesV2` ships a migration that rewrites
named imports/re-exports of `createNodesV2` from that plugin's public
specifier(s) to `createNodes` (22 plugins, incl. `@nx/gradle` +
`@nx/gradle/plugin-v1`, `@nx/js/typescript`, `@nx/react/router-plugin`).
- **devkit type migration** (`rename-create-nodes-v2-types`): rewrites
imports/re-exports of the deprecated `*V2` types from `@nx/devkit` to
their canonical names (`CreateNodesResultV2` → `CreateNodesResultArray`;
the unrelated `CreateNodesResult` is left alone).
- `@nx/angular/plugin` and `@nx/vitest` now also re-export
`createNodes`, completing the rename so the migration targets resolve.
All migrations are AST-based, modeled on devkit's `update-deep-imports`:
they handle `as` aliases, dedupe when both names are already imported,
preserve `type` modifiers and default imports, and leave
strings/comments, dynamic `import`/`require`, and unrelated specifiers
untouched. Each has unit + tree-runner specs and a `.md` doc, registered
at `23.0.0-beta.5`.
## Related Issue(s)
Follow-up to #35386. No issue to close.
## Current Behavior
The publish workflow's FreeBSD build fails computing the project graph:
`Failed to load 1 Nx plugin(s) … Cannot find module
@nx/js/src/utils/assets/copy-assets-handler`.
`tools/workspace-plugin` pins `@nx/js`/`@nx/devkit` at `23.0.0-beta.4`
(pre-dist-build
layout: `src` sources, no exports map) while the repo is on `beta.22`.
The plugin is
`type: module`, so under Node ≥ 22.18 (native TS strip) its `.ts` loads
as ESM, which
can't resolve the extensionless `@nx/js/src/...` import against an
exports-less package.
FreeBSD CI was the first env with a ≥ 22.18 Node (`pkg install node` →
node24).
The alpine/musl Node download URL also hardcoded the version, drifting
from `NODE_VERSION`.
## Expected Behavior
`workspace-plugin` resolves `@nx/js` through the beta.22 exports map (to
`dist`), so the
plugin loads under native-strip ESM on any Node — FreeBSD included. CI
Node is bumped to
26.3.0 and the musl download is derived from `${NODE_VERSION}` so it
can't drift again.
## Related Issue(s)
N/A — CI fix.
## Current Behavior
When `create-nx-workspace` is invoked with a third-party preset (e.g.
`--preset=@aws/nx-plugin`), a warning and interactive confirmation
prompt are always shown — even when the caller is a wrapper CLI that has
already established trust on behalf of the user. The only way to bypass
this today is `--no-interactive`, which suppresses *all* prompts and
still emits the warning to stdout.
## Expected Behavior
Callers that wrap `create-nx-workspace` (such as `pnpm create
@aws/nx-workspace`) can pass `--trustThirdPartyPreset` to skip the
third-party preset warning and confirmation entirely, without affecting
any other interactive prompts.
```bash
npx create-nx-workspace my-project \
--preset=@aws/nx-plugin \
--trustThirdPartyPreset
```
## Related Issue(s)
Fixes#35826
## Current Behavior
The remaining 11 first-party Nx plugins (`@nx/webpack`, `@nx/rollup`,
`@nx/docker`, `@nx/gradle`, `@nx/rsbuild`, `@nx/web`, `@nx/node`,
`@nx/nest`, `@nx/module-federation`, `@nx/rspack`, `@nx/storybook`)
build into `../../dist/packages/<name>/` with `module: commonjs` and no
`exports` map. Releases publish from a separate dist directory.
That layout has the same drawbacks the prior migrations (devkit,
workspace, nx, js, jest, eslint, eslint-plugin, vitest, cypress,
playwright, vite) already addressed:
- workspace consumers reach into `@nx/<name>/src/*` against the old
layout, blocking nodenext / ESM-friendlier resolution
- a single PR cannot publish a coordinated set because each package's
dist must round-trip through `nx release`
- `release.preserveLocalDependencyProtocols` cannot be turned on
workspace-wide
This PR is the final batch in the "Nx Local Dist Migration" project — it
unblocks every still-unmigrated workspace package and finishes the
rollout that started with `@nx/devkit` in #34946.
## Expected Behavior
All 11 packages build to `packages/<name>/dist/` with:
- `tsconfig.lib.json` set to `module: nodenext`, `moduleResolution:
nodenext`, `composite: true`, `outDir: dist`, `declarationDir: dist`,
`tsBuildInfoFile: dist/tsconfig.tsbuildinfo`
- `package.json` `main`/`types` pointing into `dist/`, an `exports` map
with `@nx/nx-source`/`types`/`default` conditions, `typesVersions` for
legacy `moduleResolution: node` consumers, and a `files` allowlist
- `project.json` `release.version.preserveLocalDependencyProtocols:
true`, `manifestRootsToUpdate: ["packages/{projectName}"]`, and
`nx-release-publish.packageRoot: packages/{projectName}`
- `assets.json` `outDir` and eslint `dist` ignore updated
- `src/utils/versions.ts` switched to `require(join('@nx/<name>',
'package.json')).version` so the self-reference survives the new layout
- `README.md` renamed to `readme-template.md` with the build's
`copy-readme.js` invocation passing explicit src/dest paths, and root
`.gitignore` updated for the generated `README.md`
- `scripts/nx-release.ts` `packagesToReset` extended so `nx release`
properly snapshots/restores these source `package.json`s
The 11 packages keep the `./src/*` wildcard in their exports map for now
— the `@nx/devkit/internal`-style lockdown (Step 14b of the
`dist-build-migration` skill) is intentionally deferred to per-package
follow-up PRs to keep this one focused on the layout move. Two runtime
fixes that mirror earlier work in this project:
- `@nx/node` now declares `@nx/webpack` as a `workspace:*` devDep so its
TS compile resolves `@nx/webpack/src/utils/ensure-dependencies` via the
new exports map; the dynamic `import()` of that subpath was swapped to
`require()` after `ensurePackage` so the temp install is visible (same
pattern as the vite/vitest fix in #35743 — under `module: nodenext` a
dynamic `import()` is preserved as a true ESM import and bypasses
`Module._initPaths`).
- `@nx/web` had two dynamic `await import('@nx/eslint/internal')` /
`await import('@nx/vitest/generators')` call sites; both swapped to
`require()` with `typeof import(...)` type annotations for the same
reason.
`e2e/nx-build/src/nx-build.test.ts` was extended to verify the new
output paths for all 11 packages.
## Related Issue(s)
Fixes NXC-3576
Fixes NXC-3577
Fixes NXC-3579
Fixes NXC-3586
Fixes NXC-3588
Fixes NXC-3591
Fixes NXC-3595
Fixes NXC-3598
Fixes NXC-3599
Fixes NXC-3602
Fixes NXC-4474
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
A watcher-driven project-graph recompute that fails while loading
plugins
takes the **whole Nx daemon down**.
`kickOffRecompute` (`project-graph-incremental-recomputation.ts`) builds
`cachedSerializedProjectGraphPromise` from an async IIFE.
`processFilesAndCreateAndSerializeProjectGraph` is wrapped in
`try/catch`
and always resolves to an `errorResult` — but the prologue hoisted in
front of it for the freshness gate (`readNxJson`, `getPluginsSeparated`,
`isStale`) is not. `getPluginsSeparated` **rejects** when a plugin fails
to load.
`scheduleProjectGraphRecomputation` calls `kickOffRecompute()`
fire-and-forget, so a rejected `myPromise` has no awaiter — Node reports
an unhandled promise rejection and the daemon process exits. (The
request
path, `getCachedSerializedProjectGraphPromise`, `try/catch`es its
`await`,
so only watcher-driven recomputes hit this.)
Observed downstream as a flaky `e2e/nx/src/spread.test.ts`: a test
mutates
`nx.json` / `tools/*` / `project.json`, the watcher-driven recompute
hits
a transient plugin-load failure, the daemon crashes mid-test, and the
following `show project` races a restarting daemon — surfacing as
`project.targets.build` being `undefined` (a graph whose plugin set
never
ran). The captured `daemon.log` shows the `AggregateError` from
`getPluginsSeparated` followed by a fresh daemon process starting.
## Expected Behavior
A plugin-load failure during a recompute resolves to a graph **error**
that the next requester surfaces — the daemon stays up. The IIFE body is
wrapped so it always resolves (never rejects), turning a prologue
failure
into an `errorResult`, the same contract
`processFilesAndCreateAndSerializeProjectGraph` already honors. The next
`getCachedSerializedProjectGraphPromise` reads `result.error`, returns
it
to the client, and clears the cached promise so the recompute retries.
Also in this PR (diagnostics / regression coverage for the flake):
- Restored the `afterEach` daemon-log dump in `spread.test.ts` (removed
in
`2f261d6903`) so a failing run prints `.nx/workspace-data/d/daemon.log`
next to the assertion in CI output.
- Added a `describe('rapid reconfiguration (race-condition stress)')`
block that mutates `nx.json` / `project.json` / `tools/*` in tight
loops with no settle time and no `reset` between iterations, so the
long-lived daemon is exercised hard — regression coverage for the crash.
## Related Issue(s)
Follow-up to PR #35650 (https://github.com/nrwl/nx/pull/35650), which
hoisted `getPluginsSeparated` out of the `try/catch`ed compute and into
the bare IIFE prologue for the freshness gate.
No separate issue number.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The e2e suite is slow and intermittently flaky, driven by a few
infrastructure issues:
- **Lazy dependency installs.** Generator dependencies (test runners,
bundlers, framework plugins) are
installed on demand during a test via `ensurePackage()`. This pushes
install cost into the middle of
the run and makes timing vary run-to-run.
- **Port collisions.** Tests pick ports semi-randomly
(`getRandomPort()`) from overlapping ranges and
don't coordinate across parallel e2e processes, so concurrent tests can
grab the same port and fail
with `EADDRINUSE`. Module federation tests also need a contiguous block
of ports (host + remotes) that
the old scheme couldn't guarantee.
- **Fixed-sleep races.** Some tests start a built app, `sleep 1`, then
assert on its output — which
loses the race on a loaded CI machine where boot + module evaluation
takes several seconds.
- **CI agent contention.** Module federation e2e tasks each build and
serve a host plus its remotes
(multiple webpack/rspack builds at once), saturating an agent; running
them alongside other e2e at
high parallelism caused timeouts.
## Expected Behavior
Faster and more deterministic e2e runs:
- **Pre-install generator deps up front.** Each `newProject({ packages:
[...] })` now declares every
plugin the test will use, so installs happen once during setup instead
of lazily mid-test.
- **Robust port reservation** (`e2e/utils/port-utils.ts`): lock files
are stamped with the owning PID,
abandoned locks are reclaimed (developer machines only — CI containers
are ephemeral), the scan
origin is randomized so parallel processes scatter instead of converging
on low ports, and
`reservePorts(count)` returns a contiguous run for module federation.
`getRandomPort()` is deprecated
in favor of `reservePort()`.
- **Poll-until-ready** instead of fixed sleeps — a built app is polled
for its "server ready" line (up
to 30s) before assertions.
- **CI tuning** (`.nx/workflows/dynamic-changesets.yaml`): module
federation e2e is pinned to
`linux-extra-large` at parallelism 1; remaining e2e runs at 3 (large) /
6 (extra-large).
- **Install visibility**: `installPackagesTask` now logs the install
command and how long it took.
## Related Issue(s)
Fixes #
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
There is no JSON schema for plugin `migrations.json` files, so editors
provide no validation or autocompletion when authoring migrations.
## Expected Behavior
The `nx` package ships a `schemas/migrations-schema.json` file
describing the `migrations.json` format. The `@nx/plugin:migration`
generator adds a `$schema` reference when creating a new
`migrations.json` file.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4455-96de17ed)
<!-- polygraph-session-end -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
The Automate Updating Dependencies feature page was orphaned during the
Astro docs migration - it existed as content but was not referenced in
sidebar.mts. Add it to the Maintenance section alongside the related
update/migration guides.
## Current Behavior
`nx migrate` shows interactive prompts even when `--no-interactive` is
passed: the mode selection prompt ("Which packages would you like to
migrate?"), the multi-major migration prompt, and the agentic flow
prompts in `--run-migrations` only check for a TTY and CI.
## Expected Behavior
`--no-interactive` suppresses all prompting: the mode prompt silently
defaults to `all`, the multi-major check falls back to the warn-only
path, and the agentic flow is skipped (with a warning when it was
explicitly requested).
## Related Issue(s)
Fixes NXC-4513
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/fix-nx-migrate-no-interactive-23fbc4b0)
<!-- polygraph-session-end -->
## Current Behavior
Each agentic step of `nx migrate` ends with a permission prompt when the
agent writes its handoff file with Claude Code. The agent also writes a
`status: "failed"` handoff on its own when it hits a problem, ending the
session without giving the user a chance to weigh in.
## Expected Behavior
The handoff write is pre-authorized for Claude Code via
`--allowedTools`, scoped to `.nx/migrate-runs/**`, so steps complete
without an approval prompt. The handoff contract now writes the success
handoff immediately, asks the user when the agent needs direction, and
only writes a failed handoff when the user says to give up.
## Implementation Notes
- Codex and opencode invocations are unchanged: codex's default sandbox
(workspace-write + on-request approvals) and opencode's default `edit:
allow` permission already allow the write without prompting. Overriding
a user-hardened config (codex read-only sandbox, opencode `edit: ask`)
would discard a deliberate choice, and for opencode an injected
permission object replaces, not merges with, the user's own patterns.
- The allow rule uses the constant `.nx/migrate-runs/**` glob
(cwd-relative; the runner pins cwd to the workspace root) instead of the
exact handoff path: absolute-path rules have unverified `//` prefix
semantics on Windows, and package/migration names can contain
glob-special characters that would silently break matching.
- Verified empirically with claude 2.1.165: the rule allows the handoff
write without prompting, and writes outside the glob (or outside the
cwd) are still denied. Also confirmed `--allowedTools` is variadic: a
positional argument placed right after its value is swallowed, so the
arg order keeps `--system-prompt` between the rules and the user prompt
(guarded by a comment and spec).
- The agent is steered to use its file-write tool for the handoff:
shell-based writes are not covered by the allow rule and would still
prompt.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4514-89cfaf38)
<!-- polygraph-session-end -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The `@nx/angular` SCAM generators (`scam`, `scam-directive`,
`scam-pipe`, `scam-to-standalone`) are not marked as deprecated, even
though SCAMs are superseded by Angular standalone components.
## Expected Behavior
The four SCAM generators are marked as deprecated via `x-deprecated` in
`generators.json` (reported by `nx g` and shown in `--help`) and
`@deprecated` JSDoc on their programmatic entry points. The messages
point to the `component`, `directive`, and `pipe` generators as
replacements, and `scam-to-standalone` users are told to convert any
remaining SCAMs before upgrading. They will be removed in Nx v24.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4512-9a06eb63)
<!-- polygraph-session-end -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Nx Console's migrate UI breaks on the two migration shapes introduced in
#35718: prompt-only migrations crash with
`MigrationImplementationMissingError` when run, and hybrid migrations
give no signal that the AI prompt phase is still pending nor a way to
record that it was run.
## Expected Behavior
Both shapes render and complete correctly in the migrate UI without
turning Nx Console into an agentic runner. Prompt-bearing cards show an
AI badge, the prompt status, and a clickable prompt path that opens the
prompt file in the editor; the user runs the prompt themselves and marks
it done (`Mark as Run` for prompt-only, `Approve Changes` for hybrid),
which persists the completion.
> [!IMPORTANT]
> Depends on the matching change in nrwl/nx-console#3153 to handle the
new `acknowledge-prompt` and `view-prompt` events. Without it,
completion never records from Nx Console and clicking the prompt path
does nothing until the extension is updated. This is accepted
version-skew degradation.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4477-03fa3f4b)
<!-- polygraph-session-end -->
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
## Current Behavior
`@nx/next` has no below-floor guard; generators silently fall back to
latest install constants on unsupported Next versions. The base
`20.7.1-beta.0` migration bumps `eslint-config-next` ungated, hitting
v14 users.
## Expected Behavior
`assertSupportedNextVersion` (floor Next 14) throws on sub-floor and is
the first statement in every generator. The base `20.7.1-beta.0`
migration is gated to `>=15` so v14 users keep the v14 eslint-config
lane. Support window kept at v14+v15+v16.
## Related Issue(s)
Fixes NXC-4395
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/multi-4395-ae050ce9)
<!-- polygraph-session-end -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`@nx/react` has no `peerDependencies` for `react`/`react-dom`, installs
packages unconditionally without preserving user-pinned versions, and
`migrations.json` cross-major bumps lack `requires` gates. The `22.3.4`
entry mixes a `react-router-dom` v6 patch with a `react-router` v7
cross-major under AND-semantics, which cannot express two
mutually-exclusive user states.
## Expected Behavior
- `peerDependencies` `react`/`react-dom` `>=18.0.0 <20.0.0` declared
- `minSupportedReactVersion = '18.0.0'` floor constant +
`assertSupportedReactVersion` wrapper
- Floor assert fires as first statement in all 17 generator entry
functions
- `addDependenciesToPackageJson` preserves user-pinned versions
(`keepExistingVersions: true`) across all generators
- `react-router`/`react-router-dom` resolved per React major from the
version map
- `migrations.json` cross-major `packageJsonUpdates` entries gated with
bilateral `requires` ranges
- `22.3.4` split into dual lanes (v6 `react-router-dom` / v7
`react-router` + `@react-router/*`)
- `22.7.0` gated to v7 lane
- `all-generators-enforce-floor.spec.ts` parameterized spec (24/24 pass)
## Related Issue(s)
NXC-4399
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/multi-version-jack-398d33f1)
<!-- polygraph-session-end -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`@nx/rollup`, `@nx/webpack`, and `@nx/module-federation` pin their
primary third-party packages as **bundled `dependencies`**:
- `@nx/rollup` → `rollup` (`^4.14.0`)
- `@nx/webpack` → `webpack`, `webpack-dev-server` (v5)
- `@nx/module-federation` → `@module-federation/enhanced`,
`@module-federation/node`, `@module-federation/sdk` (v2)
This causes version conflicts with the version a workspace actually has
installed, prevents `nx migrate --first-party-only` from upgrading Nx
without dragging these along, and lets generators silently overwrite a
user's pinned version. Several cross-version `packageJsonUpdates`
migrations also bump packages **without `requires` gates**, so they fire
on workspaces outside the intended source range (e.g. the `@nx/webpack`
`sass-loader` v12 → v16 bump, and all of `@nx/module-federation`'s
`@module-federation/*` bumps).
## Expected Behavior
Each plugin declares the packages it invokes at runtime as **optional
peer dependencies** matching its real support window, so the workspace
controls the version:
- `@nx/rollup` → `rollup` `^3.0.0 || ^4.0.0`
- `@nx/webpack` → `webpack` / `webpack-dev-server` / `webpack-cli`
`^5.0.0` (the effective floor — the plugin already calls webpack-5-only
APIs such as `ids.HashedModuleIdsPlugin`, `webpack.sources`, and
`experiments.cacheUnaffected`)
- `@nx/module-federation` → `@module-federation/enhanced` /
`@module-federation/node` `^2.0.0` (`@module-federation/sdk` stays a
dependency — it is a type-only import surfaced in the public `.d.ts`)
Generators assert the supported floor (with a parameterized
`all-generators-enforce-floor` spec), preserve user-pinned versions
(`keepExistingVersions` now defaults to `true`), and install the
now-peer build tool for new workspaces. Because every workspace that
uses these tools already has the corresponding `@nx/*` plugin as a
direct dependency, a `packageJsonUpdates` bridge in each plugin installs
the peer into existing workspaces on `nx migrate` (and correctly skips
workspaces that only carry the plugin transitively). Cross-version
migrations are gated on their source range.
### Changes per plugin
- **`@nx/rollup` (NXC-4403)** — `rollup` → optional peer; floor assert
(v3) in all generators; `keepExistingVersions` default `true`; install
`rollup` on both the inferred-plugin and executor paths; bridge
migration for existing workspaces. No runtime branching/version map is
needed — the plugin's Rollup usage is already v2/v3/v4-agnostic.
- **`@nx/webpack` (NXC-4410)** —
`webpack`/`webpack-dev-server`/`webpack-cli` → optional peers (`^5`);
floor assert (v5) in all generators; `keepExistingVersions` default
`true`; gate the `sass-loader` v12 → v16 migration on `^12`; install
`webpack`/`webpack-dev-server` for new workspaces; bridge migration for
existing ones.
- **`@nx/module-federation` (NXC-4393)** —
`@module-federation/enhanced`/`node` → optional peers (`^2`); add
`requires` gates (on the `@module-federation/enhanced` source range,
mirroring the existing `@nx/angular` MF migrations) to every
`packageJsonUpdates` entry, and split the independent
`http-proxy-middleware` v2 → v3 bump into its own gated entry. This is a
runtime-only library (no generators/executors), so there is no floor
assert/spec.
## Related Issue(s)
Tracked in Linear under the "Multi-version supported across plugins"
milestone:
- NXC-4403 — `@nx/rollup`
- NXC-4410 — `@nx/webpack`
- NXC-4393 — `@nx/module-federation`
No GitHub issue to auto-close.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
First-party plugins can attach an AI `prompt` markdown file to a
migration in `migrations.json`. `@nx/next` and `@nx/nuxt` reference
prompt files under `src/migrations/**/*.md`, but their build
`assets.json` only copied `migrations.json` — not the `.md` files it
points at. Those prompt files were therefore **missing from the
published packages**.
As a result, `nx migrate` to a version that contains such a migration
fails while fetching migrations:
```
NX Failed to fetch migrations for @nx/next@23.0.0-beta.23
Command failed: pnpm add -w @nx/next@23.0.0-beta.23
Could not find prompt file "./src/migrations/update-22-2-0/ai-instructions-for-next-16.md"
for migration "update-22-2-0-create-ai-instructions-for-next-16" in package "@nx/next@23.0.0-beta.23".
```
The message is also misleading: the install (`pnpm add -w`) actually
succeeded — the missing prompt file is the real cause. The
fetch-via-install helper wrapped **every** failure (the install,
reading/validating migrations, and resolving prompt files) as a `Command
failed: <pm> add <pkg>` error, so non-install failures were attributed
to the install command.
## Expected Behavior
- Migration prompt markdown files are now included in the published
packages. Added `{ "glob": "src/migrations/**/*.md" }` to `@nx/next` and
`@nx/nuxt` (the affected packages), and to `@nx/storybook` for
consistency (its prompt currently ships via the existing `**/files/**`
glob). `@nx/vite`, `@nx/vitest`, and `@nx/expo` already had the glob.
- `nx migrate` no longer fails to fetch migrations for these packages.
- When fetching migrations via install fails, the error reports the
**actual** cause. Only genuine install failures are labeled `Command
failed: <pm> add <pkg>`; errors from reading/validating migrations or
resolving prompt files are surfaced as-is under `Failed to fetch
migrations for <pkg>`.
### Verification
Built `@nx/next` and `@nx/nuxt` and confirmed the prompt files now land
in the package output
(`dist/packages/<pkg>/src/migrations/update-22-2-0/ai-instructions-*.md`).
`@nx/storybook` still builds and continues to ship its prompt via
`**/files/**`. Existing `formatCommandFailure` unit tests pass.
## Related Issue(s)
Found while dogfooding `nx migrate` to `23.0.0-beta.23`; no existing
issue.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
tsconfig `include` entries like `"."` produce a bare `{projectRoot}`
task input that matches no files, so source changes don't invalidate the
cache for the inferred `typecheck`/`build` tasks. Directory `exclude`
entries (e.g. `"dist"`) are emitted as literal negations that match
nothing, and excludes from sibling tsconfigs can suppress files covered
by another tsconfig's include.
## Expected Behavior
Include and exclude entries are interpreted the way TypeScript
interprets them: `include: ["."]` expands to the project's source files,
directory excludes cover their whole subtree, and excludes covered by
another tsconfig's non-glob include are not emitted as negations.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/ts-plugin-include-dot-issue-7fb1ea4f)
<!-- polygraph-session-end -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`addProjectConfiguration` exposes a 4th `standalone` parameter
(defaulting to `true`). Nx only supports standalone projects, so any
value other than `true` is ignored — passing `standalone: false` simply
prints a warning and otherwise has no effect. Despite being a no-op, the
parameter is still part of the public signature, and a number of
first-party generators expose a matching `standaloneConfig` schema
option whose only job is to feed this dead parameter.
## Expected Behavior
`addProjectConfiguration` now presents two TypeScript overloads:
- a clean 3-argument signature (`tree`, `projectName`,
`projectConfiguration`), and
- a `@deprecated` 4-argument signature that still accepts `standalone`
for backwards compatibility (the runtime behavior is unchanged — it
continues to warn when set to `false`).
Existing callers keep compiling, but new ones are steered onto the
3-argument form and editors surface a deprecation strikethrough on the
old one.
The vestigial `standaloneConfig` option has been removed from the
affected generator schemas (`schema.json` + `schema.d.ts`) across
`expo`, `node` (application + library), `nest`, `storybook`, `workspace`
preset, `plugin`, and `express`, since it only ever populated the
ignored parameter. The remaining internal 4-argument call sites (`expo`,
`node`, and the `angular` ng-add e2e migrator) were reduced to the
3-argument overload, the `nest`/`workspace` passthroughs were cleaned
up, and the storybook generator specs no longer pass `standaloneConfig`.
Verified locally: `lint` passes for the 9 touched projects, `build`
(typecheck) passes for `nx`, `node`, `expo`, `nest`, and `workspace`,
and the storybook `configuration` spec passes.
## Related Issue(s)
N/A
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The `composePlugins`, `withNx`, `withWeb` (`@nx/webpack`, `@nx/rspack`)
and `withReact` (`@nx/rspack`, `@nx/react/webpack`) config helpers emit
an Nx-specific config function that only runs under the
`@nx/webpack:webpack` / `@nx/rspack:rspack` executors. There is no
deprecation signal steering users toward the inferred plugins.
## Expected Behavior
The helpers are deprecated for removal in Nx v24. They keep working in
v23 but log a warn-once-per-package message pointing at the real plugin
classes
(`NxAppWebpackPlugin`/`NxAppRspackPlugin`/`NxReactWebpackPlugin`/`NxReactRspackPlugin`)
and `nx g @nx/<bundler>:convert-to-inferred`. `@deprecated` JSDoc is
added to each helper, plus caution asides on the webpack config/plugins
and react asset docs. The warning fires only for user-authored configs:
the rspack executor, storybook preset, and next.js component-testing
preset compose these helpers internally and are wrapped in a suppression
scope. Warn-only, no codemod, no generator changes.
## Related Issue(s)
NXC-4324
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4324-2bacd010)
<!-- polygraph-session-end -->
## Current Behavior
`nx migrate --interactive` enables the per-package `x-prompt`
confirmations regardless of the target version. This conflicts with the
new `--mode` functionality: `--mode=third-party --interactive` still
fires the prompts, and the legacy opt-out flow runs for v23+ migrations
where `--mode` supersedes it.
## Expected Behavior
`--interactive` is gated behind the same v23+ availability gate as the
first-party/third-party modes: passing it for a v23+ Nx-equivalent
target throws with a pointer to `--mode`, and combining it with
`--mode=third-party` is rejected. Pre-v23 and non-Nx targets keep the
legacy interactive behavior.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4412-766d87a4)
<!-- polygraph-session-end -->
## Current Behavior
Running any `nx` command in this repo prints a warning on Node versions
with native TypeScript stripping:
```
Warning: Failed to load the ES module:
.../tools/workspace-plugin/src/plugins/copy-assets-plugin.ts.
Make sure to set "type": "module" in the nearest package.json file or use the .mjs extension.
```
The `copy-assets` plugin is registered in `nx.json` as a source `.ts`
file and is authored in ESM (`import`/`export`). The nearest
`package.json` (`tools/workspace-plugin/package.json`) declares `"type":
"commonjs"`, so Node classifies the file as CommonJS, hits the ESM
syntax, emits the warning, and fails the native load. Nx then recovers
via its swc fallback — so the plugin still works, but the warning is
printed as noise on every command.
## Expected Behavior
No warning. `tools/workspace-plugin` is `private` (never published) and
its sources are authored in ESM, so declaring `"type": "module"` is
accurate and lets Node load the plugin without the spurious
CommonJS-parse warning.
Verified with `"type": "module"`:
- `nx show projects --affected` — exit 0, **0 warnings**, project graph
builds
- `nx build workspace-plugin` — passes
- `nx test workspace-plugin` — 38/38 pass
- `nx lint workspace-plugin` — passes
- `nx conformance` — all 7 rules pass, no violations
## Related Issue(s)
N/A — internal developer-experience fix (no linked issue).
The Nx, Nx Cloud, Nx Console, and Lerna logo packs had no home in the
reposiotry, so there was no stable URL to download them from. Hosting
the zips on master makes them publicly accessible at predictable raw
URLs under `assets/brand-kits/`.
Add a dedicated docs page under Technologies > Module Federation covering a
Vite-based federation setup using @module-federation/vite, with Nx orchestrating
host/remote tasks. Links the example workspace maintained by Giorgio Boa.
Part of DOC-514.
## Current Behavior
Two Nx task-pipeline sandbox violations were reported by Nx Cloud:
1. **`nx publish MsbuildAnalyzer` — unexpected write** at
`packages/dotnet/analyzer/obj/Release/PublishOutputs.<hash>.txt`.
`dotnet publish` writes its incremental-publish state file into the
intermediate (`obj`) directory, but the `@nx/dotnet`-inferred `publish`
target only declared the publish directory (`bin/Release/publish`) as an
output — `obj` was undeclared. The `pack` target has the same latent gap
(it declares only `*.nupkg`).
2. **`nx test angular-rspack` — unexpected read** at
`packages/angular-rspack-compiler/tsconfig.spec.json`. Vitest runs on
Vite, which transforms a dependency's sources and resolves their
TypeScript project references. When a dependency's root `tsconfig.json`
references its `tsconfig.spec.json`, that file is read during resolution
— but the `production` named input excludes `tsconfig.spec.json`
(`!{projectRoot}/tsconfig.spec.json`), so `^production` does not cover
it. The dependency edge itself is correctly declared; only this one file
was missing from the inputs.
## Expected Behavior
The files the tools genuinely touch are declared, so no sandbox
violations:
- **`@nx/dotnet`:** the inferred `publish` and `pack` targets now
include the intermediate (`obj`) directory in their outputs, mirroring
how the `build` target already declares it. The `MsbuildAnalyzer`
`project.json` publish-outputs override is updated too (a project-level
`outputs` override *replaces* the inferred value, so it needs `obj` as
well). New/updated C# output-path unit tests cover both publish and pack
(29/29 passing).
- **`@nx/vitest`:** the inferred `test` target now declares `{ fileset:
'{projectRoot}/tsconfig.spec.json', dependencies: true }`, so a
dependency's spec tsconfig is tracked as an input. This mirrors the
existing dependency-fileset precedent in the `@nx/js/typescript` plugin.
Plugin snapshot updated.
Both fixes are systemic (at the plugin layer) so every dotnet/vitest
project benefits, not just the two that surfaced the violations.
The two commits are independent and scoped separately (`fix(dotnet)` and
`fix(vitest)`) — happy to split into two PRs if preferred.
## Related Issue(s)
Surfaced by Nx Cloud sandbox-violation reports; no standalone GitHub
issue.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/two-sandbox-viols-aefb1b3a)
<!-- polygraph-session-end -->
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`resolvePathsBaseUrl` in `packages/js/src/utils/typescript/ts-config.ts`
walks the tsconfig `extends` chain using `JSON.parse(readFileSync(...))`
to find where `paths` is defined. `JSON.parse` cannot handle `//` or `/*
*/` comments (JSONC), which are extremely common in tsconfig files. When
parsing fails, the `catch` block silently skips the file, breaking the
extends chain walk. The function never reaches `tsconfig.base.json`
(where `paths` and `baseUrl` are defined), so it falls back to the
project's own directory as the resolution base, producing incorrect path
mappings like `{workspaceRoot}/libs/my-lib/dist/libs/data-layer` instead
of `{workspaceRoot}/dist/libs/data-layer`.
This is a regression introduced in v22.7.0 by PR #34965.
## Expected Behavior
Buildable library builds should resolve workspace dependencies
correctly, even when tsconfig files in the extends chain contain JSONC
comments (`//` or `/* */`). The fix uses TypeScript's `readConfigFile`
(which handles JSONC) — the same approach already used by `readTsConfig`
in the same file.
## Related Issue(s)
Fixes#35537Fixes#35824
## Current Behavior
The repo's `mise.toml` defaults the Node.js toolchain to `24.11.0`.
Contributors — and the mise-provisioned CI jobs (`ci.yml`, CodeQL) —
therefore run on Node 24, which ships npm 11.6.1. That npm is too old to
honor the `min-release-age` supply-chain cooldown (added in npm
11.10.0).
## Expected Behavior
`mise.toml` defaults the Node.js toolchain to `26.3.0` (latest Node,
ships npm 11.16.0). Local development and the mise-provisioned CI jobs
move to Node 26.
Scope / notes:
- The default remains overridable via the `NODE_VERSION` env var — the
template is otherwise unchanged.
- `publish.yml` (pinned `NODE_VERSION: 22.16.0` plus hardcoded musl
native builds) and `e2e-matrix.yml` (matrix `node_version`) set
`NODE_VERSION` explicitly, so they are **unaffected** and stay on Node
22.
- Utility workflows using `actions/setup-node` (`pr-title-validation`,
`generate-embeddings`, `banner-monitor`, `issue-notifier`) remain on
Node 24 and are out of scope for this change.
- CI on this PR validates that the core build/test/lint suite passes on
Node 26.
## Related Issue(s)
N/A — internal toolchain bump.
## Current Behavior
The canonical plugin API types are suffixed with `V2` (`CreateNodesV2`,
`CreateNodesContextV2`, `CreateNodesFunctionV2`, `CreateNodesResultV2`,
`NxPluginV2`), left over from when the V1 signatures existed. V1 was
removed in Nx 22 (#32951), freeing up the un-suffixed identifiers but
leaving the V2 names as the public API.
## Expected Behavior
The un-suffixed names (`CreateNodes`, `CreateNodesContext`,
`CreateNodesFunction`, `NxPlugin`, plus a new `CreateNodesResultArray`)
become the canonical public types. The `V2` identifiers remain as
`@deprecated` type aliases so plugin code authored against them keeps
compiling. The plugin loader still checks both `plugin.createNodes` and
`plugin.createNodesV2` property names at runtime, so third-party plugins
that export under either name continue to load.
### Rename map
| Old (now `@deprecated` alias) | New canonical |
|-------------------------------|-----------------------|
| `CreateNodesV2<T>` | `CreateNodes<T>` |
| `CreateNodesContextV2` | `CreateNodesContext` |
| `CreateNodesResultV2` | `CreateNodesResultArray` |
| `CreateNodesFunctionV2<T>` | `CreateNodesFunction<T>` |
| `NxPluginV2<T>` | `NxPlugin<T>` |
### Changes
- `packages/nx` internals (plugin loader, isolation worker, utils,
lock-file, package/project.json plugins, specs) switched to canonical
types.
- `packages/devkit` utilities (`addPlugin`, `findPluginForConfigFile`,
`targetDefaultsUtils`, `calculateHashForCreateNodes`,
`replaceProjectConfigurationsWithPlugin`, `getNamedInputs`,
`executor-to-plugin-migrator`, etc.) migrated to canonical types.
`addPlugin` now registers plugin objects with `createNodes:` as the
canonical key.
- 50 first-party plugin packages updated to canonical type names. Seven
packages whose only primary export was `createNodesV2` now export
`createNodes` as the primary value, with `createNodesV2 = createNodes`
preserved as a backward-compat alias for older Nx consumers.
## Related Issue(s)
N/A
## Current Behavior
When `nx migrate` can't read a package's migrations from the registry,
it falls back to installing the
package in a temporary directory to read them. That fallback detected
the package manager from the
temp directory itself — which has no lock file — so detection fell back
to npm even in yarn / pnpm /
bun workspaces. The fetch then ran `npm install`, ignoring the workspace
package manager's registry,
auth, and release-age configuration (for example, a private registry
configured in `.yarnrc.yml`, or
pnpm's `minimumReleaseAge`), which can break migrations that resolve
through a private registry.
## Expected Behavior
The package manager is already resolved once in
`generateMigrationsJsonAndUpdatePackageJson` (before
the fetcher is created). That value is now threaded through
`createFetcher` into the install fallback,
so the temporary install uses the workspace's package manager. The fetch
helper no longer detects the
package manager at all, so it can't resolve it against the empty temp
directory.
For pnpm workspaces the install runs `pnpm add -w`, which requires a
`pnpm-workspace.yaml` to be
present in the directory (otherwise it fails with `--workspace-root may
only be used inside a
workspace`). A **sanitized** copy is now placed in the temp dir:
`packages` (workspace member globs)
and `patchedDependencies` (relative patch-file paths) are dropped
because they only resolve in the
real workspace, while `registry`, auth, and `minimumReleaseAge` are
kept. This fixes the `-w` failure
and lets the install honor the workspace's registry/auth and release-age
settings.
The install still runs in the temp directory; only the configuration and
package-manager *choice* now
come from the workspace. `createTempNpmDirectory` already copied
`.npmrc` / `.yarnrc` / `.yarnrc.yml`
/ `bunfig.toml`; this adds the sanitized `pnpm-workspace.yaml` alongside
them.
## Related Issue(s)
Relates to NXC-4499.
## Current Behavior
`withNx`/`composePlugins` from `@nx/next` wrap `next.config.js` to
transpile workspace libraries and patch CSS-module loaders. New apps
scaffold the wrapper.
## Expected Behavior
Both helpers are warn-only deprecated (removed in v24). New apps
generate a plain `next.config.js`. Next.js 16 transpiles workspace
libraries natively on Turbopack and webpack, so the wrapper is no longer
needed. Migration recipe added under the Next.js config docs.
## Related Issue(s)
NXC-4325
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4325-0010e859)
<!-- polygraph-session-end -->
## Current Behavior
`@angular/build@21.2.14` (published 2026-06-03) changed
`ComponentStylesheetBundler` to back its bundling with a persistent
esbuild `context()` instead of a one-shot `esbuild.build()`. That
context keeps an esbuild service — a child process and its sockets —
alive until it is explicitly disposed. `@angular/build`'s own
application builder handles this with a `finally { await
result.dispose() }`.
`@nx/angular-rspack` drives `ComponentStylesheetBundler` directly (a
shared singleton in `setup-compilation.ts`) but never disposed it. With
`@angular/build` <= 21.2.13 this was harmless because the one-shot
`build()` cleaned up automatically; with >= 21.2.14 the esbuild service
leaks and a one-shot `rspack build` never exits after the bundle is
written.
Because the catalog/peer range for `@angular/build` allows `< 22.0.0`, a
freshly created workspace now resolves `21.2.14`, so the `e2e/angular`
"Convert Angular Webpack Project to Rspack" test fails with `Command
timed out after 300s: build ...` even though the bundle builds
successfully.
## Expected Behavior
After a one-shot build finishes, `@nx/angular-rspack` disposes the
stylesheet bundler — releasing the esbuild service so `rspack build`
exits cleanly:
- Adds `disposeComponentStylesheetBundler()` to
`@nx/angular-rspack-compiler`, which disposes and resets the shared
singleton.
- The plugin calls it from the compiler's `done` hook on a one-shot
build. This is safe per-compiler: an SSR build runs the server compiler
after the browser one (`dependencies: ['browser']`), never concurrently,
and `setupCompilation` lazily recreates the singleton (`??=`) if a later
compiler needs it again.
- Watch builds keep the bundler for incremental rebuilds and tear it
down on process shutdown instead.
Verified directly against `@angular/build@21.2.14`: an undisposed
bundler leaves a `ChildProcess` + sockets alive (the process hangs);
calling `dispose()` closes them and the process exits.
## Related Issue(s)
Surfaced by the `e2e-angular` `misc.test.ts` regression after
`@angular/build@21.2.14` published; no existing GitHub issue.
## Current Behavior
Three error/diagnostic messages in the `nx` package misspell "occurred"
as "occured":
- `packages/nx/src/plugins/js/lock-file/lock-file.ts` — `title: 'An
error occured while creating pruned lockfile'` (shown to users on
lockfile pruning failure)
- `packages/nx/src/command-line/graph/graph.ts` — `"... occured while
processing the project graph. Showing partial graph."` (console output)
- `packages/nx/src/project-graph/error-types.ts` — doc comment `"...
errors which occured."`
## Expected Behavior
The word is spelled "occurred" in all three places.
## Related Issue(s)
N/A — spelling-only fix, no behavior change.
## Current Behavior
The recently added `nx migrate --mode` flag (`first-party` /
`third-party`) is offered and accepted regardless of the versions
involved. These modes rely on migration metadata that only exists in Nx
v23+, so running them against pre-v23 targets produces incorrect
results. In addition, a bare `nx migrate` defaults to `nx@latest` even
from old installs, and the multi-major prompt can offer a v22.x step
that would invalidate an already-selected mode.
## Expected Behavior
The modes are gated to where the metadata exists, and the surrounding
flow is made safe:
- `first-party` is offered/accepted only when migrating from Nx v22+
into a v23+ target.
- `third-party` only when the workspace is already on v23+.
- The target version (including `latest`/dist-tags) is resolved before
the mode is decided, so the gate reads a concrete major.
- A bare `nx migrate` on a pre-v22 install again requires an explicit
target instead of defaulting to `latest`.
- The multi-major prompt no longer offers a v22.x step, so a selected
mode can't be invalidated by a redirect below v23.
When the functionality isn't available, `nx migrate` falls back to
migrating all packages, matching prior behavior.
## Implementation Details
- The gate predicate lives in `resolveMode`: `first-party` requires
`installedMajor >= 22` and a v23+ target; `third-party` requires
`installedMajor >= 23`. An explicit `--mode` on an unavailable
combination throws with an actionable message; the interactive prompt
only offers the available modes (and falls back to `all` when none).
- `resolveTargetAndMode` resolves dist-tag targets up front. This moves
the single existing registry round-trip earlier — the multi-major check
then short-circuits on the concrete version. On registry failure for a
bare sentinel it assumes >= v23 so the gate stays sensible and still
degrades gracefully downstream.
- `multi-major.ts` suppresses the v22.x current-major step rather than
reordering the mode / multi-major flow.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4493-b48693de)
<!-- polygraph-session-end -->
## Current Behavior
`@nx/vue` and `@nx/nuxt` are not compliant with the multi-version
support initiative (`nx migrate --first-party-only`):
**`@nx/vue` (NXC-4409)**
- `vue`, `vue-tsc`, `vue-router` and `@vitejs/plugin-vue` are not
declared as peer dependencies, so workspaces have no signal that Vue is
required and no advertised compatible range.
- The `20.7.1` and `22.0.0` migrations bump `@vitejs/plugin-vue` across
a major version with no `requires` gate, so the bump fires for every
workspace regardless of source major.
- Generators silently overwrite user-pinned third-party versions (most
`addDependenciesToPackageJson` calls omit `keepExistingVersions`; the
init schema defaults it to `false`).
- There is no below-floor guard — a Vue 2 workspace silently gets Vue 3
scaffolding.
**`@nx/nuxt` (NXC-4397)**
- `nuxt` itself is not declared as a peer (only `@nuxt/schema` is).
- `@nuxt/schema` is declared as a peer but is missing from the version
map / install lanes.
- The `22.2.0` migration bumps `nuxt` from v3 to v4 with no `requires`
gate.
- Generators silently overwrite user-pinned versions, and there is no
below-floor throw when Nuxt 2 (or older) is detected.
## Expected Behavior
Both plugins declare an accurate support window and behave correctly
across it:
**`@nx/vue`** (supported window: Vue 3.x — Vue 2 is EOL since
2023-12-31)
- `vue` (`^3.0.0`), `vue-router` (`^4.0.0`), `vue-tsc` (`^2.0.0`) and
`@vitejs/plugin-vue` (`^5.0.0 || ^6.0.0`) are declared as **optional**
peer dependencies.
- `assertSupportedVueVersion` (floor `3.0.0`) runs as the first
statement of every generator, throwing a clear error on sub-floor Vue.
- The `22.0.0` migration gates on `@vitejs/plugin-vue` `>=5.0.0 <6.0.0`;
the v4→v5 bump is split out of `20.7.1` into its own gated entry so the
same-major `vue`/`vue-tsc`/`vue-router` bumps are not wrongly skipped.
- All generator `addDependenciesToPackageJson` calls pass
`keepExistingVersions: true`; the init schema default flips to `true`.
- The supported-versions docs table reflects the `^3.0.0` window.
**`@nx/nuxt`** (supported window: Nuxt 3 & 4 — Nuxt 2 is EOL since
2024-06-30)
- `nuxt` is declared as a peer (`>=3.10.0 <5.0.0`) alongside
`@nuxt/schema`.
- `@nuxt/schema` is added to the version map and installed per-major by
the application generator.
- `assertSupportedNuxtVersion` (floor `3.0.0`) runs as the first
statement of every generator.
- The `22.2.0` migration gates on `nuxt` `>=3.0.0 <4.0.0` (the single
gate covers the bundled Nuxt-monorepo siblings).
- All generator `addDependenciesToPackageJson` calls pass
`keepExistingVersions: true`; the init schema default flips to `true`.
Both plugins gain a parameterized `all-generators-enforce-floor` spec
that exercises every generator's floor assert.
## Related Issue(s)
Implements the multi-version support compliance tasks NXC-4409
(`@nx/vue`) and NXC-4397 (`@nx/nuxt`).
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The `@nx/angular:convert-to-with-mf` generator wasn't marked deprecated
— it was the only Angular Module Federation generator missing the
`x-deprecated` marker its siblings (`host`, `remote`, `setup-mf`,
`federate-module`) already carry.
Separately, those four siblings emitted their deprecation notice twice:
once from the `x-deprecated` manifest marker (printed by `nx generate`
and shown in `--help`) and again from a redundant in-body `logger.warn`.
## Expected Behavior
- `@nx/angular:convert-to-with-mf` is now marked deprecated via
`x-deprecated` in `generators.json`, so the CLI reports it on `nx g` and
in `nx g convert-to-with-mf --help`.
- The redundant in-body `logger.warn` calls are removed from all five
Angular MF generators; they rely solely on the declarative
`x-deprecated` marker, which already warns on run. The shared
`module-federation-deprecation.ts` helper now retains only the executor
warnings, which have no `x-deprecated` equivalent.
- All five generator `x-deprecated` messages now include the
migration-guide URL.
Angular Module Federation in Nx is no longer supported; users should
move to Angular Native Federation
(`@angular-architects/native-federation`). These generators will be
removed in Nx v24.
## Related Issue(s)
Resolves NXC-3706
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Loading a local Nx plugin from its TypeScript source (via the
`development`/source export condition, including secondary entry points)
fails when the source uses NodeNext-style explicit `.js` relative
imports — `import './nodes.js'` where the file on disk is `nodes.ts`.
Any `nx` command that builds the project graph errors with `Cannot find
module './nodes.js'`.
Node's native TypeScript stripping loads the `.ts` source but does not
rewrite the explicit `.js` specifier to its `.ts` sibling, and nothing
in the plugin-loading path did either.
## Expected Behavior
Local plugins whose TypeScript sources use NodeNext `.js` import
specifiers load correctly from source, for both `type: module` (ESM) and
`type: commonjs` packages.
## Implementation Details
Add dependency-free `.js` → `.ts` resolution on the native-strip
plugin-loading path, mirroring how `tsx` / `ts-node`'s
`experimentalResolver` patch module resolution:
- **CJS:** a `Module._resolveFilename` fallback that rewrites
`.js`/`.mjs`/`.cjs` → `.ts`/`.tsx`/`.mts`/`.cts` when the requesting
module is itself TypeScript and the `.js` file doesn't exist.
- **ESM:** a self-contained inline `module.register` resolve hook (no
`ts-node`/`swc-node` dependency) that does the same on the
dynamic-import path, leaving Node's native stripping to load the
resolved `.ts` — preserving true ESM semantics.
Both are best-effort, fire only on a not-found error, and never alter
resolutions that already succeed. `type: commonjs` sources still rely on
the existing swc/ts-node fallback to transpile their `import` syntax
(native strip can't run ESM syntax in a CJS module), after which the CJS
resolver patch handles the emitted `require('./x.js')`.
> [!NOTE]
> The ESM resolve hook is skipped when a transpiler is preloaded via
`--require`/`--import` (e.g. `--require ts-node/register`), which only
happens when Nx itself runs from `.ts` source — registering it there
would crash the `module.register` loader-hook worker. Published Nx
(compiled `.js` workers, no preload) is unaffected.
## Current Behavior
`findMatchingConfigFiles` still rematches every candidate file against
the plugin glob even though `multiGlobWithWorkspaceContext` already
returned the file list for that exact glob. The previous branch revision
only precompiled the minimatch pattern, which reduced some overhead but
still kept the redundant rematch in the hot path.
## Expected Behavior
Once `projectFiles` already comes from `multiGlobWithWorkspaceContext`
for a plugin's `createNodes` pattern, `findMatchingConfigFiles` should
only apply include/exclude filtering and should not rematch against the
same glob again.
This change removes the redundant rematch entirely and updates the unit
tests to reflect the actual contract of `findMatchingConfigFiles`: it
operates on a pre-matched file list plus include/exclude filters.
Benchmark and reproduction evidence:
- Repro issue: https://github.com/nrwl/nx/issues/35792
- Public repro repo:
https://github.com/cw-alexcroteau/nx-find-matching-config-files-repro-20260525
- Successful cross-platform perf summary:
https://github.com/cw-alexcroteau/nx-find-matching-config-files-repro-20260525/actions/runs/26409870118/attempts/1#summary-77741892587
## Related Issue(s)
Fixes#35792
---------
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com>
## Current Behavior
`@nx/vite` and `@nx/vitest` don't consistently honor their supported
version windows. Cross-major `packageJsonUpdates` and version-specific
migrations run on workspaces outside their target range, generators can
overwrite pinned third-party versions, and unsupported versions silently
fall through to the latest install constants instead of erroring.
## Expected Behavior
Both plugins enforce their support windows. Cross-major migrations
declare source-major `requires` gates so they only run where they apply,
generators throw a clear below-floor error and preserve pinned versions
(`keepExistingVersions` defaults to `true`), and peer ranges and version
maps match what each plugin actually supports.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
## Current Behavior
Under Distributed Task Execution (DTE), the Nx Cloud agent runs each
task via the
programmatic `runDiscreteTasks` API. That path builds the task
orchestrator's
schedule graph from the single task assigned to the agent, with its
dependency
edges stripped — intentionally, so the scheduler runs only that task
without
blocking on dependencies that ran (and were cached) on other agents.
`TaskOrchestrator.runBatch` then forwarded that edge-less schedule graph
to batch
executors as `context.taskGraph`. With the edges missing, `@nx/gradle`'s
batch
executor could not see that, for example, `:proj:gradle:shadowJar`
`dependsOn`
`:proj:gradle:compileKotlin`. It therefore never passed `--exclude-task`
for the
dependency, and Gradle re-ran `compileKotlin` itself instead of reusing
the
restored cache outputs — which could fail the build.
This only affected the distributed path. A normal `nx run-many` / `nx
affected`
already passes a fully-connected task graph, so the exclusion worked
there.
## Expected Behavior
`runBatch` now forwards the **full** task graph to batch executors as
`context.taskGraph`, so dependency edges are visible to executors.
`@nx/gradle`
can compute `--exclude-task` for transitive Gradle tasks that already
ran on
other agents, so they are no longer re-run under DTE.
The orchestrator field previously named `taskGraphForHashing` is renamed
to
`fullTaskGraph`, since hashing was only one of its consumers — it is now
also the
executor context graph. For non-distributed runs the schedule graph and
the full
graph are the same object, so this is a no-op there. Other batch
executors
(`maven`, `jest`, `workspace`) don't read `context.taskGraph`;
`@nx/js:tsc` reads
it for project references and now receives, under DTE, the same
connected graph it
already receives in a normal run.
A regression test asserts that `runBatch` forwards the full (edged)
graph rather
than the edge-less schedule graph.
## Related Issue(s)
N/A — surfaced while debugging a Gradle `shadowJar` failure under DTE.
## Current Behavior
`nxViteTsPaths` and `nxCopyAssetsPlugin` from `@nx/vite/plugins/*` run
silently. New TS-solution workspaces never emit them; new
non-ts-solution workspaces still do.
## Expected Behavior
Both helpers log a one-time deprecation warning on call and carry
`@deprecated` JSDoc tags. Behavior unchanged in v23; removal candidate
for v24.
- TS-solution: helpers are unused (package-manager workspaces handle
path resolution; the project root is the publishable folder so no
`package.json` copy is needed). Generator output is helper-free and a
spec test locks that in.
- Non-ts-solution: workspaces still need both helpers, so generator
output is unchanged for now. A spec test locks in the current emit with
a TODO(v24) for the swap to `vite-tsconfig-paths`.
Configure-Vite docs lead with `vite-tsconfig-paths` and `publicDir` /
`vite-plugin-static-copy` and flag the helpers as deprecated.
## Related Issue(s)
NXC-4316
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
When using `nx release publish` with pnpm (or other package managers),
if a package version has already been published to the registry, the
executor fails with an error instead of gracefully handling it.
## Expected Behavior
The release-publish executor should detect "already published" errors
(E403 with 'previously published versions', EPUBLISHCONFLICT, E409
conflicts) and skip the publish gracefully with a warning, rather than
failing the entire release process.
## Related Issue(s)
Closes#35235
Adopts the canonical multi-version support shape so workspaces on jest
v29 stay on v29 unless they explicitly opt into v30, and workspaces
below v29 fail loudly with the standardized "Unsupported version of
\`jest\` detected" message instead of silently falling through to v30
install constants.
- Declare peerDependencies (jest, ts-jest, @types/jest as ^29 || ^30,
all optional) so workspaces have an advertised compatible range.
- Add assertSupportedJestVersion wrapper around the shared
assertSupportedPackageVersion helper; asserts jest and ts-jest
independently (they install on separate version trains).
- Rewrite versions.ts to use the shared getInstalledPackageVersion
helper from @nx/devkit/internal, drop the above-ceiling throw in
versions() in favor of latest-fallback, drop the bespoke
validateInstalledJestVersion. Add a ts-jest v30 install lane.
- Delete the dead version-utils.ts file (zero usages).
- Call the assert as the first statement in init, configuration, and
convert-to-inferred generators; flip init schema's keepExistingVersions
default to true and apply the ?? true fallback to preserve user-pinned
versions on re-runs.
- Gate the 21.3.0 cross-major packageJsonUpdates entry with requires: {
jest: ">=29.0.0 <30.0.0" } so v29 users get the v30 bump but
v28-or-below users do not get silently pushed across two majors.
- Add the parameterized all-generators-enforce-floor.spec.ts to
guarantee every entry asserts the floor.
NXC-4391
---------
Co-authored-by: Claude <noreply@anthropic.com>
Add a subsection explaining that createNodes/createNodesV2 output must
be deterministic. covers stable array ordering, avoiding run-specific
values; explains the consequences of cache misses.
## Current Behavior
<!-- This is the behavior we have today -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
<!-- 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
Calling `nx release version` currently runs docker build, due to
detection of Dockerfile in the project. Users using docker outside of
nx/docker have no way of preventing this.
## Expected Behavior
Running `nx release version` should not trigger docker build unless,
docker use is explicitly enabled.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
The `@nx/eslint` package supports ESLint v8 as a first-class citizen.
Fresh installs without flat config get ESLint v8 + typescript-eslint v8,
and there is no signal to users that this support is going away.
In addition, workspaces that end up on ESLint v9 with eslintrc-style
config get a broken `workspace-rule` rule-test template. ESLint v9
dropped the eslintrc-style `RuleTester` class, but the generator emits
`TSESLint.RuleTester` (which extends `eslint.RuleTester`) regardless of
the installed major. v9 + eslintrc users currently can't run the
generated `.spec.ts` without manual edits.
## Expected Behavior
- ESLint v8 support is soft-deprecated. `@nx/eslint` generators (via
`assertSupportedEslintVersion`) and the `@nx/eslint:lint` executor emit
a deprecation warning on every invocation when the workspace's installed
ESLint major is 8. v8 keeps working unchanged; hard removal is scheduled
for Nx v24.
- Fresh installs always pull the latest supported ESLint stack (v9 +
typescript-eslint v8) regardless of the user's flat-config preference.
The eslintrc config-file shape is still honored at the file level
(ESLint v9 loads eslintrc files through `LegacyESLint`); only the
installed package versions move forward.
- The `workspace-rule` generator emits the flat-style
`@typescript-eslint/rule-tester` template (and installs that dep) for
any workspace ending up on ESLint v9+, not just flat-config ones. v8 +
eslintrc workspaces continue to use the existing `TSESLint.RuleTester`
template unchanged.
## Implementation Details
- New `warnEslintV8Deprecation()` in
`packages/eslint/src/utils/deprecation.ts`, fired from
`assertSupportedEslintVersion` (choke point for the `init`,
`lint-project`, `workspace-rule`, `workspace-rules-project`,
`convert-to-flat-config`, and `convert-to-inferred` generators) and the
lint executor.
- The `workspace-rule` generator now gates the rule-test template on
`useFlatRuleTester = flatConfig || effectiveEslintMajor >= 9`, derived
from `versions(tree).eslintVersion` so it covers both declared
workspaces and fresh installs.
- `versions(tree)` no longer consults `useFlatConfig` for the
fresh-install branch; both lanes return `latestVersions`.
`minSupportedEslintVersion` stays at `'8.0.0'` and `versionMap[8]` stays
in place so existing v8 workspaces keep getting a coherent stack until
v24 removes them.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-3986-ac90716f)
<!-- polygraph-session-end -->
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
## Current Behavior
The shared CLI examples for `nx show target` document input and output
inspection with the target before the subcommand, for example `nx show
target my-app:build inputs`.
## Expected Behavior
The examples use the canonical subcommand-first syntax shown in the
command help and intended for the Nx 22.7 sandboxing blog correction:
`nx show target inputs my-app:build` and `nx show target outputs
my-app:build`.
## Related Issue(s)
N/A - reported directly in Polygraph session
`fix-sandboxing-code-example-3443d65b`.
## Validation
- `npx prettier --write -- packages/nx/src/command-line/examples.ts`
- `git diff --check`
- Focused `pnpm exec tsx` metadata check confirming the stale
target-first examples are absent and the subcommand-first examples are
present.
Full `nx prepush` has not been run.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/fix-sandboxing-code-example-3443d65b)
<!-- polygraph-session-end -->
## Current Behavior
A migration's documentation (the co-located markdown explaining what it
does) is not referenced in `migrations.json`, so when `nx migrate
--run-migrations --agentic` drives an AI agent, the agent has no access
to it.
## Expected Behavior
Migration entries can declare a `docs` markdown path. `nx migrate`
carries it into the generated `migrations.json`, and under `--agentic`
the resolved path is surfaced to the agent (for prompt, hybrid, and
validation steps) so it has context on what the migration is meant to
do. Existing first-party migrations with co-located docs are wired up
via the new field and published with their packages.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4495-6c420036)
<!-- polygraph-session-end -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The `@nx/jest:jest` executor accepts a `setupFile` option that has been
deprecated in favor of declaring setup files via `setupFilesAfterEnv` in
the Jest configuration file. The executor merges `setupFile` into
`setupFilesAfterEnv` at runtime.
The `@nx/jest:configuration` generator accepts a `skipSetupFile` option
that has been deprecated in favor of `setupFile: 'none'`.
## Expected Behavior
The `@nx/jest:jest` executor's `setupFile` option is removed. The
`@nx/jest:configuration` generator's `skipSetupFile` option is removed.
Two migrations run automatically on `nx migrate`.
`migrate-jest-executor-setup-file` pushes existing executor `setupFile`
values into `setupFilesAfterEnv` in the project's Jest config (using
`<rootDir>/...` form), then strips the option from `project.json` and
`nx.json` target defaults. Coverage:
- Per-target `setupFile` in `project.json` (base options or
configurations).
- `setupFile` in `nx.json` `targetDefaults`, including for targets that
inherit the default without declaring their own — the migration walks
affected projects, expands `{projectRoot}` per project, and writes the
inherited value into each project's Jest config before stripping the
default.
- Configurations that override `jestConfig` and inherit the base
`setupFile` get their override Jest config migrated too, so `nx test
<project> -c <config>` keeps loading the setup file post-migration.
- Configurations with their own `jestConfig` and a different `setupFile`
from base are auto-migrated to the configuration's own Jest config.
- Static `module.exports = {...}` and `export default {...}` shapes,
including configs with spreads (`{ ...preset }` — multi-spread mirrors
object-spread "last wins" via a nullish-coalescing fallback chain) and
quoted property names.
- Custom `rootDir` declared as a string literal — the migrated path is
computed relative to the resolved `rootDir`.
- Path-aware deduplication: existing `'./src/setup.ts'` entries aren't
duplicated when the new entry resolves to the same file.
Edge cases that can't safely be auto-rewritten still strip the dead
option but surface a follow-up warning listing the affected targets so
the setup file path can be moved manually:
- Non-static configs (factory functions, dynamic exports) →
`unparseable`.
- Custom `rootDir` declared as a non-literal expression →
`nonLiteralRootDir`.
- Shared Jest config across targets with different setup files →
`sharedConfigConflict`.
- `setupFile` plus `setupFilesAfterEnv` passthrough in the same scope →
`passthroughCollision`.
- Configuration-scoped `setupFile` that would leak to the base run →
`configurationOnly`.
- Targets without any resolvable `jestConfig` →
`noResolvableJestConfig`.
`migrate-jest-configuration-skip-setup-file` rewrites `skipSetupFile`
defaults stored in `nx.json` `generators` or per-project `project.json`
`generators` (both flat `@nx/jest:configuration` and nested `@nx/jest` →
`configuration` forms): `skipSetupFile: true` becomes `setupFile:
'none'` (preserving original behavior), `skipSetupFile: false` is
dropped (it was a no-op). CLI invocations of `@nx/jest:configuration
--skipSetupFile` should pass `--setupFile=none` instead.
BREAKING CHANGE: The `setupFile` option of the `@nx/jest:jest` executor
and the `skipSetupFile` option of the `@nx/jest:configuration` generator
are removed.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
## Current Behavior
`nx migrate` is configured entirely through CLI flags — there's no way
to set workspace-wide defaults, so every run repeats the same
`--create-commits`, `--commit-prefix`, `--mode`, and so on.
When the agentic flow runs during `nx migrate --run-migrations`:
- The "Enable the agentic flow?" prompt is asked on every run, with no
way to remember the answer.
- The flow aborts when it can't start — for example in a non-interactive
terminal, or when a requested/pinned agent isn't installed.
## Expected Behavior
A new optional `migrate` section in `nx.json` sets workspace-wide
defaults for `nx migrate`. A CLI flag always takes precedence over the
`nx.json` value, which takes precedence over the built-in default (for
`multiMajorMode`: CLI flag > `NX_MULTI_MAJOR_MODE` > `nx.json` >
default).
| Option | Applies to |
| --- | --- |
| `createCommits`, `commitPrefix`, `agentic`, `validate` | running
migrations (`nx migrate --run-migrations`) |
| `mode`, `multiMajorMode` | generating migrations (`nx migrate
<target>`) |
The agentic flow is also more flexible and resilient:
- The "Enable the agentic flow?" prompt now offers to remember your
choice; choosing a "remember" option persists it to `migrate.agentic` so
you aren't asked again.
- It degrades gracefully instead of aborting. A non-interactive terminal
warns and continues without the agentic flow. A requested or pinned
agent that isn't installed warns and resolves from the agents that are
installed — prompting when several are present, auto-selecting the only
one, and erroring only when none are installed.
## Implementation Details
- Adds `NxMigrateConfiguration` to `NxJsonConfiguration`, the `nx.json`
JSON schema, and the `nx-json` docs reference.
- `nx.json` defaults are overlaid onto the parsed args in a phase-aware
way, so generate-only and run-only options never trip the existing
`--run-migrations` guards. Config values (`mode`, `multiMajorMode`,
`agentic`) are validated with the same rules as the CLI flags.
- The agentic enable prompt is a single prompt whose choices adapt to
how many agents are installed. Persisting writes the raw `nx.json` (so
an `extends` preset is never inlined into the user's file) and never
throws — a failed write just means the prompt is shown again next time.
- A custom `migrate.commitPrefix` that can't take effect because commits
aren't enabled now warns instead of being silently ignored.
- Option value lists and validation are consolidated into single sources
of truth shared by the CLI builder, the `nx.json` overlay, and the
config types.
- Covered by unit tests for the config overlay, the agentic prompt and
agent resolution, and the commit-prefix warning.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4457-5a746c6e)
<!-- polygraph-session-end -->
## Current Behavior
When running inside a Linux container or Kubernetes pod, resource
metrics report the host node's CPU and memory totals instead of the
pod's limits. A pod with constrained CPU/memory shows the underlying
node's resources.
The same gap exists for process-isolated Windows containers running
inside a Windows Job Object (e.g. Docker `--cpus` / `--memory`): metrics
report host values rather than the Job's limits.
## Expected Behavior
Resource metrics report the effective CPU and memory limits enforced by
the kernel for the calling process — derived from the cgroup it belongs
to on Linux, or the Job Object on Windows. macOS native processes
continue to report host values (no equivalent enforcement primitive
exists).
## Implementation Details
### Linux (`cgroup` module)
Resolves the calling process's actual cgroup directory by parsing
`/proc/self/cgroup` + `/proc/self/mountinfo`. Reads `cpu.max` /
`memory.max` (cgroup v2) or `cpu.cfs_{quota,period}_us` /
`memory.limit_in_bytes` (cgroup v1, including the systemd `cpu,cpuacct`
co-mount).
Walks from the leaf up to the mount point and takes the minimum finite
limit found at any level — the kernel enforces the tightest ancestor's
limit (hierarchical enforcement), so leaf-only reads can overreport when
a pod-level cgroup is tighter than the container's (common in K8s VPA
in-place resize and Burstable QoS).
Composition with `sched_getaffinity` covers cpuset / taskset
restrictions. We deliberately bypass
`std::thread::available_parallelism()` because rust-lang/rust's
implementation applies the cgroup quota with floor division internally,
which would silently underreport fractional quotas (e.g. 1.5 cores → 1).
Instead we ceil quota / period, matching HotSpot JVM, Go 1.25, .NET, and
the `num_cpus` crate.
mountinfo path fields containing spaces / tabs / newlines / backslashes
are kernel-encoded as `\040` / `\011` / `\012` / `\134` per `man 5
proc_pid_mountinfo`; we unescape before joining with the cgroup path.
`/proc/self/cgroup` itself emits paths raw (verified across kernels
v4.18 → v6.13), so no unescape is needed there.
Replaces the prior leaf-only path lookups (which broke on cgroup v1
co-mount and any non-namespaced container setup). The in-tree module is
preferred over `sysinfo`'s `cgroup_limits()` (now parent-aware in 0.39 —
see *Additional Changes*) because it also covers CPU quota and the v1
co-mount + bind-mount edge cases in a single place we control. 30 unit
tests cover cgroup discovery, parsing, ancestor walk, and the v1 / v2 /
co-mount / bind-mount / cgroupns=host cases.
### Windows (`job_object` module)
Detects Job Object resource limits via the Win32 API:
- **CPU**: `ceil(host_cpu_count × CpuRate / 10000)` from
`JobObjectCpuRateControlInformation` when `HARD_CAP` is set (Docker
`--cpus` translates to HARD_CAP). Plus the popcount of the Job's
affinity mask (when `LIMIT_AFFINITY` is set), and
`GetProcessAffinityMask` (covers Job + manual + system intersections).
Takes the minimum.
- **Memory**: minimum of `ProcessMemoryLimit` / `JobMemoryLimit` /
`MaximumWorkingSetSize` from `JobObjectExtendedLimitInformation`, gated
on the corresponding `LIMIT_*` flags. Mirrors HotSpot and .NET.
- **Skipped**: `WEIGHT_BASED` rate control (relative priority, not a
hard limit) and soft-cap rate control (kernel allows transient bursts) —
neither maps to a defensible "available cores" number.
Any Win32 failure is treated as "no information"; the caller falls back
to host values. No new crate dependency — the existing `winapi` dep is
extended with `jobapi`, `jobapi2`, `processthreadsapi`, `winbase`, and
`winnt` features.
#### Known limitation: nested Job hierarchies
[`QueryInformationJobObject(NULL,
...)`](https://learn.microsoft.com/en-us/windows/win32/api/jobapi2/nf-jobapi2-queryinformationjobobject)
returns the **immediate** Job's settings (per MSDN: *"If the job is
nested, the immediate job of the calling process is used."*) and Win32
exposes no documented API to enumerate parent Jobs.
Per
[`JOBOBJECT_CPU_RATE_CONTROL_INFORMATION`](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_cpu_rate_control_information)
Remarks, *"the rates set for the job represent its portion of the CPU
rate that is allocated to its parent job"* — nested rates compose
multiplicatively. So with parent HARD_CAP 50% × child HARD_CAP 50%, the
effective rate is 25% of host but we read the child's 50% and report
`ceil(host × 0.5)`. Per-process / per-job memory has the same shape
(kernel enforces min across the chain; we see only the immediate Job).
Affinity is unaffected — `GetProcessAffinityMask` returns the
kernel-effective mask. HotSpot and .NET CoreCLR exhibit the same memory
limitation; HARD_CAP CPU-rate detection goes beyond them for Docker
`--cpus` parity in the common single-silo case, accepting the nested-Job
overreport as the documented cost.
### Cross-platform
`SystemInfo { cpuCores, totalMemory }` shape is unchanged — consumers do
not need to update. macOS native processes continue to report host
values (no container-style enforcement primitive exists; container
runtimes on macOS run Linux VMs where the Linux path applies).
The module doc-comments cross-reference Go 1.25
`internal/runtime/cgroup`, libuv `src/unix/linux.c`, OpenJDK
`cgroupSubsystem_linux.cpp` + `os_windows.cpp`, .NET CoreCLR
`gc/unix/cgroup.cpp` + `gc/windows/gcenv.windows.cpp`, and Rust stdlib
`library/std/src/sys/thread/unix.rs::cgroups`.
## Additional Changes
Two infrastructure chores are bundled into this PR as separate commits:
### `chore(core): bump sysinfo to 0.39.1`
Bumps `sysinfo` from `0.37.2` → `0.39.1`. No source changes were
required — all signatures we use (`System`, `Process`, `Pid`, `Signal`,
`Disks`, the `*RefreshKind` types, `UpdateKind`,
`MINIMUM_CPU_UPDATE_INTERVAL`) are unchanged. `Cargo.lock` deltas:
- New transitive dep `objc2-open-directory` from sysinfo 0.39's
soundness fix for user retrieval on Apple targets.
- `windows` family bumped to `0.62.x` per sysinfo's new constraint,
which lets the graph converge on single versions of `windows-core`,
`windows-link`, `windows-result`, and `windows-strings` — four duplicate
`windows-*` entries are deduplicated as a result.
sysinfo 0.39.0 also added `Process::cgroup_limits()` and parent-cgroup
memory walking inside `System::cgroup_limits()` — overlapping
conceptually with the in-tree `cgroup` module introduced by this PR. The
in-tree module is kept because it also covers CPU quota (sysinfo's
helpers are memory-only), the cgroup v1 co-mount, and bind-mount path
translation in a single implementation under our control. Future
consolidation onto the upstream APIs is possible but explicitly out of
scope here.
### `chore(repo): bump mise rust to 1.95.0 to match rust-toolchain.toml`
`rust-toolchain.toml` was bumped to `1.95.0` in #35665 to unblock the
sysinfo upgrade, but `mise.toml` was left at `1.90.0`. CI installs Rust
via mise (which exports `RUSTUP_TOOLCHAIN`, overriding
`rust-toolchain.toml`), so CI continued to run on `1.90.0` and failed
the sysinfo 0.39 MSRV check until this commit. The two files now agree.
## Current Behavior
The canary release pipeline fails while building the `@nx/dotnet`
analyzer:
```
error NETSDK1018: Invalid NuGet version string: 'canary'.
[/home/runner/work/nx/nx/packages/dotnet/analyzer/MsbuildAnalyzer.csproj]
```
**Root cause:** the `publish` workflow's `Publish` step exported
`VERSION=canary` for the whole step. MSBuild imports environment
variables as properties (case-insensitively), so `VERSION` becomes
`$(Version)` during the `dotnet build` that runs underneath `nx-release`
(the `@nx/dotnet` package ships the compiled `MsbuildAnalyzer.dll`, so
building it during publish runs the analyzer build). On canary the value
is the non-semver string `canary`, which `GenerateAssemblyInfo` rejects.
This only began failing once `@nx/dotnet` — the repo's first project
that runs `GenerateAssemblyInfo` — entered the publish build graph; the
`VERSION` env var itself is long-standing and unchanged.
## Expected Behavior
The publish step no longer exposes a generically-named `VERSION`
environment variable that MSBuild can adopt as `$(Version)`.
The env var is renamed to `NX_PUBLISH_VERSION`. `nx-release` reads the
version as a positional CLI argument (not from the environment), so no
script change is required — only the workflow. A comment is added
warning against reusing the generic `VERSION` name.
This fixes the leak at its source rather than working around it in each
.NET project.
## Related Issue(s)
N/A
Co-authored-by: Claude <noreply@anthropic.com>
## Current Behavior
The nightly "E2E matrix" `process-result` job times out and is
cancelled, so the Slack summary stopped posting after May 23. Its
failure-detail step makes many serial, uncached GitHub API calls - a
30-run history fetch plus a per-failure binary search to pin each
error's start date - which on runs with many golden failures exceeds the
15-minute job cap and trips GitHub's secondary rate limit before the
reporting steps run.
## Expected Behavior
The start-date/streak analysis is removed entirely, along with all
related wording in the report. The Slack message now lists the failing
projects and their actual errors, with no temporal claims. The job
finishes in ~30s and posts again.
## Current Behavior
The `update-repos` script (`tools/update-repos/src/update-repo.ts`)
pushes the
`upnx` update branch and then tries to create or update a pull request
directly
via the GitHub CLI (`gh pr create` / `gh pr edit`), finally opening it
in the
browser with `gh pr view --web`. When `gh` PR creation isn't available,
the run
fails to surface a usable PR link.
## Expected Behavior
The script still pushes the `upnx` branch, but no longer creates PRs via
`gh`.
Instead, at the very end it prints a ready-to-click GitHub "create pull
request"
URL with the title and description pre-filled, for each updated repo:
```
📦 nx
Title: chore(repo): update nx to <version>
Description: Updating Nx from <from> to <to>
Open PR: https://github.com/nrwl/nx/compare/master...upnx?expand=1&title=...&body=...
```
When updating all repos concurrently, the run now uses
`Promise.allSettled` so
the PR URLs for repos that succeeded are still printed even if another
repo
fails; the run then reports which repos failed.
## Related Issue(s)
N/A
## Current Behavior
The `projectsAffectedByDependencyUpdates: "auto"` setting only works for
pnpm lock files. For npm (`package-lock.json`), yarn (`yarn.lock`), and
bun (`bun.lock`/`bun.lockb`), auto mode silently returns an empty array
-- meaning lockfile-only changes (e.g. `npm audit fix`, transitive
dependency updates) produce zero affected projects.
## Expected Behavior
Auto mode detects affected projects from lock file changes for all
supported package managers:
- **pnpm** (`pnpm-lock.yaml`): Inspects the `importers` section to
determine exactly which workspace projects had dependency changes
(unchanged).
- **npm** (`package-lock.json`): Inspects the `packages` entries to
determine which workspace projects had dependency changes.
- **bun** (`bun.lock`): Inspects the `workspaces` section to determine
which workspace projects had dependency changes.
- **yarn** (`yarn.lock`): The lock file is a flat list with no
per-project structure, so all projects are marked as affected when any
dependency changes.
- **Binary lock files** (`bun.lockb`) / **WholeFileChange**: Cannot be
parsed for granular changes, so all projects are marked as affected.
The implementation uses a `LOCK_FILE_RESOLVERS` map with a
`SupportedLockFile` type guard so that adding a new lock file format
requires adding exactly one entry -- no separate lists to keep in sync.
## Related Issue(s)
Follow-up to #34937 which documented the pnpm-only limitation.
Fixes NXC-4185
Fixes https://github.com/nrwl/nx/issues/35173
---------
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: llwt <llwt@users.noreply.github.com>
## Current Behavior
`@nx/angular`'s `zoneJsVersion` is pinned to `~0.15.0`, while
`catalogs.angular` in `pnpm-workspace.yaml` and the v21 migration entry
in `packages/angular/migrations.json` both pin `zone.js` at `~0.16.0`.
Generators and dependency-installation paths in `@nx/angular` therefore
emit `zone.js@~0.15.0` even when the project is on Angular 21, drifting
from what the migration and workspace catalog declare.
## Expected Behavior
`zoneJsVersion` matches the v21 pin (`~0.16.0`), keeping generator
output aligned with the workspace catalog and the v21 migration. The
bump is safe within Angular 21: `@angular/core@21.x` accepts `~0.15.0 ||
~0.16.0` (see `catalogs.angular-supported-versions` in
`pnpm-workspace.yaml`).
## Implementation Details
Bumps `zoneJsVersion` in `packages/angular/src/utils/versions.ts` from
`~0.15.0` to `~0.16.0`. The v21 entry in
`backward-compatible-versions.ts` inherits from `latestVersions` via
spread, so it picks up the bump automatically. The v20/v19 entries are
unchanged — those Angular majors correctly stay on `~0.15.0`.
Also extends `scripts/angular-support-upgrades/` so this drift doesn't
recur on the next Angular bump:
- `fetch-versions-from-registry.ts`: after the dist-tag fetch, resolves
`zone.js` and `rxjs` against the latest registry-published versions
matching `@angular/core@<resolved>`'s `peerDependencies` ranges. Skips
with a warning if a peer is missing or no satisfying version exists.
- `update-version-utils.ts`: adds regex bumps for `zoneJsVersion` and
`rxjsVersion` in `versions.ts`, guarded so they only run when the
version map carries those keys.
`update-package-jsons.ts` and `build-migrations.ts` need no changes —
their existing iteration over the version map already covers `zone.js`
(and `rxjs` where applicable) once the keys are populated.
`ngrxVersion` and the per-major back-compat blocks in
`backward-compatible-versions.ts` have similar drift risk but are out of
scope here (different mechanics; flagged for follow-up).
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/fix-zone-js-version-b63af779)
<!-- polygraph-session-end -->
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
## Current Behavior
**`@nx/express`**
- Peer dep declares `express: ^4.21.2` only; Express v5 has been ACTIVE
since 2025-03-31 and v4 is in MAINT, so the plugin's advertised window
doesn't match the upstream support window.
- Generators install a single literal `express ^4.21.2` regardless of
what's installed; no per-major routing for `express` or
`@types/express`.
- `keepExistingVersions` schema default is `false`, so re-running the
generator silently overwrites a user's pinned `express` version.
- No floor enforcement: a workspace on an unsupported (sub-floor)
`express` version sees no error.
**`@nx/node`**
- No `peerDependencies` for `express`, `koa`, or `fastify`.
Init/application generators write framework deps unconditionally,
ignoring what's installed.
- `keepExistingVersions` defaults to `false`; framework installs
override user pins.
- `migrations.json` `22.0.2` bumps `koa` from v2 to v3 with no
`requires` block — every workspace on koa v2 gets pushed to v3
unconditionally.
- `migrations.json` `20.4.0` mixes a cross-major fastify v4→v5 bump with
same-major express bumps under one entry, with no `requires`
(AND-semantics would gate same-major bumps incorrectly if added
naively).
- No floor enforcement for any framework.
**`@nx/nest`**
- `peerDependencies` block is missing from `package.json` entirely;
workspaces have no advertised compatible range for any `@nestjs/*`
package.
- Versions module has flat constants; the plugin ships v11-only even
though NestJS v10.4.x still receives upstream patches (N & N-1 baseline
calls for v10 + v11).
- `migrations.json` `21.2.0-beta.2` uses a bare key `nest` which is not
a real npm package — the entry is effectively a no-op. The real
cross-major v10→v11 bump for `@nestjs/common`, `@nestjs/core`,
`@nestjs/platform-express`, `@nestjs/testing` is missing, as is a
`requires` source-major gate.
- No floor enforcement for any NestJS version.
## Expected Behavior
**`@nx/express` (NXC-4390)**
- Per-major `versionMap` keyed on `express` major covering both
`express` and `@types/express` for v4 and v5; fresh installs default to
v5.1.0.
- `assertSupportedExpressVersion(tree)` (calls shared
`assertSupportedPackageVersion`) is the first statement of
`initGenerator` and `applicationGeneratorInternal`.
- Peer widened to `express: ">=4.0.0 <6.0.0"` (still optional).
- All `addDependenciesToPackageJson` call sites from generators pass
`keepExistingVersions ?? true`; schema defaults flipped to `true`. Init
now installs `@types/express` (previously a dead export).
- New `all-generators-enforce-floor.spec.ts` exercises every generator
entry at `subFloorVersion: '~3.21.0'`.
- Supported-versions docs page updated.
**`@nx/node` (NXC-4396)**
- Per-package `versionMap` + `versions(tree)` for `express` (v4+v5),
`koa` (v2+v3), `fastify` (v4+v5), and `@types/node` (v22+v24). Fresh
installs default to active LTS / latest stable.
- `assertSupportedFrameworkVersion(tree, schema.framework)` (dispatches
to one wrapper per framework) is the first statement of
`applicationGeneratorInternal`, only firing when `--framework` selects a
non-`none`/`nest` lane.
- Framework + `@types/node` installs route through `versions(tree)` and
pass `keepExistingVersions ?? true`. Init schema default flipped to
`true`.
- `migrations.json`: `22.0.2` koa v2→v3 gated with `requires: { koa:
">=2.0.0 <3.0.0" }`. `22.6.0` koa CVE patch gated bilaterally to v3
only. `20.4.0` split into the original same-major express bumps (no
gate) and a new `20.4.0-fastify` entry gated on `requires: { fastify:
">=4.0.0 <5.0.0" }`.
- Optional `peerDependencies` declared for `express`, `koa`, `fastify`.
Added `semver: "catalog:"` to deps (now imported by `versions.ts`).
`@nx/dependency-checks` allow-list updated.
**`@nx/nest` (NXC-4394)**
- Per-major `versionMap` covering NestJS v10 and v11 for the full
`@nestjs/*` family plus `rxjs` and `reflect-metadata`. Fresh installs
default to NestJS v11, with `reflect-metadata` bumped from `^0.1.13` to
`^0.2.0` to match v11's requirement.
- `assertSupportedNestJsVersion(tree)` is the first statement of the
`init`, `application`, and `library` generators.
- `ensureDependencies` and the init `addDependencies` helper route
through `versions(tree)` and pass `keepExistingVersions: true` (`??
true` on the init path); init schema default flipped to `true`.
- Optional `peerDependencies` declared for `@nestjs/core`,
`@nestjs/common`, `reflect-metadata`, `rxjs`. Added `semver: "catalog:"`
to deps and extended `@nx/dependency-checks` allow-list.
- `migrations.json` `21.2.0-beta.2` rewritten with real package keys
(the previous bare `nest` key was a typo / no-op) and gated on
`requires: { "@nestjs/core": ">=10.0.0 <11.0.0" }`. The entry now also
bumps `reflect-metadata` to `^0.2.0` for v11 compatibility.
- Supported-versions docs page updated.
## Related Issue(s)
Fixes NXC-4390
Fixes NXC-4396
Fixes NXC-4394
---------
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com>
The existing MF generators and setup are not modernized. We can simplify
the setup and remove strict dependency on Nx executors.
- Consolidate on official module naming: `host -> consumer`, `remote ->
provider`
- Always use dynamic runtime (i.e. use runtime utils from
`@module-federation/enhanced/runtime` so we don't have to start up every
provider app on dev machine
- No more Nx specific bundler plugins, only standard config as seen on
module-federation.io
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`@nx/eslint` and `@nx/eslint-plugin` have several multi-version support
compliance gaps:
- The bare `@nx/eslint:init` generator path lands fresh installs on EOL
ESLint v8 (`~8.57.0`) even though the flat-config path already ships v9.
- Local re-implementations of installed-version helpers in
`version-utils.ts` and per-major version aliases (`eslint9__*`) in
`versions.ts` drift from the shared `@nx/devkit/internal` helpers.
- Generators do not assert the supported ESLint floor at their entry
points — sub-floor workspaces silently get configs incompatible with
their installed ESLint.
- `addDependenciesToPackageJson` call sites in generators do not
preserve user-pinned ESLint dependency versions; `init`'s schema
defaults `keepExistingVersions` to `false`.
- The `@nx/eslint:lint` executor enforces a stale `7.6` floor with a
hand-rolled `Number(version[0])` check rather than the shared canonical
helper.
- Two ESLint migration generators (`update-typescript-eslint-v8.13.0`,
`add-file-extensions-to-overrides`) lack `requires` gates, so they run
on every workspace even when their target packages are absent or at
unrelated versions.
- `@nx/eslint-plugin` declares `@typescript-eslint/parser: ^6.13.2 ||
^7.0.0 || ^8.0.0` as a required peer but ships
`@typescript-eslint/utils` / `@typescript-eslint/type-utils` pinned to
`^8.0.0` — the peer claim is wider than the bundled runtime range, and
users only linting JavaScript see an unmet-peer warning.
## Expected Behavior
`@nx/eslint`:
- Fresh installs default to ESLint v9 via the canonical `versions(tree)`
route. Installed versions are respected through the version map (v8 →
`~8.57.0` lane; v9/v10 → `latestVersions`, with v10 silently falling
through — no above-ceiling throw).
- `versions.ts` follows the bundle pattern; `version-utils.ts` delegates
to `@nx/devkit/internal` helpers.
- Every `generators.json` entry asserts the supported floor (`8.0.0`) at
its working function's first statement via the canonical
`assertSupportedEslintVersion(tree)` wrapper. A parameterized
`all-generators-enforce-floor.spec.ts` pins this so a future generator
added without the assert fails the spec.
- All generator-side `addDependenciesToPackageJson` calls preserve
user-pinned versions: `init`'s schema defaults `keepExistingVersions` to
`true`, and programmatic callers default via `?? true`.
- The `@nx/eslint:lint` executor uses the shared
`assertSupportedInstalledPackageVersion` helper to enforce the `8.0.0`
floor with the canonical `Unsupported version of \`eslint\` detected`
message.
- The two migration generators are gated on `@typescript-eslint/parser
>=8.0.0` and `eslint >=8.57.0` respectively (open upper bound so `nx
migrate --from <older>` still applies them).
`@nx/eslint-plugin`:
- `@typescript-eslint/parser` peer is tightened to `^8.0.0` (matching
the bundled `@typescript-eslint/utils` / `type-utils` v8 runtime) and
declared optional via `peerDependenciesMeta` — users linting only
JavaScript no longer see unmet-peer warnings.
## Implementation Details
> [!IMPORTANT]
> This PR includes a cherry-picked commit (`feat(devkit): add
assertSupportedInstalledPackageVersion to @nx/devkit/internal`) that
originates from #35806. Whichever of the two PRs merges first, the other
will be rebased to drop the duplicate commit.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4387-fa5e84db)
<!-- polygraph-session-end -->
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
- `@nx/angular-rspack` and `@nx/angular-rspack-compiler` silently fall
through to broken behavior when the workspace's `@angular/build`,
`@rsbuild/core`, or `@rspack/core` is below the supported floor.
- `@nx/angular-rspack-compiler` keeps `@angular/build` as a direct
`dependencies` entry, forcing a version that can conflict with the
user's installed Angular major.
- `@nx/angular-rspack` calls `createAngularCompilation` with the
`browserOnlyBuild` argument inverted: when the user has a server entry,
the compiler is told it's a browser-only build, and vice versa.
## Expected Behavior
- A user on `@angular/build < 19.0.0`, `@rsbuild/core < 1.0.5`, or
`@rspack/core < 1.3.5` gets the standardized `Unsupported version of
<pkg> detected` error from the shared
`assertSupportedInstalledPackageVersion` helper rather than a downstream
crash.
- `@angular/build` moves from `dependencies` to `peerDependencies`,
matching how the rest of the first-party plugin ecosystem declares its
ecosystem-locked peers.
- `createAngularCompilation` receives `browserOnlyBuild` semantics that
match upstream Angular CLI: `true` when there is no server entry,
`false` when there is.
## Implementation Details
- Add `assertSupportedInstalledPackageVersion` to `@nx/devkit/internal`
for runtime contexts where no `Tree` is available. The helper coerces
the installed version before comparing so a valid prerelease of the
supported major (e.g. `19.0.0-rc.1`) isn't wrongly flagged as below
floor.
- Add runtime floor guards at the public entry points of both packages,
delegating to the shared helper.
- Drop the named `RspackError` import from `@rspack/core` in
`rspack-diagnostics`. The named export only exists at `>=1.4.0`, but the
declared peer floor is `>=1.3.5`; derive the type from
`Compilation['errors'][number]` instead so it works at every supported
version.
- Document the supported version window in the
`@nx/angular-rspack-compiler` introduction page.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4383-fb1ae6fe)
<!-- polygraph-session-end -->
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
## Current Behavior
`@nx/storybook` did not enforce a multi-version support window:
- Generators silently fell through to the latest install constants on
workspaces below any supported floor (e.g., Storybook v6/v7 setups).
- `packageJsonUpdates` entries pushed cross-major Storybook bumps
unconditionally — a workspace on Storybook v7 running `nx migrate` got
its `@storybook/*` siblings silently bumped to v8 while the `storybook`
core package stayed at v7 (broken mixed state).
- The `convert-to-inferred` transformers branched only on `major === 8`,
so v9 / v10 workspaces could emit incompatible CLI flags.
- The peer range advertised Storybook v7, which is no longer
upstream-supported per Storybook's top-3-majors policy.
## Expected Behavior
Resolved support window for `@nx/storybook`: Storybook v8 / v9 / v10.
- Generators reject sub-floor workspaces with a clear, standardized
error before any tree write.
- `packageJsonUpdates` entries are source-major-gated, so workspaces
outside the source range are skipped instead of being silently pushed
cross-major.
- The `convert-to-inferred` transformers branch per supported major with
above-ceiling silent fallthrough.
- The peer range tightens to `>=8.0.0 <11.0.0`.
- A parameterized floor spec exercises every generator's floor assert.
## Implementation Details
Follows the canonical multi-version-compliance shape established by the
prior plugin compliance PRs:
- **Support window declarations** (`versions.ts`):
`minSupportedStorybookVersion = '8.0.0'`; per-major `versionMap` (8 / 9
/ 10) using the canonical bundle pattern; `versions(tree)` with
`versionMap[major] ?? latestVersions` above-ceiling fallthrough (no
above-ceiling throw).
- **Floor assert** (`assert-supported-storybook-version.ts`): thin
wrapper around `assertSupportedPackageVersion` from
`@nx/devkit/internal`. Invoked as the first statement in
`initGeneratorInternal`, `configurationGeneratorInternal`,
`convertToInferred`, `migrate9Generator`, and `migrate10Generator`. The
existing `migrate-8` generator is excluded — it is the sub-floor
migrator that lifts v6/v7 workspaces onto the v8 floor.
- **`keepExistingVersions: true`** at every generator-side
`addDependenciesToPackageJson` call (`init`, `configuration`,
`convert-to-inferred`, `ensure-dependencies`). `init/schema.json`
default flipped from `false` to `true`.
- **Migration gates**: source-major gates on
`packageJsonUpdates["20.2.0"]`, `["20.8.0"]`, and `["21.1.0"]`
(`storybook >=8.0.0 <9.0.0`); post-bump tier-1 gates on
`update-21-2-0-migrate-storybook-v9` and
`update-21-2-0-remove-addon-dependencies` (`storybook >=9.0.0 <10.0.0`).
- **`convert-to-inferred` transformers**: CLI prop mappings extended to
`v8` / `v9` / `v10` with a v10 fallback for above-ceiling. The CLI flag
set across these majors was verified identical against the bundled
Storybook v9.0.6 and v10.1.0 CLI sources.
- **v7 cleanup**: peer tightened to `>=8.0.0 <11.0.0`; removed
`Constants.uiFrameworks7`, `pleaseUpgrade()`, the v7-throw branches in
both executors and `configurationGeneratorInternal`, and the `<8.0.0`
react/react-dom install branch in `ensure-dependencies`.
- **Bug fix in `storybookMajorVersion`**: the previous implementation
called `semver.major()` directly on declared ranges (e.g. `^10.1.0`),
which throws — the function then caught the throw and returned
`undefined`, defeating downstream `major === 10` checks. Now coerces via
`semver.coerce` first, so version-aware branching in `configuration`,
`convert-to-inferred`, and `edit-root-tsconfig` works as written.
- **Parameterized floor spec** (`all-generators-enforce-floor.spec.ts`):
`subFloorVersion: '~7.6.0'`, `excludeGenerators: ['migrate-8']`.
Upgrade path for any remaining v7 holdouts: the explicit `nx g
@nx/storybook:migrate-8` generator (intentionally excluded from the
floor assert) drives Storybook's own `upgrade` flow, which is cumulative
— it handles v7 → latest in one shot via Storybook's CLI.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4406-441f917f)
<!-- polygraph-session-end -->
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
When running `nx migrate --run-migrations`, the end-of-run `<K> commits
created` tally undercounts by 1 in the HEAD-resolve-race case: a
per-migration commit lands cleanly but `git rev-parse HEAD` returns null
transiently right after, so the sha is unrecoverable. The user sees `N-1
commits created` when `N` actually landed.
Additionally, when a migration throws mid-loop (before its outcome can
be recorded), the failure recap silently omits it from the
applied/deferred tally categories. The user still knows which migration
failed (the error block names it), but the recap accounting is
incomplete.
## Expected Behavior
The `<K> commits created` tally counts every commit that landed,
including HEAD-resolve-race cases. A migration that threw mid-loop is
recorded in the recap as `aborted` and listed under retained
working-tree state when it left a diff behind.
## Implementation Details
### Tally fix
The bug exists *because* the previous outcome record (`{ committedSha:
string | null, commitFailed?: boolean, committedAsPartOf?: ... }`)
collapsed two distinct states into one shape: `committedSha: null` with
no other flags covered both `commit landed, sha lost` (HEAD-race) and
`no commit attempted` (`--no-create-commits` or no-op step). No filter
expression over the previous shape can disambiguate them.
This PR encodes commit state as a tagged union:
```ts
type CommitState =
| { kind: 'none' }
| { kind: 'landed'; sha: string | null }
| { kind: 'failed' }
| { kind: 'absorbed'; into: { name: string; sha: string | null } };
```
The corrected tally is `outcomes.filter(o => o.commit.kind ===
'landed').length` — TS-enforced.
### Bundled refactors enabled by the union
- `MigrationOutcome` lifts to a discriminated union over `status`
(`'completed'` | `'aborted'`); ~30 lines of comments documenting legal
field combinations collapse to one short doc per variant.
- The `pendingMigrations` parallel list is dropped. `outcomes` is the
single source of truth; recap, tally, and retained-state derivations all
read from it. The executor's catch block records the in-flight migration
with `status: 'aborted'`, closing the gap where a
generator-throw-mid-loop migration silently disappeared from the recap's
tally.
- New `countLandedCommits` and `retainedMigrations` helpers replace
inline filters previously duplicated across `migrate.ts` and
`migrate-output.ts`, with unit tests covering the HEAD-race case and
verifying that the absorbing case is not double-counted.
- `logFailureRecap`'s merge-and-dedupe over outcomes + pendingMigrations
collapses to a single iteration over `outcomes`.
### Independent cleanups bundled
- `cleanup(repo)` — extend the `require-windows-hide` lint rule to
accept `MemberExpression` as the spawn options argument, matching its
existing handling of `Identifier` and `SpreadElement`.
- `cleanup(core)` — consolidate three identical XML blocks
(`<migration>`, `<handoff_path>`, `<advisory_context>`) across agentic
prompt builders into shared helpers in `shared-rendering.ts`.
Agent-visible prompt output byte-identical.
- `cleanup(core)` — drop the single-use `spawnOptions` hoist in
`agentic/runner.ts` (newly clean under the updated lint rule).
- `cleanup(core)` — explicit no-op `if (result.status === 'failed') {}`
branch with comment in `run-migration-process.js`, documenting the
single-migration UI child's intentional success-with-warning behavior.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nx-migrate-hardening-c5cd4e73)
<!-- polygraph-session-end -->
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The [GitHub App
Permissions](https://nx.dev/docs/guides/nx-cloud/source-control-integration/github-app-permissions)
reference page lists `Administration: Read & Write` as a required
repository permission and documents an "Administration (write)" detail
section. This permission was originally requested so users could create
workspaces from the browser.
## Expected Behavior
The Nx Cloud team has removed the browser-based workspace creation
feature and dropped the `Administration` (read & write) permission from
the GitHub App. The docs now:
- Remove `Administration: Read & Write` from the required repository
permissions list.
- Remove the corresponding `Administration (write)` detail section.
- Add a `note` callout above "Required permissions" explaining that the
permission is no longer requested and that existing installations and
functionality are unaffected.
The organization-level `Administration: Read Only` permission is
intentionally left unchanged.
## Related Issue(s)
N/A
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/update-github-app-scope-docs-d0c654d4)
<!-- 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>
## Current Behavior
`nx migrate --run-migrations` can only run generator-based migrations.
Prompt-based and hybrid migrations cannot execute, and there is no way
to validate generator output with an AI agent.
## Expected Behavior
`nx migrate --run-migrations` can drive an installed AI agent (Claude
Code, OpenCode, Codex) across the full migration pipeline.
Capabilities added:
- `--agentic[=<agent-id>]` opts into agentic mode; auto-detects an
installed agent or accepts an explicit id.
- Prompt-only and hybrid migrations (generator + prompt phases) run
end-to-end, with generator output handed to the agent.
- `--validate` runs an optional agent validation step after each
generator-only migration.
- Inside-agent mode defers prompt and validation steps and surfaces them
as directives to the outer agent.
- Per-migration commits are soft-forced under `--agentic`; redesigned
end-of-run output reports applied / deferred / retained state and gives
an explicit recap on failures.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: leosvelperez <leosvelperez@users.noreply.github.com>
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>
## Current Behavior
`createTaskGraph` builds a `Task` object for every target it schedules.
For targets that do not declare a `cache` property the field is left as
`undefined`; this flows through the NAPI boundary as
`Option<bool>::None`
and is serialised to JSON as `null`.
Nx Cloud DTE V4 has a Kotlin filter:
```kotlin
depTask.cache != false // gate: "might this task produce an artifact?"
```
In Kotlin `null != false` is `true`, so every non-cacheable
`run-commands`
target (i.e. one that never declared `cache: true`) is incorrectly
treated
as potentially cacheable. The DTE dispatcher then tries to materialise
an
artifact that was never uploaded, and the distributed worker throws a
fatal:
> Task dependency not found while downloading artifacts
## Expected Behavior
`task.cache` is always a literal `true` or `false` — never `null` /
`undefined`.
Downstream consumers (Nx Cloud, third-party runners) can rely on a
concrete
value without treating `null` as a third state.
## Changes
### `packages/nx/src/tasks-runner/create-task-graph.ts` — one line
```diff
- cache: project.data.targets[target].cache,
+ cache: project.data.targets[target].cache ?? false,
```
The `??` operator coerces both `undefined` (field absent from config)
and
`null` to `false`, matching Nx's opt-in caching semantics: a target is
cacheable only when `cache: true` is explicitly set (directly or via
`targetDefaults`).
### `packages/nx/src/tasks-runner/utils.ts` — **untouched**
`isCacheableTask` keeps its original `!== undefined` guard exactly as it
is
on `master`. The legacy `cacheableOperations`/`cacheableTargets`
fallback
inside that function is now effectively unreachable from the
`createTaskGraph`
path (because `task.cache` is always a boolean), but it stays as
harmless
defensive code for any `Task` object constructed outside of
`createTaskGraph`.
In practice `cacheableOperations` is dead in every modern workspace: the
Nx 17 migration `use-minimal-config-for-tasks-runner-options` rewrites
each
entry to `targetDefaults.<target>.cache = true` and deletes the field,
so
no plumbing is needed.
### `packages/nx/src/tasks-runner/create-task-graph.spec.ts`
All 68 expected `Task` objects updated to include `cache: false`,
matching
the normalised output.
## Related Issue(s)
https://linear.app/nxdev/issue/NXC-4486/artifact-download-fails-for-missing-task-dependency
<!-- Nx Cloud DTE V4 distributed-worker regression — no public issue
number -->
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/upnx-c498c-662192f8)
<!-- polygraph-session-end -->
---------
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## 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>
Updating the style guide based on feedback on the latest post.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
## Current Behavior
`@nx/vitest`, `@nx/cypress`, `@nx/playwright`, and `@nx/vite` all build
to the workspace-root `dist/packages/<name>/` directory and publish from
there. They use the legacy `module: commonjs` / `moduleResolution: node`
configuration, an `exports` map with a `./src/*` wildcard
(vitest/cypress), and a `nx-release-publish.options.packageRoot` that
points at the workspace-root dist tree. This is the same shape
`@nx/devkit`, `@nx/nx`, `@nx/js`, `@nx/eslint`, and `@nx/jest` already
moved off — but the M2 group of testing/build packages was still on the
old layout, blocking downstream packages (like `@nx/web`) from
migrating.
Separately, the 23.0.0 `rewrite-internal-subpath-imports` codemods
shipped with `@nx/js`, `@nx/jest`, `@nx/eslint` (and the new one in this
PR for `@nx/cypress`) walk `require(...)` / dynamic `import(...)` /
`jest.mock(...)` call expressions but leave `typeof
import('@nx/<name>/src/x')` type queries untouched. The common
typed-runtime-require idiom
```ts
const m = require('@nx/<name>/src/x') as typeof import('@nx/<name>/src/x');
```
would have its runtime arg rewritten to `/internal` while the type arg
kept pointing at the now-removed `./src/*` wildcard — leaving external
consumers with a TS error after running the migration.
## Expected Behavior
Each of the four packages now builds to `packages/<name>/dist/` with
`module: nodenext` + composite TypeScript, ships a `files` field in
`package.json` listing what to publish, declares
`release.preserveLocalDependencyProtocols: true` +
`manifestRootsToUpdate: ["packages/{projectName}"]` in `project.json`,
and publishes straight from the package directory via
`nx-release-publish.options.packageRoot: packages/{projectName}`.
Per-package highlights:
- **`@nx/vitest`** — straight structural migration. No
`@nx/vitest/src/*` consumers, so no codemod migration shipped.
`assets.json` updated to glob `src/migrations/**/*.md` so prompt-form
migrations resolve from `./dist/src/...`.
- **`@nx/cypress`** — drops the `./src/*` wildcard from the exports map,
ships a curated `internal.ts` re-export entry, and ships a
`23.0.0-beta.17` symbol-aware codemod
(`rewrite-internal-subpath-imports`) that routes `@nx/cypress/src/*`
imports to either the public `@nx/cypress` entry (for
`configurationGenerator`, `componentConfigurationGenerator`,
`cypressInitGenerator`, `migrateCypressProject`) or
`@nx/cypress/internal` (for everything else). 10 first-party consumers
in `@nx/angular` / `@nx/react` / `@nx/next` / `@nx/web` are codemodded
to match.
- **`@nx/playwright`** — straight structural migration; existing exports
already enumerated explicit subpaths, no `./src/*` wildcard to drop.
- **`@nx/vite`** — structural migration + three executor `.impl.ts`
files switched from `const schema = await import('./schema.json')` to a
top-level `import schema from './schema.json'` (required under
`nodenext`). Kept `./plugins/nx-tsconfig-paths.plugin` /
`./plugins/nx-copy-assets.plugin` /
`./plugins/rollup-replace-files.plugin` as explicit public entries
because storybook / react-native templates bake them into generated user
vite configs.
The fourth commit fixes the `typeof import()` blind spot across **all
four** of the 23.0.0 subpath-rewrite codemods (`@nx/js`, `@nx/jest`,
`@nx/eslint`, the new `@nx/cypress` one) by walking `ImportTypeNode` and
rewriting the literal-type-node argument when it points into the
package's `src/`. The cypress spec also adds explicit coverage for
`componentConfigurationGenerator` as a public-routed symbol, default and
default-plus-named imports, `jest.mock(..., factory)`, and
`it.each(MOCK_HELPER_METHODS)` for both the jest and vi mock families —
drift between those hardcoded sets and the real public/mock surface was
the most plausible future silent-regression path. The
dist-build-migration skill is updated to document the `ImportTypeNode`
handling and the expanded spec checklist.
## Validation
- `pnpm nx run-many -t build-base -p
angular,nuxt,remix,vue,web,react,vitest,cypress,playwright,vite,jest,js,eslint`
✅
- `pnpm nx affected -t build,lint --base=origin/master` ✅ (53 projects,
103 tasks).
- `pnpm nx run-many -t test -p jest,js,eslint,cypress
--testPathPatterns="rewrite-internal-subpath-imports"` ✅ — 99 passing
(32 cypress, 27 eslint, 20 jest, 20 js).
`@nx/web` is now unblocked on 4 of its 5 prior dist-build dependencies
(`@nx/vitest`, `@nx/cypress`, `@nx/playwright`, `@nx/vite`); only
`@nx/webpack` remains on the old layout.
## Related Issue(s)
Linear: [NXC-3581](https://linear.app/nxdev/issue/NXC-3581) — M2 epic,
migrate testing/build packages to local dist.
<details>
<summary>Pre-create review (run before PR open)</summary>
### Critical
- (Fixed in this PR) Codemod skipped `typeof
import('@nx/<name>/src/...')` type queries — backported the fix to
`@nx/js`, `@nx/jest`, `@nx/eslint`, and the new `@nx/cypress` migration.
- (Fixed in this PR) Spec didn't exercise
`componentConfigurationGenerator` (the 4th public symbol), default
imports, or `jest.mock` / `vi.mock` proper.
### Important
- `@nx/vite` / `@nx/vitest` / `@nx/playwright` ship no codemod despite
dropping their `./src/*` wildcards. No first-party consumers exist;
external plugin authors using deep subpaths will hit breakage. Noted as
a known breaking change for the upgrade guide rather than fixed in this
PR — codemods can land in a follow-up if the surface turns out to
matter.
- Empty `try { ... } catch {}` in `cypress-version.ts` (pre-existing;
diff only touched JSDoc). Tracked for a follow-up.
### Suggestions
- Vite executor schema imports are now eager at module load (theoretical
risk; matches the jest precedent).
- `vite/project.json` and `playwright/project.json` omit `dependsOn:
["^build", "build-base"]` while cypress/vitest include it (covered by
`nx.json` targetDefaults, cosmetic).
- `cypress` dropped legacy aliases `./generators` / `./executors` /
`./migrations` (no `.json` suffix) from the exports map — undocumented
surface, low risk.
</details>
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
15 inferred plugins call `calculateHashForCreateNodes` once per project
inside their `createNodesInternal` callback. The single-project helper
triggers a workspace-context glob per project, even when many projects
are being processed in the same `createNodesV2` invocation that all
share `context.workspaceRoot`. There is already a batch helper,
`calculateHashesForCreateNodes`, that runs a single multi-glob hash
across all project roots — only `vite`, `vitest`, `docker`, and `maven`
use it today. `nuxt` even has a `TODO(@nrwl/nx-vue-reviewers): This
should batch hashing like our other plugins` comment to that effect.
In the same per-project path, several plugins also re-invoke
`getLockFileName(detectPackageManager(context.workspaceRoot))` once per
project, which probes the lockfile redundantly.
## Expected Behavior
Each affected plugin's `createNodes` callback now does the per-workspace
work upfront:
- Pre-filter `configFiles` into `validConfigFiles` + `projectRoots`
(parallel arrays) using the existing sibling-file / metro / expo /
remix-compiler checks.
- Detect the package manager once and derive `pmc` and `lockFileName`
from it.
- Call `calculateHashesForCreateNodes(projectRoots, options, context,
additionalGlobsByProject)` once.
- Pass the pre-computed hash by index into `createNodesInternal` (4th
`idx` arg from `createNodesFromFiles`).
Plugins migrated:
- `angular`, `cypress`, `detox`, `expo`, `gradle` (v1 + v2 nodes),
`next`, `nuxt`, `playwright`, `react-native`, `remix`, `rollup`,
`rsbuild`, `storybook`, `webpack`.
`gradle`'s exported `makeCreateNodesForGradleConfigFile` factory gains
an optional `hashes?: string[]` parameter so external callers retain the
previous single-shot behavior; when a `hashes` array is supplied, each
callback invocation picks `hashes[idx]`. `nuxt`'s TODO comment is
removed since the batching is now in place.
`rspack` was not migrated — it does its own hashing with `hashFile` +
`hashArray` + `hashObject` and never used `calculateHashForCreateNodes`.
For `playwright`, the `additionalGlobs` array is per-project (each
project's `externalTsconfigInputs`), so the call passes
`projectRoots.map((_, idx) => [lockFileName,
...externalTsconfigInputsByIdx[idx]])`.
All affected plugin specs (cypress, next, storybook, rsbuild, angular,
playwright, webpack, rollup, nuxt, remix, gradle v1 + v2) pass locally.
## Related Issue(s)
N/A — this is an internal refactor / performance cleanup, not a fix for
a reported issue.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
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>
## 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)
## 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
## Current Behavior
`@nx/cypress`'s `getInstalledCypressVersion` helper in
`packages/cypress/src/utils/versions.ts` open-codes the dist-tag
normalization logic — `getDependencyVersionFromPackageJson` + an
explicit `installedVersion === 'latest' || 'next'` check + a `clean(...)
?? coerce(...)?.version ?? null` chain. This duplicates the centralized
`getDeclaredPackageVersion` helper in `@nx/devkit/internal`, which
already handles the dist-tag list (`NON_SEMVER_DIST_TAGS`) and semver
cleaning (`normalizeSemver`).
The duplication creates a drift surface: the local copy hardcodes
`'latest'` / `'next'` and would silently miss new entries if
`NON_SEMVER_DIST_TAGS` grows.
## Expected Behavior
`getInstalledCypressVersion` calls `getDeclaredPackageVersion(tree,
'cypress')` directly, matching the consolidated shape in `@nx/rspack`
(`packages/rspack/src/utils/version-utils.ts`) and `@nx/rsbuild`
(`packages/rsbuild/src/utils/version-utils.ts`). Local `clean` /
`coerce` / `getDependencyVersionFromPackageJson` usage in `versions.ts`
is removed.
The multi-version compliance skill (`canonical-shape.md`,
`anti-patterns.md`) is updated to document the consolidated shape and
the deliberate decision to omit `getDeclaredPackageVersion`'s third arg.
## Implementation Details
### Third arg (`latestKnownVersion`) omitted
`getDeclaredPackageVersion`'s third arg falls back to
`normalizeSemver(latestKnownVersion)` whenever the declared range can't
be normalized to semver — both "package missing from `package.json`" AND
"package declared as a dist tag". The helper does not distinguish those
two cases.
Cypress's open-coded helper distinguished three cases (missing → `null`,
dist tag → cleaned `cypressVersion`, valid → normalized declared).
Passing the third arg would preserve dist-tag back-compat but break the
"missing" case — init generators would think cypress was always
installed and never add it to `devDependencies`. Omitting it matches the
rspack/rsbuild precedent: both "missing" and "dist tag" return `null`;
consumers that want `?? latestVersions` semantics encode it at the call
site.
### Consumer behavior under dist-tag declarations (`"cypress": "latest"
| "next"`)
| Consumer | Before | After | Net effect |
|---|---|---|---|
| `versions(tree)` | `versionMap[15] ?? latestVersions = latestVersions`
| early-returns `latestVersions` on `null` | Same return value. |
| `init.ts` updateDependencies | skips adding cypress to devDeps | adds
cypress to devDeps, then `keepExistingVersions: true` preserves the
existing `latest` pin | Same workspace file output. |
| `configuration.ts` / `component-configuration.ts` skip-init guards |
skip init | run idempotent init | Extra setup work; idempotent for
already-configured workspaces. |
| `migrate-to-cypress-11.ts:122` `>= 10` guard | short-circuits with
"already on v10+" | proceeds into the migration body | Sub-floor
migrator (excluded from floor enforcement); now runs its
`forEachExecutorOptions` iteration on dist-tag declarations. |
The `assertMinimumCypressVersion(8)` call at
`migrate-to-cypress-11.ts:121` is unaffected (no tree → FS path).
For workspaces with a pinned semver range or with cypress missing
entirely from `package.json`, no observable behavior change at any
consumer.
### Skill doc updates
- `canonical-shape.md` §"The `getInstalled<Pkg>Version(tree?)` helper":
replaced the open-coded example with the consolidated form; added
§"Dist-tag semantics — third arg" explaining the missing-vs-dist-tag
conflation and recommending omitting the third arg.
- `anti-patterns.md` §1: added "open-coded tree-branch in
`getInstalled<Pkg>Version(tree?)`" to the rejected-patterns list with
the consolidated cypress shape as the "do instead".
> [!NOTE]
> `@nx/jest` and `@nx/vitest` still carry the open-coded shape on
master. Both are tracked in-flight via separate multi-version compliance
work and are out of scope here.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/misc-compliance-fixes-91356768)
<!-- polygraph-session-end -->
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`@nx/esbuild` does not enforce its declared `esbuild` peer range
(`>=0.19.2 <1.0.0`) at generator entry points. Generators silently
proceed on workspaces with a sub-floor `esbuild` and overwrite a
user-pinned `esbuild` version on re-runs (the `init` generator's
`keepExistingVersions` defaults to `false`).
## Expected Behavior
Generators throw a clear, standardized error naming the package, the
installed version, and the supported floor when `esbuild` is below
`0.19.2`. User-pinned `esbuild` versions are preserved by default.
## Implementation Details
- Add `minSupportedEsbuildVersion = '0.19.2'` to
`packages/esbuild/src/utils/versions.ts`.
- Add `assertSupportedEsbuildVersion(tree)` wrapper around the shared
`assertSupportedPackageVersion` from `@nx/devkit/internal`.
- Call the assert as the first statement in `esbuildInitGenerator` and
`configurationGenerator`.
- Flip `init/schema.json` `keepExistingVersions.default` to `true` and
use `schema.keepExistingVersions ?? true` at the
`addDependenciesToPackageJson` call site so user-pinned versions are
preserved by default.
- Add `all-generators-enforce-floor.spec.ts` exercising every
generator's floor assert via the shared
`assertGeneratorsEnforceVersionFloor` helper.
The executor was reviewed for cross-version API divergence; the
`esbuild` APIs used (`build`, `context`, `BuildOptions`, `BuildResult`,
`PluginBuild.onEnd`, and the forwarded options) are stable across the
supported range, so no runtime version branching is added.
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4386-dda40801)
<!-- polygraph-session-end -->
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## 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
## Current Behavior
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.
## Expected Behavior
- `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.
## Why the dep is needed on the vitest-analog path
`@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).
## Why the dep is NOT needed on the @angular/build path
- `@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.
## Implementation Details
- `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.
## Verification
- 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>
## 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>
## Current Behavior
Directory.Build.props and similar files are reported as sandbox
violations if they would be read by the .NET CLI commands
## Expected Behavior
They are considered in inputs
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
`@nx/js` has several multi-version compliance gaps:
- The `20.7.1-beta.0` and `22.5.0` migrations bump `@swc/cli` across
SemVer-breaking 0.x minor boundaries without a source-range `requires`
gate. Workspaces that missed an intermediate bump (e.g. via
`--mode=first-party`) can be jumped to the target lane in one write,
skipping the bridge. The `22.5.0` entry also mixes the cross-minor
`@swc/cli` jump with same-major patches under one combined gate.
- No migration path past `@swc/cli@^0.7.10` exists, so migrated
workspaces stay behind the fresh-install constant (`~0.8.0`).
- `@swc/cli` is not declared as a peer dependency, so the supported
window isn't visible from `package.json` and the SWC executor doesn't
surface an unmet-peer signal.
- Generators silently fall through to the latest install constants when
TypeScript is below the supported floor — no floor assert.
- Several `addDependenciesToPackageJson` call sites (`init`, `library`,
`convert-to-swc`, `setup-verdaccio`, SWC helpers) don't pass
`keepExistingVersions: true` and can overwrite pinned versions; `init`'s
schema defaults the flag to `false`.
## Expected Behavior
- `@swc/cli` migration entries gate on their source range; the
previously-mixed `22.5.0` entry is split so same-major patches still
apply on any same-major source.
- A `23.0.0-swc-cli` migration bridges `~0.7.x` to `~0.8.0`.
- `package.json` declares `@swc/cli: ">=0.6.0 <0.9.0"` as an optional
peer (via `peerDependenciesMeta`).
- All generators throw a clear "Unsupported version" message when
TypeScript is below `5.4.0`, naming the package, installed version, and
supported floor.
- Generators preserve user-pinned versions; `init`'s schema defaults
`keepExistingVersions` to `true`.
## Implementation Details
- Added bilateral `requires: { "@swc/cli": ">=N <M" }` gates on the
cross-breaking entries. `nx migrate`'s `filterDowngradedUpdates` already
handles past-target downgrades natively, so the gates are load-bearing
for stepwise chaining: a workspace at `0.3.12` running `nx migrate
--mode=all` against a newer Nx won't have the latest entry jump it
directly to `~0.8.0` — it must catch up stepwise via `nx migrate
--mode=third-party`, which walks each gated bridge in order.
- Split `22.5.0`: ungated entry keeps
`@swc/core`/`@swc/helpers`/`@swc-node/register` patches; new
`22.5.0-swc-cli` gates the `@swc/cli` jump separately so the patch bumps
still apply on workspaces outside the `@swc/cli` source range.
- Added `packages/js/src/utils/assert-supported-typescript-version.ts`
delegating to `assertSupportedPackageVersion` from
`@nx/devkit/internal`. Called as the first statement of
`initGeneratorInternal`, `libraryGeneratorInternal`,
`convertToSwcGenerator`, `setupVerdaccio`, `setupBuildGenerator`,
`setupPrettierGenerator`, and `syncGenerator`.
- Simplified `initGeneratorInternal`: removed the now-redundant
`getInstalledTypescriptVersion` helper and
`!satisfies(supportedTypescriptVersions, ...)` check. The floor assert
covers sub-floor; `addDependenciesToPackageJson` with
`keepExistingVersions: true` covers user-pin preservation.
- Renamed `supportedTypescriptVersions = '>=5.4.0'` to
`minSupportedTypescriptVersion = '5.4.0'` in `versions.ts` to match the
canonical floor-constant shape consumed by
`assertSupportedPackageVersion`. Constant was not exported from
`@nx/js/internal` so no public-API impact.
- `typescript` is intentionally **not** declared as a peer dep in this
PR. With a peer cap of `<7.0.0`, pnpm auto-resolves `@nx/js`'s
typescript subgraph to the highest matching version (e.g. `6.0.x`) even
when the workspace pins a direct `typescript: ~5.9.2`, so the `@nx/js`
executor loads a different typescript than the workspace `tsc`. Capping
at `<6.0.0` avoids that but warns users already on TS 6 + TS-solution.
Resolving implicitly (no peer declared, same as pre-compliance) keeps
the executor on the user's workspace typescript and matches prior
behavior. Deferred to a follow-up that resolves the subgraph divergence
properly.
- Added `all-generators-enforce-floor.spec.ts` (parameterized via
`assertGeneratorsEnforceVersionFloor`) and
`assert-supported-typescript-version.spec.ts` (5 canonical cases:
sub-floor / fresh-install / `latest` / `next` / in-range).
- Extended `assertGeneratorsEnforceVersionFloor` to strip a leading
`./dist/` from `generators.json` factory paths so plugins built to local
dist (like `@nx/js`, and `@nx/jest` going forward) load factories from
source under Jest. Backward-compatible — non-dist plugins are
unaffected.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## 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.
## Current Behavior
Nx has no shell tab-completion. Users have to type full command,
project, target, and generator names from memory.
## Expected Behavior
After installing the generated completion script, users get instant
tab-completion for:
- **Commands**: `nx <TAB>` lists every command (`run`, `generate`,
`add`, ...) **plus** every infix target name found in the project graph
(`build`, `serve`, `compile`, `bundle-rsc`, ...).
- **Subcommands**: `nx show <TAB>` → `project`, `projects`, `target`
- **Projects**: `nx show project <TAB>` shows all workspace projects
- **`project:target` pairs**: `nx show target <TAB>` and `nx run <TAB>`
first complete project names with a trailing `:`, then a second TAB
lists that project's targets — no backspace + manual `:` needed
- **Target names**: `nx run-many -t <TAB>` / `nx affected -t <TAB>`
- **Infix target invocations**: `nx build <TAB>` shows projects that
actually have the target. Works for **any** target name in the graph,
not a hardcoded set.
- **`nx show target <project>:<target>` keywords**: `nx show target
i<TAB>` → `inputs`, `outputs`. Also `nx show target inputs <TAB>` / `nx
show target outputs <TAB>` complete project:target.
- **Generators**: `nx g <TAB>` lists plugins (with trailing `:` for
two-stage drilling) **and** the bare generator names so `nx g app<TAB>`
→ `application`. `nx g @nx/react:<TAB>` lists that plugin's generators.
Workspace-local plugin projects (a `libs/my-plugin` with its own
`generators.json`) are discovered alongside installed npm plugins.
- **`nx add`**: `nx add @nx/r<TAB>` lists the first-party `@nx/*` set
(`@nx/react`, `@nx/rspack`, `@nx/remix`, ...).
- **Descriptions**: shells that render them (zsh, fish) get
`value\tdescription` pairs in the menu. bash and powershell get bare
names — bash has no description protocol in `compgen -W`; powershell
uses the single-arg `CompletionResult` ctor.
### Installation
```sh
nx completion bash >> ~/.bashrc
nx completion zsh >> ~/.zshrc
mkdir -p ~/.config/fish/completions && nx completion fish > ~/.config/fish/completions/nx.fish
nx completion powershell | Out-File -Append $PROFILE
```
Zsh also requires `autoload -U compinit && compinit` above the nx block;
the generated script prints a friendly warning pointing to this if
`compdef` isn't loaded. Fish's `>` redirect doesn't auto-create parent
directories, hence the `mkdir -p`.
### Debugging
If completion produces no suggestions, set `NX_VERBOSE_LOGGING=1` and
press TAB again. The wrappers stop discarding completion `stderr` when
the flag is set, and the binary's `catch` surfaces the underlying error
there. Reuses Nx's existing verbose-logging knob — no
completion-specific env var.
## Implementation notes
- **Pure JS, no native binary.** The earlier Rust prototype is dropped
in favor of a JS completion handler that reads
`.nx/workspace-data/project-graph.json` directly.
- **Bin-level short-circuit.** `bin/nx.ts` intercepts at the top of
`main()`:
- When `NX_COMPLETE=<shell>` is set (per-TAB request from the wrapper),
it skips workspace-root detection, dotenv, daemon, native module load,
and the yargs command tree, then dispatches to the value /
command-surface completion paths.
- When `nx completion <shell>` (the install command) is invoked, the
same hoisted short-circuit prints the wrapper script and exits — no
further bootstrap.
This is required because `parserConfiguration({ 'strip-dashed': true })`
defeats yargs' own completion detection, otherwise causing the `$0`
infix handler to fire and build the full project graph (~3s cold,
spawning ~25 plugin workers). The hoisting also means no scattered
`argv[2] === 'completion'` special cases downstream.
- **Wrappers as plain files.** The four shell wrappers (`bash.sh`,
`zsh.zsh`, `fish.fish`, `powershell.ps1`) live as real files under
`packages/nx/src/command-line/completion/scripts/`. `scripts.ts` is a
thin loader. Editors give real shell syntax highlighting; `shellcheck` /
`fish_indent` work; no `\$` escape forest from TS template literals.
- **Trailing-colon UX for `project:target`.** Completions for `show
target` / `run` contexts return `project:` (with colon). Bash and zsh
scripts detect trailing colons and suppress the inserted space (`compopt
-o nospace`, `compadd -S ''`). Fish has no per-completion nospace
primitive (works in practice — the user deletes the space or types `:`);
powershell same limitation.
- **zsh uses `compadd -d`, never `_describe`.** `_describe` splits on
`:` and would mangle `my-app:build` into value `my-app` with description
`build`. The wrapper parses `value\tdescription` from each completion
line and feeds parallel value/display arrays to `compadd -d`.
`formatDescription` collapses any literal TAB in untrusted plugin
descriptions to a space so the separator can't be forged.
- **fish descriptions are native.** Fish's `complete -a` reads
`value\tdescription` candidates the same way zsh's `compadd -d` does —
no wrapper change needed, just the description-emitting branch broadened
from "zsh only" to "shells that render descriptions".
- **Stale graph tolerated.** Completion reads
`.nx/workspace-data/project-graph.json` directly without triggering a
recompute. A slightly stale graph is fine; recomputing it would blow the
latency budget. There's an explicit `// do not fix this by triggering a
recompute` comment guarding the choice.
- **Workspace-root walk-up.** The completion fast path skips the normal
bootstrap that sets `NX_WORKSPACE_ROOT_PATH`, so a local
`resolveWorkspaceRoot()` walks from `process.cwd()` up to the nearest
`nx.json`. The POSIX wrappers do a parallel walk-up at TAB time to find
a workspace-local `nx` binary (falling back to PATH only outside a
workspace).
- **Generator listing avoids one file read in stage 1.** Stage 1 (`nx g
<TAB>`) only needs *names* of plugins that declare a generator
collection, not their generators — `collectPluginDirs()` reads each
plugin's `package.json` for the `generators` field, skips reading
`generators.json` until stage 2.
- **INFIX target completion is graph-driven.** `run/completion.ts` walks
the cached project graph at module load and registers a completion path
for every unique target name. Falls back to a conventional set (`build`,
`serve`, `test`, ...) for cold workspaces with no graph yet.
End-to-end completion latency: **~140ms typical**, scales with
project-graph size.
## Related Issue(s)
<!-- No specific issue — this is a new feature -->
## How to test locally
1. `pnpm nx build nx`
2. `pnpm link ./packages/nx`
3. Install the wrapper for your shell (see Installation above).
4. Open a new shell and tab away.
Verify:
- `nx <TAB>` lists commands **and** infix targets (`build`, `serve`,
plus any custom ones in your workspace)
- `nx show target <TAB>` completes to `project:` (no trailing space) —
second TAB lists targets
- `nx run-many -t <TAB>` lists target names
- `nx build <TAB>` lists projects that have a `build` target
- `nx g <TAB>` lists plugins **and** bare generator names; `nx g
app<TAB>` → `application`
- `nx g @nx/react:<TAB>` lists that plugin's generators (workspace-local
plugins also discoverable)
- `nx add @nx/r<TAB>` lists first-party plugins
- `nx show target i<TAB>` → `inputs`, `outputs`
- `NX_VERBOSE_LOGGING=1 nx <TAB>` surfaces any swallowed errors to
stderr
- Completion feels instant (~140ms typical)
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
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.
## Expected Behavior
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.
## Related Issue(s)
N/A — internal CI fix.
## 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)
## Current Behavior
The `23-0-0-convert-target-defaults-to-array` migration (which converts
`nx.json` `targetDefaults` from the legacy record shape to the new array
shape) has no co-located documentation, so on the docs site it renders
only its JSON-derived heading, version, and one-line description.
More broadly: several first-party packages build **in-place** to
`packages/<pkg>/dist` and their `migrations.json`
`implementation`/`factory` paths point at `./dist/src/migrations/...`.
The docs loader locates a migration's companion `.md` by appending
`".md"` to that resolved path — i.e. it looks under `dist`. None of
these packages' `assets.json` copied migration `.md` files into `dist`,
so every affected migration's example body was silently dropped on the
docs site. This affects `nx`, `@nx/devkit`, `@nx/eslint`, `@nx/jest`,
and `@nx/js`.
(Packages that reference `./src/migrations/...` directly — e.g.
`@nx/angular`, `@nx/react`, `@nx/vite` — are unaffected because the
loader reads their docs straight from source.)
## Expected Behavior
- Adds
`packages/nx/src/migrations/update-23-0-0/convert-target-defaults-to-array.md`
documenting the migration: the record → array shape change (nothing
dropped, insertion order preserved), the `:`-key disambiguation rules
(target vs executor, the duplicate-entry case, and the syntactic
fallback when the project graph is unavailable), and before/after
`nx.json` samples covering target, glob, and `pkg:executor` keys.
- Adds a `src/migrations/**/*.md` asset glob to the `assets.json` of
`nx`, `@nx/devkit`, `@nx/eslint`, `@nx/jest`, and `@nx/js` so their
migration docs are copied into `dist` alongside the compiled
implementations, where the docs loader reads them. This fixes rendering
for this migration and for the existing migration docs in those packages
that were also missing from `dist` (13 existing docs across the four
sibling packages).
## Related Issue(s)
N/A
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Current Behavior
The `nx.json` reference page documents that `targetDefaults` **replace**
the corresponding inferred-task config (`inputs`, `outputs`,
`dependsOn`, `options`, etc.) but never mentions the spread token
(`"..."`) that lets a target default *merge with* the base value instead
of overwriting it. The spread token is documented only on the project
configuration reference page, even though `nx.json` `targetDefaults` is
where most users define the base values it acts on.
## Expected Behavior
The `nx.json` reference now covers the spread token where users actually
configure target defaults:
- New **Spread token** subsection under **Target defaults**, showing the
array form (`["...", "{workspaceRoot}/babel.config.json"]`) and object
form (`"...": true`) with `nx.json` examples.
- A forward-pointer from the **inputs & namedInputs** subsection, where
the replace-by-default behavior is introduced, to the new section.
- A cross-link to the full spread token reference (level table +
cautions) on the project configuration page, so the detailed reference
stays in a single place rather than being duplicated.
The project configuration reference already documents the spread token
on `master` (the `### Spread token` section). This PR completes the
restoration by documenting it on the `nx.json` reference as well.
## Related Issue(s)
Fixes NXC-4438
Co-authored-by: Claude <noreply@anthropic.com>
## 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 #
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>
## 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>
## Current Behavior
AI-instructions migrations in `@nx/next`, `@nx/expo`, `@nx/nuxt`,
`@nx/vite`, `@nx/vitest`, and `@nx/storybook` are implemented as
generator wrappers whose only purpose is to copy a bundled `.md` file
into the workspace's `tools/ai-migrations/` directory.
## Expected Behavior
These migration entries declare a `prompt` field pointing directly at
the `.md` source. `nx migrate` collects the prompt files into the
workspace and surfaces a review banner, replacing the per-package
generator wrappers. The `@nx/storybook` migration becomes a hybrid
`implementation + prompt` entry (the implementation still runs `sb
upgrade`).
## Implementation Details
- **`@nx/next`, `@nx/expo`, `@nx/nuxt`, `@nx/vite` (x2), `@nx/vitest`
(x2)**: 7 entries switched from `implementation`/`factory` to `prompt`;
the 6 corresponding wrapper `.ts` files removed; the bundled `.md` files
moved out of each migration's `files/` subdirectory (a convention
required only by the deleted wrappers' `__dirname/files/...` reads).
- **`@nx/nuxt`, `@nx/vitest`**: added a `migrations.spec.ts` so their
migration entries are covered by `assertValidMigrationPaths`. Removed
stale orphans surfaced by the new nuxt spec: `src/migrations/.gitkeep`
and `src/migrations/update-18-1-0/add-include-tsconfig.{ts,spec.ts}`
(orphaned by an earlier pre-v19 migrations cleanup).
- **`@nx/storybook`**: `update-22-1-0-migrate-storybook-v10` becomes a
hybrid `implementation + prompt` entry. The `migrate-10` generator gains
a `skipAiInstructions` schema property; the migration wrapper passes
`skipAiInstructions: true` to avoid duplicating the prompt write (the
framework now writes it to the managed
`tools/ai-migrations/<scope>/<package>/<version>-<basename>.md` path).
Standalone `nx g @nx/storybook:migrate-10` invocations are unaffected
and still write the legacy `tools/ai-migrations/MIGRATE_STORYBOOK_10.md`
file.
- **`assertValidMigrationPaths`** (in
`@nx/devkit/internal-testing-utils`): updated to handle `prompt`-only
entries — per-entry validation asserts the referenced `.md` exists, and
the "all folders" check collects known dirs from both `impl` and
`prompt` paths.
- **Lint coverage parity**: `@nx/nuxt`, `@nx/vite`, `@nx/vitest` eslint
configs now include `./migrations.json` in the files block for
`@nx/nx-plugin-checks`, matching `@nx/next` / `@nx/expo` /
`@nx/storybook`.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## 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
## Current Behavior
The root `package.json` declares several devDependencies that are no
longer used anywhere in the repo:
- **PostCSS build tooling** — `postcss-preset-env`,
`rollup-plugin-postcss`. Leftovers from before the tailwind v3 → v4
migration (#35594) reshaped the postcss chain.
- **3D / homepage scene suite** — `@react-spring/three`,
`@react-three/drei`, `@react-three/fiber`, `three`. Leftovers from the
superseded homepage iteration introduced in #26893. #35625 was meant to
remove the full suite (`@types/three` and `shadergradient` were dropped
there), but these four entries didn't actually land.
## Expected Behavior
All six entries removed from root `package.json`. No source-code or
consumer-package changes.
## Implementation Details
Per-dep verification at HEAD:
- **0 real-code imports** of any of the six names — `from '<name>' |
require('<name>')` across all `.ts/.tsx/.js/.mjs/.cjs` files returns
empty. The only string mentions are:
- Comments in `@nx/rollup`'s own inlined postcss plugin
(`packages/rollup/src/plugins/postcss/index.ts` — file header: "replaces
the external rollup-plugin-postcss dependency to avoid peer dependency
conflicts").
- Lockfile test fixtures in
`packages/nx/src/plugins/js/lock-file/__fixtures__/**`.
- Demo JSON data in `astro-docs/src/assets/**` and the English word
"three" in unrelated tests/comments.
- **0 `peerDependencies` / `dependencies` declarers** in installed
`node_modules`.
- **0 references** in `project.json` / `nx.json` / `scripts/` /
`.github/`.
- **All 6 `postcss.config.*` files** in the repo (`nx-dev/nx-dev` + 5
graph projects) use only `@tailwindcss/postcss` + `autoprefixer` — no
preset-env, no rollup plugin.
### Impact
- `pnpm-lock.yaml`: **-1,679 lines, +0 lines** (pure subtraction).
- PostCSS subtree: -1,094 lines (`postcss-*` plugins, cssnano stack,
cssdb, mdn-data, p-queue, lilconfig).
- 3D subtree: -585 lines (`three`, `three-mesh-bvh`, `three-stdlib`,
`troika-three-*`, `draco3d`, `camera-controls`, `meshoptimizer`,
`react-reconciler`, `stats-gl`, etc.).
- `pnpm install` clean — no new unmet peers introduced.
### Validation
- `nx run-many -t build -p nx-dev graph-client rollup` — green (29
dependent tasks).
- `nx run-many -t test,lint -p rollup nx-dev` — green.
- `nx run-many -t build,test,lint -p graph-ui-project-details
graph-migrate graph-ui-code-block` — green.
- `nx run-many -t build,test,lint -p nx-dev rollup graph-client` (after
3D removal) — green (37 tasks total).
`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>
## 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
## Current Behavior
`@nx/eslint` and `@nx/eslint-plugin` build into the shared
`dist/packages/<name>` directory at the workspace root. `@nx/eslint`
exposes no `exports` map, so first-party packages reach into
`@nx/eslint/src/*` for internal utilities.
## Expected Behavior
Both packages build locally into `packages/<name>/dist/` with `nodenext`
module resolution and an `exports` map, matching the already-migrated
`nx`, `devkit`, `js`, and `jest`.
### `@nx/eslint`
- `tsconfig.lib.json` / `tsconfig.spec.json` switched to the nodenext +
local-`dist` pattern.
- `package.json` gains an `exports` map (`.`, `./plugin`, `./internal`,
plus the JSON configs), `typesVersions`, and a `files` field.
- `project.json` gains `release` and `nx-release-publish` configuration.
- `executors.json` / `generators.json` / `migrations.json` paths
repointed to `./dist/src/...`.
- `src/utils/versions.ts` resolves its own `package.json` via a
`@nx/eslint` self-reference.
- **Exports lockdown:** the broad `src/*` surface is dropped; a curated
`@nx/eslint/internal` entry replaces it. 27 first-party consumer files
are rewritten from `@nx/eslint/src/*` to `@nx/eslint/internal`, and a
`rewrite-eslint-internal-subpath-imports` migration rewrites user
imports (symbol-aware — public symbols → `@nx/eslint`, internals →
`@nx/eslint/internal`).
- `@nx/vite` and `@nx/remix` imported `@nx/eslint` utilities without
declaring the dependency; `@nx/eslint` is now a declared dependency of
both.
### `@nx/eslint-plugin`
- Same nodenext + local-`dist` migration.
- `package.json` `exports` map covers `.`, `./angular`, `./nx`,
`./react`, `./typescript`. No `./internal` entry — nothing imports its
`src/*`.
## Related Issue(s)
Tracked by Linear NXC-3575 (`@nx/eslint`) and NXC-4475
(`@nx/eslint-plugin`). No GitHub issue.
<details>
<summary>Notes</summary>
- `@nx/js` and `@nx/workspace` also call `@nx/eslint` utilities but
cannot declare the dependency — `@nx/eslint` depends on `@nx/js`, so
declaring it back would create a cycle. They use a runtime
`require('@nx/eslint/internal')` (no static import), which `tsc` does
not resolve, so their builds are unaffected.
- Validation: `nx affected -t build,lint` is green (only the
pre-existing `MsbuildAnalyzer` dotnet build fails). Affected `:test`
failures are pre-existing environment/snapshot drift — `devkit:test` and
`nx:test` fail identically with zero dependency on these packages, and
`web`/`react` test failure counts match `master` exactly. The
`@nx/eslint`/`@nx/eslint-plugin` suites pass apart from the pre-existing
`workspace-rules-project.spec.ts` "TS solution setup" failure (also
fails on `master`).
</details>
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
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
## Current Behavior
`packages/nx/src/plugins/js/project-graph/build-dependencies/` still
ships two TypeScript modules that were marked `@deprecated` "will be
removed in Nx 20":
- `strip-source-code.ts` — exports `stripSourceCode`, a TS-scanner-based
pre-parse pass that reconstructs only the import/export/require
statements from a file.
- `typescript-import-locator.ts` — exports `TypeScriptImportLocator`,
the legacy non-native scanner that walks a TS AST to extract
dependencies.
Both have zero remaining callers in the repo. The native (Rust) import
scanner used by `target-project-locator.ts` has fully replaced them in
the project-graph pipeline.
## Expected Behavior
The two deprecated files are deleted. `pnpm nx run nx:build-base` still
passes — confirming nothing in the workspace (inside or outside
`packages/nx`) was importing them.
No replacement API is needed: these symbols were never re-exported from
a public entry point and the JSDoc explicitly stated they "were not
intended to be exposed."
## Related Issue(s)
<!-- No specific issue; this completes the Nx 20 deprecation cycle for
these two APIs. -->
Fixes #
Co-authored-by: Claude <noreply@anthropic.com>
## 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>
## Current Behavior
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.
## Expected Behavior
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.
## Implementation Details
- 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>
## Current Behavior
`@nx/jest` builds into the shared `dist/packages/jest` directory at the
workspace root and compiles with `commonjs` module resolution. It
exposes no `exports` map, and first-party packages (`@nx/detox`,
`@nx/node`) reach into `@nx/jest/src/*` for internal utilities. It also
still re-exports the long-deprecated `jestProjectGenerator` alias.
## Expected Behavior
`@nx/jest` builds locally into `packages/jest/dist/` with `nodenext`
module resolution and an `exports` map, matching the already-migrated
`nx`, `devkit`, and `js` packages.
- `tsconfig.lib.json` / `tsconfig.spec.json` switched to the nodenext +
local-`dist` pattern.
- `package.json` gains an `exports` map (`.`, `./plugin`, `./preset`,
`./plugins/resolver`, `./internal`, plus the JSON configs),
`typesVersions`, and a `files` field.
- `project.json` gains `release` (`preserveLocalDependencyProtocols`,
`manifestRootsToUpdate`) and `nx-release-publish` configuration.
- `executors.json` / `generators.json` / `migrations.json` factory and
schema paths repointed to `./dist/src/...`.
- `assets.json` outputs to the local `dist/` and copies
`src/**/schema.json`.
- `src/utils/versions.ts` resolves its own `package.json` via a
`@nx/jest` self-reference instead of a `../../` relative path, which
breaks once source compiles into `dist/`.
- The `@nx/jest:jest` executor imports its `schema.json` statically
rather than via a dynamic `await import`.
- A curated `@nx/jest/internal` entry (mirroring `@nx/devkit/internal`)
replaces deep `@nx/jest/src/*` imports for first-party consumers;
`@nx/detox` and `@nx/node` are routed through it.
- A migration (`rewrite-jest-internal-subpath-imports`) rewrites user
`@nx/jest/src/*` imports: named imports/exports of public symbols go to
`@nx/jest`, everything else to `@nx/jest/internal`.
- A migration (`rewrite-jest-project-generator`) rewrites
`jestProjectGenerator` imported from `@nx/jest` to
`configurationGenerator`.
The `dist-build-migration` skill is also updated to document the
symbol-aware routing rule for the migration step.
### Breaking Changes
- The deprecated `jestProjectGenerator` export is removed — its "removed
in Nx v22" notice is two majors overdue. It was always just an alias for
`configurationGenerator`; the `rewrite-jest-project-generator` migration
rewrites existing usages automatically.
## Related Issue(s)
Tracked by Linear NXC-3592 — `[M4] [Epic] Migrate @nx/jest to local
dist`. No GitHub issue.
<details>
<summary>Pre-create review (run before PR open)</summary>
### Critical
- The subpath-import migration originally rewrote every `@nx/jest/src/*`
import to `@nx/jest/internal`, which would silently break consumers
importing a public symbol (e.g. `findJestConfig`) that way. **Fixed** —
the migration now partitions named imports/exports: public symbols route
to `@nx/jest`, internals to `@nx/jest/internal`, splitting mixed
declarations into two.
### Important
- `export { ... } from '@nx/jest/src/*'` and additional `jest`/`vi` mock
helpers were not handled by the initial migration. **Fixed** — export
declarations are now partitioned like imports, and all eight mock-helper
methods are covered with tests.
### Suggestions
- `versions.ts` self-resolve and the inferred `build-base` target were
flagged; both verified correct (matches the `@nx/js` pattern;
`build-base` is inferred by `@nx/js/typescript` from
`tsconfig.lib.json`).
- Softened an over-broad "used only" comment in `eslint.config.mjs`.
</details>
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## 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.
## Current Behavior
Loading `.ts` config files always registered `@swc-node/register` (or
`ts-node`) + `tsconfig-paths` up front. New workspaces shipped those
deps even though Node 22.6+ strips types natively.
## Expected Behavior
Native Node.js TypeScript stripping is now the default for `.ts` configs
and plugin loads. swc/ts-node + `tsconfig-paths` register lazily only
when native strip fails (matcher-based, ≤3 attempts; covers
`MODULE_NOT_FOUND`, `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`, ESM-as-CJS
syntax errors,`__dirname` in ESM scope, ESM-with-TLA). `@nx/js` init no
longer installs `@swc-node/register` / `@swc/core` / `@swc/helpers`. Opt
out via `NX_PREFER_NODE_STRIP_TYPES=false`.
Other changes in this PR are to ensure our generators create
Node-compatible config files. For example, not using extensions in
imports without `exports` will fail, like
`@nx/cypress/plugins/cypress-preset.js` instead of
`@nx/cypress/plugins/cypress-preset`. Or mixing in
`__dirname`/`__filename` into ESM config files. Or using
`import`/`export` in CJS files.
## Related Issue(s)
Fixes NXC-4299
## Current Behavior
Under Nx 23.0.0, the Nx Cloud agent client crashes when running tasks:
```
TypeError: runContinuousTasks is not a function
at AgentTaskManager.invokeContinuousTasks
at AgentTaskManager.reconcileContinuousTasks
at AgentTaskManager.invokeTasks
at executeTasksV3
```
The Nx Cloud client probes the nx task APIs by `require`-ing
`nx/src/index` (for the legacy `initTasksRunner`) and
`nx/src/tasks-runner/init-tasks-runner` (for the modern
`runDiscreteTasks` / `runContinuousTasks`) inside a single shared
`try/catch`.
PR #35708 removed the deprecated `initTasksRunner` API and **deleted
`packages/nx/src/index.ts` entirely** — even though that PR's own commit
message stated the file would be kept as an empty module so the `nx/src`
subpath export mapping stays valid. With the file gone, `nx/src/index`
no longer resolves (`./src/*` maps to `./dist/src/index.js`, which is
never built). The `require('nx/src/index')` throws, the client's shared
`catch` swallows it, and `runContinuousTasks` is never assigned — later
crashing when invoked.
`runContinuousTasks` itself was never removed; it still lives in
`packages/nx/src/tasks-runner/init-tasks-runner.ts` and is unreachable
here only because the earlier `nx/src/index` probe throws.
## Expected Behavior
`packages/nx/src/index.ts` is restored as an empty ES module, so the
`nx/src/index` entrypoint resolves again via the existing `./src/*`
mapping in `package.json` exports (no `package.json` change needed). The
deprecated `initTasksRunner` export stays removed — consumers still get
`undefined` for it — but the `require` no longer throws, so the Nx Cloud
client proceeds to load `runContinuousTasks` / `runDiscreteTasks`
correctly.
This restores compatibility for already-published/cached Nx Cloud client
bundles without requiring them to be updated.
## Related Issue(s)
Internal: NXC-4307 (follow-up to #35708)
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/master-1efdcf12-fbbd-49c0-954c-3637c415400a-d8a02644)
<!-- polygraph-session-end -->
---------
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The `convert-target-defaults-to-array` migration (Nx 23) converts the
legacy
record-shape `targetDefaults` in `nx.json` to the new array shape.
It declared a `projectGraph` parameter and classified each record key
against
the graph, dropping any key whose target name / executor wasn't found in
the
workspace. But the migration runner always invokes migrations as
`fn(tree, {})`
— the second argument is an empty object, never a project graph.
`{}` is truthy, so it was treated as a real (but empty) graph. Every
non-glob
key then matched neither a target name nor an executor, and the
migration
dropped it. As a result, upgrading to Nx 23 deletes every named
`targetDefaults`
entry (`build`, `test`, `lint`, …) and keeps only glob entries —
silently
breaking `build` (lost `dependsOn`) and `test` (lost env/options) across
the
workspace.
## Expected Behavior
The migration is a pure shape conversion and never drops entries:
- The entry point takes an honest `(tree)` signature — it no longer
pretends to
receive a project graph it is never given.
- Every legacy key produces at least one array entry. Globs and plain
(non-`:`)
keys become `{ target: key }`; `:` keys are disambiguated by the project
graph
(`target`, `executor`, or both), falling back to the syntactic heuristic
(`:` → executor) when the graph has no signal.
- The project graph is built internally, and only when a `:`-style key
actually
needs disambiguating.
- The pure conversion is exposed as `convertTargetDefaultsRecordToArray`
so the
disambiguation logic is unit-testable with injected graphs, without
standing
up a workspace or the migration runner.
## Related Issue(s)
N/A — caught while upgrading the Nx repo itself to `23.0.0-beta.13`.
## Problem
nx 23 changed `@nx/js`'s `package.json` `exports`: the `./src/internal`
subpath was removed (renamed to `./internal`), and `getRootTsConfigPath`
was moved to the main `@nx/js` entry.
`@nx/conformance@4.0.0` and `@nx/conformance@5.0.x` — published on npm
and still widely pinned — load workspace TypeScript conformance rules
via:
```js
const { getRootTsConfigPath } = await import('@nx/js/src/internal');
const { registerTsProject } = await import('@nx/js/src/internal');
```
On nx 23 this throws `ERR_PACKAGE_PATH_NOT_EXPORTED: Package subpath
'./src/internal' is not defined`. conformance swallows the error and
reports a misleading `Failed to resolve rule specifier`, so `nx
conformance:check` fails for every rule in any workspace using
conformance@4/5 on nx 23.
No published `@nx/conformance` supports nx 23 yet (latest `5.0.5` peers
`nx >=18 <23`), so consumers have no version to upgrade to.
## Fix
Restore `./src/internal` as a **deprecated backwards-compatibility
alias**. It cannot simply point at the same file as `./internal`,
because `getRootTsConfigPath` no longer lives there — so a small shim
re-exports both symbols from their new homes:
- `registerTsProject` ← `@nx/js/internal`
- `getRootTsConfigPath` ← main `@nx/js` entry
(`./utils/typescript/ts-config`)
### Changes
- New shim file `packages/js/src/internal.ts` re-exporting
`registerTsProject` and `getRootTsConfigPath`, clearly marked
`@deprecated` with guidance to migrate to `@nx/js/internal` / `@nx/js`.
- `package.json`: added `./src/internal` export entry (with
`@nx/nx-source` source condition) and a `src/internal` `typesVersions`
entry.
### Verification
- `import('@nx/js/src/internal')` resolves both `getRootTsConfigPath`
and `registerTsProject` as functions.
- TypeScript compilation clean (no new errors).
- Prettier reports no changes needed.
## Context
Surfaced by the failing `conformance:check` CI on nrwl/ocean PR #11347
(nx 23.0.0-beta.13 update). This fix needs to ship in a subsequent nx 23
beta for ocean to pick it up.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/upnx-51643-3e71e693)
<!-- polygraph-session-end -->
---------
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
## Current Behavior
`initTasksRunner` is exported from the `nx` package as a programmatic
entry point for running tasks outside of a CLI invocation. It has been
marked `@deprecated` since Nx 21 (#29993), the same release that
introduced `runDiscreteTasks` / `runContinuousTasks` as the replacement
task-execution path.
It also still carries a stale `TODO: Remove this in Nx 20` polyfill that
backfills `outputs` on tasks whose callers didn't provide them.
Modern Nx Cloud agents (Nx >= 21) route task execution through
`runDiscreteTasks` / `runContinuousTasks` and never call
`initTasksRunner` — it is reachable only on Nx < 21 (or when
`NX_DTE_LEGACY_APIS=true` is forced).
## Expected Behavior
The deprecated `initTasksRunner` function (and its stale `outputs`
polyfill) is removed from the `nx` package as part of the v23
deprecation cleanup.
- `initTasksRunner` and its now-unused imports are deleted from
`packages/nx/src/tasks-runner/init-tasks-runner.ts`.
- `runDiscreteTasks`, `runContinuousTasks`, and `createOrchestrator` —
the live continuous-task execution path — are kept untouched.
- `packages/nx/src/index.ts` is emptied (kept as a module so the public
`nx/src` subpath export mapping in `package.json` stays valid).
- The `Invoke Runner` e2e test, which exercised `initTasksRunner`, is
replaced with one covering `runDiscreteTasks` / `runContinuousTasks` —
the modern programmatic task-execution API that Nx Cloud agents actually
consume.
**BREAKING CHANGE:** The deprecated `initTasksRunner` export is removed
from the `nx` package. Consumers should use `runDiscreteTasks` /
`runContinuousTasks` instead.
## Related Issue(s)
Internal: NXC-4307
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`@nx/js` builds to the shared workspace-root `dist/packages/js/`
directory, uses CommonJS `module`/`moduleResolution`, has no `exports`
map, and relies on `.npmignore`-style filtering through assets.json
copies. This makes the package layout diverge from the new pattern
already adopted by `nx` and `@nx/devkit`.
## Expected Behavior
`@nx/js` follows the same local-dist build pattern as `nx` and
`@nx/devkit`:
- Builds to `packages/js/dist/` instead of `dist/packages/js/`.
- `tsconfig.lib.json` uses `module`/`moduleResolution: "nodenext"` with
`composite`, `rootDir: "."`, and `declarationDir: "dist"`.
- `package.json` declares an `exports` map with the `@nx/nx-source`
condition (workspace consumers resolve to `.ts` source, published
consumers get built `.js`), plus a `./src/*` wildcard so the ~296
existing internal imports of `@nx/js/src/...` keep working.
- `typesVersions` added for legacy `moduleResolution: "node"` consumers.
- Adopts an explicit `files` field on `package.json` instead of
asset-copying the root JSONs.
- `generators.json`, `executors.json`, `migrations.json` factory/schema
paths rewritten `./src/...` → `./dist/src/...` (matches `nx`). Workspace
dev still works via the `tryResolveFromSource` fallback in
`packages/nx/src/config/schema-utils.ts`.
- `README.md` → `readme-template.md`; build command writes the rendered
README to `packages/js/README.md`. Root `.gitignore` now ignores
`packages/js/README.md` and `packages/js/**/*.d.ts` (with
`!packages/js/src/**/schema.d.ts` exception for committed schema
declarations).
- ESLint flat config ignores `dist` and `**/*.d.ts`.
- `project.json` adds `release.version` config (with
`preserveLocalDependencyProtocols: false` so the not-yet-migrated
`@nx/workspace` dep is substituted to a concrete version at version-time
rather than left as `workspace:*` for pnpm publish to resolve from the
un-bumped source `packages/workspace/package.json`) and
`nx-release-publish.packageRoot`. The `build` target keeps its existing
`dependsOn: ["build-base"]` (cannot use `^build` because js's
`implicitDependencies` create a cycle through `eslint` /
`eslint-plugin`).
- `scripts/nx-release.ts`: adds `packages/js` to `packagesToReset` so
the source `packages/js/package.json` is restored after release.
## Related Issue(s)
Part of the ongoing migration of Nx packages to the local-dist build
layout (following `nx` and `@nx/devkit`).
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
`@nx/rspack` and `@nx/rsbuild` do not follow the multi-version support
compliance patterns used by other Nx plugins:
- `@rspack/core` is a regular dependency (not a peer) pinned via
`catalog:rspack`. `@rsbuild/core` is a regular dependency pinned at
exact `1.1.8`. No peerDependencies range, no per-major version map.
- The init generators unconditionally overwrite the workspace's
installed `@rspack/core` / `@rsbuild/core` with the plugin's pinned
version when `keepExistingVersions=false`, which is also the schema
default.
- Most generators do not assert a supported floor before mutating the
workspace, so sub-floor installs silently land in an unsupported state.
- `@nx/rspack` declares `@module-federation/enhanced` and
`@module-federation/node` as peer dependencies while
`@nx/module-federation` declares them as regular dependencies —
inconsistent across the two plugins.
- `packages/rspack/migrations.json` 21.3.0 bumps
`@module-federation/enhanced` cross-minor in the 0.x range without a
`requires` clause. The 22.2.0 entry bumps
`@module-federation/{enhanced,sdk,runtime}` in a single grouped entry
with no per-package gates.
- `packages/rsbuild/src/utils/versions.ts` (`1.1.10`) is out of sync
with `packages/rsbuild/package.json` (`1.1.8`).
- Supported-versions docs pages list only a default install version, not
the actual support window.
## Expected Behavior
Both plugins follow the multi-version compliance patterns, with the
cypress-style assert-at-entry approach:
- `@rspack/core`, `@rspack/dev-server`, `@rspack/plugin-react-refresh`
are peerDependencies of `@nx/rspack` with range `^1.0.0`.
`@rsbuild/core` is a peerDependency of `@nx/rsbuild` with range
`^1.0.0`.
- `@module-federation/enhanced` and `@module-federation/node` are
declared as regular dependencies in `@nx/rspack` to match
`@nx/module-federation`.
- New Angular-style `backwardCompatibleVersions` maps
(`packages/{rspack,rsbuild}/src/utils/versions.ts`) and detection
utilities (`version-utils.ts`) gate installed-version pickup, with
tree-based and runtime variants for generator-time and executor-time
callers.
- New `assertSupported{Rspack,Rsbuild}Version(tree)` wrappers around the
shared `assertSupportedPackageVersion` helper from
`@nx/devkit/internal`. Called at the top of **every** generator entry —
`init`, `configuration`, `convert-webpack`, `convert-to-inferred`, and
`convert-config-to-rspack-plugin` for rspack; `init` and `configuration`
for rsbuild — so sub-floor workspaces fail fast with a consistent
message before any mutation.
- Above-window installs are not thrown on. Following the pattern used by
`@nx/angular` and `@nx/playwright`, an unknown future major falls
through silently to the latest known install constants. The workspace
already has its own pin and we honor it.
- Init generator schemas flip `keepExistingVersions` default from
`false` to `true`. Init reads `options.keepExistingVersions ?? true`.
All `addDependenciesToPackageJson` callsites pass `keepExistingVersions:
true` (or thread the option through where the schema exposes it).
- Migration `requires` gates added to `@nx/rspack`:
- `21.3.0` → `requires: { "@module-federation/enhanced": ">=0.15.0
<0.17.0" }` for the enhanced bump; `@module-federation/node` bump split
out with its own `requires` clause.
- `22.2.0` split into per-package entries for `enhanced`, `sdk`,
`runtime`, and `node`, each with its own `requires` clause.
- rsbuild `versions.ts` / `package.json` drift resolved — both now flow
through the new map.
- Exact version pins (`1.6.8`, not `^1.6.8`) for the rspack/rsbuild
family entries in the version map. With caret ranges, `@rspack/cli`
floats to the latest 1.x (e.g. `1.7.11`) while `@rspack/core` stays at
`1.6.8` (pinned by `@nx/module-federation`'s transitive dep via the
catalog), causing a cli/core API skew that crashes `rspack serve` with
`Cannot read properties of undefined (reading 'web')`. Users who upgrade
rspack/rsbuild independently still work because
`getInstalled*MajorVersion` reads their pin and init honors it.
- Docs pages updated with explicit `^1.0.0` support window and a
`Default Installed` column.
### Tests
A parameterized `all-generators-enforce-floor.spec.ts` in each package
uses `assertGeneratorsEnforceVersionFloor` from
`@nx/devkit/internal-testing-utils` to verify every generator entry
throws on sub-floor installs. Catches future regressions where a new
generator forgets to call the assert.
### Scope notes
- **v0 dropped.** Plugins had no v0 support to preserve.
- **v2 in a follow-up PR.** `@rspack/core@2.0.3` and
`@rsbuild/core@2.0.6` ship as pure ESM and require additional work for
the Nx plugins (CJS) to interop. The version-map and detection
scaffolding here is structured so the v2 entries slot in trivially once
that work lands.
### Validation
- `nx run-many -t test,build,lint -p rspack,rsbuild` — green
- `nx run module-federation:test` — green
- `nx affected -t lint --base=origin/master` — green
- Floor-enforce specs pass: 6 generators (rspack) + 2 generators
(rsbuild) verified to throw on sub-floor `~0.7.0`.
## Related Issue(s)
Fixes #NXC-4404
Fixes #NXC-4405
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
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.
### 1. `createNodesFromFiles` resolution-order race
`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.
### 2. Atomized target name insertion order
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.
## Expected Behavior
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.
### How
- **`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.
### Audit of other createNodes implementations
- `@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.
### Tests
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.
## Related Issue(s)
<!-- 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>
## Current Behavior
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).
## Expected Behavior
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.
## Related Issue(s)
N/A — CI flakiness fix.
## 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`).
## 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.
## Current Behavior
The `@nx/nx-plugin-checks` ESLint rule requires every migration entry in
a `migrations.json` file to declare an `implementation` or `factory`
property. Migration entries that rely solely on the `prompt` field
(introduced in #35638) trip the `missingImplementation` error even
though they are a valid alternative.
## Expected Behavior
The rule accepts migration entries that declare a `prompt` instead of
`implementation`/`factory`, and validates the prompt path resolves to a
file on disk (mirroring how `implementation` paths are validated).
## Implementation Details
- `validateEntry` now detects a sibling `prompt` property and skips the
`missingImplementation` error when `mode === 'migration'` and a `prompt`
is present.
- A new `validatePromptNode` mirrors `validateImplementationNode`: it
asserts the value is a string literal and the path resolves to an
existing file, applying the same `outDir → rootDir` source-mapping
fallback used for implementation paths.
- The gating is restricted to migration mode — for
`generator`/`executor` entries, a stray `prompt` does not satisfy the
`implementation` requirement.
- New `invalidPromptPath` messageId follows the same single-message
pattern used by `invalidImplementationPath`.
## Current Behavior
#35648 dropped the compat shim in
`normalizeTargetDependencyWithStringProjects` that handled the legacy
`projects: 'self'` and `projects: 'dependencies'` magic strings. Without
the shim, those configs fall through to `findMatchingProjects(['self'],
…)` which returns `[]`, so the `dependsOn` task is **silently dropped**
— no warning, no error, broken task graph.
Separately, building `packages/nx` with `nodenext` resolution triggered
TS5055 ("would overwrite input file") because `@nx/key`'s `.d.ts`
transitively imports `@nx/devkit` which uses a bare
`nx/src/devkit-exports` specifier. The exports map fell through to dist
`.d.ts`, pulling nx's own emit outputs back in as inputs.
## Expected Behavior
**Shim restored, with deprecation warning.** Legacy values keep working
(`projects: 'self'` → owner project, `projects: 'dependencies'` → `{
dependencies: true }`). A `TODO(v24)` marks the shim for clean removal
next major.
The warning uses the project graph source maps to attribute each
offending entry to its origin, then collapses output by source:
- **Single external plugin** (e.g. `@nx/jest/plugin` emitting `'self'`):
one workspace-wide warning per plugin pointing at upgrading it.
- **Single config file** (`nx.json` / `project.json` / `package.json`):
one per `project:target`, lists the offending entries, advises `nx
repair`.
- **Mixed**: per-target warning with tailored advice (both `nx repair`
and plugin upgrade where applicable).
### How the warning is constructed
1. **Snapshot**: when normalization sees `projects: 'self'` or
`'dependencies'`, `warnLegacyDependsOnMagicString` pushes a
shallow-cloned violation `{ index, originalEntry }` into a per-target
collector array (or fires inline if no collector).
2. **Annotate**: at flush, each violation is looked up in the
project-graph source maps to get `plugin` and `file` (which
plugin/config emitted that `dependsOn[i]`).
3. **Shared fields**: a local `shared()` helper returns the value every
annotated item agrees on (or `null` if mixed) for `value`, `plugin`, and
`file` — these drive the variant choice.
4. **Dedupe**: external-plugin warnings key on `plugin::<name>::<value>`
(one per plugin workspace-wide); everything else keys on
`target::<project>::<target>`. A module-level Set ensures each key fires
once per process.
5. **Title + body**: one of three title templates (plugin / file /
mixed) plus, unless it's the external-plugin case, one body line per
entry rendered as ` - <JSON> (<index>[ from <plugin>...])`.
**`customConditions` added** to `packages/nx/tsconfig.lib.json` so
nodenext resolves nx's self-referencing bare imports to source instead
of dist, fixing the TS5055 cascade.
## Related Issue(s)
Partially reverts behavioral break from #35648.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
When running continuous tasks (e.g. `nx e2e` with a dev server
dependency), shutting down via Ctrl+C or SIGTERM can leave orphaned
processes and leaked resources (e.g. Docker containers still running).
The JS `tree-kill` package kills all processes flat — parents die before
children can run cleanup handlers. Children also receive redundant
signals (OS SIGINT + orchestrator SIGTERM + per-process handlers),
causing race conditions and unpredictable shutdown ordering.
## Expected Behavior
Graceful, ordered shutdown: leaf processes are signaled first, allowed
to run cleanup handlers (e.g. `docker compose stop`), then parents are
signaled. At most 1 orchestrator-dispatched signal per child. No
orphaned processes, no leaked resources, cleanup subprocesses are
protected during shutdown. Grace period configurable via
`NX_PROCESS_KILL_GRACE_PERIOD` (ms, default 5000).
## Changes
### 1. Native graceful process tree shutdown (`process_killer/mod.rs`)
Two Rust napi functions replace the `tree-kill` npm package:
- **`killProcessTree`** (sync) — snapshots tree, signals all descendants
in one pass. For `process.on('exit')` handlers where only sync work can
run. SIGKILL fallback on Windows.
- **`killProcessTreeGraceful`** (async, bottom-up) — signals leaves
first, awaits exit, promotes parents, repeats. Tracks cleanup
subprocesses spawned during shutdown so they're protected from SIGKILL
escalation. Grace period configurable via `NX_PROCESS_KILL_GRACE_PERIOD`
(default 5000ms).
Bottom-up ordering is the core decision: leaves get to run cleanup
(`docker compose stop`, etc.) before their parents die.
### 2. Single source of truth for signal dispatch
Pre-PR, children received signals from multiple paths (OS SIGINT,
orchestrator SIGTERM, per-process handlers) — race conditions,
double-signaling, unpredictable shutdown order.
- `running-tasks.ts`: `exec()` → `spawn({ shell: true, detached: true
})`. Children get their own process group via `setsid()` and no longer
receive OS SIGINT directly — only the orchestrator dispatches.
- `task-orchestrator.ts`: single `handleSignal` for
SIGINT/SIGTERM/SIGHUP, persistent listeners (no `process.once` re-raise
before async cleanup completes), `stopRequested` guard, dedup of
already-killing continuous tasks.
- `forked-process-task-runner.ts`: SIGTERM/SIGHUP handlers removed;
orchestrator owns dispatch. SIGINT runs cleanup without
`process.exit()`.
- Per-task signal handlers in `running-tasks.ts` gated by
`NX_FORKED_TASK_EXECUTOR` — direct path has the orchestrator, only the
forked path needs them.
### 3. Async cleanup, awaited end-to-end
Forked runner `cleanup()` is async and awaited by the orchestrator.
`killPromise` is cached so concurrent callers (e.g. `cleanup()` after a
fire-and-forget kill from `cleanUpUnneededContinuousTasks`) await the
same in-progress kill rather than no-oping and leaving orphans.
### Migration follow-ups
Surfaced during integration; included to keep the migration green:
- `setEncoding('utf8')` on spawn stdio — `exec()` decoded utf8 by
default; `spawn()` emits Buffers. Rust TUI `appendTaskOutput` expects
strings → crashed with `StringExpected` without this.
- Per-task process listener cleanup — `RunningNodeProcess` previously
registered `exit` (and signals in forked path) without removing, causing
`MaxListenersExceededWarning` in workspaces with many parallel
run-commands tasks.
- `BatchProcess.kill()` swept from `tree-kill` to native
`killProcessTreeGraceful` (the one usage missed in the original sweep).
- Continuous task exit treated as fulfilled when no incomplete
dependents remain — bottom-up kill can let a child exit before its
parent `nx` gets SIGTERM; pre-fix the parent saw it as a crash.
- Memoize `(exited, exitCode, exitTerminalOutput)` in
`RunningNodeProcess`/`ParallelRunningTasks`/`SeriallyRunningTasks` —
late `getResults()`/`onExit()` callers resolve immediately. Non-zero
exits before `readyWhen` matches now surface as failures (pre-PR
silently hung awaiters when a dev server crashed at startup).
### Tests & dependency sweep
- Integration tests for napi bindings: tree traversal, SIGTERM handler
execution, grace period, SIGKILL fallback, dead PID.
- `tree-kill` removed from `package.json`.
- `pseudo-terminal.ts`, `node-child-process.ts`, `run-script.impl.ts`:
tree-kill → native killers.
- Updated run-commands tests for spawn assertions.
## Related Issue(s)
Fixes#32438
---------
Co-authored-by: Alex <alex@gogl.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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>
## 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'`.
## Current Behavior
Migration entries in a plugin's `migrations.json` can only declare an
`implementation` or `factory` pointing at TypeScript code. To ship a
Markdown prompt as the migration content, plugins currently have to
write a small generator that calls `tree.write(...)` to drop the `.md`
file into the workspace, which adds boilerplate and an extra layer for
every prompt-based migration.
## Expected Behavior
A migration entry can declare a `prompt` field — the relative path to a
Markdown file shipped with the plugin (resolved from the directory of
`migrations.json`) — as an alternative or complement to
`implementation`/`factory`.
When `nx migrate` collects migrations:
- The referenced `.md` file is extracted from the package and written to
the workspace under
`tools/ai-migrations/<package>/<prompt-relative-path>` (e.g.,
`tools/ai-migrations/@nx/expo/src/migrations/update-22-2-0/files/ai-instructions-for-expo-54.md`),
preserving the prompt's location within the package.
- The entry's `prompt` field is rewritten to that workspace-relative
path so users can review and edit the prompt before running migrations.
- Hybrid entries (`prompt` together with `implementation` or `factory`)
are preserved as-is on the generated entry.
- Each entry must declare at least one of `implementation`, `factory`,
or `prompt`; missing prompt files error out at collection time.
- The post-migrate "Next steps" output reminds the user to review and
tweak the generated prompts before running migrations.
## Implementation Details
- New `prompt-files.ts` module under
`packages/nx/src/command-line/migrate/` contains the new validation,
prompt extraction (registry and install paths), and workspace-write
logic.
- `MigrationsJsonEntry` gains an optional `prompt` field.
`GeneratedMigrationDetails` gains an optional `prompt` field and
`implementation` becomes optional to allow prompt-only entries.
- `Migrator.migrate()` returns a `promptContents` map alongside
`migrations`, keyed by `<package>::<promptRelPath>`. The workspace-write
step looks up content from that map and rewrites each entry's `prompt`
to its workspace-relative path.
## 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>
## Current Behavior
- `@nx/cypress` does not consistently enforce its declared supported
Cypress version range across its generators. Generators run without
verifying the installed Cypress version is at or above the supported
floor, so sub-floor workspaces can silently land in an unsupported
state.
- The init generator overwrites already-installed `cypress` versions on
re-runs. The shared `add-linter` helper overwrites already-installed
`eslint-plugin-cypress` pins.
## Expected Behavior
- Every generator entry point asserts the installed Cypress version is
`>= 13.0.0` before executing. Sub-floor installs are surfaced with an
actionable error. Above-known-ceiling installs fall through silently to
the latest known install constants (matching the pattern used by
`@nx/angular` and `@nx/playwright`), without throwing.
- Generators preserve user pins for both `cypress` (init) and
`eslint-plugin-cypress` (configuration's linter helper).
## Implementation Details
- New `assertSupportedCypressVersion(tree)` wrapper around the shared
`assertSupportedPackageVersion` devkit helper, called first in `init`,
`configuration`, `component-configuration`, and `convert-to-inferred`.
- Parameterized floor spec (`all-generators-enforce-floor.spec.ts`) that
asserts every entry in `generators.json` throws on a sub-floor install,
with `migrate-to-cypress-11` explicitly excluded — that generator exists
to migrate sub-floor (v8–v10) workspaces onto v11 and retains its
bespoke `assertMinimumCypressVersion(8)` guard.
- The shared `assertGeneratorsEnforceVersionFloor` test helper gains an
`excludeGenerators?: string[]` option for this kind of intentional
sub-floor migrator (separate `cleanup(devkit)` commit).
- `versions()` no longer throws on unknown majors; below-floor is caught
by the generator-level assert.
- `getInstalledCypressVersion`'s filesystem path now delegates to the
shared `getInstalledPackageVersion` from `@nx/devkit/internal` for
reliable resolution in pnpm strict mode and nested install layouts. The
tree path stays inline because Cypress's "null on missing" semantics
differ from the shared helper's fallback-on-missing.
- `init` flips its `keepExistingVersions` schema default to `true` and
uses `options.keepExistingVersions ?? true` at the call site;
`add-linter` passes `keepExistingVersions: true`. `configuration` and
`component-configuration` already pass `true` and are unchanged.
Migration generators are intentionally exempt — they exist to bump.
## Current Behavior
`packages/angular/migrations.json` contains migration entries and
`packageJsonUpdates` targeting Angular versions older than 18, along
with their backing source files under
`packages/angular/src/migrations/update-17-*/`, `update-18-*/`, and
`update-19-1-0/`.
## Expected Behavior
Stale migration entries targeting Angular <18 are removed, and the
now-unreferenced migration source files are deleted.
## 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`.
## 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.
## Current Behavior
Follow-ups to review feedback on the just-merged #35497:
- `--mode` help text reads as if defaults are unconditional — the
interactive-prompt exception is buried in a parenthetical.
- `--multi-major-mode` combined with `--run-migrations` is silently
ignored (`--mode` correctly throws).
- `--multi-major-mode=gradual` silently degrades to `direct` when the
registry can't return an incremental version, hiding the disabled safety
rail.
- `getInstalledLegacyNrwlWorkspaceVersion` bypasses the
cache-pollution-safe resolver `getInstalledNxVersion` uses (regression
risk from #35444).
- After a gradual or interactive incremental redirect, the re-run
instruction is logged once at the top of the run and quickly scrolls out
of view; the closing "Next steps:" block offers no guidance to continue
toward the originally requested target.
- The `MigrateMode` union is re-typed inline in 8 places; the `<
14.0.0-beta.0` era check is duplicated across multiple call sites.
- Internal "stepwise" wording reads awkwardly for non-native English
speakers.
- Test gaps: `--to @nx/workspace@higher`, `--mode=third-party` +
`--multi-major-mode=gradual`, `filterDowngradedUpdates` with `~` ranges
and peer-deps-only, the continuation contract for gradual/prompt
redirects.
## Expected Behavior
- `--mode` describe leads with the interactive-prompt behavior; defaults
follow.
- `--multi-major-mode` + `--run-migrations` throws symmetrically with
`--mode`.
- `--multi-major-mode=gradual` surfaces a `warn` when no incremental
option can be looked up before falling through to the requested target.
- `getInstalledLegacyNrwlWorkspaceVersion` routes through
`resolvePackageJsonWithoutCachePollution`, consistent with
`getInstalledNxVersion`.
- After a gradual or interactive incremental redirect, "Next steps:"
prints a continuation command targeting the user's original target,
preserving `--mode` (including `--mode=all`) and
`--multi-major-mode=gradual` when they were the effective opt-ins.
- `MigrateMode` exported once; era checks route through a new
`isLegacyEra` helper and the existing `resolveCanonicalNxPackage`.
- "Stepwise" replaced with "incremental" across copy, comments, and the
doc-URL constant.
- New tests for each coverage gap above.
## Implementation Details
- `isLegacyEra(version)` lives in `version-utils.ts`. Four era-check
sites in `migrate.ts` plus `isNxEquivalentTarget` and
`resolveCanonicalNxPackage` all route through it. The remaining
`14.0.0-beta.0` literal mentions are now docstring/inline comments only.
- `MULTI_MAJOR_MODE_FLAG` constant extracted to keep the flag spelling
in one place; `STEPWISE_DOC_URL` → `INCREMENTAL_UPDATE_GUIDE_URL`.
- `warnGradualUnavailable` centralizes the warn message; two call sites
(dist-tag resolution failure, no-step-resolved branch in gradual) pass
distinct reason strings.
- `MigrateMode` exported from `migrate.ts`; `multi-major.ts` imports it
as a `type`-only import (erased at emit; no runtime cycle).
- `maybePromptOrWarnMultiMajorMigration` returns `MultiMajorResult = {
chosen: string; originalTarget?: string; gradual?: boolean }` so
dist-tag resolution alone isn't mistaken for a redirect, and so callers
can distinguish gradual-mode redirects (safe to propagate
`--multi-major-mode=gradual`) from interactive-prompt picks (don't lock
the user in).
- `GenerateMigrations` gains `originalTargetVersion?: string` and
`multiMajorMode?: MultiMajorMode` to thread the continuation contract
from `parseMigrationsOptions` to the Next Steps composer.
- `logGradualStep` is title-only; the re-run guidance lives in Next
Steps where it's adjacent to the other re-run lines and won't scroll out
of view.
## 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.
<!-- 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
The Gradle project graph plugin is at version 0.1.20.
## Expected Behavior
The Gradle project graph plugin is bumped to version 0.1.21, with the
corresponding migration files created
so that users upgrading Nx will automatically get the new plugin
version.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
<!-- 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
## Current Behavior
`spread.test.ts` flakes on the middle-spread case ("should resolve
spread when middle specified plugin contains '...'"). CI logs showed two
distinct project-graph recomputes running back-to-back for one `show
project` invocation, with the stale IIFE's response — built against an
older `nx.json` snapshot — being returned to the client. The result:
`build` target undefined for the project the specified plugins had just
created, blowing up the assertion downstream.
Root cause: `cachedSerializedProjectGraphPromise` was last-kickoff-wins.
Two concurrent IIFEs (one watcher-triggered, one request-triggered)
could overlap; the later kickoff replaced the cached pointer with its
own promise, but if the *earlier* IIFE finished computing against a
stale plugin set, its result still flowed back to the awaiting caller
through the chain. There was no guard ensuring the committed graph was
built against the current `nx.json`.
Two adjacent bugs surfaced during the investigation:
- The Rust watcher's `force_flush_pending` handler waited only 5ms for
in-flight notify events before snapshotting. Too short on macOS FSEvents
(and tight inotify cases) — events that `fs::write` had just produced
could miss the window.
- The Rust watcher diagnostics lived behind a bespoke
`NX_DAEMON_DEBUG_WATCHER=1` env gate using `eprintln!`, separate from
the existing `tracing` + `NX_NATIVE_LOGGING` infrastructure the rest of
`native/` uses.
## Expected Behavior
**Freshness gate on `kickOffRecompute`**
(`project-graph-incremental-recomputation.ts`). Each IIFE reads
`nx.json` once at kickoff (inside the IIFE, before any await), hashes
the `plugins` field as a snapshot, and passes the same `nx.json` object
to `getPluginsSeparated` so the plugin set and the snap reflect the same
disk state. Two checkpoints (after `getPluginsSeparated` and after
`processFilesAndCreateAndSerializeProjectGraph`) compare current disk to
the snap; on mismatch the IIFE logs `Discarding stale recompute result`,
returns the cached pointer so awaiters chain to the successor, and (if
it's still the cached one) starts the successor recompute. Closes the
spread-test race at the source.
**Per-OS force-flush grace** (`watcher.rs`). New `FORCE_FLUSH_GRACE`
constant: 50ms on macOS, 10ms on Linux. Used in the in-handler
`recv_timeout` so the kernel→notify-crate hop has room to deliver
in-flight events before the snapshot. Fixes the related
`force_flush_pending_captures_in_flight_writes` rust unit-test flake.
**Required `nxJson` parameter on `getPlugins` / `getPluginsSeparated`**.
Previously optional and defaulted to `readNxJson(root)`, which meant
callers that already held an `nxJson` were double-reading with a race
window between the two reads. Now required, forcing each caller to think
about which snapshot the plugin loader uses. All in-repo callers
updated.
**Rust watcher diagnostics on `tracing`**. Per-event `ingest path=…`
moved to `trace!` and now sits *after* the existing filterer
(Access-event flood was already dropped there); force-flush END +
backfill summaries at `debug!`. Bespoke `NX_DAEMON_DEBUG_WATCHER` env
gate removed. Users opt in via
`NX_NATIVE_LOGGING="nx::native::watch=trace"` (or `=debug`), same as
every other native module.
**Always-on TS seam logs**. Kept on the daemon: `[watcher]
routeWorkspaceChanges batch:` at the Rust→TS boundary (folded together
with the dropped-paths summary) and the human-readable `Recomputing
project graph…` / `Reusing in-memory cached project graph…` decision
log. Both write to the daemon log file only, providing a complete trail
through watcher → routing → recompute decision for future
investigations.
**Unit test in `project-graph-incremental-recomputation.spec.ts`** that
exercises the freshness gate end-to-end — verified to fail without the
gate and pass with it. Mocks `getPluginsSeparated` only to park the
first IIFE between its synchronous snap and its commit (a
millisecond-scale timing gap that's hard to control without mocks); the
gate logic itself is real.
## Related Issue(s)
Follow-up fix to #35646 (watcher hardening). No linked issue.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
<!--
_[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>
## Current Behavior
`@nx/workspace` builds to the shared workspace-root
`dist/packages/workspace/` directory, uses CommonJS
`module`/`moduleResolution`, and has no `exports` map. Other Nx packages
and external plugins reach into `@nx/workspace/src/*` subpaths for
internal utilities.
## Expected Behavior
`@nx/workspace` follows the same local-dist build pattern as `nx` and
`@nx/devkit`:
- Builds to `packages/workspace/dist/` instead of
`dist/packages/workspace/`.
- `tsconfig.lib.json` uses `module`/`moduleResolution: "nodenext"` with
`composite`, `rootDir: "."`, `declarationDir: "dist"`.
- `package.json` declares an `exports` map matching `@nx/devkit`'s
locked-down surface — only named entry points, no `./src/*` wildcard.
- `generators.json` / `executors.json` factory/schema paths rewritten
`./src/...` → `./dist/src/...`. Workspace dev still works via the
`tryResolveFromSource` fallback in
`packages/nx/src/config/schema-utils.ts`.
- `README.md` → `readme-template.md`; build command writes the rendered
README to `packages/workspace/README.md`.
- `project.json` adds `release.version` config
(`preserveLocalDependencyProtocols: true`, matching nx/devkit).
- `scripts/nx-release.ts`: adds `packages/workspace` to
`packagesToReset`.
Internal-leak cleanup so the locked-down exports map doesn't break
first-party callers:
- `TypeScriptCompilationOptions` + `compileTypeScript` moved from
`@nx/workspace` to `@nx/js` (the package that owns TypeScript
compilation).
- `@nx/remix` inlines `directoryExists` (was a one-line re-export
wrapper).
- `@nx/rspack` drops the Nx 15.7-era
`@nx/workspace/src/utils/create-ts-config` fallback.
- e2e helpers source `angularDevkitVersion` from `@nx/angular/src/utils`
instead.
- `@nx/workspace` and `@nx/remix` no longer declare `@nx/workspace` deps
they don't use.
- Adds a preflight step to the `dist-build-migration` skill that warns
about `workspace:*` deps on not-yet-migrated packages.
## Breaking Changes
**`@nx/workspace/src/*` subpath imports are no longer supported.** The
`exports` map no longer declares a `./src/*` wildcard. ~150 public
consumers across GitHub use these subpaths today (the largest cluster is
`src/utilities/fileutils` with ~60 hits). Migration:
- `@nx/workspace/src/utilities/fileutils` (`directoryExists`,
`fileExists`, `isRelativePath`, `createDirectory`) — these are thin
re-exports from `nx/src/utils/fileutils`. Inline with `node:fs`
(`statSync(p).isDirectory()` for `directoryExists`) or import from
`@nx/devkit` where equivalent.
- `@nx/workspace/src/utilities/typescript/compilation` — moved to
`@nx/js`. Internal callers should import from there; if you were
depending on the type externally, copy it or use the equivalent from
`@nx/js`.
- `@nx/workspace/src/utils/versions` — values are duplicated across
packages; reimplement against the version of Nx you're targeting.
- Other subpaths — check the `@nx/workspace`'s public `index.ts`
re-exports first; otherwise reimplement.
**`@nx/workspace/tasks-runners/default` now throws when invoked.** This
module was an alias for `nx/src/tasks-runner/default-tasks-runner`. The
Nx 17 (`use-minimal-config-for-tasks-runner-options`) and Nx 21
(`remove-custom-tasks-runner`) migrations rewrite/remove legacy
`tasksRunnerOptions` entries. If your `nx.json` still references
`@nx/workspace/tasks-runners/default`, replace it with
`nx/tasks-runners/default`.
## Related Issue(s)
Part of the ongoing migration of Nx packages to the local-dist build
layout (following `nx` and `@nx/devkit`).
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
`rust-toolchain.toml` pins Rust to `1.94.0`. This blocks upgrading
`sysinfo` to `0.39.x`, which requires Rust `1.95` and brings upstream
support for several cgroup limit features that nx currently needs to
implement locally.
## Expected Behavior
Toolchain bumped to `1.95.0`. Verified:
- `cargo build -p nx` produces zero new warnings vs. 1.94.
- `cargo build -p nx --all-targets` (compiles tests too) produces zero
new warnings vs. 1.94 — identical warning set.
- Clippy delta: +11 new clippy lints (style suggestions only — not
gating, none correctness-related).
## Related Issue(s)
N/A — maintenance/hygiene change. Unblocks the sysinfo bump that, in
turn, lets PR #35622 drop its memory-side cgroup parsing in favor of
upstream `Process::cgroup_limits()` + parent cgroup memory walking
(sysinfo PRs
[#1643](https://github.com/GuillaumeGomez/sysinfo/pull/1643) and
[#1651](https://github.com/GuillaumeGomez/sysinfo/pull/1651)).
## 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.
## 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).
## 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.
## Current Behavior
`nx migrate <target>` processes every entry in the resolved
`packageJsonUpdates`, including third-party packages outside the
target's `nx.packageGroup`. There is no way to scope a run to Nx itself,
no safety rail when jumping multiple major versions, and the cascade can
propose downgrades for deps that have already been bumped past the
historical pin.
## Expected Behavior
Adds two new flags to `nx migrate`, plus a downgrade-prevention
safeguard and a bare-invocation default.
### `--mode={first-party|third-party|all}`
Scope which packages get migrated. Only valid when the target is the
canonical Nx package (`nx`, or `@nx/workspace` as an era-aware alias).
- **`first-party`** — bumps Nx and the packages in its
`nx.packageGroup`; third-party deps are left untouched.
- **`third-party`** — anchors at the installed Nx version and walks
`--from=nx@0.0.0 --exclude-applied-migrations` internally, surfacing
third-party catch-up that earlier first-party-only runs left behind.
- **`all`** (default) — everything; matches existing behavior.
Defaults to `all` outside a TTY; prompts in an interactive terminal when
the target is canonical Nx. Rejects `--from`,
`--exclude-applied-migrations`, and out-of-bounds `--to nx@…` (or a
higher `nx@…` positional) when `mode=third-party` — those already imply
going past the installed version.
### `--multi-major-mode={direct|gradual}`
Handles the case where the target jumps two or more major versions from
installed.
- **Interactive (TTY):** prompts with the smallest-step recommendations
— `latest in current major` (when at least a minor ahead of installed),
`next major`, and `migrate directly to <target>`. The smallest available
step is tagged `[recommended]`.
- **Non-interactive:** warns and proceeds with the requested target.
- **`--multi-major-mode=direct`** (or `NX_MULTI_MAJOR_MODE=direct`) —
skip the prompt/warn and migrate directly to the requested target.
- **`--multi-major-mode=gradual`** (or `NX_MULTI_MAJOR_MODE=gradual`) —
skip the prompt and pick the smallest recommended step automatically;
re-run `nx migrate` to continue toward the originally requested target.
Falls back silently to the requested target when no stepwise option is
available.
### Downgrade prevention
`packageJsonUpdates` entries that would move a workspace dep backwards
from its current pin are now dropped from the proposed updates. Common
case: a workspace that manually bumped a dep past the version a
historical `packageJsonUpdates` entry pins.
### Bare invocation
`nx migrate` with no positional now defaults to `nx@latest` instead of
erroring, then runs through the mode + multi-major flow.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Workspace `package.json` files declare common dependencies with mixed
literals and ranges (e.g. `webpack: 5.101.3` in root vs `^5.101.3` in
plugin consumers). pnpm materializes the resulting permutations as
separate peer-resolution variants in the lockfile, even when every
consumer resolves to the same version.
## Expected Behavior
Declarations route through `pnpm-workspace.yaml` catalogs. Redundant
peer-resolution variants collapse and the catalog becomes the single
source of truth for cross-package version alignment. Lockfile shrinks by
~1,980 lines.
## Implementation Details
### Catalog changes
- **New named catalogs:** `catalogs.css` (postcss family + `less-loader`
/ `sass-loader`), `catalogs.tailwind` (`@tailwindcss/postcss`,
`tailwind-merge`), `catalogs.vite` (`vitest`).
- **Default `catalog:` additions:** `@babel/core`, `@heroicons/react`,
`@module-federation/enhanced`, `@playwright/test`, `@svgr/webpack`,
`ajv`, `gpt3-tokenizer`, `http-proxy-middleware`, `http-server`,
`jsonc-parser`, `next-seo`, `ora`, `react-textarea-autosize`, `rxjs`,
`tmp`, `tree-kill`, `verdaccio`, `webpack`, `webpack-dev-server`.
- **`catalogs.eslint` additions:** `@typescript-eslint/type-utils`,
`@typescript-eslint/utils`, `eslint-config-prettier`.
- **Existing-catalog routings:** `semver`, `tslib`, `typescript` routed
at additional consumers.
### Per-dep notes
- `ora` cataloged at `^5.3.0` — caret preserves `<6.0.0` since `ora` 6+
is ESM-only and Nx packages are CommonJS.
- `@module-federation/enhanced` (`^2.3.3`) and `verdaccio` (`^6.3.2`)
cataloged at security floors (2.3.1 had a compromised `axios`;
`verdaccio` <6.3.2 carried a vulnerable `handlebars`).
### Out of scope
- **Transitive-only drift** in `yaml`, `less`, `esbuild` — would require
`pnpm.overrides`, not catalog routing.
- **`packages/vitest` peer range** `^1 || ^2 || ^3 || ^4` — left alone
(tightening is breaking for downstream consumers).
- **Peer ranges spanning multiple majors** (`next`, `@nuxt/*`,
`@rsbuild/core`, `metro-*`, `nx`, `@typescript-eslint/parser`, etc.) —
intentional cross-major support contracts.
- **Cross-major direct declarations** (`storybook`, `loader-utils`,
`memfs`, `cypress`, `eslint`, `vite`, etc.) — each is its own migration.
- **`@nx/devkit` / `@nx/js` literal `23.0.0-beta.4`** in
`tools/workspace-plugin` — stale workspace reference; not
catalog-fixable.
- **`verdaccio` peerDep `^6.0.5`** in `packages/js` — not tightened to
security floor to avoid changing the peer constraint for downstream
consumers.
### Note on `pnpm dedupe`
Evaluated separately and abandoned — `pnpm dedupe` actively bumps minor
versions across declared ranges, which cascaded into 21 plugin `:test`
snapshot regressions when attempted (#35628). The targeted catalog
routing here avoids that class of change.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## 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>
## Current Behavior
- `@nx/playwright` generators silently fall through to the latest
install constants when a workspace has `@playwright/test` detected below
the supported floor (`1.36.0`). Generators write incompatible config
rather than failing fast.
- The init generator overwrites already-installed `@playwright/test`
versions on re-runs. The configuration generator's linter helper
overwrites already-installed `eslint-plugin-playwright` pins.
- `nxE2EPreset` auto-injects the `blob` reporter in CI regardless of
installed Playwright version — workspaces on 1.36.x silently pick up a
reporter that Playwright doesn't recognize and fail at test run time
with an unhelpful error.
- The `@nx/playwright:merge-reports` executor invokes a Playwright CLI
subcommand that doesn't exist on 1.36.x and surfaces a confusing CLI
error rather than a clear remediation message.
- Fresh installs land on `^1.36.0`, a version that pre-dates the `blob`
reporter and `merge-reports` CLI, so users adopting the plugin don't get
the full feature surface out of the box.
## Expected Behavior
- Every generator entry point asserts the supported floor up front via a
shared `assertSupportedPackageVersion`, throwing a standardized error
naming the package, installed version, and supported floor.
- Generators preserve user pins for both `@nx/playwright` and
`@playwright/test`. The configuration generator's linter helper
preserves a user-pinned `eslint-plugin-playwright`.
- `nxE2EPreset` skips auto-injecting the `blob` reporter on Playwright <
1.37.0 by default; an explicit `generateBlobReports: true` on an
unsupported version throws with a clear remediation message.
- The `merge-reports` executor fails fast with a clear "requires
Playwright >= 1.37.0" message when invoked against a sub-1.37 workspace.
- Fresh installs land on `^1.37.0` so users get the full feature set
(blob reporter + merge-reports CLI). The peer dep stays at the existing
`^1.36.0` — no regression for existing workspaces.
## Implementation Details
Bundled into this PR:
- **`@nx/devkit/internal` shared helpers** (consumed by all subsequent
plugin compliance PRs):
- `getInstalledPackageVersion(packageName)` — FS-based, for
executor/runtime contexts.
- `getDeclaredPackageVersion(tree, packageName, latestKnownVersion?)` —
tree-based, normalized, with `latest`/`next` fallback support.
- `assertSupportedPackageVersion(tree, packageName,
minSupportedVersion)` — the floor-enforcement guard.
-
**`@nx/devkit/internal-testing-utils.assertGeneratorsEnforceVersionFloor`**
— parameterized spec helper asserting every generator in a plugin's
`generators.json` throws on sub-floor detection.
- **`@nx/angular` adoption** — `assertSupportedAngularVersion`,
`getInstalledAngularVersion`, the executor-side
`getInstalledAngularVersionInfo`, and the angular all-generators floor
spec all delegate to the shared helpers. No behavioral change.
- **`@nx/playwright`** — adopts the helpers, adds the feature-vs-version
gates for blob/merge-reports, fixes user-pin preservation.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The `@nx/react`, `@nx/react-native`, and `@nx/expo` component generators
expose a `--js` option to generate JavaScript files instead of
TypeScript. The option was deprecated in #29111 (targeted for removal in
Nx v21) in favor of including the file extension directly in the `path`
argument:
```sh
nx g @nx/react:component mylib/src/lib/foo.jsx
```
We are now on Nx v23 — past the deprecation window — but the option
still exists.
## Expected Behavior
The `--js` option is removed from the component generators in
`@nx/react`, `@nx/react-native`, and `@nx/expo`. Users include the
desired file extension directly in the `path` argument; the path's
extension drives whether `.tsx`, `.jsx`, `.ts`, or `.js` files are
generated. When no extension is provided, the generators default to
`.tsx`, matching prior behavior.
The library generators in those three packages still keep their own
`--js` option (out of scope for this change). Internally they now encode
`.js` into the `path` they pass to the component generator instead of
forwarding the now-removed `js` flag.
### BREAKING CHANGE
The `--js` option has been removed from:
- `@nx/react:component`
- `@nx/react-native:component`
- `@nx/expo:component`
Migration: include the file extension in the `path` argument, e.g. `nx g
@nx/react:component mylib/src/lib/foo.jsx`.
## Related Issue(s)
Linear:
[NXC-3665](https://linear.app/nxdev/issue/NXC-3665/common-remove-js-option-from-component-generators)
## 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>
<!-- 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>
_This PR description was generated by Amp._
## Current Behavior
The TUI task sidebar appears scrolled down by one entry on render,
hiding the first continuous task from view.
<img width="1167" height="829" alt="Screenshot 2026-03-03 at 10 56 09
AM"
src="https://github.com/user-attachments/assets/83421f89-3be9-4f80-80b2-c57bd0c2c161"
/>
Here you can see that `watch-deps` is running in addition to `dev`.
## Expected Behavior
All continuous tasks should be visible in the sidebar without any
initial scroll offset.
## Related Issue(s)
N/A (discovered via visual inspection)
## Details
The viewport height calculation in `tasks_list.rs` subtracted 4 rows for
header overhead, but the actual overhead is only 3 rows:
1. `top_margin` (1 row)
2. Header content (1 row)
3. Spacing row (1 row)
This off-by-one caused the viewport to be 1 row too small, making the
task list appear scrolled down by one entry on initial render.
### Changes
1. **Unified overhead constant**: Introduced `TABLE_HEADER_OVERHEAD_ROWS
= 3` and `SCROLLBAR_Y_OFFSET = 2`, replacing scattered hardcoded values.
The scrollbar y-offset is 2 (not 3) because the scrollbar visually spans
from the spacing row downward, while the viewport only counts content
rows.
2. **Stale scroll correction**: Added logic in `set_viewport_height` to
reset `scroll_offset` to 0 when the viewport grows from a small
placeholder size (≤ 5) to the real terminal size, provided the selected
item is still visible from the top. This prevents stale scroll positions
set during initialization from hiding top items.
3. **Regression tests**: Added
`test_viewport_growth_resets_stale_scroll_offset` and
`test_viewport_growth_preserves_scroll_when_selection_not_visible` to
cover the stale scroll correction.
4. **Snapshot updates**: 14 snapshot files updated to reflect one
additional visible row.
### History
The `4` originated in commit `6541751a` (James Henry, Apr 2025) with the
comment "Reserve space for pagination and borders." However, the table
had no borders (`Block::default()` without `Borders`), and pagination
lived in its own layout chunk. The `4` was a rough estimate that was
never precisely derived from the actual header structure. When scrolling
replaced pagination in commit `1782e8c7` (Leosvel Perez Espinola, Sep
2025), the value was carried forward unchanged. The same commit also
introduced a separate `header_and_spacing_rows = 2` for the scrollbar
y-offset, confirming these values were set independently without unified
accounting.
Also documents the `copy-built-package` script in `AGENTS.md` for
AI-assisted development workflows.
Co-authored-by: Amp <amp@ampcode.com>
## 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>
## Current Behavior
`TargetDependencyConfig.projects` accepts the legacy magic strings
`'self'` and `'dependencies'` via a compat shim in
`packages/nx/src/tasks-runner/utils.ts`. The shim was added for
pre-v8.1.4 Lerna's `prepNxOptions` call shape, with a
`TODO(@agentender): Remove this part in v20` that has now been deferred
twice (originally v17, then v20 — we're on v22.6).
## Expected Behavior
The Lerna-driven case is no longer relevant:
- Lerna switched to the modern `{ dependencies: true }` shape on
2024-06-06
([lerna/lerna#4017](https://github.com/lerna/lerna/pull/4017), first
shipped in v8.1.4).
- Any user-authored `projects: 'self'` / `projects: 'dependencies'`
configs (including `{self}`/`{dependencies}` token variants) are already
rewritten to the modern form by the existing v16
`update-depends-on-to-tokens` migration.
The shim, the LERNA SUPPORT comment markers, and the stale TODO are
removed. The single-project shorthand (`projects: "my-lib"`
auto-wrapping to `["my-lib"]`) is preserved.
## Related Issue(s)
Linear:
[NXC-4308](https://linear.app/nxdev/issue/NXC-4308/address-stale-todo-v20-in-tasks-runner-utilsts-drop-lerna-compat-for)
Closes#35630 — supersedes the input-drift snapshot approach with a
simpler watcher-side fix.
## Current Behavior
The daemon's `flushPendingWorkspaceChanges` → native
`force_flush_pending` drains the watcher's notify channel with
`try_recv`. This misses events where the notify-crate thread has read
the kernel inotify event but hasn't yet finished sending it on
`notify_rx` — a window normally of microseconds but unbounded under
scheduling pressure (parallel CI jobs, container cgroups, NFS). When the
window hits, the daemon serves a project graph computed against the
pre-write state of the workspace. The `spread.test.ts` e2e has been
flaking on this race.
## Expected Behavior
**Core fix (Rust):** the force-flush handler now waits up to 5ms via
`recv_timeout` for an in-flight notify-thread send to land before
draining and snapshotting. The change is scoped to the force-flush
handler — the steady-state idle-window flush is unchanged, so there's no
latency cost on the normal event flow. `RecvTimeoutError::Disconnected`
is now surfaced via the same `fatal` path the main select arm uses,
instead of being silently masked as an empty snapshot.
**Regression coverage:**
- Rust unit test (`force_flush_pending_captures_in_flight_writes`) — 20
iterations of write-then-immediate-flush with no sleeps. Passes
deterministically with the fix; fails / is racy without it.
- TypeScript integration spec
(`project-graph-incremental-recomputation.spec.ts`) — real native
`Watcher` against a TempFs workspace, real production routing callback,
mutate a project then immediately request the graph. Asserts the daemon
returns a graph reflecting the new project (not a stale cached graph).
**Supporting refactors (needed for the integration spec to work
cleanly):**
- Extracted `routeWorkspaceChanges` from `server.ts`'s
`handleWorkspaceChanges` so the spec can exercise the real production
event-routing path without the daemon's inactivity-timer /
error-tracking bookkeeping leaking into the test.
- Live-bound `workspaceDataDirectory`, `cacheDir`, `nxProjectGraph`,
`nxFileMap`, `nxSourceMaps`, `DAEMON_DIR_FOR_CURRENT_WORKSPACE`,
`DAEMON_OUTPUT_LOG_FILE`, and `taskHistoryFile`. They were previously
`const` values frozen at module load — fine in production (the daemon
has one workspace root for its lifetime), but tests that swapped
`workspaceRoot` couldn't update them, so the daemon would write its
cache into the real workspace under test. Added an
`onWorkspaceRootChanged` subscription in `workspace-root.ts` and
converted each derived path to a refresh-on-change `let`. Production
behaviour is unchanged — listeners only fire when `setWorkspaceRoot` is
called, which never happens in normal daemon startup.
## Related Issue(s)
Closes#35630.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## 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>
## Current Behavior
`@nx/vite`'s `ensure-vitest-package-migration-23` imports the deep path
`@nx/devkit/src/generators/executor-options-utils`, which v23 dropped
from the `@nx/devkit` `exports` map. `nx migrate --run-migrations` halts
with `ERR_PACKAGE_PATH_NOT_EXPORTED` and skips every subsequent
migration. The Vite 8 AI doc also carried two stale upstream claims
(`.d.mts` types, "silently ignores `rollupOptions`"), which was true in
8.0.0, but not in subsequent patch releases.
Additionally, three v23 codemods shipped without the per-migration `.md`
doc the rest of `update-23-0-0/` provides.
## Expected Behavior
Migration import swapped to `@nx/devkit/internal`. Migration `version`
bumped to `23.0.0-beta.10` so prior-beta users get a clean re-run. AI
doc corrected.
Missing `.md` docs added for
`cypress/remove-experimental-prompt-command`,
`rspack/add-svgr-to-rspack-config`, and
`vite/rename-rollup-options-to-rolldown-options`.
## Related Issue(s)
Related to NXC-4154
## 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.
## Current Behavior
Three React module-federation e2e tests in
`e2e/react/src/module-federation/` have been flaking in CI:
### `misc-rspack-convert-to-rspack.test.ts` — silent test gap + 30s
timeout
1. **Wrong `updateFile` path.** Line 47 wrote to
`apps/${shell}-e2e/src/example.spec.ts`, but `@nx/react:host` (without
an `apps/` prefix in the project name) generates the e2e project at
workspace root. `updateFile` calls `ensureDirSync(...)` and silently
created the file under the wrong path tree, leaving the default
Nx-generated 1-test Playwright spec to run instead. The test has been
quietly exercising the default Welcome page rather than the
convert-to-rspack module-federation remote-loading behavior. Same root
cause that `dc9ba49fe3` fixed for the sibling `updateJson` call — the
matching `updateFile` was missed.
2. **Missing `runCommandUntil` timeout.** Line 63 omitted the `timeout`
option, falling back to the 30s default in
`e2e/utils/command-utils.ts:300`. The e2e step needs to bootstrap nx,
cold-compile rspack for host + remote, and run the spec across 3 browser
projects sequentially with 1 worker — which routinely exceeds 30s. PR
#34148 added `{ timeout: 120_000 }` to several sibling MF tests but
missed this one because it landed later in #32948.
### `core-rspack-basic-playwright.test.ts` and
`core-webpack-basic-playwright.test.ts` — port collisions on parallel
agents
Both tests generated their host on the default port **4200** with no
reservation. Once #35325 enabled parallel `e2e-ci` execution on the same
agent, any other concurrent test that also defaulted to 4200 (another MF
test, an `e2e-playwright` test, etc.) collided with this one. Symptoms
across recent failures:
- Shell preview server gets SIGKILL'd (`Killed` / `code 137`) mid-run
while another test fights for the same port and the OS picks a loser.
- Subsequent Playwright requests fail with `NS_ERROR_CONNECTION_REFUSED`
/ `Connection refused`.
- On retry the **other** test's dev server answers, so the assertion
fails with strings like `Welcome app9409916` or `Welcome
pw-react-app6359154` — app names that aren't even in the running test.
This is exactly the gap called out in #35325:
> e2e/cypress, e2e/playwright, and the React module-federation tests
still hardcode ports through their generators; those will be addressed
in follow-up PRs as CI surfaces collisions.
## Expected Behavior
- `misc-rspack-convert-to-rspack` writes the spec file to the correct
path so it actually exercises the convert-to-rspack MF behavior, and
gets the same 120s timeout as the sibling MF tests so cold rspack
compile + 3 browser runs fit reliably.
- `core-rspack-basic-playwright` and `core-webpack-basic-playwright` use
`reservePorts(4)` for shell + 3 remotes, pass
`--devServerPort=${shellPort}` to the host generator (which propagates
to the host's serve, preview, and the e2e project's playwright `baseUrl`
per `packages/react/src/generators/host/`), and `updateJson` each
remote's `project.json` to use its reserved port — matching the pattern
already in `misc-rspack-convert-to-rspack` and
`independent-deployability.webpack`.
## Verification
- **Local** (`NX_E2E_RUN_E2E=true pnpm nx run
e2e-react:e2e-ci--src/module-federation/<file> --skip-nx-cache`):
- `misc-rspack-convert-to-rspack.test.ts` → PASS (~162s wall, ~108s
test) with e2e step actually running.
- `core-rspack-basic-playwright.test.ts` → PASS with the port fix
applied.
- **CI**: relying on the PR's `e2e-ci` aggregate and re-runs to confirm
the flake is gone.
## Related Issue(s)
N/A — internal flake fix surfaced by repeated `e2e-react:e2e-ci`
failures on parallel CI agents. Not tracked as a GitHub issue.
## Current Behavior
- `@nx/angular` silently falls through to the v21 install constants when
a workspace has `@angular/core` detected below the supported floor
(v19). Generators add v21 packages incompatible with the user's setup
instead of failing fast.
- Three cross-major Module Federation `packageJsonUpdates` entries
(`20.2.0-module-federation`, `22.2.0`, `22.6.0-module-federation`) lack
`requires` gates, so each bump fires for every workspace regardless of
the installed `@module-federation/*` source major.
- The `update-unit-test-runner-option` migration declares
`@angular/core: >=21.0.0` despite only rewriting an Nx generator default
in `nx.json` (`unitTestRunner: 'vitest'` → `'vitest-analog'`). Pre-v21
workspaces that already have the stale default never run the migration
and stay broken (the value is no longer in the generator schema's enum).
- The `add-linting` generator overwrites already-installed third-party
versions because it doesn't pass `keepExistingVersions=true` to
`addDependenciesToPackageJson`.
## Expected Behavior
- A workspace with `@angular/core` detected below v19 throws with a
clear, standardized message naming the package, the installed version,
and the supported floor (no silent fall-through). The fresh-install path
(no `@angular/core` detected) still resolves to the latest supported
version.
- Each MF `packageJsonUpdates` entry gates on the
`@module-federation/enhanced` source range it is bumping from, so the
bump only fires for workspaces actually in that range.
- The `update-unit-test-runner-option` migration runs for any workspace
with the stale generator default, regardless of installed Angular
version.
- The `add-linting` generator preserves already-installed dependency
pins.
A new internal helper `throwForUnsupportedVersion` is added to
`@nx/devkit/internal` so first-party plugins can produce a consistent
below-floor error format. It is intentionally not part of the public
`@nx/devkit` surface — only first-party plugins will consume it as the
rest of the multi-version compliance rollout lands.
## Current Behavior
No `nx migrate` path for Vite 7 -> 8. Users hit `rollupOptions` rename,
plugin-react v6 / Oxc transition, and Angular+Vitest
`@oxc-project/runtime` gap by hand.
## Expected Behavior
`nx migrate 23` bumps `vite` to `^8.0.0` and `@vitejs/plugin-react` to
`^6.0.0`, codemods `build.rollupOptions` -> `build.rolldownOptions` in
vite config files (top-level + nested environments), and writes
`tools/ai-migrations/MIGRATE_VITE_8.md` with LLM-driven cleanup steps.
Blocked by NXC-4448 (cypress bump).
## Related Issue(s)
Fixes NXC-4154
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`@nx/cypress` pins `cypress` at `^15.8.0` and `@cypress/vite-dev-server`
at `^7.0.1`. `component-configuration` generator throws when `vite >=
8`, citing missing Cypress support. Cypress 15.14.0 (2026-04-16) shipped
Vite 8 support, so both pin and guard are stale.
## Expected Behavior
`cypress` -> `^15.14.2`, `@cypress/vite-dev-server` -> `^7.3.1`. Vite 8
guard removed. `nx migrate 23` bumps users below those versions. New
codemod strips the `experimentalPromptCommand` config flag (removed in
Cypress 15.13.0).
Unblocks NXC-4154.
## Related Issue(s)
Fixes NXC-4448
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## 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>
## Current Behavior
`@nx/rspack` exposes an `svgr` option on `withReact` and
`NxReactRspackPlugin` that wires `@svgr/webpack`. Slated for removal in
v23.
## Expected Behavior
`svgr` removed from the rspack public API. SVG handling consolidated
into the images asset rule. New
`update-23-0-0-add-svgr-to-rspack-config` migration inlines a `withSvgr`
helper into user configs to preserve behavior, mirroring the v22 webpack
migration.
## Related Issue(s)
NXC-4156
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`@nx/gradle/plugin-v1` is the legacy non-atomized Gradle plugin entry.
In Nx 21, when v2 (the atomized default) became the new `@nx/gradle`,
existing users were migrated *to* `plugin-v1` to preserve their
behavior. Since then it's been a stable opt-out, with no `@deprecated`
annotation, no runtime warning, and no docs note signaling it's going
away.
## Expected Behavior
The `@nx/gradle/plugin-v1` entry remains fully functional in Nx 23 but
emits a clear deprecation warning on plugin load and is annotated
`@deprecated` on every public symbol. Removal is scheduled for Nx 24,
giving users on the legacy non-atomized plugin one major to switch to
the default `@nx/gradle` entry.
### What changed
- `packages/gradle/plugin-v1.ts` — emits a deprecation warning on module
load via `emitPluginWorkerLog` so the message surfaces to the user even
when the daemon is enabled. Routing it through `logger.warn` would have
been swallowed into the daemon log file.
- `packages/gradle/src/plugin-v1/{nodes,dependencies}.ts` —
`@deprecated` JSDoc on the public `createNodesV2` / `createDependencies`
exports.
- `packages/nx/src/devkit-internals.ts` + `packages/devkit/internal.ts`
— re-export `emitPluginWorkerLog` so `@nx/devkit/internal` becomes the
canonical path for plugin authors who want a daemon-aware warning
channel (instead of deep-importing into
`nx/src/project-graph/plugins/isolation/`).
### Notes
- Purely additive: no code or exports removed. Plugin behavior is
unchanged.
- Runtime-guarded with `typeof emitPluginWorkerLog === 'function'` to
handle the `@nx/devkit@23 + nx@22.0–22.6` skew (the helper shipped in
nx@22.7).
- A swap migration (rewriting `@nx/gradle/plugin-v1` → `@nx/gradle` in
`nx.json`) will ship alongside the v24 removal.
## Related Issue(s)
<!-- No tracking issue yet -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Most Nx executors that have an inferred-plugin alternative
(`@nx/<pkg>/plugin`) and a `convert-to-inferred` generator are still
wired up like first-class citizens. There is no signal — at scaffold
time, schema browsing, or task execution — that they are on a path to
removal, and the existing `cypress`/`detox` deprecation messages are
inconsistent with the canonical pattern shipped most recently.
## Expected Behavior
Every executor that has an inferred-plugin migration target is now
deprecated through three surfaces, matching the canonical pattern:
- **Runtime warning.** The executor logs that it is deprecated, will be
removed in Nx v24, and points at `nx g @nx/<pkg>:convert-to-inferred`.
- **Schema-root `x-deprecated`.** Surfaces in editor / Nx Console / `nx
show project` views.
- **Generation-time warning.** When a generator is about to scaffold a
target that uses one of these executors because the corresponding
inferred plugin isn't registered, it warns at generation time and points
at the same migration path.
All warnings link to
<https://nx.dev/docs/guides/tasks--caching/convert-to-inferred>.
### Executors deprecated in this PR
| Package | Executors |
|---|---|
| `@nx/webpack` | `webpack`, `dev-server` |
| `@nx/vite` | `build`, `dev-server`, `preview-server` |
| `@nx/rollup` | `rollup` |
| `@nx/next` | `build`, `server` |
| `@nx/remix` | `build`, `serve` |
| `@nx/jest` | `jest` |
| `@nx/playwright` | `playwright` |
| `@nx/eslint` | `lint` |
| `@nx/storybook` | `storybook`, `build` |
| `@nx/rspack` | `rspack`, `dev-server` |
| `@nx/expo` | `build`, `export`, `install`, `prebuild`, `run`, `serve`,
`start`, `submit` |
| `@nx/react-native` | `build-android`, `build-ios`, `bundle`,
`pod-install`, `run-android`, `run-ios`, `start`, `upgrade` |
| `@nx/vitest` | `test` |
### Generation-time warnings wired in
- `@nx/<pkg>:configuration` for webpack, vite, rollup, jest, playwright,
storybook, rspack, vitest
- `@nx/<pkg>:application` for next, expo, react-native
- `@nx/eslint:lint-project` legacy fallback path
- `@nx/react:application` (webpack, rspack branches) and
`@nx/react:library` (rollup legacy fallback) — warning text is inlined
rather than imported. Two distinct reasons:
- **rspack:** `@nx/react` does not declare a tsconfig project reference
to `@nx/rspack`, so the deep import would not even type-check.
- **webpack and rollup:** the project reference exists, so the deep
import compiles, but `@nx/webpack` and `@nx/rollup` only expose
`./index.js` in their package `exports` field. The import would resolve
in source but throw `Cannot find module
'@nx/<pkg>/src/utils/deprecation'` at runtime in published packages.
- `@nx/react-native:web-configuration` (webpack) — inline for the same
reason as the rspack case (no project reference).
### Scope notes
- `@nx/vite:test` is intentionally **not** included — it is being
removed entirely by PR #35517 (deprecation messaging would ship as dead
code). `@nx/vitest:test` is now in scope: the `convert-to-inferred`
generator for `@nx/vitest` is in place, so the deprecation has a real
migration target.
- `@nx/cypress`/`@nx/detox` already shipped earlier; this PR retitles
their generation-time messages to the new wording (drop the redundant
"register the plugin first" guidance, swap "Scaffolding" for
"Generating") and points the detox URL at the general
convert-to-inferred guide.
- `@nx/react-native:storybook` is intentionally **not** included. The
companion `@nx/react-native:storybook-configuration` generator was
already removed in v21 and no generator has emitted this target since
Jan 2024 (RN 0.73 upgrade). It will be removed outright in a follow-up
PR rather than going through the deprecate-now / remove-in-v24 cycle.
- `@nx/webpack:ssr-dev-server`, `@nx/expo:build-list`,
`@nx/expo:sync-deps`, `@nx/expo:update`, `@nx/expo:ensure-symlink`,
`@nx/react-native:sync-deps`, and `@nx/react-native:ensure-symlink` are
intentionally left as-is — none are covered by `convert-to-inferred` and
they have no clean replacement (Nx-specific glue or non-migrating
utilities).
- `@nx/esbuild:esbuild` and `@nx/nuxt:*` ship neither an inferred plugin
nor a `convert-to-inferred` generator yet, so they're out of scope.
- Per-package READMEs, introduction-doc banners, per-package "migration
recipe" pages, and `migrations.json` entries are intentionally skipped
per the canonical pattern (NXC-4422). The runtime warning carries the
migration story.
## Related Issue(s)
Fixes NXC-4423.
Fixes NXC-4296.
Fixes NXC-4294.
Fixes NXC-4290.
Fixes NXC-4285.
Fixes NXC-4283.
Fixes NXC-4286.
Fixes NXC-4297.
Fixes NXC-4293.
Fixes NXC-4292.
Fixes NXC-4282.
Fixes NXC-4287.
Fixes NXC-4295.
Fixes NXC-4447.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## 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).
## Current Behavior
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).
## Expected Behavior
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.
## Implementation Details
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>
## Current Behavior
Nightly `E2E matrix` jobs on node 26 hang during `pnpm playwright
install --with-deps` after Chromium download (zip extract never
finishes). 20m job timeout cancels the preinstall, e2e jobs skip,
`process-result` fails on missing `outputs/*/matrix.json`.
Root cause: yauzl regression on node 26
([microsoft/playwright#40724](https://github.com/microsoft/playwright/issues/40724),
[thejoshwolfe/yauzl#168](https://github.com/thejoshwolfe/yauzl/pull/168)).
## Expected Behavior
Node 26 commented out of preinstall matrix and `process-matrix.ts`
node_versions for both ubuntu + macos. Nightly runs on v22 + v24 only.
Re-add when playwright ships the fix.
## Related Issue(s)
NXC-4451
## 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
## 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.
## 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.
## 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>
## 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 #
## 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
## 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>
## Current Behavior
The `@nx/angular:ngrx` generator is exposed and functional, but has been
deprecated since Nx v18 in favor of `@nx/angular:ngrx-root-store` (root
state) and `@nx/angular:ngrx-feature-store` (feature state).
## Expected Behavior
The `@nx/angular:ngrx` generator is removed. Users invoke
`@nx/angular:ngrx-root-store` for root state and
`@nx/angular:ngrx-feature-store` for feature state instead.
A migration splits any `@nx/angular:ngrx` generator defaults set in
`nx.json` across the two replacement generator keys, renames the
deprecated `module` option to `parent`, and drops the obsolete `root`
toggle (intent is now expressed by which generator is invoked). The
migration handles both the flat (`"@nx/angular:ngrx"`) and nested
(`"@nx/angular": { "ngrx": ... }`) defaults shapes.
BREAKING CHANGE: The `@nx/angular:ngrx` generator has been removed. Use
`@nx/angular:ngrx-root-store` for root state and
`@nx/angular:ngrx-feature-store` for feature state instead.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
CI matrices test Node 20. `@types/node` pinned to v20 in the repo
catalog and generator constants.
## Expected Behavior
Generator `typesNodeVersion` bumps to `^22.0.0`.
Node 20 dropped from e2e + nightly matrices and ESLint docs (EOL Apr
2026). Repo catalog bumps to `^24.11.0` to match
`mise.toml`. Nightly `nodeTLS` renamed to `lowestNodeLTS` and bumped to
22.
## Related Issue(s)
NXC-4159
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>
## 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.
## Current Behavior
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`).
## Expected Behavior
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.
## Related Issue(s)
N/A
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## 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 #
## 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 #
## Current Behavior
After running `nx-release` locally, `packages/devkit/package.json` is
left modified in the working tree.
The `nx-release.ts` script snapshots and restores the source
`package.json` for every package whose source folder is the publish root
(so the temporary edits made during `nx release version` — real version,
expanded `workspace:*` deps, etc. — don't leak into the working tree).
The list of those packages lives in `packagesToReset` near the top of
`scripts/nx-release.ts`.
When `@nx/devkit` was migrated to publish from its source folder in
#34946, it was not added to that list, so its source `package.json` is
no longer restored.
## Expected Behavior
`packages/devkit/package.json` is restored to its committed contents
after `nx-release` finishes (or is interrupted), matching the behavior
already in place for `nx`, `dotnet`, `maven`, `angular-rspack`, and
`angular-rspack-compiler`.
## Related Issue(s)
N/A — internal release-tooling fix.
## 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
## 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.
## 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.
## 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`.
## 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
## Current Behavior
The pnpm lockfile still resolves `@phenomnomnominal/tsquery` to `6.1.4`
for the rollup importer, even though the catalog was bumped to `~6.2.0`
in #35560.
## Expected Behavior
The lockfile resolves `@phenomnomnominal/tsquery` to `6.2.0`, matching
the catalog entry.
## Related Issue(s)
N/A
## 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 #
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.
## Current Behavior
`useLegacyTypescriptPlugin` opt-in keeps the `rollup-plugin-typescript2`
code path alive in `@nx/rollup` with a deprecation warning. The `withNx`
plugin defaults `buildLibsFromSource` to `false` while the executor
schema defaults to `true` — same option, two defaults.
## Expected Behavior
- `useLegacyTypescriptPlugin` option, schema entry, and
`rollup-plugin-typescript2` dep removed; only
`@rollup/plugin-typescript` remains.
- `withNx` `buildLibsFromSource` default flipped to `true` so it matches
the executor.
- `update-23-0-0-remove-use-legacy-typescript-plugin` migration strips
the deprecated option from existing project.json
`options`/`configurations` and from `rollup.config.{cjs,mjs,js,ts}`
`withNx({...})` calls so users see no behavior change after upgrade.
## Breaking Change Note
`@rollup/plugin-typescript` enforces that the TS `outDir` is inside the
rollup `output.dir`. Users whose custom `rollup.config.{cjs,mjs}`
overrides `output.dir` to a path outside the executor's `outputPath`
will fail with `Path of Typescript compiler option 'outDir' must be
located inside Rollup 'dir' option`. The legacy
`rollup-plugin-typescript2` was permissive here. Affected users should
align their custom config's `output.dir` with the executor's
`outputPath` (or use the function form `(config) => ({ ...config,
output: { ...config.output[0], ...overrides } })` to preserve `dir`).
Two e2e cases that exercised this legacy pattern were dropped from
`rollup-legacy.test.ts`.
## Related Issue(s)
Fixes NXC-4157
## Summary
Resolves Nx Atomizer sandbox violations on
`graph-client:build-client:release`. The original report flagged ~1116
unexpected reads — the entire `packages/nx` source tree (965 files),
`packages/devkit` source (80 files), `graph/client-e2e` Cypress files,
and various `eslint.config.mjs` / `tsconfig.spec.json` / `*.stories.tsx`
siblings across `graph/*` projects.
## Root cause
The bare `graph/` directory was registered as a **webpack context
dependency** for the styles.css module, causing
`FileSystemInfo._readContextHash` to recursively walk and hash every
file underneath it for snapshot validation.
The trigger was a single line in `graph/client/tailwind.config.js`:
```js
path.join(__dirname, '..', 'ui-*/src', glob),
```
The `ui-*` segment is a wildcard at a directory level. To resolve it,
Tailwind has to `readdir` the parent (`graph/`) to enumerate which
subdirs match. Tailwind reports that parent to PostCSS as a
`dir-dependency`, which `postcss-loader` translates into a webpack
context dependency. From there, webpack's snapshot walker:
1. Recursively walks all of `graph/` — including unrelated siblings like
`graph/client-e2e` and `graph/migrate`'s test/eslint configs.
2. Encounters `graph/ui-project-details/node_modules/@nx/devkit` — a
pnpm workspace symlink installed because that lib declares
`"@nx/devkit": "workspace:*"` as a dev dep (purely for `import type`
references).
3. Webpack's `_resolveContextTimestamp` follows the symlink target into
`packages/devkit/`, then through `packages/devkit/node_modules/nx →
packages/nx/`, hashing every file along the way (including `.rs`,
`.snap`, `.fixture` source files that aren't part of any bundle).
## Fix
Enumerate the ui-* dirs explicitly in `graph/client/tailwind.config.js`:
```js
path.join(__dirname, '..', 'ui-code-block/src', glob),
path.join(__dirname, '..', 'ui-common/src', glob),
path.join(__dirname, '..', 'ui-icons/src', glob),
path.join(__dirname, '..', 'ui-project-details/src', glob),
path.join(__dirname, '..', 'ui-render-config/src', glob),
```
With no wildcard at a directory segment, Tailwind reports each
individual `src/` dir as the context dep instead of the bare `graph/`.
Each `src/` subtree contains only source files (no `node_modules`), so
the symlink chain into `packages/{nx,devkit}` is unreachable, and
unrelated siblings like `graph/client-e2e` are no longer touched.
A comment in the config explains the trap so future maintainers know to
add new `ui-*` projects here.
## Empirical results
Local trace of file reads from the webpack-cli subprocess
(`NODE_OPTIONS=--require trace-fs.js` instrumenting `fs.read*`):
| stage | webpack-cli workspace reads | `packages/nx` |
`packages/devkit` | `graph/client-e2e` |
| --------------------- | --------------------------- | ------------- |
----------------- | ------------------ |
| baseline (before fix) | 3010 | 2030 | 112 | 24 |
| after fix | 482 | **0** | **0** | **0** |
Bundle output is byte-identical (2,930,784 bytes for
`dist/apps/graph/main.js`).
The remaining 482 reads are all inside dirs Tailwind legitimately scans
(`graph/client/src`, the explicit `ui-*/src` list, `graph/shared/src`,
plus actual project-graph deps like `graph/migrate`). The `*.stories.*`
and `*.{spec,test}.*` files within those dirs are still hit by the
snapshot walker but are already handled by the existing
`graph-client:build-client` entries in
`.nx/workflows/sandboxing-config.yaml`.
## Other changes
- **`.nx/workflows/sandboxing-config.yaml`** — removed an
outdated/incorrect comment block above the `graph-client` entry. The two
existing exclude patterns (`**/*.stories.*`, `**/*.{spec,test}.*`) cover
the residual noise inside the `ui-*/src` dirs and remain unchanged.
- **`nx.json`** — bumped `bust` to invalidate caches against the
previous attempt.
## Caveats
- New `graph/ui-*` projects require a manual entry in this list — the
config comment calls this out. Worth a follow-up if more `ui-*` packages
are added regularly; an alternative is to read the dir list from the
workspace package map at config-eval time.
- This addresses the Tailwind-driven entry path. The underlying pattern
(a `workspace:*` type-only dep planting a pnpm symlink that webpack's
snapshot walker follows) still exists for any future build that
registers an over-broad context dep. The Tailwind change closes the only
currently-known entry point.
## Verification
- ✅ `pnpm nx run graph-client:build-client:release --skip-nx-cache` →
`webpack compiled successfully`
- ✅ Bundle byte-identical to pre-fix master
- ✅ Empirical file-read trace confirms `packages/nx`, `packages/devkit`,
and `graph/client-e2e` are no longer walked
## Test plan
- [ ] Re-run `graph-client:build-client:release` in CI with sandbox
monitoring; confirm the Atomizer report shows the unexpected-reads count
drop to roughly the count of `*.stories.*` / `*.{spec,test}.*` siblings
inside the ui-* src dirs (already covered by existing sandbox excludes).
- [ ] Confirm dev-server (`nx serve graph-client`) still picks up
Tailwind class changes in each `ui-*` project. The watcher now monitors
each enumerated dir individually instead of via the parent glob.
- [ ] Visual smoke check that the production bundle still renders with
all expected Tailwind classes from `ui-*` libs (no class regressions due
to a missed enumeration).
<!-- polygraph-session-start -->
---
[View session information
↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/graph-client-build-client-bugs-81a94635)
<!-- polygraph-session-end -->
---------
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## 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>
## Current Behavior
The 23.0.0-beta.6 `@nx/devkit` deep-import migration (#35541) catches
non-named-import shapes (default / namespace / side-effect / `require()`
/ dynamic `import()` / `jest.mock`-style calls) by running a regex sweep
over the file:
```ts
const FALLBACK_RE = /(['"])@nx\/devkit\/src\/[^'"\n]+?\1/g;
updated = updated.replace(
FALLBACK_RE,
(_match, quote: string) => `${quote}${INTERNAL_SPECIFIER}${quote}`
);
```
That sweep matches **any** `'@nx/devkit/src/...'` literal anywhere in
the file, regardless of context. As a result the migration mangled:
- **Test fixtures inside template literals** — including the migration's
own `update-deep-imports.spec.ts`. Examples observed in the wild:
`packages/devkit/src/migrations/update-23-0-0/update-deep-imports.spec.ts`,
`packages/expo/src/utils/expo-project-detection.spec.ts`,
`packages/nuxt/src/plugins/plugin.spec.ts`,
`packages/react-native/src/utils/react-native-project-detection.spec.ts`,
etc. ([nrwl/nx#35565](https://github.com/nrwl/nx/pull/35565))
- **`typeof import('@nx/devkit/src/...')` type queries** — e.g.
`libs/shared/npm/src/lib/local-nx-utils/parse-target-string.ts` in
nrwl/nx-console, where the type now claims `@nx/devkit/internal` while
the runtime `importPath` next to it is built dynamically and still
points at `src/...`.
([nrwl/nx-console#3131](https://github.com/nrwl/nx-console/pull/3131))
- **Deep-import paths in comments**, doc strings, and arbitrary
string-literal arguments to unrelated functions.
## Expected Behavior
The migration only rewrites deep-import paths that are *actually* import
sites. Everything else (template strings, type queries, comments,
unrelated calls) is left alone.
This is implemented by replacing the regex sweep with a TypeScript-AST
visitor that only rewrites the string-literal argument of
`CallExpression` nodes whose callee is one of:
- `require` (identifier)
- the dynamic-`import` keyword
- `jest.mock` / `jest.unmock` / `jest.doMock` / `jest.dontMock` /
`jest.requireActual` / `jest.requireMock`
- `vi.mock` / `vi.unmock` / `vi.doMock` / `vi.dontMock` /
`vi.requireActual` / `vi.requireMock` / `vi.importActual` /
`vi.importMock`
Type queries (`ImportTypeNode`), template literals, and comments are all
naturally untouched because they are not `CallExpression` nodes — no
allowlist needed. Quote style is preserved per literal.
The named-import bucketing pass and the duplicate-collapse pass are
unchanged.
### Tests
10 new unit tests:
- 5 in a new `non-runtime string literals` block guarding template
literals, `typeof import(...)` type queries, block comments, line
comments, and unrelated call expressions.
- 5 in a new `mock helper calls` block covering `jest.mock`,
`jest.requireActual`, `vi.mock`, `vi.importActual`, and a paired
`import` + `jest.mock` + `jest.requireActual` combination.
All 39 unit tests pass; `nx build devkit` is clean.
## Related Issue(s)
Follow-up to #35541. Workspaces that have already merged the bad
rewrites need to revert those files by hand — there's no general way to
undo the over-rewrites without losing the legitimate ones.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## 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.
## Current Behavior
`readTargetsFromPackageJson` (in
`packages/nx/src/utils/package-json.ts`) receives a `workspaceRoot`
argument but never passes it to package manager detection:
```ts
for (const script of includedScripts) {
packageManagerCommand ??= getPackageManagerCommand(); // ← no workspaceRoot
res[script] = buildTargetFromScript(script, scripts, packageManagerCommand);
}
```
Two consequences:
1. **Wrong package manager** — `detectPackageManager()` defaults `dir =
''`, so the lockfile probe runs in the CWD, not the workspace. When that
finds nothing it falls back to `npm_config_user_agent`, so the inferred
`runCommand` (`npm run X` vs `pnpm run X` vs `yarn X`) on script targets
ends up depending on whoever invoked the nx process rather than on the
workspace's actual lockfile.
2. **Module-level cache** — `let packageManagerCommand` (cleared with
`??=`) memoizes the first detection result across all subsequent calls
in the process. So even if the first call had the right `workspaceRoot`,
every later call inherits that detection regardless of *its*
`workspaceRoot`. This is also why
`packages/nx/src/plugins/package-json/create-nodes.spec.ts` had four
pre-existing snapshot failures locally (`pnpm run …` instead of the
expected `npm run …`) — the first test in any process locked detection
to the host's PM.
This is a follow-up to #35116, which moved package manager detection
into the `createNodes` callback for the inferred plugins but missed this
code path.
## Expected Behavior
- Drop the module-level cache.
- Thread `workspaceRoot` into both `detectPackageManager` and
`getPackageManagerCommand`, so the lockfile probe runs in the right
directory.
```ts
if (includedScripts.length > 0) {
const packageManagerCommand = getPackageManagerCommand(
detectPackageManager(workspaceRoot),
workspaceRoot
);
for (const script of includedScripts) {
res[script] = buildTargetFromScript(script, scripts, packageManagerCommand);
}
}
```
The `packages/nx/src/plugins/package-json/create-nodes.spec.ts` fixture
now seeds `package-lock.json` into memfs in a `beforeEach`, matching the
pattern #35116 established for plugin specs. Without the lockfile the
detector still falls back to the env var; with it, every test
deterministically picks `npm`, matching the existing snapshots.
## Verification
Before this PR: 4 failures in
`packages/nx/src/plugins/package-json/create-nodes.spec.ts`:
```
✕ should build projects from package.json files
✕ should store js package metadata
✕ should add a script target if the sibling project.json file does not exist
✕ should add a script target if the sibling project.json exists but does not have a conflicting target
Tests: 4 failed, 7 passed, 11 total
```
After this PR:
```
Tests: 11 passed, 11 total
```
## Related Issue(s)
Follow-up to #35116.
## Current Behavior
After #34946, `@nx/devkit` ships a strict `exports` map. Deep imports
like `@nx/devkit/src/utils/...` and `@nx/devkit/src/generators/...` are
no longer reachable through Node module resolution. Workspaces upgrading
to `23.x` that reference those paths break with module-resolution errors
at runtime / type-check time.
## Expected Behavior
`nx migrate` runs an automated rewrite that covers the realistic shapes
of these deep imports.
The migration walks every `.ts`/`.tsx`/`.cts`/`.mts` file in the
workspace and:
1. **Buckets named imports by symbol.** Each `@nx/devkit/src/...` import
declaration is parsed via the TypeScript compiler API (lazy-loaded with
`ensurePackage`). Specifiers in the `internal.ts` re-export list go to
`@nx/devkit/internal`; all others go to `@nx/devkit`. Mixed imports
split into two declarations.
2. **Falls back for non-named shapes.** Default imports, namespace
imports, side-effect imports, `require(...)` calls, and dynamic
`import(...)` get the specifier swapped to `@nx/devkit/internal` (the
safe default — it re-exports every previously deep-importable symbol).
3. **Collapses duplicate imports.** A second AST pass groups `import {
... } from '@nx/devkit'` and `import { ... } from '@nx/devkit/internal'`
declarations by `(specifier, isTypeOnly)`, merging each 2+ group into
one declaration with deduplicated specifiers. This handles both the
duplicates the rewrite produced and any pre-existing devkit imports the
user already had.
Edits are stacked via `applyChangesToString` (devkit's offset-tracking
text-mutation helper), then `formatFiles` normalizes formatting.
### Example
Before:
```ts
import { Tree } from '@nx/devkit';
import { dasherize, names } from '@nx/devkit/src/utils/string-utils';
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
```
After:
```ts
import { Tree, names } from '@nx/devkit';
import { dasherize, addPlugin } from '@nx/devkit/internal';
```
### Tests
29 unit tests cover: single-bucket and mixed-bucket rewrites, `as`
aliases, `import type` and inline `type` modifiers, multi-line imports,
side-effect / default / namespace fallbacks, `require()` and dynamic
`import()` fallbacks, quote-style preservation, pre-existing-import
merge, public/internal independence, value-vs-type segregation,
specifier deduplication, and a sanity test that every name in
`DEVKIT_INTERNAL_SYMBOLS` is bucketed as internal.
A user-facing `update-deep-imports.md` lives next to the implementation;
the astro-docs build picks it up via the existing
`packages/*/src/migrations/**/*.md` input glob.
## Related Issue(s)
Follow-up to #34946.
## Current Behavior
`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.
## Expected Behavior
- `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).
## Related Issue(s)
None — refactor / dependency hygiene.
## Current Behavior
Many e2e-ci projects are pinned to serial execution on each CI agent via
project-specific overrides in `.nx/workflows/dynamic-changesets.yaml`:
- `e2e-gradle`, `e2e-angular`, `e2e-node`, `e2e-react` → 1 task per
`linux-extra-large`
- `e2e-next`, `e2e-plugin` → 2 tasks per `linux-extra-large`
- `e2e-release`, `e2e-nuxt`, `e2e-web`, `e2e-eslint`, `e2e-remix`,
`e2e-cypress`, `e2e-docker`, `e2e-js`, `e2e-nx`, `e2e-nx-init`,
`e2e-dotnet`, `e2e-workspace-create`, `e2e-rollup` → 1 on large / 2 on
xlarge
These overrides exist because the underlying tests collide on hardcoded
ports when run concurrently on the same agent.
## Expected Behavior
A single rule lets every `e2e-ci**` task run with parallelism 2 on
`linux-large` and 3 on `linux-extra-large`, shrinking total agent time
and removing per-project special cases.
To make that safe, this PR:
1. Adds `reservePort()`/`reservePorts()` to `e2e/utils/port-utils.ts`.
The helper claims a port via an atomic `O_EXCL` lock file under
`/tmp/nx-e2e-port-locks`, so two parallel processes on the same agent
cannot reserve the same port. The existing `getAvailablePort()` is
deprecated — probing port 0 and then binding it seconds later opens a
TOCTOU race where another e2e task could grab the same port in between.
2. Replaces hardcoded ports in the highest-risk e2e tests with
`reservePort()`:
- `e2e/vite/src/vite.test.ts` — `serve-static` test (was 8081)
- `e2e/web/src/web-webpack.test.ts` — webpack ssl serve (was 5000)
- `e2e/storybook/src/storybook-angular.test.ts` and
`storybook-nested.test.ts` (was 4400)
- `e2e/node/src/node-server.test.ts` — express/fastify/koa/nest
framework tests (was 7000–7003) and waitUntilTargets test (was
4444/4445)
- `e2e/node/src/node-esm-support.test.ts` — 9 tests previously
defaulting to port 3000
`e2e/cypress`, `e2e/playwright`, and the React module-federation tests
still hardcode ports through their generators; those will be addressed
in follow-up PRs as CI surfaces collisions.
## Related Issue(s)
N/A — internal CI optimization.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## 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>
## Current Behavior
The `@nx/cypress:cypress` executor runs silently with no signal that
it's slated for removal. Users running `nx g @nx/cypress:configuration`
or `nx g @nx/cypress:component-configuration` on workspaces that don't
have `@nx/cypress/plugin` registered get executor-based `e2e` (or
`component-test`) targets scaffolded into their `project.json` with no
warning.
The `@nx/cypress:cypress` executor is a thin wrapper over the Cypress
CLI. The `@nx/cypress/plugin` inferred plugin produces equivalent `e2e`
and `component-test` targets directly from `cypress.config.*`, and `nx g
@nx/cypress:convert-to-inferred` is the one-shot migration tool to move
existing executor-based targets onto the inferred plugin.
## Expected Behavior
The `@nx/cypress:cypress` executor is marked as deprecated for removal
in Nx v24. Users see the deprecation message on three surfaces:
- **Runtime warning** at the top of `cypress.impl.ts` — fires every
executor invocation, no throttling.
- **Scaffold-time warning** in `configuration.ts` and
`component-configuration.ts` — fires when the generators are about to
emit an executor-based target (i.e., when `@nx/cypress/plugin` isn't in
`nx.json`).
- **Schema-root `x-deprecated`** on `executors/cypress/schema.json` —
surfaces in IDE / docs metadata.
All three messages link to the general convert-to-inferred guide at
https://nx.dev/docs/guides/tasks--caching/convert-to-inferred.
The `@nx/cypress` package itself, the `@nx/cypress/plugin` inferred
plugin, the `configuration` and `component-configuration` scaffolding
generators, and the `convert-to-inferred` migration tool all **remain
supported**. Only the `:cypress` executor is deprecated. Removal is
targeted for v24; this PR is warnings only, no behavior removal.
The PR title uses `feat!` so the deprecation surfaces in the v23
changelog / release notes; individual commits stay `chore` / `docs` /
`fix` for clean per-commit history.
### Internal usage / dogfood note
`graph/client-e2e/project.json` uses `@nx/cypress:cypress` and is
**intentionally not migrated** in this PR. Hitting the deprecation
warning on every internal CI run is the team's continuous dogfood signal
that the migration tool, the wording, and the suggested path actually
work. If it's painful for us, it's painful for users; that pressure
should drive iteration on `convert-to-inferred`. The internal project
will be migrated alongside the v24 removal PR (or earlier if the warning
becomes legitimately disruptive).
### Surfaces explicitly NOT touched
- Framework-specific cypress component-testing generators (angular /
react / next / remix) — unchanged.
- The Angular `ng-add` e2e migrator — unchanged.
- `migrations.json` — no migration entry. Runtime warnings + docs are
enough.
- `npm deprecate` — not run; the package isn't deprecated, only the
executor.
- Cypress-specific docs page — no per-plugin convert-to-inferred page
exists for any other plugin (Detox, Playwright, etc.). Pointing the
warnings at the general guide keeps that consistent and avoids one more
page to maintain.
### Test plan
- [x] `pnpm nx build cypress` passes.
- [ ] `pnpm nx test cypress` — verify no regressions from the new
warning calls.
- [ ] Verify the runtime warning fires when running `nx e2e <project>`
on a project that uses the executor (e.g., `graph/client-e2e`).
- [ ] Verify the scaffold-time warning fires on `nx g
@nx/cypress:configuration` / `:component-configuration` in a workspace
without `@nx/cypress/plugin`.
- [ ] Verify `convert-to-inferred` still works end-to-end as the
migration path.
## Related Issue(s)
Fixes NXC-4264
This PR follows the canonical executor-deprecation pattern documented in
Linear NXC-4422, established by the `@nx/detox` executor deprecation
(NXC-4272 / nrwl/nx#35529).
Related Linear context:
- **NXC-4422** — Pattern: executor deprecation (canonical reference).
- **NXC-4272** / **nrwl/nx#35529** — `@nx/detox` executor deprecation,
the reference implementation.
- **NXC-4420** — Nx core: surface schema-root `x-deprecated` on
executors at runtime. Once landed, the explicit `logger.warn` in this PR
becomes redundant.
## 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>
## 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
## Current Behavior
The `@nx/detox:build` and `@nx/detox:test` executors run silently with
no signal that they're slated for removal in a future major. Users
picking Detox via `@nx/react-native:application` or
`@nx/expo:application` (when `@nx/detox/plugin` isn't registered) get
executor-based `build-ios`, `test-ios`, `build-android`, and
`test-android` targets scaffolded into their `project.json` with no
warning.
The executors fork the Detox CLI as a child process with argv glue —
they add no behavior the CLI doesn't already provide. The
`@nx/detox/plugin` inferred plugin produces equivalent `build` / `test`
/ `start` targets directly from the user's `.detoxrc` config, and `nx g
@nx/detox:convert-to-inferred` is the one-shot migration tool to move
existing executor-based targets onto the inferred plugin.
## Expected Behavior
The `@nx/detox:build` and `@nx/detox:test` executors are marked as
deprecated for removal in Nx v24. Users see the deprecation message on
three surfaces:
- **Runtime warning** at the top of `build.impl.ts` and `test.impl.ts` —
fires every executor invocation, no throttling.
- **Scaffold-time warning** in `application/lib/add-project.ts` — fires
when the application generator is about to emit executor-based targets
(i.e., when `@nx/detox/plugin` isn't in `nx.json`). Catches both direct
`nx g @nx/detox:application` invocations and the transitive
`@nx/react-native:application` / `@nx/expo:application` paths.
- **Schema-root `x-deprecated`** on the build/test executor schemas —
surfaces in IDE / docs metadata.
All three messages link to the general convert-to-inferred guide at
https://nx.dev/docs/guides/tasks--caching/convert-to-inferred.
The `@nx/detox` package itself, the `@nx/detox/plugin` inferred plugin,
the `application` and `init` generators, and the `convert-to-inferred`
migration tool all **remain supported**. Only the two executors are
deprecated. Their actual removal is targeted for v24; this PR is
warnings only, no behavior removal. The `convert-to-inferred` generator
will be removed alongside the executors in v24 since it only exists to
migrate off them.
The PR title uses `feat!` so the deprecation surfaces in the v23
changelog / release notes; individual commits stay `chore` / `docs` /
`fix` for clean per-commit history.
### Bonus: convert-to-inferred quality fixes
While walking the deprecation flow end-to-end against a test workspace,
two issues with the existing `@nx/detox:convert-to-inferred` generator
surfaced and are fixed in this PR:
1. **Mislabeled migration log** — option-migration warnings on
`@nx/detox:test` were rendering under "Encountered the following while
migrating '@nx/expo:test'" due to a wrong `executorName` constant. Now
labeled correctly.
2. **`buildTarget` option dropped silently** — the generator was
deleting the `buildTarget` option from migrated test targets and
emitting an opaque "use --reuse" warning, leaving users without the
build-before-test chain they had before. Now the original target
reference is preserved verbatim as a `dependsOn` entry on the migrated
`test` target, so the chain keeps working post-migration with no manual
fix-up.
### Surfaces explicitly NOT touched
- `init` generator — doesn't emit executor targets, no warning needed.
- `application` and `convert-to-inferred` generator entry points —
`application`'s emission is caught at the `add-project` level instead;
`convert-to-inferred` IS the migration tool.
- `packages/react-native/.../add-e2e.ts`, `packages/expo/.../add-e2e.ts`
— transitive scaffolding falls through to `add-project.ts` which warns
at executor emission.
- `migrations.json` — no migration entry. Runtime warnings + docs are
enough.
- `npm deprecate` — not run; the package isn't deprecated, only the
executors.
- Detox-specific docs page — no per-plugin convert-to-inferred page
exists for any other plugin (Cypress, Playwright, etc.). Pointing the
warnings at the general guide keeps that consistent and avoids one more
page to maintain.
### Test plan
- [x] `pnpm nx build detox` passes.
- [x] `pnpm nx run-many -t lint -p detox react-native expo` passes.
- [ ] Verify the runtime warning fires on `nx test-ios <project>` in a
test workspace.
- [ ] Verify the scaffold-time warning fires when running `nx g
@nx/detox:application <name>` without `@nx/detox/plugin` registered.
- [ ] Verify `convert-to-inferred` end-to-end: `buildTarget` becomes
`dependsOn`; warnings label as `@nx/detox:test`.
## Related Issue(s)
Fixes NXC-4272
Related Linear context:
- **NXC-4420** — Nx core: surface schema-root `x-deprecated` on
executors at runtime. Once landed, the explicit `logger.warn` in this
PR's executors becomes redundant and can be removed.
- **NXC-4419** — `@nx/rsbuild` maintenance plan (parallel audit decided
to maintain rsbuild rather than deprecate).
- **NXC-4291** — Original `@nx/rsbuild` deprecation, canceled in favor
of the maintenance path above.
## Current Behavior
Unit tests intermittently fail with `ValueExpected` JSON parse errors
against `.nx/workspace-data/<plugin>-<hash>.hash` files, e.g.:
```
Error: ValueExpected in /home/workflows/workspace/.nx/workspace-data/jest-7930610538513362720.hash at 1:1
> 1 |
| ^
```
Each createNodes plugin (jest, next, vite, vitest, storybook, docker,
rsbuild, rspack, webpack, rollup, react-native, expo, detox, eslint,
remix, react/router, nuxt, angular) maintains its own targets cache via
a roughly identical `readTargetsCache` / `writeTargetsToCache` pair
built on `readJsonFile` / `writeJsonFile`. The write is non-atomic
(`writeFileSync`); when two callers race on the same cache path, a
reader can observe the empty truncated file mid-write, and
`readJsonFile` throws `ValueExpected`.
A few drive-by issues uncovered along the way:
- `detox` was reading from `expo-${hash}.hash` (wrong filename
copy/paste).
- `detox`, `expo`, `react-native` writes were `{ ...oldCache,
targetsCache }` without spread — storing the cache object literally
under the key `targetsCache` instead of merging entries, so on-disk
cache was effectively useless across runs.
## Expected Behavior
All createNodes plugins now use the shared `PluginCache` utility from
`@nx/devkit/internal` (already in use by Gradle and the .NET analyzer).
`PluginCache`:
- Wraps the cache read in `try/catch`, treating empty/corrupt files as a
cache miss instead of throwing — eliminates the `ValueExpected` flake.
- Wipes the cache file on write error so corruption can't persist across
runs.
- Tracks LRU access order and evicts entries when serialization hits
`RangeError`.
Per-plugin pattern change:
```ts
// before
const targetsCache = readTargetsCache(cachePath);
targetsCache[hash] ??= await buildTargets(...);
const result = targetsCache[hash];
// later:
writeTargetsToCache(cachePath, targetsCache);
```
```ts
// after
const targetsCache = new PluginCache<TargetsType>(cachePath);
if (!targetsCache.has(hash)) {
targetsCache.set(hash, await buildTargets(...));
}
const result = targetsCache.get(hash);
// later:
targetsCache.writeToDisk(cachePath);
```
Plugins migrated: `angular`, `detox`, `docker`, `eslint`, `expo`,
`jest`, `next`, `nuxt`, `react-native`, `react` (router-plugin),
`remix`, `rollup`, `rsbuild`, `rspack`, `storybook`, `vite`, `vitest`,
`webpack`.
Skipped:
- `packages/dotnet/src/utils/cache.ts` — orphaned dead code with no
callers; `dotnet/src/analyzer/analyzer-client.ts` already uses
`PluginCache`.
- `packages/js/src/plugins/typescript/plugin.ts` — already has its own
resilient read (try/catch) and atomic temp+rename write; migrating would
regress on the write side.
### On-disk cache format change
`PluginCache` stores `{ entries, accessOrder }` instead of a flat
record. Stale caches written by older versions are simply discarded on
read — perf-only impact, no correctness implication.
## Validation
`pnpm nx run-many -t test --parallel=8 --skip-nx-cache` produces the
same set of failed tasks on this branch as on `master` (24 tasks, all
pre-existing snapshot/env issues unrelated to this change). Net new
failures introduced: 0.
## Related Issue(s)
N/A — addresses observed flake during unit tests; no specific issue
tracked.
## Current Behavior
`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.
## Expected Behavior
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`
## Related Issue(s)
N/A
## Current Behavior
`@nx/devkit`'s `build-base` target overrode `outputs` in `project.json`:
```json
"outputs": [
"{projectRoot}/dist/**/*.{js,cjs,mjs,d.ts}",
"{projectRoot}/*.d.ts",
"{projectRoot}/src/**/*.d.ts"
]
```
This override is missing `tsconfig.tsbuildinfo`. Any downstream task
whose inputs include `dependentTasksOutputFiles:
"**/*.{d.ts,d.cts,d.mts,tsbuildinfo}"` (e.g. `docker:build-base`) trips
a sandbox violation: when `tsc --build` walks project references it
reads devkit's tsbuildinfo, but Nx never registered that file as a dep
output, so the read isn't covered by any declared input.
The same gap exists in the `dist-build-migration` Claude skill, so every
package migrated with that playbook would reproduce the violation.
Sandbox report that surfaced this:
https://staging.nx.app/runs/HNBpzgdeNi/task/docker%3Abuild-base/sandbox-report-raw?sandboxReportId=10b903d8-b860-41c7-80b2-756b0541e690
## Expected Behavior
Drop the override entirely. The `@nx/js/typescript` plugin already reads
`outDir` and `tsBuildInfoFile` from `tsconfig.lib.json` and infers a
strictly more complete set of outputs:
```
{projectRoot}/dist/**/*.{js,cjs,mjs,jsx,json,d.ts,d.cts,d.mts}{,.map}
{projectRoot}/dist/tsconfig.tsbuildinfo
```
The inferred set picks up the tsbuildinfo plus
`.cjs`/`.mjs`/`.json`/`.d.cts`/`.d.mts`/`.map` that the manual override
was missing. Verified the violation is resolved:
```
$ pnpm nx show target inputs docker:build-base --check packages/devkit/dist/tsconfig.tsbuildinfo
✓ packages/devkit/dist/tsconfig.tsbuildinfo is an input for docker:build-base (depOutputs)
```
The `dist-build-migration` skill is updated to tell future migrations to
leave `build-base.outputs` to the plugin.
## Related Issue(s)
Follow-up to #34946.
## Current Behavior
`@nx/devkit` builds to `<workspaceRoot>/dist/packages/devkit/` — outside
its own package directory. Other packages reach into that shared
workspace `dist/` to consume devkit, and consumers using
`moduleResolution: nodenext` need a custom Module._resolveFilename hack
to find it.
This is inconsistent with `nx` itself, which already builds to
`packages/nx/dist/` (#34111).
## Expected Behavior
`@nx/devkit` builds to `packages/devkit/dist/` and is consumed via a
standard `exports` map. Workspace consumers resolve through normal pnpm
symlinks; the `@nx/nx-source` condition still lets in-repo code resolve
to `.ts` source during dev.
This unblocks the rest of the workspace from migrating in the same
direction (a `dist-build-migration` Claude skill is included as a
per-package playbook).
## How to review
The PR has **307 files but only 3 categories of work**. Most of the diff
is mechanical.
### 1. The actual migration — review carefully (~10 files)
Devkit package config and the `@nx/nx-source` condition wiring:
- `packages/devkit/package.json` — `exports` map, `typesVersions`,
`files`, `type: commonjs`, `main`/`types` repointed to `dist/`
- `packages/devkit/tsconfig.lib.json` — `outDir: dist`, `nodenext`
module/resolution
- `packages/devkit/project.json` — `build-base` outputs,
`nx-release-publish.packageRoot`, `manifestRootsToUpdate`
- `packages/devkit/internal.ts` — re-exports `src/utils/*` and
`src/generators/*` symbols (replaces the `@nx/devkit/src/*` deep-import
pattern)
- `packages/devkit/eslint.config.mjs` — ignore `dist`
- `packages/devkit/{README.md → readme-template.md}` — README is now
generated, gitignored
- `.gitignore` — ignore the generated `packages/devkit/README.md`
- `scripts/nx-release.ts` — devkit's `package.json` now lives at
`packages/devkit/package.json`, not `dist/packages/devkit/package.json`
- `scripts/patched-jest-resolver.js` — adds `@nx/nx-source` condition
### 2. Mass import rewrites — already marked viewed (~205 files)
Every internal import of `@nx/devkit/src/utils/<x>` or
`@nx/devkit/src/generators/<x>` was rewritten to `@nx/devkit/internal`:
```diff
-import { foo } from '@nx/devkit/src/utils/bar';
+import { foo } from '@nx/devkit/internal';
```
I marked these files as **viewed** in the GitHub review UI to clear them
from the unread queue. They're across nearly every plugin (angular,
react, next, vite, webpack, rspack, expo, jest, etc.).
### 3. Follow-on cleanups (~10 files)
These appear in their own commits and are easy to review individually:
- **`cleanup(angular-rspack)`** — removes the `patchDevkitRequestPath`
runtime hack from 14 example configs; devkit now resolves through
standard node_modules (the MF patch stays — module-federation isn't
migrated yet).
- **`cleanup(devkit)` typedoc** — drops dead path mappings + redundant
include manipulation in
`astro-docs/src/plugins/utils/typedoc/typedoc.ts` (verified docs build
is byte-identical with/without the removed config).
- **`cleanup(devkit)` eslint ignores** — drops `'**/*.d.ts'` from
`packages/devkit/eslint.config.mjs` since `.d.ts` files only emit to
`dist/` (already ignored).
- **`fix(angular)` eslint quote** — quote-agnostic regex in
`e2e/angular/src/projects-linting.test.ts` so the test still disables
`prefer-standalone` after the angular-eslint generator switched to
double quotes (latent bug surfaced by CI).
- **`fix(testing)` jest migration** — removes a stray unused
`@nx/devkit/internal` import.
- **e2e test fallout** — `e2e/{angular,next}/src/*.test.ts` lose access
to `@nx/devkit/src/utils/string-utils` (e2e tests can't use
`/internal`); inline equivalents using public `names()` API.
`e2e/nx-build/src/nx-build.test.ts` updates the expected output path.
## Local verification
- `pnpm nx run-many -t test,build,lint -p devkit` ✓
- `pnpm nx run astro-docs:build` ✓ — devkit reference pages still
generate (148 markdown files; identical to a build with the path
mappings re-added as a control)
- `pnpm nx run-many -t build -p
examples-angular-rspack-csr-css,examples-angular-rspack-ssr-css,examples-angular-rspack-zoneless-csr-css,examples-angular-rspack-mf-host,examples-angular-rspack-mf-remote
--skip-nx-cache` ✓ — examples build without the devkit patch
## Known follow-up (not in this PR)
- `scripts/nx-release.ts` still has `hackFixForDevkitPeerDependencies()`
(a band-aid from #32406 that re-adds `<=` to devkit's `nx` peer-dep
range after `nx release version` strips it). The proper fix is to set
`preserveMatchingDependencyRanges: true` in
`packages/devkit/project.json` and delete the hack — that needs a
release dry-run to verify, so it's queued separately.
## Related Issue(s)
Follow-up to #34111.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## 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)
## 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.
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>
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
## 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.
## 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)
## Current Behavior
The `@nx/angular:move` generator is exposed as a deprecated thin wrapper
that delegates to `@nx/workspace:move`. It was deprecated in Nx v18.
## Expected Behavior
The `@nx/angular:move` generator is removed. Consumers must use
`@nx/workspace:move` (or its `mv` alias) directly. Angular-specific
post-move logic (module/class renames, `ng-package.json` `dest`
recomputation, secondary entry-point README updates) is preserved:
`@nx/workspace:move` continues to invoke it via the internal `move-impl`
plugin export, which remains in the package.
`@nx/workspace` is dropped from `@nx/angular`'s production dependencies,
since the wrapper was its sole consumer.
The Angular e2e suite (`e2e/angular/src/misc.test.ts`) now exercises
`@nx/workspace:move` against Angular apps and libraries directly.
## Implementation notes
- Deleted:
`packages/angular/src/generators/move/{move.ts,schema.json,schema.d.ts}`
and the `"move"` entry in `generators.json` / corresponding export in
`generators.ts`.
- Kept: `packages/angular/src/generators/move/move-impl.ts` and `lib/`,
plus the `./src/generators/move/move-impl` package export — these are
still loaded at runtime by
`@nx/workspace/src/generators/move/lib/run-angular-plugin.ts`.
- The unit spec was renamed `move.spec.ts` → `move-impl.spec.ts` and
refactored to call `move-impl` directly via a small helper that mimics
the file/config moves `@nx/workspace:move` performs before invoking the
plugin. Two assertions that exercised `@nx/workspace:move`'s import-path
rewriting (not move-impl's job) were narrowed to the class-rename
behavior the plugin actually performs.
BREAKING CHANGE: The `@nx/angular:move` generator is removed. Use
`@nx/workspace:move` (or its `mv` alias) instead.
## Current Behavior
- During project graph normalization, dependsOn entries whose target
doesn't exist are left in place, which means downstream consumers have
to re-validate against the real target set every time.
- `nx show target` resolves the `Depends On` list using a heuristic on
raw `dependsOn` configs. That heuristic doesn't fully match what
`createTaskGraph` would schedule — e.g. a `{ projects: ['lib-a'] }`
entry is listed even if `lib-a` doesn't actually have the referenced
target, and there's no indication of what transitively runs when the
target is scheduled.
## Expected Behavior
### 1. Normalize-project-nodes drops broken same-project dependsOn
entries
A new `normalizeTargetsDependsOn` pass runs inside
`normalizeProjectNodes` after target defaults have been merged. It
filters:
- Bare-string entries (e.g. `"prebuild"`) when the referenced target
doesn't exist on the same project.
- Object entries with no `projects`, `projects: 'self'`, or `projects:
'{self}'` when the referenced target doesn't exist on the same project.
Cross-project entries are intentionally left alone — `"^target"`, `{
target: 'X', dependencies: true }`, and `{ projects: [...] }` need the
real project graph (and in some cases the dep edges) to validate, so
they're resolved later during task graph creation.
### 2. `nx show target` is task-graph-driven
`showTargetInfoHandler` now builds a task graph rooted at the requested
target via `createTaskGraph`. The `Depends On` list reads direct task
dependencies off the graph, giving the same filtering and resolution
behavior as `nx run`:
- Non-existent targets disappear from the list.
- `^target` expands to real dependency-project task IDs.
- `{ projects: [...] }` entries drop projects that don't actually have
the target.
A new `transientTasks` field on the JSON output surfaces everything that
runs transitively. The text renderer shows a summary line beneath
`Depends On`:
```
Depends On:
lib-a:build
and 5 build, compile transient tasks
```
With >3 unique target names, the summary collapses to a bare count (`and
12 transient tasks`) to stay scannable. Source-map hints for `--verbose`
are preserved — each direct dep task ID is matched back to its
originating `depConfig` entry.
## Related Issue(s)
None — proactive correctness + observability improvement.
## 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>
## Current Behavior
#34425 ("remove redundant `allWorkspaceFiles` from the project graph
pipeline") dropped `allWorkspaceFiles` from the `WorkspaceFileMap`
interface without a deprecation cycle.
Plugins that import `createFileMapUsingProjectGraph` from the deep path
`nx/src/project-graph/file-map-utils` — e.g. `@nx/owners` — fail to
type-check against the beta release:
```
src/plugin/plugin.ts(73,20): error TS2339: Property 'allWorkspaceFiles' does not exist on type 'WorkspaceFileMap'.
src/plugin/plugin.spec.ts(799,5): error TS2353: Object literal may only specify known properties, and 'allWorkspaceFiles' does not exist in type 'WorkspaceFileMap'.
```
The path is technically internal (not exported from `@nx/devkit`), but
real consumers (including cached nx-cloud workers — see #35502) reach in
there, and #35502 already established the pattern of restoring removed
members as `@deprecated` shims rather than breaking them outright.
## Expected Behavior
`allWorkspaceFiles` is restored on `WorkspaceFileMap` as `@deprecated`
and exposed from `createFileMap` via a **non-enumerable lazy getter**:
- **Lazy** — derived from `fileMap.projectFileMap` +
`fileMap.nonProjectFiles` only when read. Zero allocation for callers
that don't touch it.
- **Non-enumerable** — stays out of `JSON.stringify` / `Object.keys`, so
the daemon-serialization wins from #34425 are preserved if a
`WorkspaceFileMap` ever ends up on the wire.
- **Optional** — daemon-internal call sites (`updateFileMap`) that never
set the field still satisfy the type.
The hot-path improvements from #34425 stay intact:
- No `allWorkspaceFiles` on `SerializedProjectGraph` going across the
daemon wire
- No `copyFileData()` on every graph serialization
- No `buildAllWorkspaceFiles()` on incremental updates
- No `storedAllWorkspaceFiles` retained in `build-project-graph` state
- `updateFileMap` return value unchanged
The shim only sits at the plugin boundary —
`createFileMapUsingProjectGraph`, which runs in the plugin's process,
not the daemon.
Same back-compat pattern as #35502 (`hydrateFileMap` 3-arg overload).
Both can be removed in a future major once known consumers (ocean,
cached cloud workers) age out.
## Related Issue(s)
<!-- Reported via internal channels (ocean) — no public issue. -->
## 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)
## 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 #
## 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.
## Current Behavior
On macOS, `transform_event_to_watch_events` infers Create vs Update from
inode timestamps because FSEvents reports a Create flag for in-place
updates of recently-active paths. The check compared `st_mtime` and
`st_birthtime` at **whole-second** precision:
```rust
if t.st_mtime() == t.st_birthtime() {
EventType::create
} else {
EventType::update
}
```
When an in-place update lands in the same wall-clock second as the
file's creation, both timestamps round to the same value and the event
is mis-classified as `Create`. This:
- Flakes `native::watch::watcher::tests::plain_update_yields_update`
~70% of the time (writes v1, sleeps 150ms, writes v2 — both timestamps
in the same second on most runs).
- Mislabels real `nx watch` events when a developer or tool edits a file
shortly after it's created/checked-out.
Strict ns-precision equality isn't viable either: `fs::write` is
`open(O_CREAT)` (stamps birthtime) followed by a separate `write`
syscall (stamps mtime) ~100µs later, so every fresh write would
mis-classify as Update.
## Expected Behavior
Same-second in-place updates classify as `Update`; fresh writes still
classify as `Create`.
The fix compares `Metadata::modified()` and `Metadata::created()`
(`SystemTime`, ns precision) with a 50ms tolerance:
- Below the threshold → `Create`. Covers the kernel's ~100µs
`O_CREAT`→`write` gap with comfortable headroom for syscall jitter.
- Above the threshold → `Update`. Real edits separated from creation by
anything more than tens of ms classify correctly.
The watcher's accumulator merges events within `IDLE_WINDOW` (100ms) and
the `(Create, Update) → Create` rule keeps the first state, so per-event
labels inside the tolerance window don't reach consumers — only events
in separate bursts do, and those are reliably outside the window.
### Verification
- 10/10 stress runs of `npx nx test-native nx --skip-nx-cache
--no-cloud` no longer fail `plain_update_yields_update` (was 7/10 fail
rate before).
- All other 351 native tests pass on every run.
- Empirical kernel-timing probe on macOS confirmed: fresh `fs::write`
produces `mtime - birthtime ≈ 100µs`; in-place update 150ms after
creation produces `mtime - birthtime ≈ 152ms` — comfortably on either
side of the 50ms cutoff.
## 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.
## Current Behavior
The `@nx/angular/module-federation` entry point is exposed as a
deprecated re-export of `withModuleFederation` and
`withModuleFederationForSSR` from `@nx/module-federation/angular`. It
was deprecated in Nx v20.2.
## Expected Behavior
The `@nx/angular/module-federation` entry point is removed. Consumers
must import from `@nx/module-federation/angular` directly. A migration
(`update-23-0-0-update-with-module-federation-import`) rewrites existing
imports/requires in webpack configs of projects depending on
`@nx/angular` and adds `@nx/module-federation` to `package.json`.
The `@nx/angular:convert-to-with-mf` generator now installs
`@nx/module-federation` since the webpack config it emits imports from
that package.
The webpack guides (`webpack-config-setup`, `webpack-plugins`) were
updated to reflect the recommended import paths and to fix broken
Angular MF snippets that wrapped `withModuleFederation` with
`composePlugins` (not exported from `@nx/module-federation/angular`).
BREAKING CHANGE: The `@nx/angular/module-federation` entry point is
removed. Update imports to use `@nx/module-federation/angular` instead.
The `update-23-0-0-update-with-module-federation-import` migration
handles this automatically when running `nx migrate`.
## Current Behavior
The `@nx/angular` package contains deprecated functions and stale
documentation that are no longer needed.
## Expected Behavior
- Remove dead `extendAngularEslintJson` and `createEsLintConfiguration`
functions (were not exported or used internally)
- Clean up stale deprecation notice in Cypress component testing docs
(referenced Nx 18 removal which has already passed)
## 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.
## 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
## Current Behavior
`nx@23.0.0-beta.2` Nx Cloud V4 distributed-agent workers crash on every
task with:
```
Failed to get external value
at new NativeTaskHasherImpl (.../native-task-hasher-impl.js:25:23)
at new InProcessTaskHasher (.../task-hasher.js:68:27)
at createTaskHasher (.../create-task-hasher.js:13:16)
at createOrchestrator (.../init-tasks-runner.js:86:60)
at runDiscreteTasks (.../init-tasks-runner.js:114:32)
at executeAndStoreTask (.../discrete-task-worker.js:1:832861)
```
#34425 ("remove redundant `allWorkspaceFiles` from the project graph
pipeline") changed two helpers in
`packages/nx/src/project-graph/build-project-graph.ts`:
- `hydrateFileMap(fileMap, allWorkspaceFiles, rustReferences)` →
`hydrateFileMap(fileMap, rustReferences)`
- `getFileMap()` no longer returns `allWorkspaceFiles`
Cached Nx Cloud V4 workers (e.g.
`.nx/cache/cloud/2604.29.7/lib/core/runners/distributed-agent/v4/discrete-task-worker.js`)
`require('nx/src/project-graph/build-project-graph')` directly and still
call the 3-arg form:
```js
hydrateFileMap(
{ projectFileMap, nonProjectFiles },
allWorkspaceFiles, // lands in rustReferences slot on beta.2
rustReferences // silently dropped
);
```
The `FileData[]` array poisons `storedRustReferences`. Later
`createTaskHasher` reads `.projectFiles` / `.allWorkspaceFiles` off the
array (both `undefined`), passes them to `new TaskHasher(...)`, and
napi-rs throws `"Failed to get external value"` trying to coerce
`undefined` into `&External<Arc<…>>`.
## Expected Behavior
`hydrateFileMap` accepts both the new 2-arg shape and the legacy 3-arg
shape, detected by `Array.isArray()` on the 2nd argument. `getFileMap()`
re-exposes `allWorkspaceFiles: []` so cached workers that destructure it
(for telemetry / `v4log`) see the property instead of `undefined`. Both
surfaces are flagged `@deprecated` so we can remove them in a later
major once cached V4 workers age out.
A regression test pins both arities — verified red on the pre-fix code
and green with the fix.
## Related Issue(s)
<!-- Reported via internal channels (ocean) — no public issue. -->
## 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.
## 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`.
## 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>
## 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.
<!-- 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 #
## Current Behavior
This branch started as a fix for a flaky `e2e/nx/src/watch.test.ts`.
Once we flipped `NX_DAEMON='true'` in the e2e harness, several
long-latent daemon issues surfaced; they're fixed in the same PR. The
most user-visible regression — `nx release version` silently dropping
projects from a fixed release group after `git checkout` — is the last
item below and is what
`e2e/release/src/multiple-release-branches.test.ts` exercises.
**Native watcher.** `nx watch` dropped and duplicated events on
Linux/Windows:
1. `watchexec.config.pathset()` registered new directories
asynchronously — files written before the real inotify registration were
lost.
2. Every raw `notify` event fired a callback (no debounce) so one
logical write produced multiple `nx watch` invocations.
3. The daemon called `notifyFileWatcherSockets` twice per event (eagerly
for updates/deletes, again post-recompute for creates), doubling
invocations.
4. `getProjectsAndGlobalChanges` looked changed files up in
`fileMap.projectFileMap`, so brand-new files fell through to
`globalFiles` and `--projects=foo` watchers never matched.
5. `nx:test-native` didn't compile on master (walker test used
`nix::unistd::mkfifo` behind a feature that wasn't enabled).
6. macOS regressed after the notify migration: `FsEventWatcher` dropped
immediately because the closure had no cfg-active references on darwin.
**Daemon (surfaced once it ran end-to-end in CI):**
7. `getPluginsSeparated` reused `pendingPluginsPromise` via `??=` after
the plugins-config hash changed, serving stale plugins forever after `nx
add`.
8. The auto-recompute path updated the in-memory graph but never
persisted it; subprocesses reading the disk cache saw stale data (flaky
`eslint dependency-checks`).
9. `startInBackground` had an fd race: a concurrent `reset()` could null
`this._out`/`this._err` while we awaited `open()`, crashing the spawn
with `Cannot read properties of null (reading 'fd')`.
10. The daemon's env-reflection was additive only — `NX_*` vars set by
one CLI invocation persisted forever, leaking (e.g.,
`NX_PREFER_NODE_STRIP_TYPES=true` from a single `nx report` poisoned
every later plugin load). Plugin worker's `setWorkerEnv` had the same
bug.
11. Several e2e cache-hit assertions matched `[local cache]`, but the
daemon-on path emits `[existing outputs match the cache, left as is]`
(output kept rather than restored).
**Daemon graph cache (more flake hunting after the watcher rewrite
landed):**
12. `resetInternalStateIfNxDepsMissing` ran before every
`getCachedSerializedProjectGraphPromise` and reset state whenever
`project-graph.json` was missing AND a cached promise existed — but on
cold start, *every* request before the first persist matches that
condition. It was firing constantly, tearing down in-flight first
computes and forcing a redundant recompute. Worse, its `catch` branch
unconditionally reset on any `fileExists` error.
13. When `resetInternalState` fires from inside
`processCollectedUpdatedAndDeletedFiles`'s catch (e.g.,
`retrieveWorkspaceFiles` throws on partial disk state),
`cachedSerializedProjectGraphPromise` is set to `undefined`. A
concurrent stale compute that hits `chainToLatest` after the reset would
read back `undefined`; the `if (stale) return stale` guard treats
`undefined` as falsy, so the stale compute falls through and commits its
(now-stale) data to module state.
**Force-flush concurrency (flagged by Graphite review):**
14. The processing thread dropped any extra `ForceFlush` messages it
found while draining the channel (the `ProcMsg::ForceFlush(_) => { /*
drop the extras */ }` arm). Their reply senders were silently discarded;
those callers waited the full 500 ms `recv_timeout` and returned an
empty Vec. Concurrent CLI requests would each see that latency hit.
**Watcher event misclassification (the `nx release version` failure on
`multiple-release-branches.test.ts`):**
15. `git checkout` does `unlink + write` on some tracked files. inotify
fires `IN_DELETE` then `IN_CREATE` for the same path; both landed in the
watcher's accumulator within one burst.
16. `merge_event`'s priority rule was *Delete > Create > Update* —
meaning a `Create` arriving after a `Delete` for the same path was
silently dropped. The accumulator emitted only the Delete.
17. Downstream `updateFilesInContext` on the JS side then *removed* the
(still-existing) file from the Rust workspace context's index.
`multiGlobWithWorkspaceContext` no longer returned it. Plugins didn't
process it. The project was missing from the resulting graph, with no
error.
18. `release version` then evaluated `isProjectPublic`, which checks for
the project's `package.json` in the file map. With the file gone from
the index, the check returned `false`, the project was excluded as
not-public, and the release group ran with one fewer project than
expected.
19. Two related classification bugs surfaced during the diagnosis: (a)
the early-return for `!meta_exists` emitted Delete *unconditionally*,
even for `Modify`/`Create` events whose stat happened to fail during a
transient atomic-rename window; (b) notify-rs delivers a third coalesced
rename event (`Modify(Name(Both))`) that fell through to the generic
`Modify(_) → Update` arm, overriding the per-side `From` Delete on the
source path of an `fs::rename`.
**Maven (separate but bundled):**
20. Tests asserted on `BUILD SUCCESS` in batch mode, but the resident
batch runner's `BatchExecutionListener.sessionEnded` is a no-op —
Maven's footer is replaced by `Nx Maven Summary`. Those assertions could
never match.
## Solution
### Native watcher
#### Architecture
```
Before: Watcher → watchexec (async config loop) → notify → inotify/FSEvents
After: Watcher → notify → inotify/FSEvents (direct, synchronous)
```
The watcher is one Rust struct
(`packages/nx/src/native/watch/watcher.rs`) that owns:
- a `notify::RecommendedWatcher` (the OS-level subscription),
- a single `mpsc::Sender<ProcMsg>` shared by the notify event handler
and the napi force-flush method,
- a long-running **processing thread** that reads from the corresponding
`Receiver`.
#### How events are streamed
```
notify (OS) ─► NotifyForwarder ─┐
├─► mpsc<ProcMsg> ─► processing thread ─► napi tsfn ─► JS callback
JS force_flush_pending() ───────┘ (or sync_channel reply)
```
1. **Notify side.** `notify::recommended_watcher(NotifyForwarder { tx
})` hands every fs event to `NotifyForwarder::handle_event`, which wraps
it as `ProcMsg::Notify(event)` and pushes it onto the channel. No work
is done on the notify thread beyond the send.
2. **Processing thread.** A dedicated thread runs an infinite loop with
three branches:
- `ProcMsg::Notify(event)` — filter through gitignore/nxignore
(`watch_filterer.rs`), run the new-directory fast path on Linux/Windows
if the event is a `Create(Folder)`, transform notify's kinds into our
`EventType` (Create/Update/Delete), and merge each resulting
`WatchEventInternal` into a per-path `HashMap` accumulator. The merge
rules are:
- `(_, Delete)` → Delete (file is gone, final state wins)
- `(_, Create)` → Create (file is in its initial state from our
perspective; overrides earlier Update or Delete — the regression case
15-18 above)
- `(Delete, Update)` → Update (file came back with new content)
- `(Create, Update)` / `(Update, Update)` → keep existing (Create wins
on Linux's IN_CREATE+IN_MODIFY pair)
- `ProcMsg::ForceFlush(reply)` — drain whatever notify has buffered
(`while let Ok(msg) = rx.try_recv()` so anything in flight at the moment
of the request is included), **collect every queued ForceFlush reply
channel encountered during the drain**, snapshot the accumulator, and
send the same snapshot to all collected callers (closes#14 above). Used
by the daemon to absorb pending events before serving a cached project
graph (closes the IDLE_WINDOW race where a 99 ms-old event would
otherwise miss the next read).
- `Err(RecvTimeoutError::Timeout)` — the wake-up deadline elapsed; emit
the accumulator to JS via the napi `ThreadsafeFunction`
(`callback_tsfn.call(Ok(events), NonBlocking)`), clear it, and reset.
3. **Trailing-edge debounce.** Each `Notify` ingest updates a single
`flush_deadline = min(now + IDLE_WINDOW, burst_start + MAX_WAIT)`:
- `IDLE_WINDOW = 100 ms` — flush when the channel goes quiet for this
long. Resets on every arriving event, so any burst with gaps <100 ms
coalesces into one flush.
- `MAX_WAIT = 500 ms` — starvation cap from the first event of the
burst.
- The loop's `rx.recv_timeout(wait)` either picks up the next message or
returns `Timeout` exactly at `flush_deadline`. No separate timers, no
cross-flush state, no `recent_paths` book-keeping.
- When idle (`flush_deadline = None`) the loop polls on `SHUTDOWN_POLL`
so `stop_flag` is observed promptly.
4. **napi tsfn handoff.** The processing thread invokes
`callback_tsfn.call(Ok(Vec<WatchEvent>), NonBlocking)` to schedule the
JS callback on Node's main thread. The notify thread itself never
crosses into JS — only the processing thread does, and only at flush
time. `Watcher::watch` is now a thin wrapper around an internal
`watch_inner` that takes a generic callback, so unit tests can exercise
the same loop without a JS runtime.
5. **`force_flush_pending` (synchronous drain).** The daemon calls
`watcher.force_flush_pending()` from JS before reading the cached
project graph. JS-side napi method creates a
`sync_channel::<Vec<WatchEvent>>(1)` reply pair and sends
`ProcMsg::ForceFlush(reply_tx)` on the same channel notify events flow
through. The processing thread sees the request in order with any
buffered notify events, drains, takes the accumulator, and replies.
Concurrent callers all receive the same snapshot (closes#14).
#### Event classification (`types.rs::transform_event_to_watch_events`)
- A failed stat (`!meta_exists(metadata)`) only short-circuits to
`EventType::Delete` when the notify event_kind is actually `Remove(_)`.
For `Modify`/`Create` events whose stat happened to fail during a
transient atomic-rename window, we fall through to the platform-specific
path which derives the type from `event_kind` alone (closes #19a).
- `Modify(Name(RenameMode::Both | Any))` — the coalesced rename event
notify-rs delivers in addition to per-side `From`/`To` events — is now
skipped, so it can't override the `From` Delete on the source path of a
rename (closes #19b).
#### New-directory fast path (Linux/Windows)
When notify reports a `Create(Folder)` on inotify/ReadDirectoryChangesW
(which only watch the dirs they were given), the processing thread:
1. Calls `watcher.watch(new_dir, NonRecursive)` **synchronously** — the
OS watch is active the moment this returns.
2. Re-walks the new directory and merges every existing entry into the
accumulator with `EventType::Create`.
3. Recursively registers any subdirectories it found (so a `mkdir -p
a/b/c/d` still hooks every level).
Because (1) is synchronous, files written between `mkdir` and the
`watch()` call are caught by the re-walk in (2). Notify events for those
same files arriving later just merge into the same accumulator entry —
no duplication. macOS doesn't need this path: `FsEventWatcher` is
recursive.
`notify_watcher` is held as `Option<Arc<Mutex<RecommendedWatcher>>>` on
the struct so the processing-thread closure can clone the `Arc`;
otherwise the macOS `move`-closure (which has no cfg-active references)
wouldn't capture the watcher and `FsEventWatcher` would drop the moment
`watch()` returned.
### Daemon
- **Plugin reload on config change.** `getPluginsSeparated` clears
`pendingPluginsPromise` and tears down workers when `nx.json#plugins`'s
hash changes.
- **Persist project graph after auto-recompute.** New
`persistProjectGraphToDisk` helper called from `kickOffRecompute` and
`getCachedSerializedProjectGraphPromise`.
- **fd race fix.** Open log handles into locals first, assign to
`this._out`/`this._err` after both opens resolve, pass the local `fd`s
to `spawn`.
- **Two-way `NX_*` env reflection.** New
`applyDaemonEnvFromClient(newEnv)` helper writes new keys AND deletes
any `NX_`-prefixed key the daemon has that the client doesn't (skipping
daemon-side exclusions and required settings).
- **`scheduleTimeoutId` → in-flight promise** for self-documenting
recompute scheduling.
- **Single `notifyFileWatcherSockets` registration** at daemon startup;
one notification per batch.
- **Map new files by project root.** `getProjectsAndGlobalChanges` uses
`createProjectRootMappings` + `findProjectForPath`, cached by reference
identity on `currentProjectGraph`.
- **Cache invalidation gated on first persist** (closes#12). New
`cacheHasBeenPersisted` flag set in `persistProjectGraphToDisk`.
`resetInternalStateIfNxDepsMissing` returns early if the flag is false —
before the first successful write, a missing `project-graph.json` is the
*expected* state. Its `catch` branch no longer auto-resets on transient
stat errors.
- **`chainToLatest` defensive kickOff** (closes#13). When a stale
compute hits `chainToLatest` and finds
`cachedSerializedProjectGraphPromise === undefined` (because
`resetInternalState` ran), it now kicks off a successor synchronously
and returns that promise instead of `undefined`. Prevents the "stale
compute commits stale data" path that the falsy guard let through.
### Test suite
- `NX_DAEMON='true'` is the default in `e2e/utils/command-utils.ts` so
CI exercises the daemon path.
- Cache-hit assertions updated to `[existing outputs match the cache,
left as is]` for the daemon-on no-op restore path. One assertion in
`cache.test.ts:216` reverted to `[local cache]` because that test adds
an extra file to `dist/`, invalidating the outputs hash and forcing a
real restore.
- Maven batch tests assert on `Successfully ran target X` instead of
`BUILD SUCCESS`. `verbose: true` dropped where it only added Maven `-X`
debug noise.
- Watch e2e harness uses `tree-kill` and waits for `close` before
reading output. Default wait shortened from 6s/8s → 1s/2s now that
debounce is deterministic.
- Walker test uses `std::os::unix::net::UnixListener::bind` instead of
`nix::unistd::mkfifo` — same `is_hashable_file` rejection, no `nix`
feature flag needed.
- **10 Rust watcher tests** (`packages/nx/src/native/watch/watcher.rs`)
drive `Watcher::watch_inner` end-to-end with real fs ops on a tempdir —
inotify/FSEvents → notify-rs → ProcMsg channel → EventIngestor →
accumulator → callback. Cover: git-style unlink+write, vim-style atomic
rename, plain in-place update, fresh create, rm, create+rm, cross-name
rename, multi-file burst coalescing, hardcoded-ignored paths never reach
callback, and 8-way concurrent `force_flush_pending` (regression for
#14). Tests use canonicalized tempdir paths so the synthetic-gitignore
filter scopes correctly on macOS where `/tmp` symlinks to
`/private/tmp`.
## Testing
- `pnpm nx run e2e-nx:e2e-ci--src/watch.test.ts --skip-nx-cache` — 8/8
passed.
- `pnpm nx run e2e-js:e2e-ci--src/js-strip-types.test.ts` — 3/3 (was
failing on test 3 before the env-reflection fix).
- `pnpm nx run e2e-maven:e2e-ci--src/maven-batch.test.ts` — 3/3 (was
failing on test 1 + 3 before the assertion fix).
- `pnpm nx run
e2e-release:e2e-ci--src/multiple-release-branches.test.ts` — 2/2 (was
failing both before the merge-event fix; reproduced locally and
confirmed via the daemon log dump that `git checkout` was emitting a
stray Delete for one of the package.json files).
- `pnpm nx run nx:test-native` — 349 tests pass, including the 10 new
watcher tests on Linux and macOS.
- macOS path validated end-to-end with the Arc fix.
## Related Issue(s)
N/A — flaky-test investigation that grew into a daemon-stability
hardening pass.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## 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
## Current Behavior
`graph-client:build-client` triggers 21 sandbox-read violations on
staging. Webpack snapshots context dirs registered by postcss-loader via
tailwind's `dir-dependency` emission, so it hashes `*.stories.*` and
`*.spec.*` siblings of declared content (in graph-client and its
workspace deps) even though those files never enter the bundle and are
intentionally excluded from inputs.
The reads are structural to how postcss-loader translates tailwind's
`dir-dependency` into webpack's `addContextDependency(dir)` — webpack
snapshots and hashes the entire directory regardless of which entries
the glob actually matches. They happen with **any** tailwind content
glob that resolves to a directory, not because of the specific patterns
used.
Separately, this repo's tailwind configs use broad globs (or the buggy
`*!(...)` extglob form) that scan stories and specs alongside production
code — a correctness issue independent of the sandbox violations.
## Expected Behavior
The sandbox report comes back clean for `graph-client:build-client`.
Tailwind content scanning excludes stories and specs.
## Validation
This run shows all violations are gone (except the one for the root
`tsconfig.json` file, which will be addressed by [a separate
PR](https://github.com/nrwl/nx/pull/35477)):
https://staging.nx.app/runs/0W9AQdqnyQ/task/graph-client%3Abuild-client%3Arelease?batchId=635272b4-2d1b-4b5b-89df-ebac5e1f04a8
## Changes
- Add a `graph-client:build-client` task exclusion in
`.nx/workflows/sandboxing-config.yaml` for the stories/specs reads.
**This is what resolves the sandbox violations.** The files remain on
disk but aren't correctness-relevant: they're excluded from inputs by
the `production` named-input set, never enter the bundle, and only exist
in the snapshot because webpack hashes the parent dir.
- Fix tailwind content globs in
`graph/{client,migrate,ui-code-block,ui-project-details,ui-render-config}/tailwind.config.js`
and `nx-dev/nx-dev/tailwind.config.js` to actually exclude `*.stories.*`
and `*.spec.*`. **This is independent of the sandbox fix** — the file
reads still happen at the OS level; this just stops tailwind from
scanning the content of those files when computing classes for the
production CSS.
## 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.
When a `targetDefaults` entry keyed by target name carries an executor
that differs from an inferred (specified-plugin) target's executor, the
synthetic plugin built by `createTargetDefaultsResults` would be merged
on top of the specified target. The downstream "incompatible executor"
branch in `mergeTargetConfigurations` then dropped the specified
target's options/configurations and let the synthetic's executor win,
silently replacing the inferred command with the unrelated default
executor.
Skip synthesis in the specified-only branch when the resolved default is
incompatible with the specified target. The default was authored for a
different executor and shouldn't apply.
Surfaces in polyglot workspaces — e.g. a `targetDefaults['test-native']`
configured for `@monodon/rust:test` was overriding a dotnet plugin's
inferred `test-native` (`command: 'dotnet test'`), causing CI to run
`cargo test` against C# projects.
## 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>
## 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.
## 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 #
## 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.
## 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
This PR removes Less, Tailwind, and CSS-in-JS style options from React
and Vue generators.
Moving forward, it is preferable to use either CSS or SCSS, and set up
other options manually or with AI. The CSS-in-JS solutions have fallen
out of favor. Tailwind and shadcn are more likely to be used by AI, and
neither needs our generator support.
For anyone who depends on these removed options, they can wrap their own
generators on top of ours to customize further.
## Current Behavior
All non-Angular generators (React, Next.js, Vue, Nuxt, Web, Workspace)
prompt users with a long list of stylesheet options including. These
make the decision much more complicated, where a basic CSS/SCSS setup
opens up for more customization if desired without our official support.
## Expected Behavior
**Simplified style prompt** — the interactive prompt now shows only:
- CSS (default)
- SCSS
- None
Note: Code will continue to compile for now, we're removing the option
from generators. For LESS, users will receive a deprecation warning, the
same way we deprecated Stylus previously. For styled-jsx,
styled-components, emotion we will do a follow-up when we decide what to
do with built-in configs for webpack/rspack/rollup.
## Related Issue(s)
Fixes NXC-4178
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>
## 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
## 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.
## 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`
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>
## 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.
## 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.
## 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.
## 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
## 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
## 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
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
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.
## 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.
When merging project configurations (e.g., from plugins, `project.json`,
target defaults in `nx.json`), array and object properties are
completely replaced by the new value. There is no way to extend or merge
with the base value.
For example, if a plugin infers:
```json
{
"targets": {
"build": {
"inputs": ["default", "{projectRoot}/**/*"]
}
}
}
```
And `nx.json` has target defaults:
```json
{
"targetDefaults": {
"build": {
"inputs": ["production"]
}
}
}
```
The result would be `["production"]` — completely replacing the inferred
inputs rather than combining them.
This PR adds support for `"..."` as a spread token when merging
configurations. Users can now control how arrays and objects are merged
by specifying where the base value should be inserted.
**Array spread:**
```json
{
"inputs": ["production", "...", "{workspaceRoot}/.eslintrc.json"]
}
```
Results in: `["production", "default", "{projectRoot}/**/*",
"{workspaceRoot}/.eslintrc.json"]`
**Object spread:**
```json
{
"options": {
"env": {
"NEW_VAR": "value",
"...": true,
"OVERRIDE_VAR": "overridden"
}
}
}
```
Spreads the base object's properties at the position of `"..."`, with
keys defined after the spread taking precedence.
This works in:
- Top-level target properties (`inputs`, `outputs`, `dependsOn`)
- Target `options` and nested option objects (one layer deep)
- Target `configurations` and their options (one layer deep)
- Both `project.json` merging and `nx.json` target defaults
Fixes #
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`allWorkspaceFiles` (a flat `FileData[]` of every file in the workspace)
is built, stored, copied, and passed through the project graph pipeline
— even though no consumer uses it. The data is fully redundant with
`fileMap.projectFileMap` + `fileMap.nonProjectFiles`. In the daemon,
it's deep-copied on every graph serialization via `copyFileData()` and
rebuilt on every incremental update via `buildAllWorkspaceFiles()`.
## Expected Behavior
`allWorkspaceFiles` is removed from:
- `retrieveWorkspaceFiles` return value
- `buildProjectGraphUsingProjectFileMap` parameters
- `hydrateFileMap` / `getFileMap` / `storedAllWorkspaceFiles`
- `fileMapWithFiles` daemon state and `SerializedProjectGraph` interface
- `WorkspaceFileMap` interface and `updateFileMap` return value
This eliminates redundant allocations, copies, and retained references.
Native/Rust code is unaffected — it uses
`rustReferences.allWorkspaceFiles` (`ExternalObject`), which is a
separate code path.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Calling `delayedSpinner.setMessage` before it would have already
appeared causes it to appear earlier than it should
## Expected Behavior
Calling delayedSpinner.setMessage doesn't actually invoke the spinner
update message if the delayed spinner hasn't fired yet, instead it
stores the message under `lastMessage`, and whenever the spinner fires
it reads lastMessage
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
Implements a ci-workflow generator for .NET projects, following the
pattern established by the gradle ci-workflow generator.
## Blocked
Waiting for https://github.com/nrwl/nx-cloud-workflows/pull/112
## Changes
- **Generator implementation**
(`packages/dotnet/src/generators/ci-workflow/`)
- Supports GitHub Actions and CircleCI
- Configures .NET SDK 8.x
- Uses `linux-medium` agent type for Nx Cloud task distribution
- Includes Nx affected commands and self-healing CI (`nx fix-ci`)
- **Templates**
- GitHub Actions: uses `actions/setup-dotnet@v4`
- CircleCI: uses `mcr.microsoft.com/dotnet/sdk:8.0` docker image
- **Registration**: Added to `generators.json` alongside existing `init`
generator
## Usage
```bash
# Generate GitHub Actions workflow
nx g @nx/dotnet:ci-workflow --ci=github
# Generate CircleCI workflow
nx g @nx/dotnet:ci-workflow --ci=circleci
```
Fixes
https://linear.app/nxdev/issue/NXC-3356/generate-ci-workflow-for-net
> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `repo.gradle.org`
> - `staging.nx.app`
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/nrwl/nx/settings/copilot/coding_agent)
(admins only)
>
> </details>
<!-- START COPILOT CODING AGENT SUFFIX -->
<details>
<summary>Original prompt</summary>
> Issue Title: Generate `ci-workflow` for .NET
> Issue Description: This should be similar to the existing ci-workflow
generator for gradle, reference
[https://github.com/nrwl/nx/tree/master/packages/gradle/src/generators/ci-workflow](https://github.com/nrwl/nx/tree/master/packages/gradle/src/generators/ci-workflow)
>
> The generator should be scaffolded using `nx g generator
./packages/dotnet/src/generators/ci-workflow/ci-workflow`
> Fixes
https://linear.app/nxdev/issue/NXC-3356/generate-ci-workflow-for-net
>
>
> Comment by User 4215f3ef-50bd-4f09-85a0-b489c88057b6:
> [https://github.com/nrwl/nx](https://github.com/nrwl/nx)
>
> Comment by User d484ef82-7f7d-4a95-be09-9d82ca3905dc:
> 📋 I wasn't able to determine which GitHub repository to work in.
>
> I think it's one of these, but can you tell me which one is right?
>
> Comment by User :
> This thread is for an agent session with githubcopilot.
>
>
</details>
<!-- START COPILOT CODING AGENT TIPS -->
---
💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).
---------
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>
## Current Behavior
The `@nx/esbuild:esbuild` executor calls `isUsingTsSolutionSetup` at
task runtime, which reads `extends`, `files`, and `include` from the
workspace-root `tsconfig.json`. Targets using the executor do not
declare those fields as inputs, so cache hashes do not reflect changes
to them and builds can hit stale cache entries after a `tsconfig.json`
edit.
## Expected Behavior
Targets using the `@nx/esbuild:esbuild` executor include a field-scoped
`{workspaceRoot}/tsconfig.json` input covering exactly the fields the
executor reads, so hashes change when those fields change and remain
unaffected by unrelated edits.
- `addBuildTargetDefaults` gains an optional `extraInputs` parameter so
plugin generators can extend the default/production inputs when seeding
executor-keyed target defaults. Existing call sites are unchanged.
- The `@nx/esbuild:configuration` generator uses that parameter to add a
field-scoped `{workspaceRoot}/tsconfig.json` input (fields: `extends`,
`files`, `include`) to the `@nx/esbuild:esbuild` target defaults it
seeds.
- This repo's `nx.json` gains the same executor-keyed target defaults so
`tools-documentation-create-embeddings` (the only esbuild target in the
repo) inherits the fix. `dependsOn` mirrors the existing `build`
target-name default to preserve the inferred `typecheck` and
`build-base` dependencies.
## Description
Adds a `compiler` option to the `@nx/vite` plugin, mirroring the same
option already in `@nx/js` (added in #33821).
This lets users specify an alternative TypeScript compiler for the
inferred `typecheck` target — specifically `tsgo` from
`@typescript/native-preview` (TypeScript 7 Go compiler).
## Problem
`@nx/vite` hardcodes `'tsc'` for typecheck while `@nx/js` already
supports a configurable `compiler` option. Users who want `tsgo` have to
patch `@nx/vite` manually.
## Solution
3 lines in `packages/vite/src/plugins/plugin.ts`:
1. Add `compiler?: string` to `VitePluginOptions` (with JSDoc)
2. `options.compiler ?? 'tsc'` instead of hardcoded `'tsc'` (Vue
projects still use `vue-tsc`)
3. `options.compiler ??= 'tsc'` in `normalizeOptions`
## Usage
```json
// nx.json
{
"plugins": [
{
"plugin": "@nx/vite/plugin",
"options": {
"compiler": "tsgo"
}
}
]
}
```
## Benchmarks (large monorepo, ~400 projects)
| Project | `tsc` | `tsgo` | Speedup |
|---|---|---|---|
| `dashboard-web` | 2.33s | 0.43s | **5.4×** |
| `market-react` | 11.57s | 3.64s | **3.2×** |
On CI: total typecheck CPU dropped **2.7×**, allowing us to eliminate
worker sharding entirely.
Currently working around this with a pnpm patch on `@nx/vite` — happy to
remove it once this lands.
## Prior art
- #33821 — same `compiler` option added to `@nx/js`
- #35047 / #35167 — Nx team experimented with tsgo internally
---------
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
We want a page to link users to when we deprecate and start removing
`nx` exports. There should be zero imports from `nx` in the wild.
KB article:
https://deploy-preview-35412--nx-docs.netlify.app/docs/guides/tips-n-tricks/migrate-nx-imports-to-devkit
## Current Behavior
No guidance for users importing from `nx` (CLI). These imports break in
future major when `nx` stops exporting them.
## Expected Behavior
New recipe under Tips & Tricks. Lists common devkit symbols,
before/after code blocks, and a copy-prompt for AI-driven migration.
Calls out `@nx/devkit/testing` and `@nx/devkit/ngcli-adapter` subpaths.
Also fixes `llm_copy_prompt` transform: inline code, links, and ordered
list numbering were being stripped from extracted prompt text.
## Related Issue(s)
Fixes DOC-462
## Current Behavior
`NodeChildProcessWithNonDirectOutput` in
`packages/nx/src/tasks-runner/running-tasks/node-child-process.ts:48`
listens for the child process `'exit'` event. In that listener it joins
`terminalOutputChunks` into a single string and clears the buffer before
notifying `exitCallbacks`.
The child process `'exit'` event fires when the child exits, but its
stdout/stderr streams may still have buffered `'data'` events pending on
Node's event queue. When a task writes output and exits on the same tick
(e.g. `echo X && exit 0`), the `'exit'` listener can run *before* the
final `'data'` callback lands in `terminalOutputChunks`. The result: the
late chunk is pushed into a new (now-empty) array that nobody reads, and
the joined output that reaches `exitCallbacks` is missing its tail. The
effect is visible as missing stdout when running many tasks at once
(#35302).
## Expected Behavior
Switch the listener from `'exit'` to `'close'`. Per Node.js docs,
`'close'` fires only after the child has exited **and** its stdio
streams have closed — so every `'data'` event has been delivered by
then. The listener body is otherwise unchanged (`(code, signal)` is
still the argument shape).
No callers of `exitCallbacks` are timing-sensitive: they only read
`code` and `terminalOutput`. The slight delay from waiting for stdio
drain is precisely what we want here.
Added a regression test that simulates the race (`'exit'` fires, then
stdout `'data'` arrives, then `'close'` fires) and asserts the late
output is captured in the joined `terminalOutput`.
## Related Issue(s)
Fixes#35302
## Current Behavior
In 22.7.0-rc.1, `$schema` references in `nx.json` and `project.json` no
longer resolve in VS Code / JetBrains / any editor that reads `$schema`
as a filesystem path.
Root cause — two commits combined:
1. #34111 moved schemas from `packages/nx/schemas/` to
`packages/nx/dist/schemas/`.
2. #35109 introduced a `files` allowlist that shipped only `dist/`,
dropping the legacy top-level `schemas/` from the published npm tarball.
Node subpath `exports` (`"./schemas/*": "./dist/schemas/*.json"`) do
**not** redirect filesystem paths — editors bypass Node resolution
entirely. So the regression affected both existing workspaces upgrading
from 22.6.x (with `$schema` already in their configs) and fresh installs
(since `nx init`, `create-nx-workspace`, and the project-configuration
generator all write `./node_modules/nx/schemas/...`).
## Expected Behavior
`schemas/*.json` ship at the root of the published `nx` package again,
so `./node_modules/nx/schemas/nx-schema.json` (and the project/workspace
variants) resolve on disk — no migration required, no changes to the
paths generators write.
Schemas are static JSON assets, not build artifacts — they're now
published from source, not copied through `dist/`:
- `packages/nx/package.json` `files`: `"dist/schemas"` → `"schemas"`.
- `packages/nx/package.json` `exports`: `./schemas/*` and
`./schemas/*.json` both map to `./schemas/*.json`.
- `packages/nx/assets.json`: dropped the `schemas/*.json` → `dist/` copy
step.
Verified via `npm pack --dry-run` — tarball now contains
`schemas/nx-schema.json`, `schemas/project-schema.json`,
`schemas/workspace-schema.json` at root, with no `dist/schemas/`
entries.
## Related Issue(s)
Fixes#35411
## Current Behavior
The patched Jest resolver does not redirect imports of `@nx/cypress`,
`@nx/maven`, `@nx/plugin`, or `@nx/vitest` to their source entrypoints,
so Jest runs against the built output for those packages.
## Expected Behavior
The patched Jest resolver redirects imports of `@nx/cypress`,
`@nx/maven`, `@nx/plugin`, and `@nx/vitest` to source, matching the
treatment of the other `@nx/*` packages.
## Current Behavior
Nx reads its own version through a package.json path derived from
__filename. When Nx internals are bundled into another tool, that
relative path can point outside the original package layout.
## Expected Behavior
Nx resolves its version through the exported nx/package.json
self-reference, so bundlers can resolve it statically and the runtime no
longer depends on the source/dist file layout.
## Related Issue(s)
N/A
## Current Behavior
When `@nx/maven` runs Maven targets in Nx batch mode, the Maven 3 batch
runner builds a `MavenExecutionRequest` and only calls
`MavenExecutionRequestPopulator.populateDefaults()`. That path injects
default remote repositories but does not merge user and global
`settings.xml` (mirrors, servers, proxies, profile repositories, etc.).
Resolution can ignore corporate mirrors and behave differently from the
`mvn` CLI and from non-batch execution.
## Expected Behavior
Batch mode should apply the same effective settings as the `mvn` CLI:
build settings with `SettingsBuilder`, then `populateFromSettings()`
before `populateDefaults()`. Global `settings.xml` should resolve the
same way as the launcher (`${maven.conf}/settings.xml`), so `maven.conf`
is set from `maven.home` when unset.
## Related Issue(s)
Fixes https://github.com/nrwl/nx/issues/34948
## Current Behavior
I noticed this while investigating a 6-minute hang in our ~2600-project
monorepo for any target that uses `run_command` (in our case, a
`check-types` target that runs `tsc --noEmit`). The hang happens after
Nx appears to have run all the tasks, as seen in github actions log with
the "timestamps" setting enabled:
<img width="1258" height="336" alt="image"
src="https://github.com/user-attachments/assets/14a66362-3253-4649-b750-a69d9baf7c1d"
/>
We've patched our Nx instance locally (`pnpm patch`) with the changes in
this PR and the hang is now gone for us. Offering this PR upstream in
case anyone else is affected.
What seems to be happening is that each `RunningNodeProcess` registers 4
process-level event handlers (`exit`, `SIGINT`, `SIGTERM`, `SIGHUP`) in
`addListeners()` but never removes them after the child process exits.
This causes two problems:
1. **`MaxListenersExceededWarning`** when more than ~10 `run-commands`
tasks execute in parallel
2. **Multi-minute synchronous hang at process exit** — when
`process.exit()` is called, Node.js runs every leaked `exit` handler
sequentially. Each one calls `treeKill()` on an already-dead PID, which
takes ~143ms. With thousands of tasks, this adds minutes of dead time at
the end of every CI run.
### Reproduction
Run any target using `nx:run-commands` on a monorepo with 1000+
projects. After "Successfully ran target..." prints, the process hangs
for several minutes before exiting.
### Measured impact
On our 2610-project monorepo:
- **2610 leaked exit handlers** × **~143ms each** = **~6.2 minutes** of
synchronous blocking after every `check-types` CI run
- Identified via `process.on('exit')` instrumentation showing each
handler calling `treeKill` on dead PIDs from
`RunningNodeProcess.addListeners` at `running-tasks.ts:522`
## Expected Behavior
Process exit handlers registered by `RunningNodeProcess` are cleaned up
when the child process exits. The Node.js process exits promptly after
task completion with no leaked listeners.
## Fix
Store the 4 signal/exit handlers as named references and remove them via
`process.removeListener()` when the child process exits or errors. This
is a minimal, targeted change — no new dependencies, no behavioral
changes to signal handling during task execution.
## Current Behavior
`nx run -p my-project -t test:unit` crashes with a `TypeError` instead
of running the target:
```
NX parsedArgs[PROJECT_TARGET_CONFIG]?.lastIndexOf is not a function
TypeError: parsedArgs[PROJECT_TARGET_CONFIG]?.lastIndexOf is not a function
at parseRunOneOptions (.../nx/src/command-line/run/run-one.js:105:44)
```
Two bugs are combining here:
1. `--target` / `-t` is never registered as a yargs option on the `run`
command (`withRunOneOptions` only declares `--project` and `--help`), so
the flag silently falls through into the overrides array and the target
value is lost.
2. With no positional value provided but flags present, yargs assigns
boolean `true` to the `project:target:configuration` positional that the
`run [project][:target][:configuration] [_..]` signature declares.
`parseRunOneOptions` then calls `.lastIndexOf(':')` on `true` and
crashes.
Workaround today is to escape the colon: `nx run my-project:test\:unit`.
That works but is non-obvious, and the flag-based form is what most
users reach for first.
## Expected Behavior
`nx run -p my-project -t test:unit` runs the `test:unit` target on
`my-project`. Escaped and long-form invocations continue to work.
Changes:
- Register `--target` / `-t` as a real yargs option in
`withRunOneOptions`, and add `-p` as an alias for `--project`.
- Guard the positional check in `parseRunOneOptions` with `typeof
parsedArgs[PROJECT_TARGET_CONFIG] === 'string'` so a non-string value
can never crash `.lastIndexOf`.
- Add unit tests for the flag-based invocation (short, long, and `=`
forms) and for the boolean-positional guard. Update `compareArgs` in
`command-object.spec.ts` to strip the new `p`/`t` alias keys when
comparing infix vs. `run` invocations.
Verified end-to-end against a reproduction workspace: `nx run -p
my-project -t test:unit` now runs successfully.
## Related Issue(s)
Fixes#35098
## Current Behavior
`nx init` error telemetry is largely opaque: ~22% of starts land in a
bare `Command failed: npm install` bucket, and ~5% record an empty
`errorMessage`. We can't tell what's actually going wrong. `nx connect`
has no start/error events at all — failures (missing remote, auth,
network) go untracked.
## Expected Behavior
Telemetry-only change. Child-process calls in init pipe stderr so the
captured output reaches the error payload; error events now include
`errorName` (from Node `e.code` or an extracted `E…`/`ERR_…` token like
`E404`, `ERESOLVE`, `EINTEGRITY`, `ERR_PNPM_*`) and the same env context
(`nodeVersion`, `os`, `packageManager`, `isCI`, `aiAgent`) as start
events. `toErrorString` fixes the empty-message bucket. `nx connect`
gains proper start/complete/error events.
No behavioral fixes — once the enriched data comes in we'll prioritize
real fixes by actual failure distribution.
## Related Issue(s)
Fixes NXC-4262
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
## Current Behavior
The `nx-dev:sitemap` task invokes `next-sitemap`, whose `ManifestParser`
reads four manifests from the `nx-dev:next:build` output
(`build-manifest.json`, `export-marker.json`, `prerender-manifest.json`,
`routes-manifest.json` under `.next/`). None of these are declared as
inputs on `sitemap`, so the reads show up as sandbox violations and the
sitemap cache key doesn't change when those manifests change.
## Expected Behavior
The four manifests are declared inputs via a single
`dependentTasksOutputFiles` entry, narrowly scoped to
`**/.next/{build-manifest,export-marker,prerender-manifest,routes-manifest}.json`
so unrelated `.next/` artifacts don't invalidate the sitemap cache. `nx
show target inputs nx-dev:sitemap --check` confirms all four paths
resolve as declared inputs.
## Current Behavior
The `@nx/js:prune-lockfile` executor only identifies workspace module
dependencies when the `package.json` version string starts with
`workspace:`, `file:`, or `link:`. npm workspaces reference sibling
packages using plain semver (e.g. `"@repo/schemas": "0.0.1"` or
`"@repo/schemas": "*"`), so those dependencies are never rewritten to
point at `workspace_modules/` and pruning fails for npm-based
workspaces.
## Expected Behavior
Workspace module dependencies are correctly identified and rewritten to
`file:./workspace_modules/<pkg>` regardless of package manager,
including npm with plain-semver versions.
The fix pulls the set of workspace packages from the project graph via
`getWorkspacePackagesFromGraph` and treats any dependency whose name
matches a workspace package as a workspace module, in addition to the
existing protocol-prefix checks.
A regression e2e test covers the npm plain-semver case (`"*"`): it
asserts the pruned `package.json` has its dependency rewritten to
`file:./workspace_modules/@scope/nodelib`. The existing npm test case
used `file:../<lib>` which matched the `file:` prefix check and would've
passed even without the fix, so it didn't actually cover the reported
scenario.
## Related Issue(s)
Fixes#33523
## Current Behavior
The Resources dropdown in the astro-docs header contained links
returning 404 on nx.dev:
- `/podcast`
- `/resources-library?*` -> `/resources?...`
Additionally, `/webinar` redirected to `/webinars` (canonical).
## Expected Behavior
- Podcasts entry removed (page no longer exists).
- Books / Case Studies / Whitepapers repointed to the new
`/resources?filterBy=...` URLs.
- Webinars points directly to `/webinars` (no redirect).
All 12 Resources dropdown links verified returning 200. Books link
clicked through from the local astro-docs dropdown and confirmed to load
the Resources Library page.
## Current Behavior
The project's eslint config ignores common generated directories
(`dist/`, `.astro/`, `.netlify/`, ...) but not
`astro-docs/src/content/banner.json`. That file is a build artifact
produced at prebuild time by `astro-docs:prebuild-banner` (fetches
banner config from a remote URL and writes it to disk) and is
`.gitignore`d. The root eslint config registers `jsonc-eslint-parser`
for `**/*.json`, so `eslint .` walks into this generated file and parses
it — wasted work, and its content is irrelevant to lint correctness.
As a side effect, the undeclared read also shows up as a sandbox
violation for `astro-docs:lint` because Nx's `{projectRoot}/**/*` input
expansion (correctly) excludes gitignored files.
## Expected Behavior
`astro-docs/eslint.config.mjs` excludes the generated banner file
alongside the other build-artifact paths it already ignores, so eslint
no longer reads it. The sandbox violation disappears as a consequence.
## Current Behavior
`@nx/js:typescript-sync` always materializes every project-graph edge of
a project as a TypeScript project reference in the corresponding runtime
tsconfig. The existing `nx.sync.ignoredReferences` opt-out keeps
user-authored reference paths from being pruned, but there is no way to
tell the generator "don't add a reference for this dependency in the
first place."
That becomes a problem when the project graph contains intentional
cycles — for example when `@nx/workspace` declares its lazy-loaded
plugin peers (`@nx/js`, `@nx/angular`, etc.) as optional peers, and
those same plugins depend back on `@nx/workspace`. Materializing both
edges as TS project references produces `TS6202: Project references may
not form a circular graph`. The only workaround today is
`implicitDependencies: ["!name", …]` in `project.json`, which removes
the edge from the project graph entirely and hides it from every other
consumer (dependency tooling, graph visualizations, supply-chain
audits).
## Expected Behavior
Tsconfig files now accept an `nx.sync.ignoredDependencies: string[]`
field (sibling of the existing `nx.sync.ignoredReferences`). When the
sync generator processes that tsconfig, any project-graph dependency
whose project name is in the set is skipped — no new reference is added
for it, and any existing reference for that dependency is pruned as
stale.
This lets a workspace keep real, cyclic project-graph edges (so the
package.json peer relationships stay visible) while opting the affected
tsconfig out of materializing the cycle into project references. The
task graph is kept acyclic separately via explicit `dependsOn` entries.
Co-authored-by: Claude <noreply@anthropic.com>
## Current Behavior
`nx-dev:build` is a no-op aggregator over `sitemap` and `copy-redirects`
but only declares `{projectRoot}/.next` as its output. Tasks that depend
on `nx-dev:build` and use `dependentTasksOutputFiles` — e.g.
`astro-docs:validate-links`, which reads
`nx-dev/nx-dev/public/sitemap-0.xml` to cross-check links — never pick
up the sitemap as a declared input. Under sandboxing this surfaces as an
unexpected read, and it also means the sitemap doesn't participate in
the consumer's input hash.
Separately, `nx-dev:next:build` lists `public/sitemap*.xml` as an output
even though `next build` never writes sitemaps (the `sitemap` target
does). At best the glob captures nothing; at worst, on a re-run it
snapshots stale files from a previous build into `next:build`'s cache.
## Expected Behavior
`nx-dev:build` and `nx-dev:deploy-build` declare
`{projectRoot}/public/sitemap*.xml` alongside `{projectRoot}/.next`,
matching what their dependsOn chain actually produces. This mirrors the
existing `nx:noop` atomizer pattern used by `@nx/playwright` and
`@nx/cypress`, where the rollup target declares the superset of outputs
its children produce.
`nx:next:build` no longer claims an output it doesn't write.
Verified: `nx show target inputs astro-docs:validate-links --check
nx-dev/nx-dev/public/sitemap-0.xml nx-dev/nx-dev/public/sitemap.xml` now
reports both as inputs.
## Current Behavior
Running `nx test dotnet` resolves `@nx/dotnet/*` specifiers to compiled
`dist/*.js` files instead of TypeScript source.
`scripts/patched-jest-resolver.js` maintains a `workspacePackages`
allowlist of `@nx/*` packages that should route to their TS source
during tests. `@nx/dotnet` is missing from that list, so the resolver
falls through to `enhanced-resolve`, which honors
`packages/dotnet/package.json#exports` — and those entries all point at
`./dist/*.js`.
Every other `@nx/*` plugin in the repo is on the allowlist, so this is
specific to `@nx/dotnet`.
## Expected Behavior
`nx test dotnet` resolves `@nx/dotnet/*` imports to TypeScript source
files under `packages/dotnet/src/` like every other workspace package,
so tests exercise the current source and don't require a prior build.
## Related Issue(s)
<!-- None; internal dev loop fix surfaced while running the dotnet test
suite. -->
The generate-workspace-files spec wrote each rendered README to
`__dirname/tmp/<preset>-<nxCloud>/README.md` via raw fs, landing inside
the source tree and triggering Nx sandbox violations during `nx test
workspace`. The toMatchSnapshot assertion that followed was always the
real check; the filesystem writes were leftover debug scaffolding from
the bulk README regeneration in #27038.
Also drops the now-unused `fs` and `path` imports.
## Summary
- avoid prepending `outputPath` twice when `@nx/js:node` derives the
runnable file for `@nx/js:tsc` and `@nx/js:swc`
- keep the existing source-relative behavior for normal `src/...`
entries while preserving nested paths already inside the output
directory
- respect a configured `rootDir` on the build target so the node
executor does not add an extra path segment that tsc/swc stripped from
the output
- reuse the canonical `normalizePath` and
`getRelativeDirectoryToProjectRoot` utilities instead of re-declaring
them
- add focused coverage for the `dist/main.js`, source-entry,
nested-output, and `rootDir` cases
Fixes#35044Fixes#33577
## Validation
- `corepack pnpm exec prettier --check
packages/js/src/executors/node/node.impl.ts
packages/js/src/executors/node/lib/output-file.ts
packages/js/src/executors/node/lib/output-file.spec.ts`
- `corepack pnpm exec eslint packages/js/src/executors/node/node.impl.ts
packages/js/src/executors/node/lib/output-file.ts
packages/js/src/executors/node/lib/output-file.spec.ts`
- `pnpm nx test js --testPathPatterns=output-file` — all 4 cases pass
---------
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
## Current Behavior
Running `nx run rollup:test` produces three unexpected writes to the
workspace root that are not declared as outputs:
- `dist/index.js/index.cjs.default.js`
- `dist/index.js/index.cjs.mjs`
- `dist/index.js/package.json`
These show up as sandbox violations because the spec file at
`packages/rollup/src/plugins/package-json/update-package-json.spec.ts`
is, in reality, writing to the filesystem during test runs:
1. `jest.spyOn(utils, 'writeJsonFile')` is used without
`.mockImplementation(...)`. `spyOn` wraps the function to record calls
but does not replace its behavior, so the real `writeJsonFile` runs and
writes `package.json`.
2. `update-package-json.ts` also calls `fs.writeFileSync` directly (for
the `.cjs.mjs` / `.cjs.default.js` CJS-interop shims), which the spec
never mocked at all.
Every run pollutes the workspace root with a `dist/index.js/` directory.
## Expected Behavior
Tests verify call arguments via the spies without touching the real
filesystem. No `dist/index.js/` directory is created, and the three
sandbox violations for `rollup:test` go away.
- All 11 `writeJsonFile` spies now use `.mockImplementation(() =>
undefined)`.
- A `beforeEach` / `afterEach` adds a `jest.spyOn(fs,
'writeFileSync').mockImplementation(() => undefined)` using
`require('fs')` so the spy lands on the real fs module (using `import *
as fs from 'fs'` would go through `esModuleInterop`'s `__importStar`
copy and miss the call site in `update-package-json.ts`).
- Assertions continue to work — `toHaveBeenCalledWith(...)` tracks calls
on spies with or without a mock implementation.
Verified: `nx test rollup --testPathPatterns=update-package-json.spec` →
11/11 passing, and no `dist/index.js/` is created during the run.
## Related Issue(s)
N/A — caught via the sandbox-violation report for `rollup:test`.
## Current Behavior
When running \`nx release version --preid rc\` with a project filter,
projects with a dependent patch bump (from another project being bumped)
do not have the preid applied. semver.inc('1.0.0', 'patch', 'rc')
returns '1.0.1' because semver silently ignores preid for non-prerelease
specifiers.
## Expected Behavior
The preid is applied to dependent patch bumps, so dependents get
'0.0.2-rc.0' instead of '0.0.2'. The fix adds `applyPreidToBumpType`
which converts patch to prepatch, minor to preminor, and major to
premajor when a preid is set.
## Related Issue(s)
Fixes#33488
## Current Behavior
Several `ensurePackage('@nx/X', nxVersion)` and `ensurePackage<typeof
import('@nx/X')>(...)` references are invisible to Nx's project-graph
source analysis, so the corresponding workspace edges are missing today:
-
`packages/storybook/src/generators/configuration/lib/util-functions.ts`
lazy-loads `@nx/web`
- `packages/web/src/generators/application/application.ts` lazy-loads
`@nx/cypress`, `@nx/eslint`, `@nx/jest`, `@nx/playwright`, `@nx/vite`,
`@nx/webpack`
- `packages/vitest/src/utils/ignore-vitest-temp-files.ts` lazy-loads
`@nx/eslint`
- `packages/vue/src/**` lazy-loads `@nx/cypress`, `@nx/playwright`,
`@nx/rsbuild`, `@nx/storybook`
This means tasks in those packages read files from their lazy-loaded
dependencies at module-load time without declaring them as inputs — the
motivation behind the sandbox-violation work in #35377.
## Expected Behavior
Declares each lazy-loaded package as an optional `peerDependency` with
`peerDependenciesMeta.*.optional: true`.
`explicit-package-json-dependencies` walks `peerDependencies` alongside
`dependencies`, materializing the edges in the graph. `optional: true`
keeps them off the user's install surface — package managers don't
auto-install optional peers and don't warn when they're missing.
`@nx/vitest` is already declared as a `devDependency` of `@nx/web`, so
the `web → vitest` edge is already present; no change needed there.
Follows the pattern established in #35377 (`@nx/eslint → @nx/jest`).
### Scope
Narrower subset of #35392, scoped to `@nx/storybook`, `@nx/web`,
`@nx/vitest`, and `@nx/vue`. Those four close the most commonly hit
transitive lazy-load chains (`storybook → web → jest`, `vue → storybook
→ web → jest`, `web → vitest → eslint`, etc.) without needing the
`implicitDependencies: ["!name", ...]` cycle negations that
`@nx/workspace` and `@nx/js` require in #35392.
Note: `@nx/vitest` is not covered by #35392 at all, so this PR adds at
least one edge that isn't in the broader PR.
### Verification
- Regenerated project graph locally — all new edges appear as `static`
type:
- `storybook → web`
- `web → {cypress, eslint, jest, playwright, vite, webpack}`
- `vitest → eslint`
- `vue → {cypress, playwright, rsbuild, storybook}`
- Full cycle scan: **0 cycles introduced**.
- `nx prepush` passes cleanly.
## Related Issue(s)
<!-- No open issue; follow-up to #35377 and narrower alternative to
#35392 -->
## Current Behavior
`create-nx-workspace` randomly serves one of three Nx Cloud prompt copy
variants during the template flow; `nx init` pins a baseline copy that
predates the test.
A/B results (NXC-4336):
- Variant 0 (baseline): `Enable remote caching to speed up builds with
Nx Cloud?` — 15.4% yes / 29.2% never
- Variant 1: `Never rebuild the same code twice — enable Nx Cloud?` —
13.4% yes / 28.4% never
- **Variant 2: `Speed up GitHub Actions, GitLab CI, and more with Nx
Cloud?` — 17.8% yes / 22.4% never**
## Expected Behavior
Both `create-nx-workspace` (`setupNxCloudV2`) and `nx init`
(`setupNxCloud`) serve the winning variant. Footer is reworded to lead
with the free-tier messaging:
> Free for small teams. Remote caching and task distribution. 2-minute
setup: https://nx.dev/nx-cloud
The CNW `setupNxCloudV2` array collapses from 3 variants to 1;
`PromptMessages.getPrompt` already falls back to index 0 when
`flowVariant >= length`, so existing flow-variant plumbing keeps
working. Spec updated to assert the locked-in code for all flow variants
(0/1/2) and docs generation.
## Related Issue(s)
Fixes NXC-4336
## Current Behavior
Three devkit specs exercise helpers that do `await
import('@nx/<plugin>/plugin')` at runtime (`findPluginForConfigFile`
when a registration has `include`/`exclude`, and
`addE2eCiTargetDefaults` unconditionally for every e2e plugin
registration). Combined with the custom jest resolver in
`scripts/patched-jest-resolver.js` — which maps `@nx/<pkg>` subpaths to
workspace source — the dynamic imports pull the real plugin source plus
everything transitively re-exported by `@nx/js` and `@nx/vite` into the
jest process.
Concretely, `devkit:test` ends up reading ~49 files across
`packages/js/src/**` and `packages/vite/**` that are not declared (and
cannot be declared without creating a project-graph cycle since `@nx/js`
and `@nx/vite` both depend on `@nx/devkit`). The sandbox flags all 49 as
undeclared-read violations.
## Expected Behavior
None of these specs aim to validate the real plugin's behavior. They
exercise devkit's own logic — how a registration is matched to a config
file, how target defaults are written into `nx.json`, and how e2e web
server info is resolved from a registered plugin — treating
`@nx/<plugin>/plugin` as an opaque reference. The plugin's
`createNodesV2[0]` glob is used by the devkit helpers only as a pattern
pre-filter against conventional config filenames (`vite.config.ts`,
`cypress.config.ts`); what the real plugin does beyond that is outside
the scope of these tests.
Stubbing the three plugin modules with `jest.mock(..., { virtual: true
})` that exposes just the real plugin's glob pattern:
- Preserves the minimal contract the devkit helpers rely on (the module
resolves, and its glob matches the conventional config filenames used in
the tests).
- Drops the incidental loading of the real plugin and its entire
transitive graph — removing all 49 reported sandbox violations.
- Eliminates an accidental coupling to the pinned `@nx/cypress` version
currently pulled from `node_modules`, which also transitively requires
`@nx/js` workspace source via the custom resolver.
Changes are test-only; no production code is touched.
## Current Behavior
`update-repos` spawns child processes to run `pnpm install`, `nx
migrate`, etc. inside each cloned target repo. The child inherits the
launching shell's `PATH`, which mise activation populated with hardcoded
version-specific install paths (e.g. `/.../installs/node/24.11.0/bin`).
Mise activation is a shell hook on `cd` — it does not re-fire when a
child process changes its own `cwd`. As a result, every cloned repo's
commands run against the outer shell's pinned tool versions rather than
the versions in the cloned repo's own `mise.toml`.
## Expected Behavior
Each cloned repo's commands honor the tool versions pinned in that
repo's `mise.toml`.
`execWithOutput` now prefixes every spawned command with `mise exec --`
(except commands that already start with `mise`, so `mise trust` / `mise
install` aren't wrapped in themselves). `mise exec` re-reads `mise.toml`
from the `cwd` at invocation time and activates the correct tool
versions for that repo.
## Related Issue(s)
Fixes #
## Current Behavior
`NX_*` env vars can be added to Nx source without being documented in
`astro-docs/src/content/docs/reference/environment-variables.mdoc`.
Nothing catches the drift.
## Expected Behavior
Adds a conformance rule (`env-vars-documented`) that fails when an
`NX_*` var is read in source but missing from the docs. Covers TS/JS
`process.env.NX_*` and Rust `env::var` / `env!` patterns, skipping tests
and fixtures. An `ignore` list in `nx.json` handles internal markers not
meant to be documented.
Also documents the user-facing vars the first run surfaced (self-hosted
cache, provenance, plugin isolation, Nx Cloud timeouts, etc.) and marks
`NX_CLOUD_AUTH_TOKEN` and `NX_CLOUD_DISTRIBUTED_EXECUTION_AGENT_COUNT`
as deprecated.
## Related Issue(s)
N/A — internal tooling improvement.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: barbados-clemens <barbados-clemens@users.noreply.github.com>
This pull request contains a small fix in the `build-project-graph.ts`
file to correct the way `Set` is initialized for tracking in-progress
plugins. The change removes the unnecessary spread operator when
creating the `Set` from plugin names.
* Fixed `inProgressPlugins` initialization in both
`updateProjectGraphWithPlugins` and `applyProjectMetadata` functions by
removing the spread operator, ensuring plugin names are correctly added
to the `Set`.
[[1]](diffhunk://#diff-14bd07dde50d74ec748f1bce0f9d8cf05504e36c83928d1ad077dcff088b6822L323-R323)
[[2]](diffhunk://#diff-14bd07dde50d74ec748f1bce0f9d8cf05504e36c83928d1ad077dcff088b6822L441-R441)
## Current Behavior
`packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts:37`
calls `ensurePackage<typeof import('@nx/jest')>('@nx/jest', nxVersion)`.
This runtime-optional reference (type-only import plus a string
argument) is invisible to Nx's static project-graph analysis, so the
graph has no `@nx/eslint → @nx/jest` edge today.
Combined with the custom jest resolver in
`scripts/patched-jest-resolver.js` — which maps `@nx/jest` subpaths to
workspace source — the `eslint:test` task ends up reading
`packages/jest/index.ts` and 19 files under `packages/jest/src/**` at
module-load time. Those reads are undeclared inputs and are flagged as
sandbox violations by the staging sandbox reports.
## Expected Behavior
Declaring `@nx/jest` as an **optional** `peerDependency` of `@nx/eslint`
materializes the missing edge in Nx's project graph (the graph reader in
`explicit-package-json-dependencies.ts` walks `peerDependencies`
alongside `dependencies`). The inferred test-task input `^production` is
transitive, so `packages/jest/**` production source is then covered as
declared inputs of `eslint:test` and the 20 violations disappear.
`peerDependenciesMeta.@nx/jest.optional: true` keeps `@nx/jest` off the
user's install surface — package managers don't auto-install it and
don't warn about missing optional peers. Users who invoke the
`workspace-rules-project` generator with jest scaffolding still get
`@nx/jest` via the existing `ensurePackage` runtime install path,
unchanged.
## Current Behavior
`@react-router/dev` peer dep tops out at Vite 7, so the React app
generator forces Vite 7 when `--use-react-router` is passed, and the
react-router typecheck e2e test is skipped in CI.
## Expected Behavior
`@react-router/dev` 7.14.2 expands its peer dep to include Vite 8
(`^5.1.0 || ^6.0.0 || ^7.0.0 || ^8.0.0`), so:
- `useViteV7: true` is no longer forced when generating a React Router
app
- The `useViteV7` field is removed from
`ViteConfigurationGeneratorSchema` (was only added for this workaround
and was never wired into the configuration generator body)
- The react-router typecheck e2e test is un-skipped
- `reactRouterVersion` is bumped to `^7.14.2`
- A new `22.7.0` packageJsonUpdate migrates `@react-router/*` packages
to `7.14.2` in existing workspaces
## Related Issue(s)
Fixes NXC-4182
## Current Behavior
When using @nx/next:build with Yarn PnP, the generated
.nx-helpers/with-nx.js requires semver at runtime, but semver is not
included in the generated package.json. This causes the application to
fail when starting in a deployed environment with the error: "Required
package: semver, Required by: /app/.nx-helpers/with-nx.js"
## Expected Behavior
The generated package.json should include semver as a dependency so that
.nx-helpers/with-nx.js can resolve it at runtime in all package manager
environments, including Yarn PnP.
## Related Issue(s)
Fixes#34095
allow setting a timeout for nx agents based on the last time output was
emitted.
By default this is 10m. this is set per launch template but can be
overridden per step via env var: `NX_NO_OUTPUT_TIMEOUT`
fixes DOC-488
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
The nx repo uses the legacy eslintrc format (`.eslintrc.json`,
`.eslintignore`) with ESLint v8 and outdated plugin versions. Several of
those plugins ship code that crashes under ESLint v9 (`context.getScope`
removed, etc.), and the `@nx/eslint` / `@nx/eslint-plugin` sources still
reference APIs removed in v9 (`Linter.defineParser`,
`Linter.defineRule`, classic `Linter.Config` shape).
## Expected Behavior
Every project in the repo uses an `eslint.config.mjs` flat config, the
ESLint ecosystem is on the latest v9-compatible versions, and
`@nx/eslint` / `@nx/eslint-plugin` sources compile and test cleanly
against the v9 type split.
### Main changes
- Replaced every `.eslintrc.json` / `.eslintignore` with an
`eslint.config.mjs`.
- Rewrote the root `eslint.config.mjs`:
- Exports a named `baseConfig` that leaf configs import, plus a default
export that opts out of linting (`**/*` ignored) so the root acts as a
shared base and isn't lint-checked directly.
- Drops `FlatCompat` in favor of native plugin imports.
- Exports shared `reactHooksV7Off`, `e2eTestOnlyIgnores`, and a
`storybookConfigs` wrapper that filters v10's preset down to
`storybook/*` rules (the preset references foreign rules like
`import-x/*` that we don't register).
- Cleaned up leaf configs emitted by the generator: deduplicated file
entries, merged split `package.json` blocks, fixed the jsonc parser
namespace import after v3 dropped its default export.
- Scoped the 15 e2e projects to `*.test.ts` files only via a shared
export.
- Bumped the ESLint ecosystem to the latest v9-compatible versions:
`eslint@^9.39.4`, `@eslint/js@^9.39.4`, `@typescript-eslint/*@^8.58.2`,
`eslint-plugin-storybook@^10.3.5`, `eslint-plugin-react-hooks@^7.1.0`,
`eslint-plugin-jsx-a11y@^6.10.2`, `eslint-plugin-import@^2.32.0`,
`eslint-plugin-playwright@^2.10.1`, `eslint-plugin-cypress@^6.3.1`,
`eslint-plugin-jest@^29.15.2`, `toml-eslint-parser@^1.0.3`,
`jsonc-eslint-parser@^3.1.0`, `angular-eslint@^21.3.1`. Dropped
`@eslint/eslintrc`, `@types/eslint`, `@types/eslint__js`.
- Aligned `@nx/eslint` source with the v9 type split: `Linter.Config` →
`Linter.LegacyConfig`, `ESLint.Options` → `ESLint.LegacyOptions`
wherever the underlying shape is eslintrc.
- Rewrote `@nx/eslint-plugin` rule specs (`dependency-checks`,
`enforce-module-boundaries`) as flat-config inline tests since
`Linter.defineParser`/`defineRule` were removed in v9.
- Aligned react/angular/expo/react-native add-linting helpers with the
v9 type split.
- Migrated `tools/eslint-rules` specs from `TSESLint.RuleTester` (legacy
config, rejected by v9's flat Linter) to
`@typescript-eslint/rule-tester`; added `isolatedModules: true` so
ts-jest resolves the new types under `module: node16`.
- Downgraded the new react-hooks v7 rules to `off` via a shared export
so the migration doesn't require rewriting legacy code.
- Auto-fixed unused eslint-disable directives
(`linterOptions.reportUnusedDisableDirectives` defaults to warn in v9).
- Ignored `packages/workspace/**/__fixtures__/**` in lint and updated
the affected snapshot so the fixture matches what the jest generator
templates actually emit.
### Note on ESLint v10
This PR stays on ESLint v9. A few plugins we rely on
(`eslint-plugin-import`, `eslint-plugin-jsx-a11y`,
`eslint-plugin-react`) still don't declare v10 peer support. The jump to
v10 will happen in a follow-up PR once those plugins publish
v10-compatible releases.
## Current Behavior
`isUsingTsSolutionSetup()` (in both `@nx/js` and `@nx/workspace`) falls
back to `new FsTree(workspaceRoot, false)` when called without a tree,
reading the real repo's `tsconfig.json` / `tsconfig.base.json`. Many
unit tests indirectly invoke it (cypress-preset, playwright-preset,
plugin `createNodesV2`, executor `normalize`, etc.), which surfaces as
sandbox violations across ~13 test targets (angular, rspack, vite,
rollup, webpack, react, next, js, cypress, remix, workspace, nest,
node).
## Expected Behavior
Unit tests should never touch the real workspace FS. A global mock in
`scripts/unit-test-setup.js` short-circuits `isUsingTsSolutionSetup()`
when called without a tree, returning `true` to match the de-facto
behavior of hitting the real FS (the Nx repo is a TS solution workspace)
and preserve every test's existing expectations. Calls that pass an
explicit (virtual) tree still run the real implementation.
Two specs that deliberately simulate a non-TS-solution workspace (via
`node:fs` / `workspaceRoot` mocks) add a per-file override returning
`false` to keep expressing that intent:
- `packages/vite/src/plugins/plugin-vitest.spec.ts`
- `packages/rollup/src/plugins/with-nx/normalize-options.spec.ts`
## Current Behavior
`.claude/settings.json` enables `polygraph@nx-claude-plugins`, but the
`nx-claude-plugins` marketplace only ships the `nx` plugin. The
`polygraph` plugin lives in the separate `polygraph-plugins`
marketplace, which is not declared in `extraKnownMarketplaces` either —
so the entry refers to a plugin+marketplace pair that doesn't exist.
Every contributor picks up the broken id when they pull the repo.
The bad id was introduced in #34790, which moved to the new plugin but
left the marketplace name pointing at the old one.
## Expected Behavior
Polygraph is now installed globally via the session-start opt-in prompt,
so the repo-level `enabledPlugins` entry is no longer needed. Remove it
entirely rather than pointing it at a marketplace the settings file
doesn't declare.
## Current Behavior
The createNodesV2 function in the nx/cypress plugin calculates the hash
based only on the output of the standard `calculateHashForCreateNodes`.
This means that when multiple Cypress configuration files exist within a
single application hash. As a result, only one set of inferred targets
is generated, and the additional configuration variants are effectively
ignored.
## Expected Behavior
The createNodesV2 function now takes the configuration file name into
the hash calculation, removing the limitation of a single config within
an application.
---------
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com>
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
## Current Behavior
The scheduled `NPM Audit` workflow is failing on `master` due to a
critical advisory
([GHSA-xq3m-2v4x-88gg](https://github.com/advisories/GHSA-xq3m-2v4x-88gg))
in `protobufjs@7.5.3`, pulled in transitively via:
```
@grafana/faro-web-sdk > @grafana/faro-core > @opentelemetry/otlp-transformer > protobufjs
```
Failing run: https://github.com/nrwl/nx/actions/runs/24753360724
Separately, the workspace carries a `@nx/jest@22.7.0-beta.12` patch even
though the workspace is on `22.7.0-beta.16`, and `allowUnusedPatches:
true` was set in `pnpm-workspace.yaml` to suppress the warning.
## Expected Behavior
- `NPM Audit` workflow passes.
- No stale patches, and unused patches fail loudly rather than being
silently allowed.
### Changes
1. **Remove `@grafana/faro-web-sdk` and `@grafana/faro-web-tracing`.** A
`git grep` confirms neither package is imported anywhere in the codebase
— they were listed in `package.json` but unused. Removing them drops the
transitive `protobufjs@7.5.3` entirely and clears the critical advisory
(audit verified locally).
2. **Delete the stale `@nx/jest@22.7.0-beta.12` patch and drop
`allowUnusedPatches: true`** from `pnpm-workspace.yaml` so future stale
patches surface immediately.
## Related Issue(s)
N/A (fixes failing scheduled CI workflow).
Add sitemap for blog:
- https://deploy-preview-35363--nx-dev.netlify.app/sitemap.xml
- https://deploy-preview-35363--nx-dev.netlify.app/sitemap-2.xml
(rewritten to blog)
## Current Behavior
The root sitemap.xml index references only /sitemap-1.xml (Framer via
edge-function proxy) and /docs/sitemap-index.xml (astro-docs). Blog
posts published by nx-blog (BLOG_URL) are not advertised to crawlers
through the root nx.dev sitemap.
## Expected Behavior
The root sitemap index additionally references /sitemap-2.xml, which is
served by a consolidated `additional-sitemaps.ts` edge function that
proxies the per-source sitemaps:
/sitemap-1.xml -> <NEXT_PUBLIC_FRAMER_URL>/sitemap.xml
/sitemap-2.xml -> <BLOG_URL>/blog/sitemap.xml
URLs in the upstream XML are rewritten from the source origin to nx.dev.
The previous per-source edge functions (framer-sitemap.ts,
blog-sitemap.ts) are replaced by the single additional-sitemaps.ts to
match the "additionalSitemaps" language used in the Next.js sitemap
config.
## Related Issue(s)
Fixes DOC-486
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
The Maven analyzer prints `[Maven Analyzer] Starting analysis with
options: { verbose: false }` unconditionally to stdout. This corrupts
commands that expect clean stdout output, such as `nx show projects
--json`, where consumers end up parsing:
```
[Maven Analyzer] Starting analysis with options: { verbose: false }
["@somnex/krista","@somnex/somnex"]
```
## Expected Behavior
The startup message should only be shown in verbose mode, matching the
rest of the analyzer diagnostics in this file that already use
`logger.verbose`. Normal `nx show projects --json` output stays clean.
## Related Issue(s)
Fixes NXC-4253
## Current Behavior
On process segfault there is no way at all to diagnose where it is
coming from
## Expected Behavior
On segfault we write a report file that has information in it and log
that info on the next nx command if one happens to be ran.
Because segfaults exit the node process immediately, we do not have
another way of handling them so we do have to rely on a follow up
command to surface them. Unfortunately the errors are pretty rare so the
likelihood of this helping isn't awesome, but it could give us some
info.
## AI summary
This pull request introduces a new utility for surfacing Node.js fatal
error diagnostic reports and refactors how project graph data is
accessed in the format command. The most significant changes are the
addition of the fatal error reporting mechanism and the simplification
of how project graph data is loaded, which should improve
maintainability and reliability.
**Fatal error reporting utility:**
* Added a new `surfaceFatalErrorReports` function in
`report-on-fatal-error.ts` that scans for Node.js fatal error reports in
the workspace data directory, surfaces them in the console, and emits
GitHub Actions workflow annotations and step summaries if running in CI.
Reports are renamed after processing to prevent duplication.
**Format command refactoring:**
* Refactored `format.ts` to remove unused imports (`FileData`,
`allFileData`) and to only load the project graph when necessary, which
reduces unnecessary file system and computation overhead.
[[1]](diffhunk://#diff-ff26ab1495d5bd9854f0377c1aad51b1a749fa0ea82d2c66d363c06d417ebc67L6-R6)
[[2]](diffhunk://#diff-ff26ab1495d5bd9854f0377c1aad51b1a749fa0ea82d2c66d363c06d417ebc67L23)
* Updated the logic in `getPatterns` and related functions to lazily
load the project graph and to remove the need for passing all workspace
files, simplifying function signatures and improving performance.
[[1]](diffhunk://#diff-ff26ab1495d5bd9854f0377c1aad51b1a749fa0ea82d2c66d363c06d417ebc67L104)
[[2]](diffhunk://#diff-ff26ab1495d5bd9854f0377c1aad51b1a749fa0ea82d2c66d363c06d417ebc67R111)
[[3]](diffhunk://#diff-ff26ab1495d5bd9854f0377c1aad51b1a749fa0ea82d2c66d363c06d417ebc67L135-R138)
[[4]](diffhunk://#diff-ff26ab1495d5bd9854f0377c1aad51b1a749fa0ea82d2c66d363c06d417ebc67L155)
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
The `@nx/js:node` executor fails when combined with the inferred
`@nx/js/typescript` build target — in two distinct ways:
1. **Script-to-run resolution**: fails with `Could not find
<project>/dist/**/*.{js,cjs,mjs,jsx,d.ts,d.cts,d.mts}{,.map}/main.js.
Make sure your build succeeded.` — the executor's `getFileToRun` was
appending `main.js` onto the glob output pattern.
2. **Buildable-dep import resolution**: when the node app imports a
buildable lib that also uses the inferred build target,
`require('@scope/my-lib')` throws `MODULE_NOT_FOUND` because
`calculateResolveMappings` passes the literal glob into `NX_MAPPINGS`,
which the CJS require override / ESM loader then tries to resolve.
Both regressed after #35041 narrowed the inferred build target's
`outputs[0]` from `{projectRoot}/dist` to
`{projectRoot}/dist/**/*.{js,...}{,.map}` (to prevent cross-OS cache
pollution).
## Expected Behavior
`outputs` entries are cache patterns and may legitimately contain globs.
The node executor now strips the glob portion back to the last path
separator before using the value as a directory, in both `getFileToRun`
and `calculateResolveMappings`. Handles `**`, `*`, `?`, character
classes, brace expansion, extglob, and Windows/POSIX separators.
## Related Issue(s)
Fixes#35198Fixes#35301
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The `astro-docs:format` target runs `prettier **/*.mdoc --check`.
Prettier has `--editorconfig` enabled by default and walks up from each
source file to read `.editorconfig`, deriving options like `endOfLine`,
`tabWidth`, `useTabs`, and `printWidth` from it. The target's declared
inputs do not include `.editorconfig`, so the task sandbox flags it as
an unexpected read and changes to `.editorconfig` do not invalidate the
cache even though they can change `--check` results.
## Expected Behavior
`.editorconfig` is declared as an input of the `astro-docs:format`
target so the sandbox recognizes the read and the cache key reflects
changes to the file.
## Current Behavior
The `@nx/js/typescript` plugin's inferred `typecheck` target only
declares `.d.ts`/`.d.ts.map` as outputs (in the `emitDeclarationOnly`
branch) and only `.d.ts` in the dependency/dependent-task input globs.
When a project has `.mts` or `.cts` sources, `tsc` emits
`.d.mts`/`.d.mts.map` (and `.d.cts`/`.d.cts.map`) declaration files that
fall outside the declared outputs, causing sandbox violations and missed
cache inputs from upstream projects that produce those files.
## Expected Behavior
Declaration file globs cover all three TypeScript declaration extensions
(`.d.ts`, `.d.cts`, `.d.mts`) consistently across inputs and outputs,
matching what `tsc` actually emits for `.ts`/`.cts`/`.mts` sources.
Changes in `packages/js/src/plugins/typescript/plugin.ts`:
- `getOutputs` (`emitDeclarationOnly` branch): `**/*.d.ts{,.map}` →
`**/*.{d.ts,d.cts,d.mts}{,.map}`
- `getInputs` `dependentTasksOutputFiles`: `**/*.{d.ts,tsbuildinfo}` →
`**/*.{d.ts,d.cts,d.mts,tsbuildinfo}`
- `getInputs` dependencies fileset: `{projectRoot}/**/*.d.ts` →
`{projectRoot}/**/*.{d.ts,d.cts,d.mts}`
The non-`emitDeclarationOnly` branch already used the full extension
set, so this brings the other paths in line.
## Current Behavior
Direct `nx:run-commands` tasks on the non-PTY path can forward `Buffer`
chunks into the TUI lifecycle after the recent `exec()` to `spawn()`
change. When native progressive output handling receives that value,
task execution can fail with `StringExpected` instead of surfacing the
underlying command output.
## Expected Behavior
Spawned `run-commands` output is decoded to UTF-8 strings before it
reaches progressive output consumers, so TUI rendering can continue
streaming command output without crashing.
## Current Behavior
On aarch64 Linux kernels with page sizes larger than 4 KiB — 16 KiB on
Asahi Linux / Apple Silicon, 64 KiB on some Ampere/Fedora server configs
— every Nx command (including `nx --version`) aborts immediately:
```
<jemalloc>: Unsupported system page size
<jemalloc>: arena_new: allocation failed
memory allocation of 576 bytes failed
Aborted (core dumped)
```
The cause: jemalloc is compiled with `--with-lg-page=12` (4 KiB
allocator page) by default, and refuses to operate when the system page
size exceeds the compiled-in value.
## Expected Behavior
Nx native binaries run correctly on every shipping aarch64 Linux kernel
(4 KiB, 16 KiB, 64 KiB pages), without losing the performance benefits
of jemalloc on the common 4 KiB-page configurations (Graviton, cloud
ARM, Raspberry Pi OS, Debian/Ubuntu ARM).
The fix: build `@nx/nx-linux-arm64-gnu` and `@nx/nx-linux-arm64-musl`
with `JEMALLOC_SYS_WITH_LG_PAGE=16` (64 KiB allocator page). jemalloc's
documented contract is `allocator_page_size ≥ system_page_size`, so a
binary built this way runs on any aarch64 Linux kernel with page size ≤
64 KiB — which covers all known configurations.
## Prior art
The same `JEMALLOC_SYS_WITH_LG_PAGE=16` approach is used by:
- **rustc** —
[rust-lang/rust#145353](https://github.com/rust-lang/rust/pull/145353):
`cargo.env("JEMALLOC_SYS_WITH_LG_PAGE", "16")` for aarch64 targets in
`src/bootstrap/src/core/build_steps/tool.rs`
- **swc** —
[swc-project/swc#6541](https://github.com/swc-project/swc/pull/6541):
`export JEMALLOC_SYS_WITH_LG_PAGE=16` in the publish workflow for both
aarch64 gnu and musl rows
- **fd** — [sharkdp/fd#1547](https://github.com/sharkdp/fd/pull/1547)
and follow-up [#1549](https://github.com/sharkdp/fd/pull/1549) moving
the env var into `Cross.toml` via `passthrough`
- **Homebrew** —
[`Library/Homebrew/extend/os/linux/extend/ENV/super.rb`](https://github.com/Homebrew/brew/blob/master/Library/Homebrew/extend/os/linux/extend/ENV/super.rb):
`self["JEMALLOC_SYS_WITH_LG_PAGE"] = "16"` (applied globally to Homebrew
Linux aarch64 builds)
- **Arch Linux ARM** —
[`extra/fd/PKGBUILD`](https://github.com/archlinuxarm/PKGBUILDs/blob/master/extra/fd/PKGBUILD):
`[[ $CARCH == "aarch64" ]] && export JEMALLOC_SYS_WITH_LG_PAGE=16`
## Related Issue(s)
Fixes#35345
## Current Behavior
The `e2e-gradle:e2e-ci--**/*.test.ts` tasks produce hundreds of sandbox
violations: ~163 unexpected reads + ~160 unexpected writes under
`packages/gradle/project-graph/build/**`, plus reads of workspace-root
gradle config files.
The root cause is `e2e/gradle/src/utils/create-gradle-project.ts`
invoking `./gradlew :gradle-project-graph:publishToMavenLocal
-PskipSign=true` inline via `execSync` during test setup, which compiles
Kotlin sources inside the sandboxed task and produces all those file
accesses.
## Expected Behavior
The `publishToMavenLocal` step runs as a proper Nx task dependency
before the e2e test, outside the sandbox. Its outputs are declared by
the `@nx/gradle`-inferred target.
### Changes
- **Move publishing out of the test** — delete the inline
`execSync(gradlew :gradle-project-graph:publishToMavenLocal)` in
`create-gradle-project.ts`; make the `e2e-local` and `e2e-ci--**/**`
targets on `e2e-gradle` depend on
`:gradle-project-graph:gradle:publishToMavenLocal`.
- **Fix signing** — the inferred `publishToMavenLocal` target doesn't
pass `-PskipSign=true`, so replace the `skipSign` flag in
`packages/gradle/project-graph/build.gradle.kts` with `setRequired({
gradle.taskGraph.hasTask(":gradle-project-graph:publish") })`. Signing
is required for the Maven Central path (`publish` lifecycle task) but
silently no-ops for `publishToMavenLocal` when no GPG keys are
provisioned.
- **Declare workspace wrapper inputs** — the bootstrap `gradle init`
call must use the workspace wrapper, so add
`gradle/wrapper/gradle-wrapper.jar`,
`gradle/wrapper/gradle-wrapper.properties`, and `gradle.properties` as
inputs on the e2e targets, matching the `@nx/gradle` plugin's inferred
gradle-task input set.
- **Disable cache on the publish task** — `publishToMavenLocal` writes
to `~/.m2/repository/` (outside the workspace, can't be declared as an
Nx output). A remote cache hit would skip launching gradle, leaving
`~/.m2/` empty on the agent and breaking plugin resolution. Override
`cache: false` on `:gradle-project-graph:gradle:publishToMavenLocal`.
- **Narrow `e2eInputs` tsconfig input** — use `{ json, fields }` in
`nx.json` to hash only the fields that affect compilation (same pattern
as `@nx/playwright`).
- **Tighten `astro-docs:validate-links` inputs** — swap the
`**/sitemap*.xml` dep-task-outputs glob for `**/*.html` +
`**/sitemap*.xml` (the actual files the script reads) and drop the dead
`nx-dev/public/sitemap-0.xml` branch since #35315 removed `next-sitemap`
from nx-dev.
## Related Issue(s)
NXC-3981
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`nx --version` (and other trivial CLI invocations) eagerly load the
daemon client, dotenv loader, analytics + perf-logging stack, and
`init-local` at the top of `bin/nx.ts`. That's ~225ms of unrelated
module load time before `main()` starts running.
On a typical workspace, the cost looks like:
| Layer | Time |
|---|---|
| `daemonClient` import | ~88ms |
| `perf-logging` (loads `analytics` → native binding) | ~63ms |
| `init-local` | ~36ms |
| `dotenv` | ~19ms |
| `analytics-prompt` | ~19ms |
End-to-end: `nx --version` was ~440ms via `pnpm exec`.
## Expected Behavior
`--version` (and other no-workspace fast paths) only load what they
actually need.
This PR:
- Lazy-loads the heavy modules inside the code paths that actually use
them (cloud commands, local install handoff, etc.).
- Adds a `--version` fast path at the top of `main()` that exits before
any heavy module is touched.
- Reworks `src/utils/perf-logging.ts` to lazy-require `analytics` /
daemon logger inside the `PerformanceObserver` callback. Importing the
module no longer pulls in the native binding.
- Updates `benchmarks/bench:*` scripts to invoke the workspace nx
directly via `node ../packages/nx/dist/bin/nx.js` instead of `pnpm exec
nx`, which removes ~220ms of pnpm wrapper overhead from the measurement
and works on CI agents (which don't sync per-project
`node_modules/.bin`). `goals.json` is adjusted to the new floor.
## Benchmark Results
Measured locally against the `benchmarks/` workspace (1,110 projects).
Deltas are shown as `(vs Goal | vs Baseline)`.
| Benchmark | Goal | Baseline | Current |
|---|---|---|---|
| version | 50ms | 440ms | **41ms** (-19% \| -91%) |
| show-projects | 100ms | 1.07s | **434ms** (+334% \| -60%) |
| cat-warm | 300ms | 1.88s | **1.04s** (+245% \| -45%) |
| copy-warm | 500ms | 1.48s | **1.31s** (+163% \| -11%) |
| build-warm | 750ms | 1.71s | **1.16s** (+54% \| -32%) |
`version` clears the goal; the other benchmarks pick up the
wrapper-overhead delta too, but several are still above goal and remain
targets for follow-up work.
## Related Issue(s)
N/A — internal performance work on the `speed-version` branch.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
The previous clean-up was overly zealous and removed both `sitemap.xml`
and `sitemap-0.xml` because I assumed there was nothing from Next.js
app. This restores both.
A follow-up will be to move these out of Next.js, but for now the root
one will point to both Framer and Next.js routes, and the latter has
`/courses/*` which we use still.
## Current Behavior
[isitagentready.com](isitagentready.com) scan flagged two gaps on
nx.dev: no agent-useful Link relation types in response headers, and no
Content-Signal directives in robots.txt.
## Expected Behavior
- Netlify serves Link response headers pointing agents at /llms.txt
(describedby), /llms-full.txt (service-doc), and /sitemap-index.xml
(sitemap).
- Production robots.txt declares permissive Content-Signal preferences
(search=yes, ai-input=yes, ai-train=yes) so AI crawlers know the docs
are intentionally open.
<img width="873" height="1269" alt="image"
src="https://github.com/user-attachments/assets/70fbcbee-04df-4f5c-b274-c360f420060d"
/>
Note: robots.txt won't work until we go to prod, so I'll re-run
benchmarks again once deployed.
## Related Issue(s)
DOC-479
---------
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
This PR updates our `@module-federation/enhanced` range from `^2.1.0` to
`^2.3.3` since `2.3.1` has a dependency on a compromised axios version.
This does not block existing workspaces from updating themselves, but
ensures `npm install` in an existing workspace will force an update of
the enhanced package.
Fixes ##35311
## Current Behavior
The `dev.nx.gradle.project-graph` Gradle plugin is pinned to version
`0.1.19` in the Nx Gradle plugin source and referenced version constant.
## Expected Behavior
The plugin version is updated to `0.1.20`, with an accompanying
migration that updates consumer `build.gradle(.kts)` files and version
catalogs automatically when users upgrade to `22.7.0-beta.16`.
## Related Issue(s)
No related issue.
## Current Behavior
The `main-linux` orchestrator job in `.github/workflows/ci.yml` installs
several system packages, Chrome, and Playwright browsers on every run:
```yaml
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y ca-certificates lsof libvips-dev libglib2.0-dev libgirepository1.0-dev
- name: Install Chrome
uses: browser-actions/setup-chrome@...
- name: Install Playwright
run: pnpm playwright install --with-deps
```
None of these are actually used by tasks the orchestrator executes. The
orchestrator runs `format:check`, `sync:check`, `conformance:check`,
`check-imports`, `check-lock-files`, `check-codeowners`, and kicks off
`nx affected` -- which distributes all
`lint`/`test`/`build`/`e2e`/`e2e-ci` tasks to Nx agents per
`.nx/workflows/dynamic-changesets.yaml`.
Historically these steps were copy-pasted during the CircleCI to GHA
migration (5b9d43f9) without evaluating whether the orchestrator
actually needed them. Tracing each back to its origin:
- **`lsof`** (added in c9cd67ea) -- only used by `kill-port` in e2e
tests (`e2e/next`, `e2e/storybook`, etc.), which run on agents.
- **`libvips-dev` / `libglib2.0-dev` / `libgirepository1.0-dev`** (added
in cfcedb48 for storybook/Next.js image tests) -- only needed if `sharp`
falls back to a source build. Sharp ships prebuilt binaries for
linux-x64 and is only reached via `astro-docs`/Next.js builds, which run
on agents.
- **`ca-certificates`** -- already present on `ubuntu-latest`.
- **`browser-actions/setup-chrome`** -- no orchestrator-local task uses
Chrome. No Karma/Puppeteer tests run here. Browser-driven tests run on
agents, which get Chromium via Playwright.
- **`pnpm playwright install --with-deps`** -- only needed by browser
tests, which run on agents (agents install Playwright themselves, see
`.nx/workflows/agents.yaml` lines 51-54).
## Expected Behavior
The `main-linux` orchestrator skips the unnecessary install steps,
saving ~30-60s per CI run with no functional change. Nx agents continue
to install these packages themselves when they need them.
The macOS React Native job (`main-macos`) keeps its Playwright install
because those e2e tests run directly on the macOS runner, not on agents.
## Related Issue(s)
N/A -- cleanup.
## Current Behavior
`inferExtensionsFromInputProperties` uses `is AbstractCompile` to
determine when to add `.class` as an inferred input extension. As a
result, Kotlin compile tasks are silently excluded from the `.class`
branch, producing incomplete inferred inputs in the Nx project graph for
Kotlin projects.
## Expected Behavior
All Kotlin compile tasks should be recognized alongside
`AbstractCompile` tasks and produce `.class` as an inferred input
extension, regardless of which Gradle plugin hierarchy they belong to.
Java-only projects are unaffected.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
The pseudo-IPC channel (used when Rust forks a Node wrapper that then
re-forks the Node task process) and the plugin isolation channel (used
between the main Nx process and its plugin-worker subprocesses) both
serialize every message with `JSON.stringify` / `JSON.parse`. That means
payloads such as `Buffer`, `Date`, `Error`, `undefined`, or objects with
cycles can't flow over these channels, even though the daemon
client/server already supports them via the shared `serialize()` helper
in `daemon/socket-utils.ts` (v8 with JSON fallback).
Notably, `Error` objects over the plugin channel currently flatten to
`{}` under JSON, losing stack and message.
The read-side `isJsonMessage ? JSON.parse :
v8.deserialize(Buffer.from(..., 'binary'))` detection was also
duplicated across three files.
## Expected Behavior
Pseudo-IPC and plugin isolation both use the same opt-in v8/JSON
serialization convention as the daemon channel, so non-JSON-safe
payloads flow through consistently. Default behavior is unchanged — v8
serialization is gated by `isV8SerializerEnabled()` and falls back to
JSON otherwise.
The duplicated read-side detection is hoisted into a single
`parseMessage<T>()` helper next to `isJsonMessage` in
`utils/consume-messages-from-socket.ts`. Round-trip coverage for the
helper is added.
Commits:
1. `fix(core): use v8 serialization in pseudo-IPC channel` — matches
daemon wire format on the pseudo-IPC server/client.
2. `refactor(core): hoist parseMessage helper for socket payloads` —
deduplicates the parse pattern and adds unit tests.
3. `refactor(core): adopt parseMessage/serialize in plugin isolation
IPC` — applies the same convention to the internal plugin-worker channel
(not public API).
## Related Issue(s)
N/A
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
Add support for `--redirect-to-prod` flag for 16, 17, 18 which we do not
have versioned docs for. Also update README.md with more details on how
Netlify and Squarespace are used to support versions docs.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
## Current Behavior
`nx-dev` is a monolithic Next.js app still serving blog, docs,
changelog, pricing, podcast, resources-library, whitepaper pages, etc. —
all of which now live in `astro-docs` (docs), `nx-blog` (blog), or have
been deprecated. The top-level `docs/` folder (~333MB) duplicates
content already in astro-docs, and 20+ `nx-dev/*` UI/feature libraries
are maintained despite only a handful being reachable from a live route.
## Expected Behavior
`nx-dev` only serves:
- `/ai-chat` — the AI chat UI
- `/api/query-ai-handler` — streaming chat endpoint
- `/api/query-ai-embeddings` — doc-search endpoint used by the Nx MCP
doc search tool
- `/courses` — video courses landing + detail + lesson pages (moved into
nx-dev from `docs/courses`)
### Changes
- Deleted routes: `/blog`, `/podcast`, `/pricing`, `/changelog`,
`/resources-library`, `/whitepaper-fast-ci`, `/500`, `/brands`, and
every other page that previously rendered here.
- Deleted `nx-dev/*` libraries not reachable from the surviving routes:
`feature-feedback`, `ui-podcast`, `ui-pricing`, `ui-resources` (plus a
handful of now-unused transitive helpers).
- Moved `docs/courses/` → `nx-dev/nx-dev/courses-content/` so the
top-level `docs/` folder could be deleted. Updated `CoursesApi` to
accept a configurable `authorsPath`.
- Deleted the entire top-level `docs/` folder (~333MB of stale content).
- Removed consumers of `docs/`:
- Removed `blog-description` and `blog-cover-image` conformance rules +
their registrations in `nx.json`.
- Removed
`scripts/documentation/{map-link-checker,internal-link-checker,prebuild-banner}.ts`.
- Removed `check-documentation-map` npm script.
- Removed `validateCrossSiteLinks` from `astro-docs/validate-links.ts`.
- `tools/documentation/create-embeddings` no longer includes
`docs/*.json` in its tsconfig; default `--mode` is now `astro`.
- Simplified `feature-ai`: dropped `feature-analytics`, `ui-common`, and
`ui-markdoc` deps. AI markdown rendering now uses a minimal inline
renderer on top of `@markdoc/markdoc` core.
- Simplified `_app.tsx`, `_document.tsx`, and `app/layout.tsx`: no more
`bannerCollection`, `GlobalSearchHandler`, `FrontendObservability`, or
GTM scripts.
- `/ai-chat` uses an inline minimal header instead of the full marketing
Header.
### Verification
- `pnpm nx build nx-dev` ✅ — emits all four routes above (`Route (app)`
for `/courses/*`; `Route (pages)` for `/ai-chat`, `/api/query-ai-*`).
- `/_redirects` still copied into `.next/` for Netlify.
- `/docs/*`, `/llms.txt`, `/llms-full.txt` rewrites to astro-docs
preserved.
## Related Issue(s)
Fixes DOC-478
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
## Current Behavior
Nx Cloud agents crash on warm-cache runs with:
```
Error: Failed to convert JavaScript value `Null` into rust type `String`
at DbCache.getBatch (.../cache.js:115:41)
at TaskOrchestrator.fetchCacheHits (.../task-orchestrator.js:270:47)
at TaskOrchestrator.resolveCachedTasks (.../task-orchestrator.js:564:38)
at runDiscreteTasks (.../init-tasks-runner.js:133:42)
code: 'StringExpected'
```
Regression introduced by #35172 (warm-cache perf optimization). Cloud
agents call `runDiscreteTasks` with `task.hash = null` (they
intentionally null hashes — see `ocean/.../execute-tasks-v3.ts`).
`init-tasks-runner.ts::createOrchestrator` queues `processTask` promises
via fire-and-forget `processAllScheduledTasks()`, but `runDiscreteTasks`
immediately calls `resolveCachedTasks` which doesn't await them.
`cache.getBatch(tasks.map(t => t.hash))` then receives nulls and the
napi binding rejects them.
## Expected Behavior
`resolveCachedTasks` awaits the queued `processTask` promises (which set
`task.hash` via `hashTask`) before passing hashes into `cache.getBatch`.
The single-task `runTaskDirectly` path already awaits
`this.processedTasks.get(task.id)` for the same reason — this fix
mirrors that pattern in the bulk path.
Bonus: a second tiny commit changes coordinator step 1's pre-hash guard
from `unhashed.length > 1` to `> 0`. The `> 1` micro-optimization
silently skipped cache lookup for single-task cycles, because
`resolveCachedTasksBulk` filters candidates by `task.hash &&` — a
length-1 unhashed task got dropped and ran without a cache check. Cost
more in lost cache hits than it saved in batch setup.
## Related Issue(s)
Surfaced internally; no GitHub issue.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
CI agents launched by `.nx/workflows/agents.yaml` are failing during the
`Install system deps` step on a majority of agents:
```
E: Failed to fetch http://archive.ubuntu.com/ubuntu/dists/jammy-updates/main/binary-amd64/Packages.gz
File has unexpected size (4263778 != 4263737). Mirror sync in progress? [IP: 91.189.92.22 80]
```
`archive.ubuntu.com` is serving a `Packages.gz` whose size/hash doesn't
match its own `InRelease` metadata while the canonical mirror is
mid-sync. Apt detects the mismatch and refuses to use the index. Without
that index, `apt-get install` fails to find packages and the agent dies
before any task runs. Every agent hits the same mirror, so every agent
fails simultaneously — taking down distributed CI runs.
This is currently affecting both PR CI and master CI on staging
(confirmed via `gh run` and Nx Cloud CIPE status).
## Expected Behavior
`apt-get update` succeeds reliably on agent provisioning, regardless of
canonical-mirror sync races.
Switch the agent's apt sources from `archive.ubuntu.com` /
`security.ubuntu.com` to `azure.archive.ubuntu.com` via a one-line `sed`
on `/etc/apt/sources.list`. Azure's mirror is what GitHub Actions
runners use by default — historically much more stable than canonical
for the "many concurrent CI agents hammering one mirror" workload that
triggers the sync race.
## Related Issue(s)
No GitHub issue — surfaced today via the linked CI failures.
## Current Behavior
Before this change, `nx/js/dependencies-and-lockfile` assumed
`pnpm-lock.yaml` contained a single YAML document.
When `pnpm-workspace.yaml` enables `managePackageManagerVersions: true`,
pnpm 11 writes a package-manager metadata document before the workspace
lock document. Nx then fails while building the project graph with:
```text
An error occurred while processing files for the nx/js/dependencies-and-lockfile plugin.
- pnpm-lock.yaml: expected a single document in the stream, but found more
```
## Expected Behavior
After this change, Nx reads multi-document pnpm lockfiles, selects the
workspace lockfile document, and continues parsing dependencies
normally.
This allows monorepos to keep pnpm 11 package-manager metadata enabled
while still running Nx commands such as `bun x nx sync` without
disabling `managePackageManagerVersions`.
## Related Issue(s)
Fixes Issue https://github.com/nrwl/nx/issues/35270
## Validation
- `pnpm exec jest
packages/nx/src/plugins/js/lock-file/pnpm-parser.spec.ts --config
packages/nx/jest.config.cts --runInBand`
- Verified `getPnpmLockfileNodes` against the repository multi-document
`pnpm-lock.yaml` via direct source invocation
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
- fix(core): allow controlling migration dep install concurrency
there are cases where parallel installs of a migration dependencies can
cause concurrent writes to package managers cache which can cause fs
errors of files overwriting each others peer deps. this most often
occurs when a migration for a 3rd party plugin is hosted in a private
registry that doesn't support custom metadata e.g. GH npm registry.
This is now controllable via the `NX_MIGRATE_INSTALL_CONCURRENCY` env
var. if it's not set then the default behavior of running all installs
in parallel still occurs
- docs(core): add `NX_MIGRATE_INSTALL_CONCURRENCY` env var info
## Current Behavior
The FreeBSD native build in the publish workflow fails with "filesystem
full" or OOM. Two issues:
1. **Jest plugin OOM**: PR #35231 changed `createNodes` to load all jest
configs upfront in a sequential `for` loop before hash computation. Each
`loadConfigFile` registers a ts-node transpiler via `registerTsProject`
(`packages/nx/src/plugins/js/utils/register.js`), whose dedup is
refcounted. Serial register/unregister cycles drive refCount to 0
between iterations and delete the Map entry — but ts-node's
`transpilerCleanup` is a no-op, so the service stays alive in
`require.extensions`. The next iteration creates a fresh ts-node
service. Across 96 TS jest configs under `NX_PREFER_TS_NODE=true` (set
for the FreeBSD build), 96 ts-node services stack and OOM at V8's ~2GB
heap limit.
2. **Core dump fills disk**: When the jest plugin OOMs, FreeBSD writes a
2.3GB `node.core` file to the workspace root, filling the remaining 4GB
of free disk space before cargo can run.
### Failing runs
- [beta.13 publish (Apr
15)](https://github.com/nrwl/nx/actions/runs/24480325293/job/71547815608)
— `filesystem full` during project graph, cargo never ran
- [Canary after beta.12 (Apr
10)](https://github.com/nrwl/nx/actions/runs/24260184601/job/70841801347)
— jest plugin OOM at 2GB heap, `node.core` filled disk
- [Diagnostics
run](https://github.com/nrwl/nx/actions/runs/24490680528/job/71574929552)
— confirmed 2.3GB `node.core` file in workspace root
### Passing run (with pnpm patch)
- [Fix validation run (Apr
16)](https://github.com/nrwl/nx/actions/runs/24508325992/job/71632565433)
— jest plugin no longer OOMs, cargo compiles successfully (validated an
earlier lazy-load form of the patch; the current form uses parallel load
but is equivalent for FreeBSD's resource envelope)
## Expected Behavior
The FreeBSD build completes successfully. The jest plugin loads configs
in parallel so the ts-node transpiler dedup holds across all
registrations and only one service is created.
### Changes
**Jest plugin memory fix** (`packages/jest/src/plugins/plugin.ts`):
- Convert the upfront config-loading `for` loop to
`Promise.all(validConfigFiles.map(async ...))`. Keeps all
`registerTsProject` registrations alive concurrently so refCount goes
`0→1→2→…→N→N-1→…→0` and only one ts-node service is ever created.
- Preserves #35231's hash correctness: preset path and tsconfig extends
chain remain inputs to `calculateHashesForCreateNodes`; `needsDtsInputs`
is still derived from the real jest config + ts-jest transform
inspection.
**pnpm patch** (`patches/@nx__jest@22.7.0-beta.12.patch`):
- Same fix applied to the installed `@nx/jest@22.7.0-beta.12` so the
FreeBSD build (which uses the published package, not source) gets the
fix immediately. Generated via `pnpm patch` from the built source output
— installed `plugin.js` is byte-identical to
`dist/packages/jest/src/plugins/plugin.js`.
**Workflow hardening** (`.github/workflows/publish.yml`):
- `ulimit -c 0` to disable core dumps (prevents 2.3GB files from filling
disk)
- `NODE_OPTIONS='--max-old-space-size=4096'` as a safety net
- Disk usage diagnostics on build failure for future debugging
### Local verification
Cold cache (`npx nx reset` before each run), `NX_PREFER_TS_NODE=true
NX_CACHE_PROJECT_GRAPH=false NX_DAEMON=false`, 96 TS jest configs:
- **Pre-fix (serial for-loop)**: per-iteration heap grows `40 → 60 → 160
→ 756 MB` at iter 1/10/20/30, then OOM at iter ~33 with
`--max-old-space-size=4096`.
- **Post-fix (parallel)**: heap flat at **272 MB from load 1 through
96**. `require.cache` constant at 599 entries. No accumulation.
- Real `nx show projects` end-to-end: 600 MB RSS, 5 s.
### Follow-up (separate PR)
`packages/nx/src/plugins/js/utils/register.js` has a latent leak: when
the transpiler has no real cleanup (ts-node always, swc sometimes),
`registered.delete(registrationKey)` on refCount==0 allows the next
registration with the same key to stack a fresh service. Any Nx plugin
that loads configs serially hits this. Worth gating `registered.delete`
on whether the transpiler is actually disposable, analogous to the
existing `isTsEsmLoaderRegistered` flag. Not in scope for this PR.
## Related Issue(s)
Fixes the FreeBSD build failure in the publish workflow.
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
## Current Behavior
No skill for investigating Nx sandbox violations.
## Expected Behavior
The `diagnose-sandbox-report` skill provides a structured workflow for
diagnosing sandbox violations, with a TypeScript script that automates
report parsing, Nx context gathering, violation validation, and file
classification.
## Current Behavior
Running cached tasks in a large workspace takes much longer than the
work actually warrants. The task orchestrator hashes, cache-checks,
schedules, and reports each task individually:
- Per-task JS→Rust→SQLite cache lookups (~N boundary crossings).
- Per-task daemon IPC calls for recording/matching output hashes (~2N
sequential round-trips).
- Per-task filesystem scans for output expansion.
- `TasksSchedule` re-sorts the full schedule array on every insert
(O(n²·log n)).
- The coordinator awaits each worker one at a time via a single-task
dispatch path.
On a 1,110-project benchmark workspace, this accumulates into
multi-second overhead even when every task is a cache hit with nothing
to do.
## Expected Behavior
Warm cache runs should resolve quickly. Every hot-path operation that
can be batched — cache lookups, daemon calls, scheduling, output-hash
tracking, filesystem scans — is batched, and the coordinator dispatches
discrete workers in parallel rather than sequentially.
### Rust native
- **`NxCache.get_batch`** (`cache.rs`) — single `UPDATE … WHERE hash IN
(…) RETURNING` with Rayon-parallel terminal-output reads replaces N
individual round-trips. Uses `rarray` for the `IN` clause, groups the
query / build stages as separate helpers, and collects rows straight
into a `HashMap`.
- **`get_files_for_outputs_batch`** — Rayon-parallel filesystem scanning
for cached-output expansion. Drop the old singular
`get_files_for_outputs` napi export now that every caller goes through
the batch path.
### Daemon output tracking
- Replace the non-batch `recordOutputsHash` / `outputsHashesMatch` chain
(client → server handlers → outputs-tracking helpers) with `*Batch`
equivalents. The single-entry path was dead after the orchestrator
routed everything through the batch methods.
- Short-circuit `outputsHashesMatchBatch` when the daemon has no
recorded hashes — avoids an unnecessary Rayon filesystem scan right
after `nx reset`.
- Skip recording for `local-cache-kept-existing`: the daemon already has
the right hash.
### Task orchestrator
- **Coordinator loop** rewrite: bulk-resolve all cache hits up-front,
batch-hash remaining unhashed tasks before any per-task lifecycle fires,
then dispatch cache-miss workers concurrently up to `parallelism`.
Tracks in-flight workers as a `Set<Promise<void>>` instead of a counter,
and the dispatch is extracted to `dispatchDiscreteWorker` +
`handleDiscreteWorkerFailure` instead of an inline fire-and-forget IIFE.
- **Split cache check from task execution**: `resolveCachedTasksBulk`
reports hits/misses without touching the execution path, so the
coordinator can batch lifecycle calls for the hit set and only dispatch
workers for misses.
- **Route `applyCachedResults` through `DbCache.getBatch`** — one daemon
call per cycle instead of one per task.
- **Group slots**: `closeGroup` can overflow without throwing when all
slots are claimed (parallelism gating is enforced elsewhere), and every
task keeps a single `groupId` through `runDiscreteTasks`.
- **Drop the dead single-entry fallback paths** in the orchestrator
(`shouldCopyOutputsFromCache`, `recordOutputsHash`, and the `processTask
will hash individually` try/catch around `hashTasks`).
- **`getExecutorForTask` takes a `projects` record directly** instead of
re-deriving it from the project graph per call via a module-level
`WeakMap`. Callers compute the record once.
### Task scheduler
- **Parallelism gating + sort stability** — batch scheduling collects
all schedulable roots, pushes them in one pass, and sorts the array once
per cycle instead of after every insert.
### Repo / infra
- `benchmarks/package.json`: run benches through `pnpm exec nx` so
hyperfine sub-shells always hit the workspace-local CLI.
- `fix(core): honor NX_NO_CLOUD / neverConnectToCloud in runner
selection` (`run-command.ts`) — surfaced while unblocking the benchmark
CI: with an ambient `NX_CLOUD_AUTH_TOKEN` on the CI agent,
`getTasksRunnerPath` still routed through the cloud shell even when
`NX_NO_CLOUD=true` was set. The cloud client's light-client require
bridge then loaded the default tasks runner from the parent workspace's
`node_modules/nx` (a published version), creating a cross-version API
mismatch. The guard now short-circuits runner selection to the default
path when cloud is explicitly opted out of.
- Test updates for the new `TasksSchedule(projectGraph, projects,
taskGraph, options)` signature.
## Benchmark Results
Measured locally against the `benchmarks/` workspace (1,110 projects)
with the `bench:*` scripts. Baseline is committed in
`benchmarks/baseline.json`.
| Benchmark | Goal | Baseline | Current | vs Goal | vs Baseline |
|---|---|---|---|---|---|
| version | 50ms | 348ms | 333ms | +566% | -4% |
| show-projects | 100ms | 710ms | 647ms | +547% | -9% |
| cat-warm | 300ms | 2.44s | 1.17s | +290% | -52% |
| copy-warm | 1.00s | 6.85s | 1.35s | +35% | -80% |
| build-warm | 5.00s | 17.75s | 1.44s | -71% | -92% |
## Related Issue(s)
Fixes#31067
No specific issue — performance-focused, driven by benchmark wall time
on large workspaces.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
Vite <8 uses esbuild to bundle config files (`vitest.config.mts`).
esbuild walks up from the entry point and reads every `tsconfig.json` in
ancestor directories plus their `extends` chains. These files are not
declared as task inputs, so changes to them don't invalidate the test
cache.
Sandbox violation:
https://staging.nx.app/runs/B5EjkJA6p1/task/angular-rspack-compiler%3Atest?batchId=7f8749c3-4e9a-4a93-b8f4-56efa69f14ab
## Expected Behavior
The `@nx/vite` and `@nx/vitest` plugins walk ancestor directories and
`extends` chains, declaring discovered tsconfig files as selective JSON
inputs that only hash `compilerOptions`. This avoids cache invalidation
from irrelevant tsconfig changes (`include`, `exclude`, `references`,
etc.) while correctly invalidating when compilation-affecting settings
change.
Files already covered elsewhere are excluded:
- Inside the project root (covered by `default`)
- The root tsconfig handled by the native `TsConfiguration` hasher
- Inside `node_modules` (invalidated via lockfile)
- Outside the workspace
## Current Behavior
No automated way to create static snapshots of the docs site for
versioned branches (e.g., v22.nx.dev). The existing `release-docs.ts`
force-pushes the full source branch, requiring Netlify to build from
source.
## Expected Behavior
`node ./scripts/create-versioned-docs.mts v22` creates a deployable
orphan branch with the pre-built static site:
- Fetches latest stable git tag for the major version (e.g., `22.6.4`)
- Builds `astro-docs` (v21+) or `nx-dev` with static export (v18-v20)
- Creates orphan branch with pre-built files at `nx-dev/nx-dev/.next/`
- Includes `netlify.toml` that skips `@netlify/plugin-nextjs` for pure
static serving
- Resolves `GITHUB_TOKEN` from 1Password or env var
- Server-side redirects for `/docs` → `/docs/getting-started/intro`
- `--force` flag to overwrite existing branches
Deployed via Netlify branch deploys at `v{major}.nx.dev`.
### Usage
```bash
node ./scripts/create-versioned-docs.mts v22
node ./scripts/create-versioned-docs.mts v21 --force
git push -f origin v22
```
### Tested
- v21 https://v21.nx.dev/docs
- v20 https://v20.nx.dev/docs
- v19 https://v19.nx.dev/docs
## Related Issue(s)
Fixes DOC-69
## Current Behavior
The secondary entry point generator rewrites all tsconfig
include/exclude entries by stripping the `src/` prefix (e.g.
`src/**/*.ts` → `**/*.ts`). This also strips `src/` from literal file
paths like `src/test-setup.ts`, breaking the exclude rule since the file
still lives at that path.
## Expected Behavior
The generator should add new include/exclude entries scoped to the
secondary entry point directory instead of mutating existing entries.
This is additive — existing entries are left untouched and new
`<name>/src/**/*.ts` patterns are appended for the secondary entry
point.
## Related Issue(s)
Fixes#33051
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
---------
Co-authored-by: Caleb Ukle <caleb@nrwl.io>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: meeroslav <meeroslav@users.noreply.github.com>
## Current Behavior
In large monorepos where the root tsconfig lacks `files`/`include`,
`@nx/next:server` (and other executors that rely on
`readTsConfigPaths()` via `withNx`) can hang for minutes and fail with
`ECONNREFUSED` as the Next.js server never gets a chance to start.
## Expected Behavior
`readTsConfigPaths()` returns the configured path mappings quickly
regardless of workspace size, so the Next.js dev server starts normally.
## Technical Details
`readTsConfigPaths()` only needs `compilerOptions.paths`, but
TypeScript's default `ParseConfigHost` enumerates every `.ts` file under
the tsconfig directory when `files`/`include` are absent. Stubbing
`readDirectory` on the host skips the source-file scan while preserving
`extends` resolution.
## Current Behavior
When generating a `@nx/node:application` from a pnpm workspace, the
generated `launch.json` sets `runtimeExecutable` to the full
`getPackageManagerCommand().exec` string (e.g. `"pnpm exec"`). On
Windows, VS Code rejects this:
```
Can't find Node.js binary "pnpm exec": path does not exist.
Make sure Node.js is installed and in your PATH, or set the
"runtimeExecutable" in your launch.json.
```
The same issue affects npm (`npm exec --`) and yarn (`yarn exec`).
## Expected Behavior
`runtimeExecutable` should be a real binary path (`pnpm`, `npm`,
`yarn`), and the `exec` / `exec --` tokens should live in `runtimeArgs`.
## Fix
Split `getPackageManagerCommand().exec` on whitespace, feed the first
token into `runtimeExecutable`, and prepend the remaining tokens to
`runtimeArgs`. Bun (`bunx`) is unaffected because it has no space.
## Related Issue(s)
Fixes#35276
HMR is freezing on compilation failures but should support recompilation
when updating files after a previous compilation error
## Current Behavior
When running dev server with HMR, compilation errors are freezing the
dev server
## Expected Behavior
HMR should not abort or freeze and should allow recompilation when
saving updated files.
Fixes#35040
## Current Behavior
Playwright reads tsconfig files that aren't declared as task inputs:
- The config loader walks the project tsconfig `extends` chain at
compile time.
- The Playwright worker reads the workspace root `tsconfig.json` at
runtime via `isUsingTsSolutionSetup` (called by `nxE2EPreset`).
When any of these files live outside the project root, sandboxed runs
report violations and cached tasks can become stale when those files
change.
## Expected Behavior
The `@nx/playwright` plugin walks the project tsconfig `extends` chain
and also declares the workspace root `tsconfig.json` (when present and
not already handled by the native `TsConfiguration` hasher), exposing
them as selective JSON filesets that hash only `compilerOptions`,
`extends`, `files`, and `include`. This invalidates the cache for
compilation-affecting changes while staying stable when unrelated fields
(e.g. `references`) churn.
Files already covered elsewhere are excluded:
- Inside the project root — covered by `default`
- The native `TsConfiguration` hasher file (`tsconfig.base.json` when it
exists, otherwise `tsconfig.json`)
- Inside `node_modules` — invalidated via the lockfile
- Outside the workspace
Conformance commands should remain as `nx-cloud conformance` not `nx
conformance`.
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Current Behavior
<!-- This is the behavior we have today -->
When `npm publish` or `pnpm publish` fails, the executor assumes
`err.stdout` (always) contains valid JSON. If a lifecycle script (e.g.
`prepublishOnly`) fails, it writes plaintext to stdout instead. This
causes `JSON.parse` to throw and the actual error to be swallowed by the
outer catch, printing only a generic "something unexpected went wrong"
message.
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Wrap `JSON.parse(err.stdout...)` in a try/catch block. If parsing fails,
fall back to logging raw stderr/stdout directly and return early, so
lifecycle script failures and other non-JSON errors are always visible
to the user.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes#34497
## Current Behavior
The `fileReplacements` option in `@nx/angular-rspack` is passed to the
Angular AOT compiler but not added to rspack's `resolve.alias`
configuration. This means file replacements only work during TypeScript
compilation, not during module resolution/bundling.
## Expected Behavior
File replacements should work consistently, replacing modules both
during compilation and bundling - matching the behavior of `@nx/rspack`.
## Related Issue(s)
https://github.com/nrwl/nx/issues/32647
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
## Current Behavior
Daemon graph errors are cached until a file change triggers
recomputation
## Expected Behavior
Avoid caching daemon errors, and refresh daemon env on request start
## AI Summary
This pull request introduces several important improvements to the Nx
daemon's client-server communication, focusing on standardizing message
types, improving environment variable handling, and cleaning up imports
and type usage. The main changes include the introduction of a unified
`DaemonMessage` type, a new mechanism for synchronizing environment
variables between client and daemon, and significant import and type
refactoring for clarity and maintainability.
**Key changes:**
### Message Type Standardization
- Introduced a new `DaemonMessage` type in `daemon-message.ts` to serve
as the standard for all messages exchanged between the Nx client and
daemon, replacing the previous generic `Message` type. This includes a
type guard function `isDaemonMessage` for runtime checks.
- Updated all relevant client and server methods, such as `sendMessage`,
`sendToDaemonViaQueue`, and `sendMessageToDaemon`, to use the new
`DaemonMessage` type, ensuring type safety and consistency throughout
the codebase.
[[1]](diffhunk://#diff-dbd790ee3e31e772f1c6d54bf0681b982d9eac6adacc3149463d28e60e11f59dL7-R9)
[[2]](diffhunk://#diff-dbd790ee3e31e772f1c6d54bf0681b982d9eac6adacc3149463d28e60e11f59dL26-R25)
[[3]](diffhunk://#diff-ac77bbed6ea29034032e92619972b1ad989e0accad1b05b5462a276707549da0L1053-R1062)
[[4]](diffhunk://#diff-ac77bbed6ea29034032e92619972b1ad989e0accad1b05b5462a276707549da0R1222-R1232)
### Environment Variable Synchronization
- Implemented a new mechanism to synchronize environment variables
between the client and daemon:
- Added a `getDaemonEnv` function to centralize the construction of the
environment object.
- Modified the client to send environment variables to the daemon with
the first message after startup, and the daemon to update its
`process.env` accordingly.
[[1]](diffhunk://#diff-ac77bbed6ea29034032e92619972b1ad989e0accad1b05b5462a276707549da0R1222-R1232)
[[2]](diffhunk://#diff-ac77bbed6ea29034032e92619972b1ad989e0accad1b05b5462a276707549da0L1323-R1343)
[[3]](diffhunk://#diff-0b91261b5503dc84fcf6ba119ee1c27ae7edff8cf9535a126a6a46439179f1fdR255-R260)
### Import and Type Refactoring
- Refactored imports in both client and server files to remove unused or
redundant imports, group related imports, and improve code organization
and readability.
[[1]](diffhunk://#diff-ac77bbed6ea29034032e92619972b1ad989e0accad1b05b5462a276707549da0R45-R52)
[[2]](diffhunk://#diff-ac77bbed6ea29034032e92619972b1ad989e0accad1b05b5462a276707549da0L86-L92)
[[3]](diffhunk://#diff-ac77bbed6ea29034032e92619972b1ad989e0accad1b05b5462a276707549da0L120-L123)
[[4]](diffhunk://#diff-d5bf3c66e62cac1884a071bf07fd1991320a3e62b07bfc03af3b9557b714c892R11-R17)
[[5]](diffhunk://#diff-d5bf3c66e62cac1884a071bf07fd1991320a3e62b07bfc03af3b9557b714c892R26-R29)
[[6]](diffhunk://#diff-d5bf3c66e62cac1884a071bf07fd1991320a3e62b07bfc03af3b9557b714c892L30-L43)
[[7]](diffhunk://#diff-0b91261b5503dc84fcf6ba119ee1c27ae7edff8cf9535a126a6a46439179f1fdL4-R135)
[[8]](diffhunk://#diff-0b91261b5503dc84fcf6ba119ee1c27ae7edff8cf9535a126a6a46439179f1fdR144-R148)
[[9]](diffhunk://#diff-0b91261b5503dc84fcf6ba119ee1c27ae7edff8cf9535a126a6a46439179f1fdR161-L173)
### Project Graph Error Handling
- Improved error handling in project graph recomputation by ensuring
that if errors are encountered, the cached project graph promise is
cleared, preventing stale or erroneous state from persisting.
These changes collectively make the daemon-client architecture more
robust, maintainable, and extensible, particularly as Nx evolves to
support more complex workflows and integrations.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`nx daemon` (status, no flags) calls `generateDaemonHelpOutput`, which
`spawnSync`s a helper Node process (`exec-is-server-available.js`) just
to bridge the async `daemonClient.isServerAvailable()` probe into a sync
caller. The only caller — `daemonHandler` in
`packages/nx/src/command-line/daemon/daemon.ts` — is already `async`, so
the sync workaround isn't needed.
That subprocess also has a bug. It is spawned with `cwd: __dirname`,
which points inside `packages/nx/dist/src/daemon/client`. When `nx` is
installed via a pnpm workspace symlink from a nested workspace (e.g. a
`benchmarks` project with `"nx": "workspace:*"`), the child's `cwd`
resolves through the symlink into the parent repo. The child's
workspace-root detection walks up from there and stops at the **outer**
`nx.json`, so the probe queries the wrong workspace's socket.
Reproducer:
```
cd benchmarks
nx daemon --start # starts benchmarks daemon, succeeds
nx daemon # reports "Nx Daemon is not running."
```
The daemon is running — the status command is just looking at the parent
repo's socket.
## Expected Behavior
`nx daemon` reports the status of the workspace it was invoked from,
regardless of how `nx` is installed, and without paying for a second
Node process.
This PR inlines the status check directly in `daemonHandler` via `await
daemonClient.isServerAvailable()` and deletes the two helper files
(`generate-help-output.ts`, `exec-is-server-available.ts`).
Behavioral equivalence: socket errors are already resolved to `false`
inside `isServerAvailable()`, so the "running"/"not running" outputs are
byte-identical. The one deliberate change is that `VersionMismatchError`
— which `isServerAvailable()` explicitly `reject`s — now surfaces to the
caller instead of being swallowed as "not running" by the old child's
try/catch.
## Related Issue(s)
N/A — surfaced while benchmarking against a nested workspace that
symlinks `nx` from the host repo.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The `index.d.ts` for native bindings had an outdated doc comment for
`inspectInputs` that didn't reflect the `JsonFileSet` resolution added
in #35248.
## Expected Behavior
The doc comment accurately describes `JsonFileSet` resolution behavior.
## Current Behavior
The `run-commands` and `run-script` executors use Node.js
`child_process.exec()` to run shell commands. `exec()` internally
buffers **all** stdout/stderr into memory and compares the total against
a `maxBuffer` limit (set to ~1GB via `LARGE_BUFFER`). When a command
produces output exceeding this limit, Node.js kills the child process
and throws `ERR_CHILD_PROCESS_STDIO_MAXBUFFER`:
```
NX stdout maxBuffer length exceeded
Pass --verbose to see the stacktrace.
```
This is the **default code path on CI**, because
`PseudoTerminal.isSupported()` checks `process.stdout.isTTY`, which is
`false` when stdout is piped (as it is on all CI runners). So while
local development typically uses the Rust PTY path (which streams output
with no buffer limit), every `run-commands` task on CI goes through the
`exec()` fallback.
The reason this hasn't been hit more often is that the 1GB
`LARGE_BUFFER` is large enough for most commands. But commands that
produce very large output quickly (e.g., deploying a site with thousands
of files) can exceed it.
## Expected Behavior
Commands that produce arbitrarily large output should complete
successfully without crashing Nx. Output should stream through data
events with no internal buffering limit.
This PR replaces `exec()` with `spawn()` + `{ shell: true }` in both the
`run-commands` and `run-script` executors. `spawn()` provides identical
shell-based command execution but uses streaming I/O — there is no
`maxBuffer` at all. Since both executors already consumed output via
stream event listeners (`stdout.on('data')`) rather than the `exec()`
callback, this is a safe swap with no behavioral change.
The PR also includes a test that directly demonstrates the issue: the
same command that crashes `exec()` with a maxBuffer error completes
successfully under `spawn()`.
## Related Issue(s)
N/A — encountered during a deploy task producing large stdout.
## Current Behavior
When configuring task inputs for cache hashing, Nx hashes entire files.
For JSON config files like `tsconfig.json` or `package.json`, changing
any field invalidates the cache — even fields irrelevant to the task
(e.g., changing `description` in package.json invalidates a build task).
The only special case is tsconfig, which has hardcoded selective hashing
in the native hasher.
## Expected Behavior
A new `json` input type allows users and plugins to specify exactly
which fields from a JSON file should be included in the hash. This
enables more granular cache invalidation.
```jsonc
// Only hash the "engines" field from package.json
{ "json": "{projectRoot}/package.json", "fields": ["engines"] }
// Hash all of compilerOptions except paths
{ "json": "{workspaceRoot}/tsconfig.json", "fields": ["compilerOptions"], "excludeFields": ["compilerOptions.paths"] }
```
### Features
- **`{workspaceRoot}` and `{projectRoot}` tokens** — same syntax as
`fileset` inputs
- **Glob support** — e.g. `{projectRoot}/tsconfig*.json`
- **Dot notation** — nested field paths like `compilerOptions.target`
- **Allowlist (`fields`) and denylist (`excludeFields`)** — can be used
together
- **Deterministic hashing** — canonical JSON serialization with sorted
keys
### Files changed
- **TypeScript**: `InputDefinition` type + JSON schemas for IDE support
- **Rust NAPI bridge**: `JsonInput` struct, `Either8` → `Either9`,
`Input::Json` variant
- **Rust hash planning**: `HashInstruction::JsonFileSet`, planner emits
it from `gather_self_inputs`
- **Rust hasher**: new `hash_json.rs` with field filtering, canonical
serialization, and 10 unit tests
## Related Issue(s)
<!-- Link related issues here -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
add a kb article explaining using nx w/ claude code sandboxes w/ the
recommended fix (`allowAllUnixSockets: true`) and alternative
workarounds with their tradeoffs.
added some cross-linking to this article so users can discover it in ai
specific pages
Fixes DOC-456
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: barbados-clemens <barbados-clemens@users.noreply.github.com>
The nx.dev/docs site has several SEO issues a few minor a few larger
impacts.
Changes:
- **robots.txt** is served by Next.js with correct `Sitemap:
https://nx.dev/sitemap.xml` and explicit AI crawler policies
- **AI crawlers** (GPTBot, ClaudeBot, Google-Extended, PerplexityBot,
OAI-SearchBot) have explicit Allow rules
- **llms.txt** and **llms-full.txt** are served correctly
(Astro-generated, proxied via Next.js)
- **Sitemap index** has no duplicate entries
- **Every docs page** has BreadcrumbList + TechArticle JSON-LD schema
markup
- **Logo images** are eager-loaded (`loading="eager"`), improving mobile
LCP
- **Docs sitemap** includes `lastmod` dates (build timestamp)
- **Security headers** (Referrer-Policy, Permissions-Policy) set on
Astro docs
- **Page title** expanded to 47 chars with key terms
- **Twitter card** meta tags explicitly set on all docs pages
- **CSS bundle** no longer scans unused @nx/nx-dev-ui-icons and
@nx/nx-dev-ui-animations packages
## Related Issue(s)
closes DOC-473
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Current Behavior
The Nx Release guide pages (`/docs/features/manage-releases` and
`/docs/guides/nx-release/*`) reference CLI commands like `nx release`,
`nx release version`, `nx release publish`, etc. but don't link to their
CLI reference pages. Developers configuring CI pipelines have to
navigate separately to find the full list of available flags and
options.
## Expected Behavior
Release guide pages now link to the relevant CLI reference pages
(`/docs/reference/nx-commands#nx-release-*`) so developers can quickly
look up available flags and options while reading the guides.
Changes across 8 files:
- **`features/manage-releases.mdoc`** — Added all 5 release subcommand
links to the "References" section
- **`publish-in-ci-cd.mdoc`** — Linked `nx release`, `nx release
publish`, `nx release version`, and `nx release changelog` where
subcommands are introduced
- **`file-based-versioning-version-plans.mdoc`** — Linked `nx release
plan` where the command is introduced
- **`release-projects-independently.mdoc`** — Linked `nx release` CLI
reference near the `--projects` flag discussion
- **`release-groups.mdoc`** — Linked `nx release` CLI reference in the
filters section
- **`release-docker-images.mdoc`** — Linked `nx release` CLI reference
near docker-specific flags
- **`programmatic-api.mdoc`** — Linked `nx release` CLI where the CLI is
mentioned as the counterpart to the programmatic API
- **`automatically-version-with-conventional-commits.mdoc`** — Linked
`nx release version` CLI reference for versioning options
## Related Issue(s)
<!-- Linear issue DOC-472: Add CLI reference links from Nx Release guide
-->
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: barbados-clemens <barbados-clemens@users.noreply.github.com>
## Current Behavior
The `nx-dev:test` task reads 124 `.js`/`.ts` files from the `.next/`
build output directory. These reads come from jest-haste-map's
filesystem crawl — it scans all files under the project root matching
`moduleFileExtensions` to build its module index and compute SHA-1
hashes, and `.next/` is not excluded.
## Expected Behavior
Jest should not scan the `.next/` build output directory during its
haste-map crawl, as these files are not needed for test discovery or
execution.
## Fix
Add `modulePathIgnorePatterns: ['<rootDir>/.next']` to the nx-dev jest
config. This tells jest-haste-map to skip the `.next/` directory
entirely during its initial filesystem crawl, eliminating all 124
unexpected file reads.
## Current Behavior
- The Next.js plugin infers `default` instead of `production` for build
inputs, meaning test file changes invalidate the build cache
unnecessarily
- The Next.js plugin doesn't infer `.d.ts` dependent task output files,
causing sandbox violations when builds read type declarations from
dependencies
- nx-dev's `project.json` overrides inputs but is missing
`externalDependencies: [next]` and `.d.ts` dependent task outputs that
the plugin would normally provide
- nx-dev's `next:build` doesn't depend on `^build` or `^typecheck`, so
dependency type declarations aren't produced before the build
- `banner.json` (a generated file) was listed as a fileset input instead
of a dependent task output file
- `banner.json` was not excluded from eslint
## Expected Behavior
- Next.js plugin uses `production` for build inputs (matching Vite)
- Next.js plugin infers `dependentTasksOutputFiles: **/*.d.ts` for
builds
- nx-dev build has all necessary inputs and dependsOn to work correctly
with the sandbox
- Generated files are properly handled as dependent task outputs
## Related Issue(s)
N/A - discovered during sandbox violation investigation
On Node 22.18+ (which supports native TypeScript execution via type
stripping), Nx incorrectly warns "Unable to locate swc-node or ts-node.
Nx will be unable to run local ts files without transpiling." even
though Node can handle .ts files natively without any transpiler.
The warning should only appear on Node versions that cannot natively
execute TypeScript files. On Node 22.6+ (where
process.features.typescript is truthy), no warning should be emitted
when swc-node and ts-node are absent.
Fixes#32567
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
<!-- 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 -->
Somehow, these versions got out of sync
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
These versions are in sync with the package.json in the root
## 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>
## Current Behavior
On Windows, `nx serve project --configuration=some-i18n-configuration`
fails with "cannot find project in graph.nodes" because
`posix.normalize()` does not convert backslashes to forward slashes.
Windows-style paths from `path.relative()` produce backslash-separated
strings that never match the forward-slash keys in the project root map.
## Expected Behavior
i18n configurations should resolve correctly on Windows. The path lookup
in `findProjectForPath` should succeed regardless of whether the input
path uses backslashes (Windows) or forward slashes (POSIX).
## Related Issue(s)
Fixes#32864
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
## Current Behavior
Vitest test targets can resolve workspace dependency imports to build
artifacts (e.g., `dist/*.js`). These build outputs aren't declared as
task inputs, causing sandbox I/O violations (unexpected reads from
dependency `dist/` directories).
## Expected Behavior
The vite and vitest plugins include `dependentTasksOutputFiles` in the
inferred test target inputs. This declares dependency build outputs as
inputs when a test target depends on build tasks via `dependsOn`. The
input is a no-op when there are no build task dependencies. When
`typecheck.enabled` is configured, `.d.ts` files are also included since
`tsc --noEmit` reads type declarations from dependencies.
Also removes redundant explicit `inputs` overrides from `angular-rspack`
and `angular-rspack-compiler` test targets — the plugin-inferred
defaults are now more complete.
## Current Behavior
- `/powerpack` redirects to the `/enterprise` marketing page, which
doesn't help users who have an expired or missing activation key for
shared cache plugins (`@nx/s3-cache`, `@nx/gcs-cache`, etc.)
- The self-hosted caching guide lists available packages but requires
clicking through to individual reference pages to find install commands
and key setup instructions
- No short, stable URL exists for CLI error messages to link to
## Expected Behavior
- `/powerpack` redirects to the self-hosted caching guide
(`/docs/guides/tasks--caching/self-hosted-caching`), which is the right
landing page for cache plugin users
- `/powerpack/conformance` and `/powerpack/owners` redirect to their
respective enterprise docs pages
- A new `/remote-cache` short URL points to the self-hosted caching
guide for use in CLI error messages (e.g., the `nx-key` hardcoded link
in ocean)
- The self-hosted caching page includes a table with install commands
(`nx add @nx/...`), key setup instructions (`.nx/key/key.ini`, `NX_KEY`
env var, `nx register`), and a section pointing former Powerpack users
to conformance/owners enterprise docs
Preview:
https://deploy-preview-35240--nx-docs.netlify.app/docs/guides/tasks--caching/self-hosted-caching
## Related Issue(s)
Fixes DOC-477
**Follow-up:** The `nx-key` hardcoded link in the ocean repo
(`libs/nx-packages/nx-key/src/consts.rs` L1) should be updated to
`nx.dev/remote-cache` in a separate PR.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Nx generators set `compilerOptions.baseUrl: "."` in generated tsconfig
files and write path mappings as bare relative paths (e.g.,
`my-lib/src/index.ts`). `baseUrl` is deprecated in TS 6 and removed in
TS 7.
## Expected Behavior
Nx no longer generates `baseUrl` in any tsconfig. Path mappings use `./`
prefix (e.g., `./my-lib/src/index.ts`), making them relative to the
tsconfig file without needing `baseUrl`. Existing user tsconfigs with
`baseUrl` continue to work correctly.
### Generator and template changes
- Remove `baseUrl` from all templates and generator code
- `addTsConfigPath` normalizes lookup paths with `./` prefix
- Move and remove generators handle `./`-prefixed paths correctly
- Angular secondary entry points and Remix server entry paths use `./`
prefix
### `resolvePathsBaseUrl` helper
New function (in `ts-config.ts`, duplicated in `register.ts`) that walks
the tsconfig `extends` chain to determine the correct directory for
resolving `paths` values. Finds where `paths` is defined, then looks for
the applicable `baseUrl` from that point toward the root — ignoring
child overrides that don't apply to the paths-defining tsconfig. When no
`baseUrl` applies, returns the directory of the tsconfig that defines
`paths`. All path resolver plugins and buildable-libs-utils use this
helper.
### Runtime and bundler fixes for baseUrl-less tsconfigs
- **Rollup**: resolve path mappings to absolute in compiler options
override using `resolvePathsBaseUrl`; use original tsconfig path (not
tmp) for resolution base
- **Module Federation**: add `workspaceRoot` to `resolve.modules` in all
8 MF plugin variants (Angular/React webpack, Angular/React rspack,
webpack SSR, rspack SSR, Angular rspack plugin, rspack plugin) so
workspace-relative expose paths resolve without `baseUrl`
- **Next.js/Jest**: null out SWC `resolvedBaseUrl` in generated jest
configs to prevent SWC from doing incorrect path alias resolution; Nx
jest resolver handles this via `resolvePathsBaseUrl`
- **register.ts**: use `resolvePathsBaseUrl` for correct path alias
registration
- **Path resolver plugins** (webpack, rspack, vite, expo, react-native,
jest, react component testing): use `resolvePathsBaseUrl` for correct
path resolution
- **buildable-libs-utils**: resolve tmp tsconfig paths to absolute so
they work without `baseUrl`
- **eslint-plugin**: handle `./`-prefixed paths in AST utils
## Related Issue(s)
Fixes#32958
## Current Behavior
Searching for `nx.json` on nx.dev/docs does not surface the actual
nx.json reference page in the top results. The `.NET Plugin for Nx` and
other technology introduction pages rank higher because they have
`weight: 5` in frontmatter, which inflates their body text scoring via
Pagefind's `data-pagefind-weight` attribute (~25x impact via quadratic
scaling). The same issue affects `project.json`, `inputs`, and other
reference pages.
## Expected Behavior
Reference pages whose title exactly matches the search query should rank
first or near the top. Technology introduction pages should still be
discoverable for their framework name but should not outrank reference
pages for terms they merely mention in body text.
**After this change:**
- `nx.json` → "nx.json Reference" is `#1` (was `#7+`)
- `project.json` → "Project Configuration" is `#1`
- `angular` → "Angular Plugin for Nx" is `#1` (preserved)
- `nest` → "Nest.js Plugin for Nx" is `#1` (preserved)
- `react` → "React Plugin for Nx" is `#2` (React Native `#1` — valid
match)
**Changes:**
- Reduce `weight` on 35 technology introduction pages from `5` → `2` to
reduce body text inflation while keeping a modest boost
- Reduce `weight` on adding-to-monorepo guide from `6.4` → `2`
- Adjust Pagefind ranking params: `termFrequency: 0.75 → 0.65`,
`pageLength: 0.5 → 0.3` to reduce the penalty on long reference pages
## Related Issue(s)
Fixes DOC-475
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Current Behavior
After the custom eslint hasher was removed in d64aeef5df, the
`@nx/eslint:lint` executor target defaults are missing `^default` and
`{workspaceRoot}/tools/eslint-rules/**/*` from their inputs. The old
custom hasher was compensating for this by manually handling dependency
hashing, but now that it's gone, the inputs need to be self-sufficient.
This means:
- Changes in dependencies don't invalidate the lint cache (problematic
for type-aware rules that inspect imported types)
- Changes to custom workspace eslint rules don't trigger re-linting
The inferred/plugin path (`plugin.ts`) already includes these inputs
correctly.
## Expected Behavior
The executor target defaults should include `^default` (dependency file
changes) and `{workspaceRoot}/tools/eslint-rules/**/*` (custom workspace
rules) to match what the eslint plugin already sets for inferred
targets.
## Related Issue(s)
Follows up on d64aeef5df (remove custom eslint hasher).
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
When running `nx init` in an empty git directory (no `package.json`),
the V2 init handler silently defaults to the `.nx` installation method
without prompting the user. This may not be suitable for users who
intend to create a JavaScript/TypeScript project and would prefer a
`package.json`-based setup.
## Expected Behavior
When running `nx init` in an empty git directory, users are now prompted
to choose between two setup methods:
- **`.nx installation`** — recommended for non-JavaScript projects
(Gradle, .NET, etc.)
- **`package.json installation`** — recommended for
JavaScript/TypeScript projects
The prompt only appears when:
- No `package.json` exists in the directory
- The `--useDotNxInstallation` flag was not explicitly passed
- Running in interactive mode (not AI agent mode)
If the user chooses `package.json`, a minimal `package.json` is created
and the existing npm-repo setup flow takes over. If they choose `.nx`,
the existing dot-nx setup flow is used. In both cases, the workspace is
created in the current directory (not a subfolder).
## Related Issue(s)
Fixes NXC-3983
## Current Behavior
When a `package.json` has both a script entry and an `nx.targets` entry
for the same target name, and the `nx.targets` entry uses command
shorthand (`command: "tsc"`) or an explicit executor, the two targets
are merged together. This produces an invalid hybrid target that has
both `executor: "nx:run-script"` (from the inferred script target) and
the `command` property (from the nx prop), causing the node to fail to
merge into the project graph.
## Expected Behavior
When the `nx.targets` entry specifies how to run (via `executor` or
`command`), it should completely overwrite the inferred script target
instead of merging with it. Targets without `executor` or `command`
(e.g., just adding `outputs` or `dependsOn`) should continue to merge as
before.
## Related Issue(s)
Fixes NXC-3923
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The `dotnet:build-analyzer` target runs `node
./scripts/run-native-target.js _build-analyzer dotnet` but does not
include the `run-native-target.js` script itself in its `inputs` array.
This means changes to the script won't invalidate the Nx cache,
potentially leading to stale cached results.
## Expected Behavior
The `run-native-target.js` script is included as an input to the
`build-analyzer` target, ensuring cache correctness when the script
changes.
## Related Issue(s)
Fixes NXC-4219
Large refactor for `nx show target` to increase clarity and fixup a few
issues.
## Custom Hashers
Notes when a custom hasher is used and inputs will not be considered.
See screenshots
<img width="646" height="107" alt="image"
src="https://github.com/user-attachments/assets/a6fecf17-a512-4956-964b-f04557567334"
/>
<img width="646" height="225" alt="image"
src="https://github.com/user-attachments/assets/0d4e0a85-2a8b-4bac-b0ff-4b6ea9e6b0dd"
/>
## Duplicated inputs
- Fixes issue where inputs could show up multiple times and appear
identical. In practice, this was from filesets that had specific
projects arrays that differed.
e.g. `nx show target populate-local-registry-storage` in latest shows
```
Inputs:
- !{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)
- !{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)
- !{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)
- !{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)
- !{projectRoot}/.eslintrc.json
- !{projectRoot}/.eslintrc.json
- !{projectRoot}/.storybook/**/*
- !{projectRoot}/.storybook/**/*
- !{projectRoot}/jest.config.[jt]s
- !{projectRoot}/jest.config.[jt]s
- !{projectRoot}/src/test-setup.[jt]s
- !{projectRoot}/src/test-setup.[jt]s
- !{projectRoot}/tsconfig.spec.json
- !{projectRoot}/tsconfig.spec.json
- !{projectRoot}/tsconfig.storybook.json
- !{projectRoot}/tsconfig.storybook.json
- default
- default
- {projectRoot}/**/*.rs
- {projectRoot}/**/Cargo.*
- {workspaceRoot}/scripts/local-registry
- {"runtime":"node -p '`${process.platform}_${process.arch}`'"}
- {"runtime":"rustc --version"}
- {"externalDependencies":["npm:@monodon/rust","npm:@napi-rs/cli"]}
```
After this PR it is
```
Inputs:
- {projectRoot}/**/*.rs
- {projectRoot}/**/Cargo.*
- {workspaceRoot}/.cargo/config.toml
- {workspaceRoot}/Cargo.lock
- {workspaceRoot}/Cargo.toml
- {workspaceRoot}/clippy.toml
- {workspaceRoot}/scripts/local-registry
- {"input":"production","projects":["tag:npm:public"]}
- {"input":"production","projects":["tag:maven:dev.nx.maven"]}
- {"runtime":"node -p '`${process.platform}_${process.arch}`'"}
- {"runtime":"rustc --version"}
- {"externalDependencies":["npm:@monodon/rust","npm:@napi-rs/cli"]}
Outputs:
- {workspaceRoot}/dist/local-registry/storage
```
## Others
- When running with --verbose, data includes its source location.
- Changes defaultConfiguration display to `(default)` badge after a
config if it is indeed default, instead of (default: ...) at the end of
the list
Fixes NXC-4077
Fixes NXC-4068
Fixes NXC-4200
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
## Current Behavior
The `@nx/eslint` plugin does not infer extended tsconfig files as inputs
for the inferred lint target. Projects whose `tsconfig.json` extends a
file outside the project root (e.g. `../../tsconfig.base.json`) omit
that upstream file from the lint task's inputs. Tools that walk the
tsconfig chain during linting — the typescript-eslint parser in
type-aware mode, `@nx/enforce-module-boundaries`, Angular template
parsers, and similar — read these files, so sandboxing reports them as
undeclared reads and changes to upstream tsconfigs don't invalidate the
lint cache.
## Expected Behavior
The inferred lint target declares every tsconfig file reached via the
`extends` chain of the project's `tsconfig.json` as an input, when those
files live outside the project root. Files inside the project root are
already covered by `default` (`{projectRoot}/**/*`); shareable configs
resolved from `node_modules` are invalidated via the lockfile; paths
that escape the workspace cannot be declared as `{workspaceRoot}/...`
inputs.
A new lightweight `walkTsconfigExtendsChain` helper is introduced in
`@nx/js/src/internal` so other plugins that need to inspect a tsconfig
extends chain can reuse it. It reads tsconfigs as JSONC without loading
the `typescript` package, walks `extends` arrays in reverse precedence
(matching TypeScript semantics), and accepts a visitor that can
short-circuit for precedence-aware lookups or walk exhaustively for
input collection. The helper is cycle-safe and accepts a caller-supplied
JSON cache to dedupe reads across overlapping chains.
## Current Behavior
When ts-jest runs without `isolatedModules`, it creates a TypeScript
Language Service that reads `.d.ts` files from dependency projects.
Changes to those `.d.ts` files don't invalidate the test cache, leading
to stale test results.
## Expected Behavior
The jest plugin detects ts-jest usage without `isolatedModules` and adds
`dependentTasksOutputFiles: '**/*.d.ts'` as a transitive input. This
ensures dependency type declaration changes correctly invalidate the
test cache.
The plugin:
- Inspects jest config transforms (including presets) for ts-jest
- Walks the tsconfig `extends` chain to resolve the effective
`isolatedModules` value, reusing the lightweight walker from `@nx/js`
(intentionally avoids loading `typescript` for performance)
- Handles `verbatimModuleSyntax` as implying `isolatedModules`
- Respects ts-jest v29 vs v30 semantics for the deprecated
`isolatedModules` transform option
- Mirrors `ts.findConfigFile` upward walk (capped at workspace root)
when no explicit tsconfig is configured
- Tracks external file references (presets, tsconfigs outside project
root) for correct hash computation
## Additional Changes
- **Plugin hash correctness**: config loading moved before hash
computation so external file references (preset files, tsconfig extends
chains) are included in the hash. Previously, changes to files outside
the project root that the plugin reads during inference (e.g., a shared
jest preset or base tsconfig) would not invalidate the cached target
configuration. The lockfile is also now included as a hash input.
- **e2eInputs**: added `dependentTasksOutputFiles` to the `e2eInputs`
named input in `nx.json` since e2e target defaults override
plugin-inferred inputs.
- **Jest preset**: pre-resolve the `@swc-contrib/mut-cjs-exports` SWC
plugin to an absolute path. Tests that `chdir` into temp dirs would fail
SWC plugin resolution since the temp dir has no `node_modules`.
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## 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: juristr <juristr@users.noreply.github.com>
## Current Behavior
When using `generateLockfile: true` (e.g. with `@nx/next:build`), the
generated `package-lock.json` is missing overridden packages for two
reasons:
1. `normalizePackageJson()` strips the `overrides` field, so the
generated lockfile lacks overrides both at the top level and in
`packages[""]`.
2. `findTarget()` uses semver satisfaction to match dependency edges,
but npm overrides can force versions outside the declared range (e.g.
`minimatch@^9.0.4` overridden to `10.2.1`). This causes overridden
packages and their transitive deps to be dropped from the pruned graph
entirely.
Running `npm ci` in the output directory fails:
```
npm error Missing: minimatch@10.2.1 from lock file
```
Note: yarn (`resolutions`) and pnpm (`pnpm.overrides`) were already
working correctly.
## Expected Behavior
The generated `package-lock.json` includes `overrides` and all
overridden packages. `npm ci` succeeds.
This is tested in the original issue repro repo, where with the applied
patch `npm ci` works from dist.
<img width="1272" height="362" alt="image"
src="https://github.com/user-attachments/assets/2cdbb266-71f8-45c8-8ee0-cec6dcd12705"
/>
The missing parts are both `overrides` in `package.json`, but also the
lockfile must include the pacakges in the overrides. In this example it
is like this in `package-lock.json`:
```
"node_modules/minimatch": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz",
"integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
```
## Related Issue(s)
Fixes#34529
The custom eslint hasher was an optimization from the Node.js hashing
era that stripped dependency file hashes for non-type-aware lint rules.
With the native Rust hasher this optimization is no longer needed.
Deprecate `hasTypeAwareRules` option (removal in v23).
The example builds read dependent task outputs (devkit, angular-rspack,
angular-rspack-compiler dist JS files) and shared workspace files
(patch-devkit-request-path.js, tsconfig.base.json) that were not
declared as inputs, causing sandbox violations.
## Current Behavior
The `angular:test` task sandboxing run shows unexpected file reads from
`@nx/storybook` and `@nx/playwright` packages — including `.template`
and `__tmpl__` generator files. These packages are loaded dynamically
via `ensurePackage()` calls (in `generate-storybook-configuration.ts`
and `add-e2e.ts`), so the static dependency analyzer doesn't detect them
as dependencies of the angular project. This means their files are not
included in the test task's input hash, causing sandbox violations.
Relevant run: [staging.nx.app task
run](https://staging.nx.app/runs/B5EjkJA6p1/task/angular%3Atest?batchId=77ad1700-bc33-4663-b344-cbfab899c6c4)
## Expected Behavior
The `storybook` and `playwright` projects are declared as
`implicitDependencies` of the angular project (alongside the existing
`vite` entry which exists for the same reason). This ensures their files
are included in the angular test task's input hash via the `^production`
input qualifier, resolving the sandbox failures.
## Related Issue(s)
Fixes NXC-4216
## Current Behavior
The `astro-docs:format` target is cached but has no explicit `inputs`,
so it uses the default named input which only tracks
`{projectRoot}/**/*`. Changes to workspace-root prettier configuration
files (`.prettierrc`, `.prettierignore`) don't invalidate the cache,
potentially returning stale format check results.
## Expected Behavior
The `format` target explicitly lists its inputs: the `.mdoc` files it
formats and the prettier config files that control formatting behavior.
This ensures the cache invalidates correctly when prettier configuration
changes.
## Related Issue(s)
Fixes NXC-4217
## Current Behavior
Three packages (`angular-rspack-compiler`, `angular-rspack`, `esbuild`)
have build targets that run `copy-readme.js` but don't correctly declare
all required inputs:
- `angular-rspack-compiler` and `angular-rspack` manually listed partial
inputs (missing `.prettierignore` and using a bare directory path
`{workspaceRoot}/scripts/readme-fragments` that doesn't resolve to
files)
- `esbuild` had no inputs at all for its build target, falling back to
the default `["production", "^production"]`
This means changes to `.prettierignore` or `readme-fragments/*.md` would
not invalidate the build cache for these projects.
## Expected Behavior
All three packages use the `copyReadme` named input (defined in
`nx.json`), consistent with every other package in the repo. This
ensures `.prettierignore`, `readme-fragments/**/*`, `copy-readme.js`,
and project README files are all correctly tracked as build inputs.
## Related Issue(s)
Fixes NXC-4214
## Current Behavior
`nx init` uses a different cloud prompt (`code: "enable-ci"`, message:
"Would you like to enable AI-powered Self-Healing CI and Remote
Caching?") with only Yes/Skip choices. There is no way to permanently
opt out of the prompt, and package manager install output is printed
during init.
## Expected Behavior
`nx init` now uses CNW's variant 1 prompt copy:
- **Prompt:** "Enable remote caching to speed up builds with Nx Cloud?"
- **Footer:** "Free for small teams. 2-minute setup with GitHub — cache
locally and in CI"
- **Choices:** Yes / Skip for now / No, don't ask again
Behavior per choice:
| Choice | Action |
|--------|--------|
| **Yes** | Connect to Nx Cloud (set `nxCloudId`) |
| **Skip for now** | Do nothing |
| **No, don't ask again** | Set `neverConnectToCloud: true` in nx.json |
Additional changes:
- Telemetry now tracks the raw choice via `nxCloudArg` field
(yes/skip/never)
- `nx migrate` cloud prompt also supports the "never" choice
- Install stdout suppressed during init (stderr preserved for errors)
<img width="1392" height="978" alt="init"
src="https://github.com/user-attachments/assets/fdacc72b-64ed-4e87-b4c3-01a467051e24"
/>
## Related Issue(s)
Fixes NXC-4189
The setInterval(async, 10) polling loop used to connect to plugin
workers created overlapping connection attempts because setInterval does
not await its callback. This caused phantom socket connections, event
loop saturation, and cascading worker deaths.
Changes:
- Replace setInterval with recursive setTimeout so only one connection
attempt is ever in flight at a time
- Close the worker's server on first connection to reject phantom
connections that would create duplicate load timeouts
- Detect worker exit during polling and reject immediately instead of
burning through 10,000 attempts against a dead socket
- Clear _connectPromise on failure so ensureAlive() retries instead of
re-awaiting a permanently rejected promise
Fixes: #34388
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`closed` issues weren't scraped correctly. If no prior data was present,
we scraped the full repo which caps at 10k issues, when we have 11k now.
## Expected Behavior
Scraping works consistently
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
The `native` named input in `nx.json` only includes
`{projectRoot}/**/Cargo.*`, which misses workspace-root files that
Cargo/Clippy read: `Cargo.toml` (workspace manifest), `Cargo.lock`,
`clippy.toml`, and `.cargo/config.toml`. This means changes to these
files don't invalidate the cache for native tasks.
Additionally, Cargo writes intermediate build artifacts to
`dist/target/` which causes sandbox violations in Nx Cloud CI.
## Expected Behavior
Native task cache is correctly invalidated when workspace-root
Cargo/Clippy config files change. Sandbox no longer flags `dist/target/`
reads/writes as violations.
## Related Issue(s)
N/A
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
React module federation e2e suites that exercise webpack generation
paths are failing because webpack 5.106.0 removed
`lib/util/create-schema-validation.js`, which
`@module-federation/enhanced@2.3.1` still depends on.
## Expected Behavior
These known-broken suites should be skipped until the upstream module
federation dependency is compatible with webpack 5.106.0+ so they do not
keep failing CI.
## Related Issue(s)
N/A
## Summary
This PR temporarily disables the affected React module federation e2e
suites by switching their top-level test groups to `describe.skip(...)`
and adding a short note about the webpack /
`@module-federation/enhanced` incompatibility.
It covers the webpack-specific suites as well as the mixed Rspack
interoperability/convert flows that still generate webpack-based module
federation apps.
## Validation
- `npx prettier --write` on the modified test files
- `git push -u origin fix/disable-mf-webpack-tests`
- repository pre-push hook passed during push (`nx` prepush checks)
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
When e2e tests publish packages to the local Verdaccio registry with the
same version number (e.g., `23.0.0`), npm and yarn serve stale cached
tarballs from previous test runs instead of fetching the freshly
published packages. This causes e2e tests to run against outdated code.
## Expected Behavior
Each e2e test run uses a fresh package manager cache directory, ensuring
that npm and yarn always fetch the latest packages from the local
registry — even when the version number hasn't changed.
- **npm**: `npm_config_cache` set to a temp directory
- **yarn v1**: `YARN_CACHE_FOLDER` set to a temp directory
- **yarn v2**: `YARN_ENABLE_GLOBAL_CACHE` set to `false`
- **pnpm**: not affected (content-addressed store)
- **bun**: already handled via `--no-cache` flag
## Related Issue(s)
<!-- No specific issue — discovered during local e2e testing -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
If the publish fails for any reason, the `package.json` files are not
reset, this can leads to `expand-deps` erroring in subsequent runs. Wrap
`resetPackageJsons` in finally block.
Also ensures `expand-deps` can run twice without errors, at least
locally, not in CI.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
When Nx pulls `nx@latest` into a temporary directory (for example from
daemon latest resolution or `configure-ai-agents`/`init` handoff), Yarn
Berry can still run lifecycle scripts unless disabled via environment
configuration.
## Expected Behavior
All temporary `nx@latest` installs disable lifecycle scripts
consistently, including Yarn Berry, by always setting
`YARN_ENABLE_SCRIPTS=false` in the install process environment.
## Related Issue(s)
N/A. Follow-up parity change related to
https://github.com/nrwl/nx-console/pull/3108.
Fixes #N/A
## Current Behavior
When Nx loads multiple .env files (such as `apps/nx-22-5/.env`,
`.local.env`, `.env.local`, and `.env`), each file is loaded and has its
variables expanded in sequence. This means referencing works up the
priority list but not down. For example, root `.env` correctly
references variables in project-specific `apps/example/.env`, but not
vice versa.
**Example:**
If root `.env` contains:
```env
WILL_RESOLVE=$FIRST_APP_NAME
GLOBAL_NX_VERSION=22.5.1
```
And `apps/nx-22-5/.env` contains:
```env
WILL_NOT_RESOLVE=$GLOBAL_NX_VERSION
FIRST_APP_NAME=nx-22-5
```
The `WILL_NOT_RESOLVE` variable will not expand correctly to `22.5.1`.
Instead, it becomes an empty string because `GLOBAL_NX_VERSION` is not
available in the environment at the time `apps/nx-22-5/.env` is being
processed. However, `WILL_RESOLVE` correctly resolves to` nx-22-5`.
This is because project-specific .env files are loaded before root .env
files, and variable expansion happens immediately during loading rather
than after all files are loaded.
## Expected Behavior
Variables in one .env file should be able to reference variables from
other .env files that are loaded together, regardless of their loading
priority. Both up-chain and down-chain references should work.
Using the example above:
- `WILL_RESOLVE` should resolve to nx-22-5 ✅ (works today)
- `WILL_NOT_RESOLVE` should resolve to 22.5.1 ✅ (fixed by this PR)
## Changes Made
Updated `loadAndExpandDotEnvFile` in
`packages/nx/src/tasks-runner/task-env.ts` to accept an array of file
paths instead of a single path. The function now:
1. Loads all .env files first to collect the complete set of variables
2. Performs variable expansion once with all variables available
3. This ensures bi-directional cross-file variable references work
correctly
Also updated `loadRootEnvFiles` in `packages/nx/src/utils/dotenv.ts` to
use the new batched loading approach.
Testing
A reproduction repository demonstrating the issue is available at:
[https://github.com/dullbenz/nx-22-5-env-referencing-example](vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)
After this fix, both `WILL_RESOLVE` and `WILL_NOT_RESOLVE` should
correctly expand their variable references.
Related Issue(s)
Fixes#34955
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
## Current Behavior
When Nx receives SIGINT (Ctrl+C), `performCleanup()` in the task
orchestrator kills continuous tasks and run-commands tasks, but **not
discrete tasks** (executor-based builds like `@nx/js:tsc`,
`nx:run-script`, etc.). In TUI mode, fork workers run in separate PTY
process groups and don't receive SIGINT directly, so cleanup hangs
waiting for tasks that are never terminated.
Additionally, `BatchProcess.kill()` only sends a signal to the immediate
child process, leaving grandchild processes (e.g. Java JVM, Gradle
daemon, Gradle workers) alive.
The Gradle batch executor also uses `execSync`, which blocks the Node
event loop and prevents the worker from responding to signals.
## Expected Behavior
All task types — discrete, continuous, and run-commands — should be
explicitly killed during SIGINT cleanup. Batch processes should use
`tree-kill` to terminate the entire process tree. The Gradle batch
executor should use async `spawn` instead of blocking `execSync`.
## Related Issue(s)
N/A — found via code inspection and confirmed with tests.
## Current Behavior
The Maven plugin version is `0.0.16`.
## Expected Behavior
The Maven plugin version is bumped to `0.0.17`, with a corresponding
migration for Nx `22.7.0-beta.11` that updates `pom.xml` files in user
workspaces.
## Related Issue(s)
N/A — routine version bump.
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Gradle project graph plugin 0.1.19 should be tied to Nx `22.7.0.beta.11`
## Current Behavior
The `ci.yml` workflow runs `pnpm install --frozen-lockfile` and `pnpm
playwright install --with-deps` without any caching. Every CI run:
- Downloads all npm packages from the registry from scratch
- Downloads Playwright browser binaries (~hundreds of MB)
- Installs ~100+ system apt packages for Playwright dependencies
This was partially caused by #33772 which migrated from
`actions/setup-node` (which had built-in `cache: 'pnpm'`) to
`mise-action` without adding back equivalent caching.
## Expected Behavior
- **pnpm store** is cached between runs, so `pnpm install` only links
from the local store instead of downloading
- **Playwright browsers** are cached by version, so browser downloads
are skipped on cache hit
- System apt deps still install on cache hit (they're fast), but browser
downloads are skipped
## Related Issue(s)
N/A — performance improvement for CI install times.
## Current Behavior
During atomized CI target generation, `buildTestCiTarget` is called once
per test class discovered in a project. Each invocation independently
recomputes `taskInputs`, `outputs`, and `dependsOn` for the same
`testTask`. Internally, `getInputsForTask` was called with `null` for
`dependsOnTasks`, which forced `getDependsOnTask(task)` — an uncached
call to `task.taskDependencies.getDependencies(task)` — on every
invocation. For a project with 158 test classes backed by the same
Gradle task, this results in 158 redundant dependency tree walks during
project graph discovery.
## Expected Behavior
The shared computation (`getDependsOnTask`, `getInputsForTask`,
`getOutputsForTask`, `getDependsOnForTask`) is performed once per
`testTask` in `processTestFiles` and the pre-computed values are passed
into `buildTestCiTarget`. This eliminates redundant work proportional to
the number of test classes, reducing CPU overhead during project graph
generation — especially on cold Gradle daemon starts.
## Related Issue(s)
Fixes #
## Current Behavior
The `astro-docs` project is not listed in the isolated build assignment
rules in the dynamic changesets CI workflow. This means it runs
alongside other projects instead of being isolated like `nx-dev`.
## Expected Behavior
The `astro-docs` project is added to the isolated build assignment rules
alongside `nx-dev`, ensuring its build runs in isolation during CI.
## Related Issue(s)
N/A — internal CI configuration improvement.
## Current Behavior
- The **Tutorials** sidebar section is collapsed by default with a "New"
badge
- **How Nx Works** (Concepts) and **Platform Features** sections are
expanded by default
- Only ~10% of traffic reaches the CI tutorial, suggesting tutorials are
not discoverable enough
## Expected Behavior
- **Tutorials** section is expanded by default (no badge) to increase
discoverability
- **How Nx Works** and **Platform Features** sections are collapsed by
default to reduce noise and draw attention to tutorials
- This should drive higher engagement with the tutorial flow including
CI setup
<img width="502" height="735" alt="image"
src="https://github.com/user-attachments/assets/76a63a36-ab5d-4459-aab6-dc04282e3779"
/>
## Related Issue(s)
Fixes DOC-474
## Current Behavior
The Gradle project graph plugin is at version 0.1.18.
## Expected Behavior
The Gradle project graph plugin is bumped to version 0.1.19, with the
corresponding migration files created so that users upgrading Nx will
automatically get the new plugin version.
## Related Issue(s)
N/A - routine version bump
## Current Behavior
When output directories are empty (e.g. on a clean build), the plugin
cannot discover file extensions from actual files on disk. This means
`dependentTasksOutputFiles` glob patterns are missing for extensions
like `.class` and `.jar`, leading to incomplete cache inputs.
## Expected Behavior
Add `inferExtensionsFromInputProperties` to supplement file-based
extension discovery using task type checks:
- `Test` tasks → `class` + `jar` (they consume compiled code and library
jars on the test classpath)
- `AbstractCompile` tasks → `class` only (they produce/consume compiled
classes, not jars)
- `AbstractArchiveTask` dependents → their declared archive extension
(jar, war, zip, etc.)
This works at configuration time without requiring files to exist on
disk.
## Related Issue(s)
N/A
## Current Behavior
`nx` depends on `@ltd/j-toml` which is licensed under **LGPL-3.0**. This
creates licensing concerns for projects that bundle or distribute nx, as
LGPL requires downstream users to allow relinking/modification of the
LGPL component.
A previous attempt
([3446dd2](https://github.com/nrwl/nx/commit/3446dd2f77a1b182f9b64a83586ab68a2f0c063f))
replaced it with `@iarna/toml`, but that library:
- Only supports TOML 1.0.0-rc.1 (not even the final 1.0.0 spec)
- Has been effectively unmaintained since ~2020
- Is significantly slower than alternatives
## Expected Behavior
Use a permissively-licensed, actively maintained, fast TOML parser.
## Solution
Replace `@ltd/j-toml` with
[`smol-toml`](https://github.com/squirrelchat/smol-toml) (BSD-3-Clause):
| | @ltd/j-toml (current) | @iarna/toml (other PR) | **smol-toml (this
PR)** |
|---|---|---|---|
| License | LGPL-3.0 | ISC | **BSD-3-Clause** |
| TOML spec | 1.0.0 | 1.0.0-rc.1 | **1.1.0** |
| Weekly downloads | — | 2.7M | **6.8M** |
| Maintained | Yes | Dormant (~2020) | **Active (2026)** |
| Performance | Baseline | ~same | **2-4x faster** |
| CJS support | Yes | Yes | **Yes** |
### Changes
- Replace `@ltd/j-toml` imports with `smol-toml` in
`set-up-ai-agents.ts` and `test-utils.ts`
- Remove j-toml-specific APIs (`TOML.Section()`, `TOML.inline()`,
`newlineAround` option) in favor of smol-toml's simpler
`parse()`/`stringify()`
- Update inline test snapshots (single quotes → double quotes, minor
formatting differences)
### Note on inline snapshots
Some inline snapshots may need a final update via `--updateSnapshot`
once CI runs the full test suite. The snapshot changes included here
follow the pattern observed from smol-toml's output format
(double-quoted strings, no leading newline before first section).
## Test plan
- [ ] CI passes with updated snapshots
- [ ] `nx configure-ai-agents` generates valid Codex config.toml
- [ ] Release versioning for Rust/Cargo projects still produces correct
Cargo.toml output
---------
Co-authored-by: Alexandre Ducarne <aducarne@ripple.com>
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
- Add `handles_cursor_movement: Arc<AtomicBool>` field to PtyInstance
- Short-circuit `has_cursor_movement_in_output` on subsequent calls once
a cursor-movement sequence has been detected, skipping the O(n) UTF-8
buffer scan on every arrow-key event
- The flag is monotonic (false → true, never reverts) so Relaxed
ordering
is sufficient; Arc makes it Clone-safe across async resize threads
This pull request refactors the TUI (terminal user interface) codebase
to improve performance, safety, and code clarity. The most significant
changes include switching from debouncing to throttling for PTY resize
operations, updating method signatures to use string slices (`&str`)
instead of owned `String` where possible, and making related adjustments
throughout the codebase. These changes help reduce unnecessary
allocations, improve responsiveness, and clarify intent.
**PTY Resize Handling Improvements:**
* Replaced the `debounce_pty_resize` method with `throttle_pty_resize`,
which limits PTY resize operations to at most one every 200ms
(fire-then-block), reducing excessive work during rapid events like
window resizing. All calls to the old debounce method are updated to use
the new throttle method.
[[1]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL473-R471)
[[2]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL490-R493)
[[3]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL590-R590)
[[4]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL615-R608)
[[5]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL1149-R1131)
[[6]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL1543-R1525)
[[7]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL1852-L1862)
[[8]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL2550-R2541)
[[9]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL2659-R2643)
**API and Type Signature Updates:**
* Changed many method signatures (such as `update_task_status`,
`select_task`, and `select_batch_group`) to accept `&str` instead of
`String`, reducing unnecessary allocations and clarifying ownership. All
corresponding call sites and trait implementations are updated.
[[1]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL220-R220)
[[2]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL417-R426)
[[3]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL534-R527)
[[4]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL590-R590)
[[5]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL1543-R1525)
[[6]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL2550-R2541)
[[7]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL2573-R2558)
[[8]](diffhunk://#diff-c899b0b1f501248ea65ff62c761294b7e6c5f5ece77ed422eae9c3a4849ac85aL218-R224)
[[9]](diffhunk://#diff-c899b0b1f501248ea65ff62c761294b7e6c5f5ece77ed422eae9c3a4849ac85aL709-R709)
[[10]](diffhunk://#diff-c899b0b1f501248ea65ff62c761294b7e6c5f5ece77ed422eae9c3a4849ac85aL833-R847)
**Cloning and Ownership Adjustments:**
* Replaced some `.clone()` and `.to_string()` calls with `.to_vec()` and
`.to_owned()` where more appropriate, further reducing unnecessary
allocations and clarifying intent.
[[1]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL168-R172)
[[2]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL189-R189)
[[3]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL203-R203)
[[4]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL368-R368)
[[5]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL2630-R2614)
**Code Cleanliness and Logic Simplification:**
* Simplified and cleaned up logic in several places, such as input
handling and filter mode transitions, to make the code more readable and
maintainable.
[[1]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL972-R970)
[[2]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL1006-R990)
[[3]](diffhunk://#diff-a7ecbb01dd3a8b83922f97d7040b457813c44325d346172693d1638b2b45a48fL1017)
Overall, these changes improve performance, safety, and maintainability
of the TUI code.
---------
Co-authored-by: Claude <noreply@anthropic.com>
When a Maven project sets maven.install.skip=true, the install:install
goal is a no-op at runtime. However, the NxTargetFactory still generated
a full batch executor target with cache:false, causing unnecessary Maven
invocations on every CI agent. This led to flaky failures in DTE when
the batch runner's graph setup failed on some agents.
Now detects maven.install.skip=true and emits an nx:noop target with
cache:true instead. The target still acts as a synchronization point in
the task graph (dependsOn chain is preserved) but avoids spinning up the
batch runner entirely.
<img width="2124" height="842" alt="image"
src="https://github.com/user-attachments/assets/b6c09b3e-c34e-45f8-9c33-2d6ce493bc3d"
/>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Current Behavior
issue-notifier.yml uses an outdated syntax that throws
## Expected Behavior
it doesn't throw
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
There is no e2e test that verifies the nx build process works correctly
end-to-end — that the source code compiles, produces expected output
files, and correctly detects source changes on rebuild.
The verdaccio `max_body_size` was set to 20mb, which is too small for
the nx package. The nx `.npmignore` was also missing exclusions for
`.rs` and `.snap` files.
## Expected Behavior
- A new `e2e-nx-build` test project that:
- Clones the nx repo to a temp directory
- Swaps `@nx/*` and `nx` dependency versions to match what's published
in the local verdaccio registry
- Installs dependencies from the local registry
- Builds all `tag:npm:public` packages (same as nx-release)
- Verifies key output files exist (`bin/nx.js`, `src/index.js`,
`src/index.d.ts`)
- Modifies a source file (`bin/nx.ts`), rebuilds, and verifies the
change appears in the output — catching cache misconfiguration (verified
by sabotaging `inputs: []` and confirming the test fails)
- Verdaccio `max_body_size` bumped to 100mb
- `.rs` and `.snap` files excluded from the nx npm package
## Related Issue(s)
N/A — new test infrastructure
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
When `skipProjectGraph` is passed to the `generate()` function, use
`retrieveProjectConfigurationsWithoutPluginInference` instead of
`createProjectGraphAsync`. This loads only default plugins (js,
package-json, project-json) and skips dependency edge computation,
making generation faster while still supporting local plugins.
This is a private API — callers must import the `generate` function
directly and pass `skipProjectGraph: true`.
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Current Behavior
When running `nx run-many -t clean --batch` in a multi-module Maven
project, the batch executor hangs indefinitely. Workers use
`taskQueue.poll()` which returns `null` immediately when the queue is
temporarily empty. Workers exit their loop prematurely — even though
other workers are still processing tasks that will produce new root
tasks. Eventually all workers exit and `completionLatch.await()` blocks
forever.
## Expected Behavior
`nx run-many -t clean --batch` completes successfully for Maven projects
of any size. Worker threads now use `taskQueue.take()` which blocks
until a task is available, instead of exiting when the queue is
momentarily empty. When all tasks complete, `executor.shutdownNow()`
interrupts any workers still blocked on `take()`, allowing clean
shutdown.
## Related Issue(s)
Fixes#34757
## Current Behavior
The `packages/nx` project uses `tsgo` (Go-based TypeScript compiler) via
a dedicated `@nx/js/typescript` plugin entry with `compiler: "tsgo"`.
All other packages use `tsc`.
When a downstream package (e.g., `node:build-base`) runs `tsc --build`
with project references to `nx`, `tsc` detects the `.tsbuildinfo` was
produced by a different compiler version (`7.0.0-dev` vs `5.9.2`) and
recompiles `nx` entirely. This cascades through the reference chain —
every project referencing `nx` is then considered out of date,
triggering rebuilds across devkit, js, workspace, eslint, jest, docker,
and the originating project.
Verified locally with `tsc --build --verbose`:
```
Project '../nx/tsconfig.lib.json' is out of date because output for it
was generated with version '7.0.0-dev.20260327.2' that differs with
current version '5.9.2'
```
## Expected Behavior
All packages use `tsc`, eliminating the compiler version mismatch. `tsc
--build` finds all referenced projects up to date and skips
recompilation.
> **Note:** We'll switch all packages to `tsgo` once they're all
migrated to `nodenext` and prepared for it, so we use a single compiler
across the workspace at a time.
## Current Behavior
When file changes trigger rapid project graph recalculations (e.g.,
during development with `nx graph` or a dev server running), each call
to `populateProjectGraph` spawns a new Gradle process via
`execGradleAsync`. If the previous Gradle daemon is still busy
processing the prior request, Gradle spawns a **new daemon**. These
daemons persist for 3 hours by default (Gradle's idle timeout), leading
to dozens of orphaned `java.exe` processes consuming significant memory.
Maven has a similar issue — `runMavenAnalysis` spawns a long-lived
process with no timeout or cancellation support at all.
### Root Cause Analysis
The daemon explosion happens due to three compounding issues:
1. **No cancellation of in-flight processes**: When a newer project
graph request arrives, the previous invocation continues running. Each
concurrent invocation finds the existing daemon busy and spawns a new
one.
2. **Windows process tree issue**: On Windows, `execFile` with `shell:
true` runs `cmd.exe → gradlew.bat → java.exe`. Node's `AbortSignal` only
terminates `cmd.exe` (the immediate child process), leaving `java.exe`
running as an orphan.
3. **No timeout for Maven**: Maven analysis could run indefinitely with
no way to cancel or time out.
### Reproduction
1. Run `nx graph` in a workspace with `@nx/gradle` registered
2. Rapidly modify a `build.gradle.kts` file (e.g., `while true; do echo
"// tick" >> build.gradle.kts; sleep 0.01; done`)
3. Watch `java.exe` processes accumulate: `tasklist | grep java`
(Windows) or `ps aux | grep java` (Unix)
4. Without this fix: **15+ Gradle daemons** within seconds, persisting
for 3 hours each
5. With this fix: **1 Gradle daemon** remains stable under the same
conditions
## Expected Behavior
When rapid file changes trigger multiple project graph recalculations:
- The previous invocation is cancelled before starting a new one
- Cancelled calls that have already spawned a process get their entire
process tree killed (not just the shell wrapper)
- Only 1 daemon remains active at any time, rather than accumulating
dozens
- Both Gradle and Maven have configurable timeouts with clear error
messages
## Changes
### Gradle
#### 1. Self-contained cancellation in `get-project-graph-lines.ts`
Moved the `AbortController` from
`get-project-graph-from-gradle-plugin.ts` into
`get-project-graph-lines.ts`, closer to where processes are spawned.
`getNxProjectGraphLines` now manages its own abort controller —
cancelling any in-flight request before starting a new one. Uses
`abort('cancelled')` reason to distinguish external cancellation from
timeout.
#### 2. Tree-kill on abort (`exec-gradle.ts`)
Instead of passing the `AbortSignal` directly to Node's `execFile`
(which only kills the immediate child process), we intercept the signal
and use `tree-kill` to terminate the entire process tree. This ensures
`java.exe` is killed along with `cmd.exe` and `gradlew.bat` on Windows.
### Maven
#### 3. Timeout and cancellation support for `maven-analyzer.ts`
Added the same abort controller + tree-kill + timeout pattern to Maven
analysis:
- Configurable timeout via `NX_MAVEN_ANALYSIS_TIMEOUT` env var (default:
120s local, 600s CI)
- `cancelPendingMavenAnalysis()` cancels in-flight processes on repeated
calls
- `tree-kill` ensures the entire Maven process tree is killed on abort
- Clear timeout error messages with actionable steps
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
<!-- 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 -->
We are using an old version of `ejs` and it is not pinned.
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
We are using the latest version of `ejs` and it is pinned.
## 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>
## Current Behavior
The `nx-cloud onboard` command and its subcommands are not documented in
the Cloud CLI reference page.
## Expected Behavior
The Cloud CLI reference page includes complete documentation for the
`nx-cloud onboard` command, covering:
- Main `onboard` command with interactive and non-interactive modes
- All subcommands: `status`, `connect-workspace`, `connect github`,
`connect github poll`, `orgs list`, `orgs create`, `repos list`,
`templates list`, `vcs status`, and `workspace create`
- Options tables for each subcommand
- Automation notes for AI agent integration
Also changed `--non-interactive` to `--no-interactive` to match the Nx
CLI convention (yargs `--no-` prefix).
## Related Issue(s)
Fixes DOC-451
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Current Behavior
The `nx`, `create-nx-workspace`, and `nx-dev` packages pin `axios` at
version `1.12.0`, which has a known security vulnerability
([CVE-2026-25639](https://github.com/advisories/GHSA-43fc-jf86-j433)).
## Expected Behavior
Axios is pinned at `1.13.5`, which includes the fix for CVE-2026-25639,
eliminating the security vulnerability.
## Related Issue(s)
Fixes#35145
## Current Behavior
The launch templates reference page only lists Node 20-based agent
images (`ubuntu22.04-node20.11-*` and `ubuntu22.04-node20.19-*`).
## Expected Behavior
The launch templates reference page also lists the new Node 22 and Node
24 agent images:
- `ubuntu22.04-node22.22-v1`
- `ubuntu22.04-node24.14-v1`
These images were added to the cloud infrastructure config map via
[CLOUD-4403](https://linear.app/nxdev/issue/CLOUD-4403).
## Related Issue(s)
N/A (documentation update to reflect infrastructure changes from
CLOUD-4403)
---------
Co-authored-by: Caleb Ukle <caleb@nrwl.io>
## Current Behavior
Disabled tests (`.skip()`, `.todo()`, `xit()`, `xdescribe()`, `xtest()`)
can be committed freely with no guardrails. Over time this leads to
tests silently rotting — there are currently ~65 disabled tests across
the workspace.
## Expected Behavior
ESLint warns on any disabled test via `jest/no-disabled-tests`, and
`--max-warnings` caps prevent the count from growing.
### How it works
The setup is like a ratchet — existing disabled tests are grandfathered
at their current counts, but adding new ones fails lint:
- **Global default**: `max-warnings: 5` in `nx.json` target defaults —
covers most projects
- **Per-project overrides** for projects that exceed 5, capped at their
current count:
- `graph-client`: 15 (react-hooks/exhaustive-deps, unused-vars)
- `e2e-angular`: 9, `e2e-react`: 10, `e2e-next`: 8, `e2e-node`: 6,
`e2e-storybook`: 6
- `nx-dev-feature-package-schema-viewer`: 7, `nx-dev-ui-markdoc`: 7
- **e2e `.eslintrc.json` files** only un-ignore test files (`*.test.ts`,
`*.spec.ts`) to avoid exposing unrelated lint errors in non-test code
### Changes
- Install `eslint-plugin-jest` and add `jest/no-disabled-tests: warn` to
root `.eslintrc.json` for test file overrides
- Add `.eslintrc.json` to 15 e2e projects
- Set `max-warnings: 5` globally and per-project overrides where needed
- Ignore Maven `target/` build output from linting
## Current Behavior
The Gradle project graph plugin (`dev.nx.gradle.project-graph`) has no
observability into where time is spent during project graph generation.
When users report slow `nx show projects` or project graph resolution,
there is no way to identify bottlenecks in the Kotlin plugin code.
## Expected Behavior
The plugin now has opt-in OpenTelemetry distributed tracing. When
`OTEL_EXPORTER_OTLP_ENDPOINT` is set, spans are created for key
operations and exported via OTLP/gRPC to any compatible collector
(Jaeger, Grafana Tempo, etc.). When the env var is not set, tracing is
completely no-op with zero overhead.
Run any Nx command that triggers Gradle project graph generation:
```bash
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 pnpm nx show projects
```
## Related Issue(s)
N/A — internal observability improvement for debugging Gradle project
graph performance.
## Current Behavior
The `workspace-plugin:build` target explicitly runs `tsc --build
tsconfig.lib.json`, which is the same command the inferred `build-base`
target runs. Since `build` depends on `build-base`, tsc runs twice — the
second time finding nothing to do.
The `nx` package was listed as a dependency but is only referenced in
generator template files, not in compiled source.
## Expected Behavior
- The `build` target is a pure dependency orchestrator with no command.
`build-base` (inferred by `@nx/js/typescript`) handles the actual
compilation.
- The `@nx/dependency-checks` rule is configured with `buildTargets:
["build-base"]` to align with all other packages in the repo.
- The `nx` package is removed from dependencies since it's not imported
in any source file.
## Current Behavior
If a user has `overrides` set for the `nx` package, we error with
"cannot find the implementation of xxx" if the override pins nx to a
version prior to that migration's inclusion.
## Expected Behavior
We point out the `overrides` field for easier debugging
## 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>
## Current Behavior
Two issues in plugin code:
1. **Package manager detection at module scope**:
`getPackageManagerCommand()` is called at module level in all 18 plugin
files. This detects the package manager based on the CWD or
`npm_config_user_agent` at import time, rather than using the actual
workspace root. This produces inconsistent `pmc.exec` commands (e.g.
`pnpm exec` vs `npx`) depending on how the process was invoked.
2. **Platform-dependent paths in target configs**: Plugins use
`path.join()` and `path.relative()` to build target configuration values
(outputs, commands, env vars). On Windows this produces backslashes,
making the project graph differ between platforms.
## Expected Behavior
1. Package manager detection uses `context.workspaceRoot` from the
`createNodes` callback, then passes `pmc` down to per-project
target-building functions.
2. All paths in target configs use forward slashes regardless of OS,
using `joinPathFragments` and `normalizePath` from `@nx/devkit`.
## Changes
**Plugin source (20 files):**
- All 18 plugin files: moved `getPackageManagerCommand()` from module
scope into the `createNodes` callback where `context.workspaceRoot` is
available
- `packages/playwright/src/plugins/plugin.ts`: replaced
`path.join`/`posix.join`/`posix.relative` with
`joinPathFragments`/`normalizePath` for target config paths
- `packages/webpack/src/plugins/plugin.ts`,
`packages/vite/src/plugins/plugin.ts`,
`packages/vitest/src/plugins/plugin.ts`,
`packages/nuxt/src/plugins/plugin.ts`: replaced `path.join` with
`joinPathFragments` in `normalizeOutputPath()`, added `normalizePath`
for relative test paths
**Tests (19 files):**
- Added `package-lock.json` to TempFs dirs for deterministic package
manager detection
- Converted next/nuxt root project tests from `workspaceRoot: ''` to
TempFs-based setup
- Fixed all backslash path separators in snapshots and inline snapshots
## Related Issue(s)
<!-- No specific issue — discovered during test debugging -->
## Current Behavior
When a workspace-local Nx plugin cannot be resolved (e.g., the
`node_modules` symlink for a workspace package is missing), the error
message displays `[object Object]` instead of the underlying module
resolution error:
```
NX Failed to load 1 Nx plugin(s):
- @repro/my-plugin/plugin: [object Object]
```
This happens because plugin workers serialize errors as plain objects
via `createSerializableError()`. When these serialized errors are
received back in the parent process, the `instanceof Error` check fails,
and `String(reason)` on a plain object produces `[object Object]`.
## Expected Behavior
The actual error message is displayed:
```
NX Failed to load 1 Nx plugin(s):
- @repro/my-plugin/plugin: Cannot find module '@repro/my-plugin/plugin'
```
The fix adds a `reasonToError` helper that checks for an object with a
`message` property (serialized error) before falling back to `String()`
conversion. This properly handles:
- Real `Error` instances (unchanged behavior)
- Serialized error objects from plugin workers (now extracts `message`
and `stack`)
- Other non-error rejection reasons (unchanged `String()` fallback)
## Screenshot of fix
<img width="972" height="244" alt="image"
src="https://github.com/user-attachments/assets/ca87d9a6-43a4-4fd4-a215-71277f6dcc8b"
/>
## Related Issue(s)
Fixes#35137
## Current Behavior
Broad globs in `assets.json` (`**/*.json`, `**/*.js`, `**/*.d.ts`) cause
`copy-assets` targets to claim cache ownership over files also produced
by `build-base` (tsc). Even though a recent change reordered
`copy-assets` to run before `build-base` (reducing the likelihood of the
race condition), the underlying task ownership model is still broken —
both targets claim overlapping files in their output patterns.
## Expected Behavior
Each target exclusively owns its output files. `copy-assets` only claims
non-tsc assets (templates, type declarations, native artifacts), and
`build-base` owns all compiler outputs.
## Changes
**37 `assets.json` files** — replaced broad extension globs with narrow,
destination-safe patterns: template dirs (`**/files/**`), schema type
declarations (`src/**/schema.d.ts`), non-tsc extensions (`.jar`,
`.node`, `.wasm`, `.md`), and explicit file paths for package-specific
assets.
**30 `tsconfig.lib.json` files** — removed `**/*.json` from `include` so
tsc only compiles TypeScript. JSON files are now handled by copy-assets
with explicit entries
(`@(package|executors|generators|migrations).json`,
`src/**/schema.json`).
**Exception:** jest and vite use `import('./schema.json')` which
requires JSON in tsconfig scope with `composite: true`. These keep
`src/**/schema.json` in tsconfig.
**vite** — excludes `test-utils.ts` from lib build (only used by specs)
and includes it in `tsconfig.spec.json`.
## Current Behavior
`nx migrate` can hide the actual package manager failure behind parent
wrapper noise, and invalid migration metadata can lead to confusing
follow-up failures.
## Expected Behavior
`nx migrate` should surface the underlying fallback install error
clearly, avoid noisy parent-level wrapper failures, and only fail on
invalid migration metadata when the invalid update is actually consumed.
## Current Behavior
The generated package.json (used for deployment) only copies
`pnpm.overrides` from the root package.json. Other pnpm fields that
affect `pnpm install` behavior are missing, causing issues like
lifecycle scripts not running (pnpm v10+) or wrong platform-specific
dependencies being installed.
## Expected Behavior
All pnpm configuration fields that affect `pnpm install` in a deployment
context should be copied to the generated package.json:
- `onlyBuiltDependencies` — allowlist for lifecycle scripts (pnpm v10
requirement)
- `neverBuiltDependencies` — denylist for lifecycle scripts
- `allowBuilds` — unified replacement for the above two (pnpm 10.26+)
- `supportedArchitectures` — platform-specific dependency selection
- `ignoredOptionalDependencies` — skip optional dependencies
## Related Issue(s)
Fixes#30240
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: leosvelperez <leosvelperez@users.noreply.github.com>
## Current Behavior
When `configure-ai-agents` runs for Gemini, it copies skills to the
shared `.agents/skills` directory (used by Codex, Cursor, and Gemini).
However, workspaces that were configured with an older version of Nx
still have a `.gemini/skills` directory containing duplicate Nx-managed
skills. These legacy files are never cleaned up, leaving stale
duplicates in the workspace.
## Expected Behavior
When `configure-ai-agents` runs for Gemini, it should remove any
`.gemini/skills` entries that also exist in `.agents/skills` (indicating
they are Nx-managed skills that have been migrated). User-created custom
skills in `.gemini/skills` that have no counterpart in `.agents/skills`
are preserved.
## Related Issue(s)
N/A — discovered during investigation of template repo AI agent
configurations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
- The `docker` package has its build output path pointing to
`{workspaceRoot}/build/packages/docker/README.md`, but the
`copy-readme.js` script writes to `dist/packages/docker/README.md`. This
means the cache output doesn't match the actual output location.
- The `vue` package has an empty build target (`{}`) with no README copy
step, unlike every other publishable package.
## Expected Behavior
- The `docker` package output path correctly points to
`{workspaceRoot}/dist/packages/docker/README.md`.
- The `vue` package has a proper build target that copies and processes
the README, matching the pattern used by all other packages.
## Related Issue(s)
N/A — found during audit of build target outputs.
## Current Behavior
Newly generated JS/esbuild-based projects still default `esbuild` to
`^0.19.2`. That conflicts with Vite 8 which requires `esbuild ^0.27.0`.
## Expected Behavior
Newly generated projects should default to an `esbuild` version
compatible with Vite 8. If a workspace already has `esbuild` installed,
generators should preserve that version instead of blindly bumping it,
and Vite init should fall back to Vite 7 with a warning when the
installed `esbuild` range is incompatible with Vite 8.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`nx-dev-e2e` has no `tsconfig.json`. When `eslint .` runs,
`@typescript-eslint/parser` walks up from the project root to the
workspace root `tsconfig.json`. This read is not declared as a lint
input, causing a sandbox violation.
## Expected Behavior
A minimal `tsconfig.json` extending `tsconfig.base.json` exists in the
project, so the parser resolves locally and the sandbox violation is
eliminated.
## Why not fix in the `@nx/eslint/plugin`?
The plugin would need to resolve the ESLint config chain, identify which
parser is in use, and replicate that parser's tsconfig resolution logic
(walking up directories, following `extends` chains). This adds file I/O
and config parsing to `createNodesV2`, which runs on the critical path
during project graph computation — every `nx` command pays the cost. Not
worth it for an edge case that only surfaces when a project has no local
tsconfig.
## Why not an input override?
An input override in `project.json` would duplicate the plugin's
inferred inputs and could drift if the plugin changes what it infers in
the future.
## Current Behavior
Nightly `e2e-nx-init` runs can fail when the Angular CLI legacy suite
restores a cached workspace into an already-existing temp project
directory. With `NX_E2E_SKIP_CLEANUP=true`, the previous test workspace
is intentionally left on disk, and `fs-extra.copySync` then hits
symlinked `node_modules/.bin` entries and aborts with errors like
`Cannot copy '../which/bin/which.js' to a subdirectory of itself`.
## Expected Behavior
Restoring the cached Angular CLI workspace should be idempotent even
when cleanup is skipped between tests. The restore step should start
from a clean target directory so the suite can reuse the cached baseline
without tripping over stale symlinks from the previous test run.
## Related Issue(s)
No tracked issue. Investigated from nightly GitHub Actions run
`23833817151`.
Validation:
- `pnpm nx typecheck e2e-nx-init`
- Focused local repro on macOS/npm with `NX_E2E_SKIP_CLEANUP=true`
failing before the change and passing after it
## Current Behavior
`start-local-registry.ts` uses `require.resolve('nx')` to find the nx
CLI binary for forking a child process. This resolves the package's
`main` entry point, which only incidentally points to the binary. In
pnpm strict mode, this resolution fails when called from within `@nx/js`
because `@nx/js` doesn't declare `nx` as a direct dependency.
## Expected Behavior
Use `require.resolve('nx/bin/nx')` to explicitly resolve the CLI binary
entry point. This is semantically correct (the intent is to fork the nx
CLI) and doesn't rely on the `main` field or pnpm hoisting behavior.
`require.resolve('nx)` can fail for a number of reasons:
- If `./nx` exists, it'll resolve to that first, and fail
- If module resolution is set differently it may resolve to the index
file, or something unexpected
## Current Behavior
pre-install says rust isn't available if mise isn't trusting the dir,
but doesn't mention mise and suggests installing rust. This can trip up
AI agents if they see the output and think rust should be installed
## Expected Behavior
Error message mentions mise
## Notes
This pull request significantly refactors and improves the
`scripts/preinstall.js` dependency check script. The script now performs
more robust version checks for Node, pnpm, and Rust, and adds support
for detecting and guiding users regarding `mise` trust status. The
refactoring also improves maintainability by modularizing checks and
error handling.
**Dependency checks and error handling:**
* Refactored version checks for Node, pnpm, and Rust into separate
functions for better readability and maintainability.
* Improved error messages and guidance, including specific instructions
for updating or installing missing tools, and added checks for minimum
required versions (`Node 20.19.0+`, `pnpm 10.0.0+`, `Rust 1.70.0+`).
**Support for mise integration:**
* Added detection of `mise` installation and trust status, with user
guidance to run `mise trust` when the directory is untrusted and Rust is
missing or outdated.
**Codebase improvements:**
* Consolidated tool version gathering into a single `getToolData`
function and replaced repeated code with a reusable `execOrNull` helper.
* Changed process exit logic to only exit when errors are detected,
improving script robustness. (F43b11f
## Current Behavior
The `lint-pnpm-lock` task only declares `pnpm-lock.yaml` as input.
ESLint also reads its config chain (`.eslintrc.json`, `.eslintignore`),
`tsconfig.json`, and the custom rules plugin (`tools/eslint-rules/**`),
causing sandbox violations for 10 undeclared reads.
## Expected Behavior
All files ESLint needs are declared as inputs so sandbox reports no
violations for `@nx/nx-source:lint-pnpm-lock`.
## Current Behavior
The `nx` npm package includes unnecessary files in the published
artifact:
- ~137 Rust source files (`.rs`) from `src/native/`
- Test fixtures, snapshots, and other dev-only files
- Duplicate source directories (`bin/`, `plugins/`, `schemas/`, etc.)
alongside their compiled `dist/` equivalents
The `.npmignore` blocklist approach missed several file types, and the
CI `.node` file cleanup (`find ./dist`) no longer works because the `nx`
package now builds to `packages/nx/dist/` instead of
`dist/packages/nx/`.
## Expected Behavior
- Only `dist/` and essential root JSON files (`migrations.json`,
`executors.json`, `generators.json`) are published
- No Rust source, test fixtures, snapshots, or duplicate source dirs in
the published package
- Native type declarations (`src/native/index.d.ts`) are copied to
`dist/` via `assets.json` so all exports reference `dist/` consistently
- CI correctly removes `.node` files from `packages/nx/dist/` before
publishing to npm
## Related Issue(s)
N/A — discovered during package audit.
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
## Current Behavior
Packages that build to `{projectRoot}/dist` (`maven`, `dotnet`,
`angular-rspack-compiler`) use `!**/*` in their project-level
`.eslintrc.json` `ignorePatterns` to un-ignore dotfiles. This negation
also overrides the root config's `**/dist` ignore pattern, causing
`eslint .` to traverse and read build artifacts in `dist/` during
linting. This produces sandbox violations (46 unexpected reads for
`@nx/maven:lint`).
## Expected Behavior
ESLint skips the `dist/` directory during linting, matching the behavior
already in place for `packages/nx` and `packages/angular-rspack` which
explicitly re-ignore `dist` after the `!**/*` pattern.
## Current Behavior
Tutorial pages are standalone with no visible series navigation. Path
analysis shows most users drop off after the first tutorial, even though
they generally follow the intended path.
## Expected Behavior
Each of the 8 tutorial pages now shows a "Tutorial Series" aside after
the intro paragraph, listing all tutorials with the current one bolded.
This makes the series feel connected while still allowing users to jump
around or skip as needed.
Additionally, prerequisites are standardized as plain paragraphs (not
asides) across all tutorial pages for a cleaner, less noisy layout.
<img width="971" height="816" alt="image"
src="https://github.com/user-attachments/assets/b5389d98-4809-4400-8f9f-ab9834caba94"
/>
## Related Issue(s)
Closes DOC-466
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
They don't specify the right inputs, so you will get the wrong bin file
whenever you build CNW and CNP.
The problem is ordering:
1. `build-base` runs → sees source change → compiles fresh
`bin/create-nx-workspace.js` into `dist/`
2. `build` runs next → checks inputs (`copyReadme`) → cache hit →
restores its cached outputs
3. That cached output includes the old `bin/create-nx-workspace.js`,
which overwrites the fresh one from step 1
So even though the `build-base` is compiling new source with `tsc`, the
`build` task overrides just the bin. This is for both CNW and CNP.
## Current Behavior
The react-router typecheck e2e test fails in CI because pnpm resolves
both vite 7 and vite 8 in the generated project. `@react-router/dev`
picks up vite 8 plugin types while `defineConfig` uses vite 7 types,
causing a TypeScript incompatibility.
## Expected Behavior
The test is skipped until `@react-router/dev` adds Vite 8 support, at
which point the `useViteV7` workaround and this skip can both be
removed.
## Related Issue(s)
Follow-up to #35101
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
Telemetry initialization fails with `no such table: metadata` because
the metadata table was removed during the DB schema refactor that
decoupled DB version from Nx version. The telemetry service depends on
this table to store and retrieve session IDs for analytics tracking.
## Expected Behavior
The metadata table is created as part of DB initialization alongside the
other tables (`task_details`, `running_tasks`, `task_history`).
Telemetry session tracking works without errors. The DB version is
bumped from `1` to `2` so existing databases without the table are
recreated with the correct schema.
### Changes
- Add metadata table creation to `create_all_tables` in `initialize.rs`
- Bump `DB_VERSION` from `1` to `2` to trigger fresh DB creation for
users with v1 databases
- Widen `initialize` module and `initialize_db` visibility to
`pub(crate)` for testability
- Add regression tests in `telemetry/mod.rs` that verify the session
query works against a freshly initialized DB and that session
persist/retrieve round-trips correctly
- Refactor `initialize.rs` tests to use `NxDbConnection` instead of raw
rusqlite `Connection`
## Related Issue(s)
<!-- No open issue for this bug -->
## Current Behavior
Six e2e tests were skipped in #34969 because they were failing with a
Cypress uncaught exception: `[HMR] Hot Module Replacement is disabled`.
The error originated from the webpack `styles.js` bundle during the
`before each` hook due to an upstream tapable issue
(webpack/webpack#20693).
Skipped tests:
- `e2e-nx:e2e-ci--src/workspace-legacy.test.ts`
-
`e2e-react:e2e-ci--src/module-federation/independent-deployability.webpack.test.ts`
-
`e2e-react:e2e-ci--src/module-federation/core-webpack-basic-host-remote-generation.test.ts`
-
`e2e-react:e2e-ci--src/module-federation/misc-rspack-interoperability.test.ts`
-
`e2e-react:e2e-ci--src/module-federation/dynamic-federation.webpack.test.ts`
-
`e2e-react:e2e-ci--src/module-federation/federate-module.webpack.test.ts`
## Expected Behavior
The upstream tapable issue has been resolved (webpack/webpack#20693).
All 6 tests should be re-enabled and passing.
## Related Issue(s)
Upstream fix: webpack/webpack#20693
Reverts the skip from #34969
## Current Behavior
The Gradle project graph plugin is at version 0.1.17.
## Expected Behavior
The Gradle project graph plugin is bumped to version 0.1.18 with a
corresponding migration entry at version 22.7.0-beta.9
## Related Issue(s)
N/A - routine version bump
## Current Behavior
`copy-assets` depends on `build-base`, which means it has to wait for
the entire `build-native` → `build-base` chain to finish before it can
start — even though asset copying doesn't need compiled output.
## Expected Behavior
`build-base` depends on `copy-assets` instead. This lets `copy-assets`
start immediately (in parallel with `build-native` and `^build-base`)
rather than waiting for them to complete first.
<img width="767" height="721" alt="image"
src="https://github.com/user-attachments/assets/6a1d8090-8390-4075-850e-bfd309cdc6c9"
/>
### Changes
- Removed `dependsOn: ['build-base']` from the `copy-assets` target
generated by `copy-assets-plugin.ts`
- Added `copy-assets` to `build-base.dependsOn` in `nx.json` and
project-level overrides (`gradle`)
- Removed now-transitive `copy-assets` references from `build.dependsOn`
in `nx.json`, `packages/nx`, `packages/angular`, and `packages/gradle`
- Removed `build-base` from `copy-assets.dependsOn` in `packages/nx` and
`packages/dotnet` project overrides
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
When Kotlin compilations are associated (e.g. test -> main), the
`friendPathsSet` `@Input` provider creates implicit dependencies on
producer tasks like `compileKotlin`, `compileJava`, and `jar`. Without
detecting these, Nx excludes them via `--exclude-task`, causing a
provider resolution error at execution time when Gradle tries to resolve
the `friendPathsSet` provider.
The previous implementation only detected lifecycle-based provider
dependencies (Phase 1), missing the `@Input` property-based ones.
## Expected Behavior
`findProviderBasedDependencies` now detects both:
1. **Lifecycle dependencies** — `ProviderInternal` and `TaskProvider`
entries in `lifecycleDependencies` (e.g.
`checkKotlinGradlePluginConfigurationErrors`)
2. **`@Input` property dependencies** — producer tasks discovered by
walking task properties with Gradle's `PropertyWalker` +
`PropertyVisitor`, resolving `taskDependencies` from each `@Input`
`PropertyValue` (e.g. `compileKotlin`, `compileJava`, `jar`)
These tasks are added to `includeDependsOnTasks` so they are not
excluded from Gradle execution.
The function is refactored into two immutable collectors
(`collectLifecycleDependencies` and `collectInputPropertyDependencies`)
merged in the parent, replacing the previous mutable-set-passing
pattern.
## Related Issue(s)
Fixes NXC-4174
## Current Behavior
Every lock threads run is failing
## Expected Behavior
Lock threads at least occasionally passes
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
Creating a custom React workspace with React Router for server rendering
(framework mode) fails due to a peer dependency conflict between Vite 8
and React Router. Vite 8 is the current default for new workspaces, but
`@react-router/dev` does not yet support it.
## Expected Behavior
The React application generator uses Vite 7 when React Router is
selected, avoiding the peer dependency conflict until React Router adds
Vite 8 support.
<img width="1270" height="1204" alt="image"
src="https://github.com/user-attachments/assets/87495e85-20df-4086-a1f0-eb3380a78771"
/>
## Related Issue(s)
Fixes NXC-4176
## Current Behavior
`@nx/webpack` depends on `postcss-loader@^6.1.1`, which pulls in
`cosmiconfig@7` → `yaml@1.x`. The `yaml@1.x` package has a known stack
overflow vulnerability
([GHSA-48c2-rrv3-qjmp](https://github.com/advisories/GHSA-48c2-rrv3-qjmp)).
## Expected Behavior
By bumping `postcss-loader` to `^8.2.1`, the transitive dependency chain
is eliminated entirely — `postcss-loader@8` uses `cosmiconfig@9`, which
no longer depends on `yaml` at all. This is a cleaner fix than applying
a `pnpm.overrides` workaround.
The upgrade is safe because:
- `postcss-loader@8` peer deps (`postcss ^7||^8`, `webpack ^5`) are
unchanged
- The `implementation` option and function-based `postcssOptions` API
used by `@nx/webpack` are fully supported in v8
- Nx already requires Node 18+, matching postcss-loader@8's engine
requirement
## Related Issue(s)
Fixes#35025
## Current Behavior
1. **Sandboxing false positives**: `tsc --build` reads `.tsbuildinfo`
files as an optimization hint, and the `nx-plugin-checks` lint rule
reads `schema.json` from `dist/` directories. Both are flagged as
sandbox violations even though they don't affect caching correctness.
2. **Missing dependencies in project graph**: `typeof import('...')`
inside multi-line generic type parameters (e.g. `ensurePackage<typeof
import('@nx/playwright')>()`) is not detected by the import analyzer.
The newline between `<` and `import()` resets the import type to
Dynamic, so packages like `@nx/playwright` and `@nx/storybook` are
missing from the dependency graph.
3. **ensurePackage mock duplication**: Multiple test files individually
mock `@nx/devkit` just to override `ensurePackage` so it resolves from
source instead of `node_modules`. This is repetitive and easy to miss in
new tests.
## Expected Behavior
1. **Sandboxing**: `.tsbuildinfo` reads are globally excluded.
`dist/**/*.json` reads are excluded for lint targets.
2. **Import analyzer**: `typeof import('...')` inside multi-line
generics is correctly detected as a static import by tracking angle
bracket depth and preserving import type across newlines inside
generics.
3. **ensurePackage mock**: A global `ensurePackage` mock in
`scripts/unit-test-setup.js` replaces per-file mocks, using
`jest.requireActual` to resolve from source code.
## Related Issue(s)
<!-- No directly related open issues found -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The `nx` package's `exports` field uses conditional exports that
restrict subpath access. Consumers relying on deep imports (e.g.,
`nx/src/command-line`, `nx/src/project-graph/plugins`) or importing with
file extensions (e.g., `nx/bin/nx.js`) can't resolve modules or types.
The `typesVersions` field is also incomplete and out of sync with
`exports`, breaking type resolution for consumers using
`moduleResolution: "node"` (node10).
## Expected Behavior
The `nx` package exposes all necessary subpaths through both `exports`
(for modern resolution) and `typesVersions` (for node10 resolution),
keeping them in sync. A new conformance rule prevents future drift
between the two fields.
> **Note:** This restores backwards compatibility to avoid breaking
changes in the current major version. Deep imports into `nx/src/*`
access private/internal APIs that are not part of the public contract —
they are not guaranteed to be stable and may break without notice. In Nx
v23, we plan to constrain the exports to a well-defined public API,
which will be a breaking change.
## Changes
- **Restore `nx` package exports**: expand `exports` and `typesVersions`
to cover all public subpaths including `bin/*`, `plugins/*`,
`src/command-line`, `src/project-graph/plugins`, `release/*`,
`tasks-runners/*`, and their `.js` extension variants
- **Add `types-versions-exports-sync` conformance rule**: enforces that
every `exports` entry with a `types` condition has a corresponding
`typesVersions` entry and vice versa, preventing future drift
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## 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: meeroslav <meeroslav@users.noreply.github.com>
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## 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>
## Current Behavior
PR #35041 narrowed the `build-base` target outputs to only match
tsc-produced file types (e.g. `**/*.{js,d.ts,...}{,.map}`), preventing
cross-OS cache pollution from native binaries. However, `.tsbuildinfo`
files were not included in the narrowed glob, so they are no longer
captured as build outputs. The tsbuildinfo handling was also spread
across multiple conditional branches with duplicated logic.
## Expected Behavior
`.tsbuildinfo` files are always included as a build output since `tsc
--build` implicitly enables `incremental: true` and always produces
them. A new `getTsBuildInfoOutputPath` helper centralizes the logic for
determining the tsbuildinfo file location (respecting `tsBuildInfoFile`,
`outFile`, `outDir`, or the default), and is called once unconditionally
at the end of the output resolution loop.
## Related Issue(s)
Follow-up to #35041
<!-- 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
The Gradle executor's task exclusion logic represents running tasks and
dependency relationships using colon-delimited string IDs (e.g.
"project:target"). Parsing task identity by splitting on : breaks
silently when project or target names contain colons — a common pattern
in Gradle (e.g. :sub:project, compile:java).
## Expected Behavior
Task identity is represented as a structured ProjectTarget object with
explicit project and target fields, eliminating string-splitting
ambiguity. The getExcludeTasks, getAllDependsOn, and getGradleTaskName
functions now accept and return typed objects, and test fixtures use
object-notation dependsOn entries. A new test case verifies correct
behavior when names contain colons.
## 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: lourw <lourw@users.noreply.github.com>
This PR removes 62% of dependabot alerts, stemming from unit test
fixtures.
## Current Behavior
Dependabot scans `package.json` files in lock-file test fixtures and
raises vulnerability alerts for packages that are only used as test data
(e.g. `express`, `minimatch`, `postcss`). These are not real
dependencies.
## Expected Behavior
Renaming fixture files from `package.json` to `package.fixture.json`
prevents dependabot from scanning them. The `.fixture.json` extension
still ends in `.json`, so Node's `require()` continues to work without
any test logic changes — only the file paths in the spec files needed
updating.
## Related Issue(s)
Fixes NXC-4169
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
## Current Behavior
`nx init` emits telemetry meta as a CSV string (e.g. "22.6.3,enable-ci")
via `recordStat`, only at the cloud prompt step. There is no start or
error tracking, no AI detection, and no environment context.
## Expected Behavior
`recordStat` now accepts a typed `RecordStatMeta` object and serializes
as JSON, matching the CNW format. Three lifecycle events are recorded:
- **start**: nodeVersion, os, packageManager, aiAgent, isCI
- **complete**: same env info plus pluginsInstalled, useCloud
- **error**: errorCode, errorMessage, aiAgent
The existing cloud prompt `recordStat` calls also now include env info.
## Related Issue(s)
Closes NXC-4168
## Current Behavior
CNW rejects "." and absolute paths (e.g. `/tmp/acme`) as workspace
names, causing over 1,300 INVALID_WORKSPACE_NAME and DIRECTORY_EXISTS
errors per month. This is the #1 input validation error for both AI
agents and humans.
## Expected Behavior
- "." and "./" in a non-empty directory suggests using `nx init` instead
- "." and "./" in an empty directory resolves to the directory's
basename and creates the workspace in-place
- Absolute paths like `/tmp/acme` extract the basename as the workspace
name and create the workspace at the specified location
The `workingDir` override is threaded through `CreateWorkspaceOptions`
to downstream functions (`createWorkspace`, `createEmptyWorkspace`,
`createPreset`, `cloneTemplate`) without mutating `process.cwd()`.
## Related Issue(s)
Closes NXC-4172
## Current Behavior
The pnpm catalog pins picomatch to `4.0.2`, which has two high-severity
vulnerabilities:
-
[GHSA-3v7f-55p6-f55p](https://github.com/advisories/GHSA-3v7f-55p6-f55p)
— Method Injection in POSIX Character Classes causes incorrect glob
matching
-
[GHSA-c2c7-rcm5-vvqj](https://github.com/advisories/GHSA-c2c7-rcm5-vvqj)
— ReDoS via extglob quantifiers
Running `npm audit` on any workspace using `@nx/angular`, `@nx/js`, or
`@nx/workspace` reports these vulnerabilities.
## Expected Behavior
No picomatch-related vulnerabilities reported by `npm audit`. The bump
to `4.0.4` is a patch release that only fixes the security issues with
no API changes.
## Related Issue(s)
Fixes#35068
## Current Behavior
CNW vue-monorepo and nuxt presets pin sass@1.62.1, but Vite 8 requires
sass >= ^1.70.0, causing ERESOLVE failures on npm during workspace
creation.
## Expected Behavior
Workspaces created with vue-monorepo and nuxt presets install
successfully with Vite 8 by using a compatible sass version range.
## Related Issue(s)
Fixes NXC-4171
## Current Behavior
When `--bundler=vite` is passed with an Angular preset
(`angular-monorepo` or `angular-standalone`), yargs accepts it since
`--bundler` is a shared `type: 'string'` option with no per-stack
`choices` constraint. The invalid value flows through to the preset
generator which rejects it after a full pnpm install (~25s), wasting the
user's time.
## Expected Behavior
Validate the bundler early in `determineAngularOptions` before any
install starts. Invalid bundlers throw `CnwError('INVALID_BUNDLER')` so
the error is:
- Properly recorded via `recordStat` for telemetry
- Surfaced as NDJSON for AI agents
- Shown as a clear `output.error()` for interactive users
Valid Angular bundlers: `esbuild`, `rspack`, `webpack`.
## Related Issue(s)
Fixes NXC-4166
## Current Behavior
All packages in the workspace use the standard `tsc` compiler via the
`@nx/js/typescript` plugin.
## Expected Behavior
The `packages/nx` project uses the new Go-based TypeScript compiler
(`tsgo`) for faster builds and typechecks, while all other projects
continue using `tsc`.
### Changes
- Install `@typescript/native-preview` as a dev dependency
- Add a separate `@nx/js/typescript` plugin entry scoped to
`packages/nx` with `compiler: "tsgo"`
- Override `baseUrl: null` in the nx tsconfig to clear the inherited
`baseUrl` (removed in tsgo)
- Set `strict: false` to match existing tsc behavior (tsgo defaults to
strict mode)
## Related Issue(s)
N/A — exploratory adoption of tsgo
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
When creating a workspace with `create-nx-workspace` using presets that
install vitest (react-monorepo with vite bundler, nuxt with vitest, vue,
etc.) and **npm** as the package manager, `npm install` fails with
`ERESOLVE unable to resolve dependency tree`.
**Root cause:** vitest `~4.0.x` depends on `@vitest/mocker@4.0.x` which
has a peer dependency on `vite: "^6.0.0 || ^7.0.0"` — it does **not**
support vite 8. Since Nx now defaults to installing `vite@^8.0.0`, npm
cannot satisfy `@vitest/mocker`'s peer dependency.
Additionally, `@vitejs/plugin-react-swc@^3.5.0` has a peer dependency on
`vite: "^4 || ^5 || ^6 || ^7"` (no vite 8 support), and the SWC compiler
path in `ensure-dependencies.ts` lacks the vite-version detection logic
that the babel path already has.
## Expected Behavior
- Workspaces using vite 8 + vitest install successfully with npm
- `@vitejs/plugin-react-swc` version is selected based on the installed
vite version (v4.3+ for vite 8, v3.x for older vite)
## Changes
1. **Bump `vitestV4Version`** from `~4.0.0`/`~4.0.8` to `~4.1.0` in both
`@nx/vite` and `@nx/vitest` — vitest 4.1.x's dependencies support vite 8
2. **Bump `vitestV4CoverageV8Version` and
`vitestV4CoverageIstanbulVersion`** to `~4.1.0` to match
3. **Update `vitePluginReactSwcVersion`** from `^3.5.0` to `^4.3.0`
(first version with vite 8 peer dep support), add
`vitePluginReactSwcV3Version = '^3.5.0'` for backward compat
4. **Add vite-version detection** to the SWC path in
`ensure-dependencies.ts` (both `@nx/vite` and `@nx/vitest`), mirroring
the existing babel path logic
## Related Issue(s)
Fixes NXC-4164
## Current Behavior
The `publish.yml` workflow defines `NX_GRADLE_PROJECT_GRAPH_TIMEOUT` and
`NX_VERBOSE_LOGGING` as workflow-level env vars, but the `docker run`
command for Linux native builds only passes `-e PNPM_VERSION`. This
means those env vars are not available inside the Docker containers,
causing Gradle timeouts and missing verbose logs during publish.
## Expected Behavior
`NX_GRADLE_PROJECT_GRAPH_TIMEOUT` and `NX_VERBOSE_LOGGING` are passed
into the Docker containers via `-e` flags so they take effect during
Linux native builds.
## Related Issue(s)
N/A
## Current Behavior
The npm security audit CI job fails due to two critical vulnerabilities:
- **handlebars** (GHSA-2w6w-674q-4c4q): JavaScript Injection via AST
Type Confusion in versions `>=4.0.0 <=4.7.8`, pulled in transitively via
`verdaccio > @verdaccio/hooks > handlebars@4.7.7`
- **underscore** (GHSA-cf4h-3jhx-xvhq): Arbitrary Code Execution in
versions `<1.13.8`, pulled in via `parse-markdown-links > remarkable >
argparse > underscore`
## Expected Behavior
The npm security audit CI job passes with zero critical vulnerabilities.
## Changes
- Update `verdaccio` from `6.0.5` to `6.3.2` (drops direct handlebars
dependency)
- Remove unused direct `handlebars` devDependency (nothing in the repo
imports it)
- Add `pnpm.overrides` for `handlebars@4.7.9` (needed because
`@verdaccio/hooks` still pins `handlebars@4.7.7` with no stable fix
available) and `underscore@^1.13.8` (needed because
`parse-markdown-links` pins `remarkable@1.7.1` with no fix available)
- Update verdaccio generator version to `^6.3.2` so new workspaces get
the safe version
- Add migration for `22.6.4` to bump verdaccio in existing workspaces
## Related Issue(s)
Fixes the failing [npm-audit CI
job](https://github.com/nrwl/nx/actions/runs/23672915671/job/68970040091)
## Current Behavior
The Gradle plugin's `createNodes` project graph generation has a fixed
60-second timeout. For large Gradle workspaces, this is too short and
causes timeouts — especially in CI environments where builds may be
slower.
## Expected Behavior
- **Local**: Default timeout increased to 3 minutes (180s)
- **CI**: Default timeout increased to 10 minutes (600s)
- The `NX_GRADLE_PROJECT_GRAPH_TIMEOUT` env var still works as an
override
- The publish workflow explicitly sets
`NX_GRADLE_PROJECT_GRAPH_TIMEOUT=600` for extra safety
## Current Behavior
The `@nx/js/typescript` plugin claims the entire `outDir` (e.g.
`{projectRoot}/dist`) as the output for `build-base` targets. When other
tasks like `copy-assets` also write into the same directory (e.g.
`.node` or `.wasm` native binaries), those files get captured in the
`build-base` cache. This causes cross-OS cache pollution — linux native
binaries get cached and restored on macOS (or vice versa).
## Expected Behavior
`build-base` outputs are scoped to only the file types that `tsc`
actually produces:
- `**/*.{js,cjs,mjs,jsx,d.ts,d.cts,d.mts}{,.map}` (default)
- `**/*.{js,cjs,mjs,jsx,json,d.ts,d.cts,d.mts}{,.map}` (when
`resolveJsonModule` is enabled)
Native binaries (`.node`, `.wasm`) and other non-tsc files in the same
output directory are no longer captured by the `build-base` cache,
preventing cross-OS cache corruption.
## Related Issue(s)
N/A — discovered during investigation of cross-OS cache artifacts.
## Current Behavior
The `@nx/dependency-checks` lint rule detects `tslib` as a required
dependency by checking if the build command contains `tsc` (via
`/\btsc\b/` regex). When a project uses `tsgo` as its compiler, the
build command is `tsgo --build` which doesn't match — causing a false
positive "tslib is not used" lint error.
## Expected Behavior
The regex also matches `tsgo`, so projects using either `tsc` or `tsgo`
correctly detect `tslib` as needed when `importHelpers: true` is set.
## Related Issue(s)
N/A — discovered while enabling tsgo for the nx package
## Current Behavior
- The copy-assets plugin copies `assets.json` into the output directory
(it doesn't exclude itself)
- The copy-assets executor catches errors with `error.message` but
`error` is typed as `unknown`, which can fail at runtime
- No way to skip e2e cleanup when debugging locally
## Expected Behavior
- `assets.json` is excluded from being copied into the output directory
- Error handling properly checks `instanceof Error` before accessing
`.message`
- Setting `NX_E2E_SKIP_CLEANUP=true` preserves the test project
directory for debugging
## Related Issue(s)
Follow-up to #34994 — addresses review comments from that PR.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`/blog` and `/changelog` paths are always served by the Next.js app. The
Netlify edge function excludes these paths (`/blog/*` in `excludedPath`,
`/blog` and `/changelog` in `nextjsPaths`) so they bypass the Framer
proxy and fall through to Next.js.
## Expected Behavior
When the `BLOG_URL` env var is set in Netlify, `/blog/*` and
`/changelog/*` requests are proxied to the standalone blog site (e.g.,
`nrwl-blog.netlify.app`) via the existing Netlify edge function. When
`BLOG_URL` is **not** set, behavior is unchanged — paths fall through to
Next.js as before.
**Changes to `netlify/edge-functions/rewrite-framer-urls.ts`:**
- Read `BLOG_URL` env var
- Conditionally remove `/blog` and `/changelog` from `nextjsPaths` when
`BLOG_URL` is set
- Remove `/blog/*` and `/changelog` from static `excludedPath` so the
edge function can intercept these paths
- Add blog proxy branch: when `BLOG_URL` is set and path matches, proxy
to blog site with URL rewriting (`blogUrl` → `https://nx.dev`) and
security headers (`X-Frame-Options`, `CSP`)
- When `BLOG_URL` is unset, blog/changelog paths fall through to Next.js
via explicit `context.next()` guard
**Asset resolution note:** The edge function only handles `text/html`
requests. Blog site assets (CSS, JS) will need to be served from the
blog site's own CDN (via `build.assetsPrefix` or equivalent in the blog
repo). All existing `/_next/*` assets remain in `excludedPath` and are
unaffected.
## Related Issue(s)
Fixes DOC-455
This PR address some common errors seen during CNW around `--template`
and `--preset` during non-interactive flows that are not AI agents. Also
sees that some template names are not qualified with
`nrwl/<name>-template` so we can normal though (which also makes it
shorter in docs).
## Current Behavior
In non-interactive contexts (IDE terminals, scripts, SSH without `-t`),
`determineTemplate()` returns `'custom'` which routes to the preset
flow. Without `--preset` provided, this throws "Preset is required",
which affects ~15 users/day (~145 occurrences Mar 18-27).
Users must also pass full template paths like
`--template=nrwl/angular-template`.
## Expected Behavior
Non-interactive mode defaults to `nrwl/empty-template` (template flow)
instead of `'custom'` (preset flow) when neither `--preset` nor
`--template` is provided.
Shorthand template names are supported:
- `--template=angular` → `nrwl/angular-template`
- `--template=react` → `nrwl/react-template`
- `--template=typescript` → `nrwl/typescript-template`
- `--template=empty` → `nrwl/empty-template`
## Related Issue(s)
Fixes NXC-4153
## Current Behavior
The TypeScript plugin emits a bare `**/*.d.ts` fileset input (without a
`{projectRoot}/` prefix) for dependency file tracking. The Nx hasher
expects all filesets to start with either `{projectRoot}/` or
`{workspaceRoot}/`, so it logs a warning for every project:
```
NX **/*.d.ts does not start with {workspaceRoot}/. This will throw an error in Nx 20.
```
## Expected Behavior
No warning is emitted. The fileset correctly uses
`{projectRoot}/**/*.d.ts` so the hasher knows it's scoped to the
dependency project's root.
## Related Issue(s)
N/A — discovered while debugging e2e test failures.
## Current Behavior
Each package defines a `legacy-post-build` target in `project.json` with
inline asset copy configuration. Inputs are not accurately declared,
leading to sandbox violations in CI. The asset globs, ignores, and
outputs must be manually kept in sync across 37 packages.
## Expected Behavior
A `copy-assets` createNodesV2 plugin reads `assets.json` from each
package and automatically generates the target with:
- Inputs derived from asset globs (positive patterns first, then
negations)
- Outputs derived using the same dest logic as `CopyAssetsHandler`
- `dependentTasksOutputFiles` for gitignored build artifacts (jars,
native binaries)
- Automatic `outDir` exclusion from asset copies
## Changes
**New infrastructure:**
- Add `copy-assets` createNodesV2 plugin in `tools/workspace-plugin`
- Add `copy-assets` executor (simplified from `legacy-post-build` — just
copies assets, no package.json field manipulation)
- Extract `normalizeAssets` and `getAssetOutputPath` into reusable
module in `packages/js`
- Add `copyReadme` namedInput for copy-readme build targets
**Migration (all 37 packages):**
- Create `assets.json` for every package defining what to copy
- Remove all `legacy-post-build` targets from project.json files
- Remove `legacy-post-build` target defaults from nx.json
- Remove redundant `copy-local-native` target (replaced by
`.node`/`.wasm` in asset glob)
**Cleanup:**
- Remove dead config: `creator-files` globs, non-existent template
files, typo'd directory names
- Use root-level `tsconfig*.json` ignore instead of recursive (so
template tsconfigs in `files/` dirs are still copied)
- Replace `!(*.ts)` extglob patterns with explicit globs (extglobs don't
work correctly in Nx inputs)
- Fix gradle lint: add `buildTargets: ["build-base"]` and `tslib`
dependency
- Add jar outputs to maven `_package` target for correct
`dependentTasksOutputFiles` resolution
**Other fixes:**
- Exclude `.swc` directories from sandbox write checks
- Enable typecheck for `angular-rspack` packages (remove
`addTypecheckTarget: false`)
- Pin workspace-plugin deps to explicit versions, add `@nx/plugin` to
root package.json
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
The cloud prompt in CNW is disabled (shouldShowCloudPrompt returns
false). Users see no cloud prompt during workspace creation.
## Expected Behavior
Re-enables the cloud prompt with 3 copy variants tied to the flow
variant (NX_CNW_FLOW_VARIANT), using the existing A/B testing
infrastructure:
- Variant 0 (baseline): "Connect to Nx Cloud?"
- Variant 1 (remote caching): "Enable remote caching to speed up builds
and CI?"
- Variant 2 (CI-first): "Speed up your CI with Nx Cloud?"
Each variant has a unique tracking code for measuring yes/skip/never
rates. New variants emphasize concrete benefits (remote caching, CI
speed), mention CI providers (GitHub, GitLab), and note free tier and
2-minute setup.
Also removes unused shouldShowCloudPrompt() function.
## Screenshots
Variant 0 (current prompts):
<img width="1392" height="935" alt="cnw-variant-0"
src="https://github.com/user-attachments/assets/38187e44-5c8f-41b1-b2d9-bdb0eead3f7d"
/>
Variant 1 (remote cache to speed up builds, free for small teams):
<img width="1392" height="939" alt="image"
src="https://github.com/user-attachments/assets/ba7ab127-c91e-4566-bae6-6f7fe797959e"
/>
Variant 2 (speed up CI, mention CI provides):
<img width="1392" height="935" alt="cnw-variant-2"
src="https://github.com/user-attachments/assets/05bc027a-dcc9-47c4-944c-35ca2fce2007"
/>
## Related Issue(s)
Closes NXC-4113
Currently, updating to 22.7.0-beta.5 causes Angular and React unit tests
to fail when we are pulling in Vite 8 instead of 7.
Cypress component tests do not support Vite 8 yet, so let's ensure that
the unit tests set up v7 for now.
https://github.com/cypress-io/cypress/issues/33078
## Current Behavior
The FreeBSD native build in the publish workflow runs out of disk space
(`No space left on device`). The VM has an 11G disk and the Rust build
fills it completely. The build was already on the edge — the Mar 25 run
succeeded with only 11MB to spare, and the Mar 26 run failed after rustc
1.94.1 slightly increased artifact sizes.
A major contributor is OpenJDK17 and its ~30 X11/font dependencies
(~500MB) being installed solely for the `@nx/gradle` plugin's project
graph step, which is irrelevant to building native Rust bindings.
## Expected Behavior
The FreeBSD build completes successfully with comfortable disk headroom
by disabling the Gradle plugin via `NX_GRADLE_DISABLE=true`, which
eliminates the need for Java and its heavy dependency tree.
## Related Issue(s)
Fixes the FreeBSD build failure:
https://github.com/nrwl/nx/actions/runs/23613960439/job/68776656960
## Current Behavior
The webinar banner has a hardcoded dark background (`bg-zinc-950`) with
white text in both light and dark mode. This makes the close button
barely visible in light mode since Starlight's global styles override
inherited text colors. The banner design also differs noticeably from
the Framer marketing site, which uses a light background in light mode.
## Expected Behavior
The banner adapts to the current theme:
- **Light mode**: White background, subtle border, dark text, dark CTA
button — matching the Framer marketing site design
- **Dark mode**: Retains the existing dark background with white text
and pink CTA button
All interactive elements (close button, CTA buttons) have proper
contrast in both modes.
Dark:
<img width="759" height="499" alt="image"
src="https://github.com/user-attachments/assets/684113e8-cc48-43df-b17d-431ae8c864fc"
/>
Light:
<img width="635" height="318" alt="image"
src="https://github.com/user-attachments/assets/9ed514b7-dfe9-45c6-91d7-158434aa6cdc"
/>
## Related Issue(s)
Fixes DOC-457
## Current Behavior
When the Gradle plugin invokes `gradlew nxProjectGraph` to generate the
project graph, there is no timeout. If Gradle hangs or takes an
unexpectedly long time, the Nx process blocks indefinitely with no
feedback to the user.
## Expected Behavior
The Gradle project graph generation now has a configurable timeout that
defaults to 60 seconds. If the process exceeds this limit:
- The Gradle process is aborted via an `AbortController` signal
- A clear error message is shown with actionable remediation steps:
- Run `gradlew --stop` and `gradlew clean`
- Increase the timeout via `NX_GRADLE_PROJECT_GRAPH_TIMEOUT`
- Disable the plugin entirely via `NX_GRADLE_DISABLE=true`
Users can configure the timeout by setting the
`NX_GRADLE_PROJECT_GRAPH_TIMEOUT` environment variable (in seconds).
Invalid or non-positive values fall back to the 30-second default.
Documentation for the new environment variable has been added to the
environment variables reference page.
## Related Issue(s)
Fixes NXC-4140
## Current Behavior
Getting started pages have inconsistent navigation patterns. Some use
card grids, some use bullet lists, and some have no next steps at all.
The new tutorial series is not linked from most getting started pages.
## Expected Behavior
All getting started pages use consistent bullet-list navigation with
clear next steps. Every page links to the tutorial series so first-time
users can discover it from any entry point.
For example, on the "Add to an existing project" page:
<img width="1011" height="709" alt="image"
src="https://github.com/user-attachments/assets/6534d490-1bdf-4026-9962-0a60506564ce"
/>
### Changes
- **intro.mdoc**: Updated tutorial link from generic "Follow a tutorial"
to "Follow the tutorial series" pointing to the first tutorial
- **installation.mdoc**: Added "Next steps" section linking to new
project, existing project, and tutorial series
- **start-new-project.mdoc**: Added "Next steps" section with tutorial
series, editor setup, and CI setup links
- **start-with-existing-project.mdoc**: Replaced card grid with bullet
list for in-depth guides, added "Keep learning" section with tutorial
series link
- **sidebar.mts**: Fixed label consistency ("Reduce boilerplate" →
"Reducing boilerplate")
## Current Behavior
Running `nx watch --projects nx -i -- nx build nx` enters an infinite
rebuild loop. Two issues cause this:
1. **Overly broad output globs**: `build-base` declared
`{projectRoot}/src/**/*.d.ts` and `legacy-post-build` declared
`{projectRoot}/**/*.d.ts` as outputs. Cache restore deletes and
recreates committed `.d.ts` files (like `schema.d.ts`,
`perf-hooks.d.ts`) that aren't actual build outputs, triggering the file
watcher.
2. **`declarationDir` in source tree**: With `declarationDir: "."`, tsc
wrote `.d.ts` files into the source tree, making them vulnerable to
cache restore churn.
## Expected Behavior
`nx watch` should not loop when the build produces the same outputs.
Only files actually produced by a build target should be declared as
outputs.
## Changes
- **Move `declarationDir` to `dist`**: Declaration files now go to
`dist/` alongside `.js` output, keeping the source tree clean
- **Remove manual output overrides from `build-base`**: The
`@nx/js/typescript` plugin correctly infers `{projectRoot}/dist`
- **Narrow `legacy-post-build` outputs**: Only declares
`{projectRoot}/dist` since that's the only directory the executor writes
to
- **Fix package.json exports**: Replace invalid `**` subpath patterns
with `*` (Node.js exports only support `*` as wildcard)
- **Add `typesVersions`**: Ensures TypeScript projects using
`moduleResolution: "node"` (which ignores exports maps) can still
resolve `.d.ts` files in `dist/`
- **Remove `@types/minimatch`**: `minimatch@10` ships its own types; the
`@types` package was outdated and conflicted
- **Add verbose watcher logging**: Behind `NX_VERBOSE_LOGGING`, shows
which files were created/updated/deleted by the workspace watcher
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
The nightly failure report already includes per-error details, but has
several accuracy and completeness issues:
1. **Incorrect per-error start dates** — e.g., `e2e-web`'s
`file-server-legacy.test.ts` reports "failing since 2026-03-24 (1 day)
NEW" when the `edgesOut` error actually started on 2026-03-18 (7 days).
The validation only compared test file names, not error content, so
same-file-different-error changes were missed.
2. **Different combo errors merged** — `e2e-web` fails with different
root causes on different combos (yarn: "could not find a copy of vite to
link", npm: "Cannot read properties of null (reading 'edgesOut')"), but
only one error block is shown with all combos listed together.
3. **Build failures show no useful details** — when tests don't run
because a build step fails (e.g., `gradle:build-base` with TS errors),
the report shows "No test output available" instead of the actual build
errors.
4. **No clickable links to jobs** — the golden projects table is a plain
code block with no links to the specific failed jobs.
5. **Other projects table** wastes vertical space with a full code-block
table.
## Expected Behavior
All issues above are fixed:
**Summary section** — golden projects with clickable job links, compact
other-projects list:
```
🌟 Golden Projects
✅ Passing: 18 | ❌ Failing: 2
🚨 Failed Golden Projects
e2e-react-native
· MacOS/npm/20 ← clickable link to job
e2e-web
· MacOS/npm/20 ← clickable link to job
· Linux/yarn/20 ← clickable link to job
· Linux/npm/20 ← clickable link to job
⚠️ Failed Other Projects: e2e-angular (4), e2e-nuxt (4), e2e-nx (10), ...
```
**Failure details** — per-combo errors shown separately with accurate
start dates:
```
e2e-web — 3 combos (npm+yarn)
Project failing since 2026-03-13 | Last fully passing: 2026-03-12
📋 file-server-legacy.test.ts (Linux/yarn/20) — failing since 2026-03-13 (12 days)
error Invariant Violation: could not find a copy of vite to link ...
📋 file-server-legacy.test.ts (MacOS/npm/20, Linux/npm/20) — failing since 2026-03-18 (7 days) ⚠️ error changed mid-streak
npm error Cannot read properties of null (reading 'edgesOut') ...
```
**Build failures** — when tests don't run, the report now shows the Nx
failure summary AND the actual failed task output:
```
⚠️ Tests did not run — failed at step: Run e2e tests with pnpm (Linux/Windows)
NX Running target e2e-local for project e2e-docker and 153 tasks it depends on failed
Failed tasks: gradle:build-base
❌ > nx run gradle:build-base
> tsc --build tsconfig.lib.json
error TS6307: File '...assert-supported-platform.ts' is not listed within the file list ...
```
## Changes
- **Error signature comparison** — compares normalized error messages
(not just file names) between the current run and first-failing run.
Dynamic parts (temp paths, random IDs, timestamps, versions) are
stripped before comparison. Different errors on different combos are
detected and reported separately.
- **Signature-based binary search** — when the error changed mid-streak,
finds when the current error ACTUALLY started by checking error
signatures in historical runs across all combos.
- **Failed Nx task block capture** — extracts the `❌ > nx run <task>`
output block with actual build errors, plus a fallback for generic error
patterns when no Nx task markers are found.
- **Clickable job links** — each failing combo in the golden projects
summary links to its specific GitHub Actions job.
- **Compact other-projects** — replaced the code-block table with a
one-liner count summary.
- **Graceful degradation** — if failure details collection fails, the
brief report still posts with a warning.
## Current Behavior
The Tutorials section has three large framework-specific tutorials
(React Monorepo, Angular Monorepo, TypeScript Monorepo) that each teach
everything at once. They are 500+ lines, tightly coupled to a specific
tech stack, and don't build knowledge incrementally.
## Expected Behavior
Seven focused, technology-agnostic tutorials that each teach one concept
in ~5 minutes. The design follows progressive disclosure so users learn
one thing well before moving to the next.
Preview:
https://deploy-preview-34998--nx-docs.netlify.app/docs/getting-started/tutorials/crafting-your-workspace
### Design goals
- **Learn the basics in an hour**: A user going through all 7 tutorials
covers workspace structure, dependencies, tasks, running, caching,
debugging, and plugins.
- **AI agent friendly**: Each page has an `llm_copy_prompt` at the top
that an AI agent can use as a tutor prompt. A user can paste this into
Claude Code, Cursor, or any AI tool and be guided through the topic in
their own workspace.
- **No assumed workspace state**: Each tutorial works whether you used
`create-nx-workspace`, ran `nx init` on an existing repo, or jumped to a
specific topic. Pages link back to prerequisites when needed.
- **One concept per page**: Progressive disclosure means plugins aren't
mentioned until tutorial 7, `nx graph` isn't shown until tutorial 6, and
caching configuration doesn't appear until tutorial 5.
### Tutorial sequence
1. **Crafting your workspace** - Nx as a build intelligence layer,
workspace structure, package manager workspaces, TypeScript
solution-style project references, adding projects
2. **Managing dependencies** - Workspace libraries, buildable vs
non-buildable (with `exports` and `customConditions`), `workspace:*`
protocol, single version policy with catalogs
3. **Configuring tasks** - `package.json` scripts vs `project.json`
targets, `dependsOn` with `^` syntax, continuous tasks, named
configurations, target defaults
4. **Running tasks** - `nx run`, shorthand, `run-many` with
`--targets`/`--projects`, task ordering with SVG diagram, parallelism
control, passing arguments
5. **Caching tasks** - Run-twice demo, computation hashing with SVG
diagram, inputs/outputs, named inputs, env vars/runtime inputs,
sandboxing, remote caching with `nx connect`
6. **Understanding your workspace** - `nx graph` as entry point, project
graph with edge screenshots, task graph with screenshots, `nx show
project`/`nx show target`, affected analysis with `--base`/`--head`,
cache debugging
7. **Reducing configuration boilerplate** - `targetDefaults`, Nx
plugins, inferred tasks, `nx add`, configuration cascade, presented as
optional
### Other changes
- **CI tutorial rewritten** to cover remote caching, affected (with
`NX_BASE`/`NX_HEAD`), Nx Agents, and self-healing (was previously
self-healing only)
- **Concept pages** ("How Nx works") link to relevant tutorials via
"Learn by doing" callouts
- **Redirects** from old tutorial URLs to new pages
- **Cross-references** updated across 11 technology/guide pages
- **SVG diagrams** for task dependency ordering and cache hash flow
(same style as sandboxing page)
- **Screenshots** for project graph edge detail and task graph views
- **Sidebar**: Tutorials group has "New" badge, collapsed by default
## Screenshots
Tutorials topics as ordered in sidebar:
<img width="324" height="290" alt="image"
src="https://github.com/user-attachments/assets/87b27e6a-30f6-4c9b-b52d-b371f66e72e8"
/>
AI instructions that can be copied and pasted into Claude Code to act as
a tutor:
<img width="803" height="308" alt="image"
src="https://github.com/user-attachments/assets/2dcf402e-19a6-4cf4-bcac-7005491529bf"
/>
Linking to prev/next tutorials at the bottom of each tutorial topic:
<img width="871" height="457" alt="image"
src="https://github.com/user-attachments/assets/b9a198b5-edc7-4afd-8c13-fd70c10cf858"
/>
For concept pages that are covered by a tutorial, link to the tutorial
in `Next steps`:
<img width="945" height="435" alt="image"
src="https://github.com/user-attachments/assets/a05d1c0b-c95a-42c5-baec-1fb30dff7c64"
/>
## Related Issue(s)
Fixes DOC-452
## Current Behavior
The `@nx/vite` plugin only supports Vite 5, 6, and 7. Users on Vite 8
get peer dependency errors:
```
peer vite@"^5.0.0 || ^6.0.0 || ^7.0.0" from @nx/vite
```
## Expected Behavior
Full Vite 8 support for new and existing workspaces:
**Nx plugin support:**
- Update peer deps to include `^8.0.0` in `@nx/vite` and `@nx/vitest`
- Default new workspaces to Vite 8 (`viteVersion = '^8.0.0'`)
- Bump `@vitejs/plugin-react` to `^6.0.0` (required for Vite 8, uses Oxc
instead of Babel)
- Add `useViteV7` backward compatibility flag (follows existing
`useViteV5`/`useViteV6` pattern)
**Rolldown migration (Vite 8 replaced Rollup with Rolldown):**
- Handle both `rollupOptions` (Vite <8) and `rolldownOptions` (Vite >=8)
in build executor and plugin detection
- Fix build executor environments API to preserve env-specific
`rolldownOptions` config
- Update e2e tests for Rolldown's different module counts
**Type fixes for Vite 8's ESM-only declarations:**
- Vite 8 ships `.d.mts` type declarations not resolvable under
`moduleResolution: "node"`
- Fix `typeof import('vite')` and `import type` usages across
`@nx/vite`, `@nx/vitest`, `@nx/cypress`, `@nx/react`, `@nx/angular`,
`@nx/remix` with inline casts
- All fixes have `TODO(jack)` comments to remove when switching to
`moduleResolution: "nodenext"`
**Plugin compatibility:**
- `@vitejs/plugin-react@^6.0.0` only supports Vite 8; `^4.2.0` for Vite
<=7
- `ensure-dependencies` detects installed vite version and picks the
correct plugin-react version
- Cypress CT does not support Vite 8 yet — e2e test downgrades to Vite 7
**Angular vitest fix:**
- `@angular/build` depends on `rolldown` which injects
`@oxc-project/runtime` helpers at transform time without declaring it as
a dependency
- Add `@oxc-project/runtime` as an explicit devDependency in the Angular
vitest generator
**Docs:**
- Updated supported versions table to include `^8.0.0`
- Added `rolldownOptions.input` to buildable project detection docs
**E2E coverage:**
- New Vite 8 + React build/test e2e test
- New Vite 7 + React backward compat e2e test (downgrades vite +
plugin-react)
- Updated environments API test for Vite 8 `rolldownOptions`
- Updated incremental build test for Rolldown's module counts
- Cypress CT e2e downgrades to Vite 7 (Cypress doesn't support Vite 8
yet)
## Other notes
We'll do a follow-up PR to include migrations for 22.7.0. This PR's
scope is only to ensure peer deps and our generators work for workspaces
that are already using Vite 8.
## Related Issue(s)
Fixes#34849
Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
## Current Behavior
The `update-all-repos` target explicitly lists each repo target in its
`dependsOn`. Adding a new repo requires updating this list manually.
There is no target for updating the nx-labs repo.
## Expected Behavior
- New `update-nx-labs-repo` target added for updating the nx-labs
repository
- `update-all-repos` uses a `update-*-repo` glob pattern in `dependsOn`,
so new repo targets are automatically picked up
## Current Behavior
The `pushToGitHub` flow in create-nx-workspace calls `gh api user` and
`gh repo create --push` with no timeout. When `gh` CLI is wrapped by
1Password SSH agent, credential managers with GUI prompts, SSH keys with
passphrases, or corporate SSO, these calls hang indefinitely — freezing
the CLI after workspace creation with no indication of what's happening.
YTD data shows a **96.3% failure rate** (16,426 failures out of 17,058
push attempts).
## Expected Behavior
- The initial `gh` auth check (`gh api user`) has a **2-second
timeout**. If `gh` is slow to respond (hung on credential prompt), the
push flow is skipped gracefully instead of freezing.
- The `gh repo create --push` command has a **15-second timeout**. If
the push hangs, the process is killed and a helpful message is shown.
- If `gh` CLI is not installed, the push flow is skipped immediately
without attempting any network calls.
- Error messages now include actionable fallback: *"You can push
manually with: git push -u origin main"*.
The error is now captured in `recordStat` as well, so when users see
this we'll also see it in our stats.
<img width="1026" height="334" alt="image"
src="https://github.com/user-attachments/assets/10c0b4a8-9083-4063-9558-88ebb8a5a850"
/>
## Related Issue(s)
Fixes#34482, NXC-4141
## Current Behavior
When a task is prematurely completed (e.g., due to a failure that causes
early termination) before its dependents have been scheduled, calling
`scheduleNextTasks` crashes the batch executor. The crash occurs in
`processTaskForBatches`, which traverses reverse dependencies and
attempts to read `notScheduledTaskGraph.dependencies[task.id]` for a
task that was already removed from `notScheduledTaskGraph` via
`complete()`. This yields `undefined` for the dependencies array,
causing downstream code to throw.
## Expected Behavior
When a task has been prematurely completed before batch scheduling runs,
`processTaskForBatches` should skip that task gracefully rather than
crashing. Tasks that were removed from `notScheduledTaskGraph` early
should be detected and skipped during batch traversal.
## Related Issue(s)
Fixes NXC-4144
This pull request makes a targeted improvement to how TypeScript
dependency tracking is handled in the build system. Specifically, it
ensures that all `*.d.ts` files from dependent projects are included as
inputs, improving type safety and correctness in incremental builds.
Dependency tracking improvements:
* Updated the `getInputs` function in `plugin.ts` to add all `*.d.ts`
files from dependencies as tracked inputs, ensuring that changes to type
definition files in dependent projects are properly detected and trigger
rebuilds.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
## Current Behavior
When a user selects "yes" to connect to Nx Cloud during
`create-nx-workspace`, the Cloud setup URL is only displayed in the
terminal banner. The user must manually click or copy the link to
complete onboarding.
## Expected Behavior
After displaying the banner, the setup URL is automatically opened in
the user's default browser, removing friction at the critical Cloud
conversion moment.
- Skips browser open in CI environments (`CI=true`, GitHub Actions,
etc.)
- Fails gracefully if the browser cannot be opened (e.g., headless/no
display server) — the URL remains visible in the terminal
- Only triggers when user actually connected to Cloud (not for "Maybe
later" / `skipCloudConnect`)
## Related Issue(s)
Fixes NXC-4112
## Current Behavior
When `nx migrate latest` runs, it installs the target version of `nx`
into a temporary directory and executes that published CLI. During
migration, Nx checks whether `nx` is already installed in the workspace
by resolving `nx/package.json`.
This used to resolve through the workspace-oriented lookup paths, so
migrate correctly detected the workspace-installed `nx` package and
expanded the first-party Nx package group.
That changed recently with `chore(core): build nx to local dist and use
nodenext (#34111)`. As part of that work, the published `nx` package
gained an `exports` map. In Node, a package with `name: "nx"` and
`exports` can self-reference by package name. As a result, when code
running inside the temporary published `nx` package resolves
`nx/package.json`, Node now resolves that request back to the temporary
package itself.
The resolved path points outside the workspace root, so migrate's
existing safety check treats `nx` as not installed in the workspace.
Once that happens, migrate only updates `nx` and never expands the rest
of the first-party Nx package group.
Separately, provenance package-group lookup assumes a
source-layout-relative path to `package.json`, which does not hold for
published artifacts built into local `dist/`.
## Expected Behavior
Published temporary migrate CLIs should still resolve the
workspace-installed `nx` package when migrate determines what is
installed in the workspace. That preserves the existing package-group
migration behavior even after the recent `exports`-based packaging
change.
Provenance package-group lookup should resolve the manifest of the
currently running `nx` package in a way that works for published
artifacts as well as source checkouts.
## Current Behavior
When using `compiler: 'swc'` with `useLegacyTypescriptPlugin: false` in
rollup config, builds fail with errors like:
```
ERROR failed to read input source map: failed to find input source map file "index.js.map"
```
SWC defaults `inputSourceMap` to `true`, causing it to look for
`.js.map` files for TypeScript source inputs that obviously don't have
them.
## Expected Behavior
Builds should succeed without source map resolution errors. Rollup
handles source maps via its own output pipeline — the SWC transform step
should not independently try to resolve input source maps.
## Related Issue(s)
Fixes#32671
## Current Behavior
When removing or moving a project in an Nx workspace, the `owners` and
`conformance` sections in `nx.json` are not updated. This leaves stale
project references in:
- `conformance.rules[].projects` (both plain strings and `{ matcher }`
objects)
- `owners.patterns[].projects` (top-level and section-level for GitLab)
## Expected Behavior
- **On project removal**: Strip the removed project from all conformance
rules and owners patterns. Remove entries that become empty after
cleanup (rules with no projects, patterns with no projects).
- **On project move/rename**: Rename all references to the old project
name with the new name in conformance rules and owners patterns
(including section-level patterns for GitLab-style CODEOWNERS).
- **Schema**: Add `owners` configuration schema to `nx-schema.json` for
validation and IDE support, including sections (GitLab CODEOWNERS).
## Related Issue(s)
<!-- No specific issue linked -->
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: barbados-clemens <barbados-clemens@users.noreply.github.com>
## Current Behavior
The `TaskResult` interface exported from the devkit is missing
`startTime` and `endTime` properties. At runtime, these properties are
present on `TaskResult` objects — set by the task orchestrator during
execution and consumed by multiple lifecycle implementations (profiling,
timings, history, store-run-information) — but the public type does not
declare them. This means the [TaskResult reference
docs](https://nx.dev/docs/reference/devkit/TaskResult) don't show these
fields.
## Expected Behavior
The `TaskResult` interface includes optional `startTime` and `endTime`
properties (Unix timestamps) matching what is actually present on
runtime objects. The generated devkit reference docs will now include
these fields.
## Related Issue(s)
N/A
## Current Behavior
`shut down after last hook`
## Expected Behavior
`shut down after [hook]`
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
Currently, rebuilds produce a summary table that is not accurate.
A key difference in how Rspack sets `chunks.rendered` compared to
Webpack resulted in the logic for determining rebuilt chunks to be
incorrect.
## Expected Behavior
Ensure the summary table for emitted chunks is accurate on rebuild
## Related Issues
It does not _fully_ solve #34936, however it should be a good first step
to finding out if too many chunks are being emitted
<!-- 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 -->
lists invalid images
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
lists valid images
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes#34970
## Current Behavior
The `@nx/js` TypeScript plugin ignores the `configName` option when
constructing the typecheck target command. It always runs `tsc --build
--emitDeclarationOnly` without specifying which tsconfig to use,
defaulting to whatever `tsc` resolves on its own. This means custom
`configName` values (e.g. `tsconfig.lib.json`) have no effect on
typechecking.
## Expected Behavior
The typecheck target should pass `configName` to the `tsc --build`
command, matching how the build target already works: `tsc --build
<configName> --emitDeclarationOnly`.
## Related Issue(s)
Fixes#34274
<!-- 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 -->
Current plugin version is at 0.1.16
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Bump version to 0.1.17
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Current Behavior
- Searching for a framework (e.g. "nestjs", "vite", "angular") returns
random pages that mention the term instead of the plugin introduction
page.
- The launch templates page is too long impacting word saturation
- Search event tracking uses wrong event
## Expected Behavior
- Technology introduction pages rank first or second when searching for
their framework name.
- Launch templates are split into a focused reference page and a
separate examples page, both cross-linked.
- Use correct search event and track user selected item from query
- enforce usage of pagefind filter names in content collection schemas
Fixes DOC-408
## Current Behavior
The `postinstall` script in `packages/nx/package.json` runs `node
./dist/bin/post-install` which prints noisy error logs to stderr when
the `dist` directory hasn't been built yet (e.g. during `pnpm install`
in the workspace). The script already exits cleanly via `|| exit 0`, but
the error output is confusing.
## Expected Behavior
The postinstall script silently succeeds when the dist directory doesn't
exist, without printing error logs to the console.
## Related Issue(s)
N/A - minor DX improvement
## Current Behavior
Running `nx add @nx/vitest` does not register the `@nx/vitest` plugin in
`nx.json`. The init generator had a wrapper function (`initGenerator`)
that hardcoded `addPlugin: false` before spreading the user-provided
schema. Since `nx add` calls the generator via CLI without passing
`addPlugin`, the value stayed `false` — and the `??=` default logic in
`initGeneratorInternal` never fired because `false` is not nullish.
## Expected Behavior
Running `nx add @nx/vitest` should register the `@nx/vitest` plugin in
`nx.json`, matching the behavior of other plugins like `@nx/jest`,
`@nx/vite`, and `@nx/playwright`.
## Related Issue(s)
<!-- No existing issue tracked for this -->
## Changes
- Merged `initGenerator` and `initGeneratorInternal` into a single
`initGenerator` function that uses the `??=` default logic directly
- Added unit tests for the init generator covering plugin registration
defaults, `NX_ADD_PLUGINS` env var, `addPlugin: true` vs `false`
behavior, package.json handling, and namedInputs
<!-- 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
When the Gradle plugin's regex-based test parser (used as a fallback
when AST parsing fails) encounters a Kotlin file
containing enum classes, those enum classes are incorrectly included as
test atomization targets. This causes spurious
test entries to appear for enum types like TestStatus or Priority that
are not actual test classes.
## Expected Behavior
Enum classes are excluded from the list of discovered test classes
during atomization, consistent with how abstract
classes and annotation classes are already filtered out. Only real test
classes should be identified as atomization targets.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
The `update-repos` tool clones repositories into `/tmp` and runs `pnpm
install` / `nx migrate` without trusting the repo's mise configuration.
This means the wrong tool versions (node, bun, etc.) may be used.
Additionally, `nrwl/nx-labs` is not included in the update-repos config.
## Expected Behavior
- Run `mise trust` before installing dependencies if the cloned repo has
a `mise.toml` or `.mise.toml`, ensuring correct tool versions are
activated.
- Include `nrwl/nx-labs` in the repos that get updated.
## Related Issue(s)
N/A - internal tooling improvement
This pull request refactors the caching strategy in the task hashing
logic to improve cache isolation and correctness. The main change is to
eliminate shared, long-lived caches in favor of creating fresh caches
for each invocation, preventing stale data from persisting across CLI
commands. Additionally, new caches for project and workspace file set
hashes are introduced, and cache handling is streamlined for better
maintainability.
**Cache management improvements:**
* Removed the `runtime_cache` field from the `TaskHasher` struct and now
create fresh `DashMap` caches for runtime, project file set, and
workspace file set hashes within each invocation of the main hash
function. This ensures no stale cache data persists across CLI commands.
[[1]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bL153)
[[2]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bL184)
[[3]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bL196-R208)
* Updated the `hash_runtime` function and its usage to accept a
reference to a `DashMap` instead of an `Arc<DashMap>`, simplifying
ownership and concurrency concerns.
[[1]](diffhunk://#diff-77cbfff0572545027ac8ad107859d5135e6f78ee7c32babc56b2c9bbf89cfaccL5-R11)
[[2]](diffhunk://#diff-77cbfff0572545027ac8ad107859d5135e6f78ee7c32babc56b2c9bbf89cfaccL54-R61)
**File set hash caching:**
* Introduced the `CachedFileSetHash` struct to store both the hash value
and the list of matched files for project and workspace file set
hashing, enabling more efficient input collection and cache lookups.
* Added per-invocation caches for project and workspace file set hashes,
with logic to check the cache before computing a new hash and to insert
results after computation.
[[1]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bL196-R208)
[[2]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bR353-R385)
[[3]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bR412-R438)
[[4]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bR554-R556)
**Function signature and argument updates:**
* Updated function signatures and argument passing to propagate the new
cache references throughout the hashing logic, including
`HashInstructionArgs` and related functions.
[[1]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bR264-R266)
[[2]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bR341-R343)
[[3]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bR554-R556)
Fixes#30170
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Nx uses its package version as the DB version. Any Nx version change
**wipes the entire database**, losing all task history and cache
metadata. With multiple worktrees, each worktree maintains its own
separate DB — so switching between them constantly nukes task history,
and worktrees can't benefit from each other's cached results.
## Expected Behavior
1. **Versioned DB filenames**: DB version is decoupled from Nx version.
A new `DB_VERSION` constant is bumped only when the schema changes. The
version is encoded in the filename (`{machine_id}-v{DB_VERSION}.db`), so
multiple schema versions coexist on disk without wiping each other.
2. **Shared DB across worktrees**: When running inside a git worktree,
Nx detects the main repo root and stores the DB in the main repo's
`.nx/workspace-data/` directory. All worktrees share the same task
history, cache metadata, and task details — but **running tasks are
tracked per-worktree** since they are inherently local to each working
copy.
### Changes
- Add `DB_VERSION` constant in `initialize.rs` (bumped only on schema
changes)
- Encode version in DB filename: `{machine_id}-v1.db`
- Remove `nx_version` parameter from `connect_to_nx_db` and
`initialize_db`
- Remove metadata table entirely — schema version is in the filename, so
no runtime version check needed
- Simplify `initialize_db` to use file-existence check instead of
querying metadata
- Add `get_main_worktree_root` napi function in a dedicated `worktree`
module for worktree detection via `git rev-parse --git-common-dir`
- Add `sharedWorkspaceDataDirectory` in TypeScript that resolves to the
main repo's workspace-data dir when in a worktree
- Add `getLocalDbConnection` for per-worktree data (e.g. running tasks)
that should not be shared
- Running tasks use a local DB to avoid false "already running"
conflicts between worktrees
- Daemon stays per-worktree (no changes needed)
- Stale DB files from old schema versions are cleaned up automatically
after 7 days
### What's shared vs local
| Data | Scope | Why |
|------|-------|-----|
| `task_details` | Shared | Keyed by content hash — same code = same
hash regardless of worktree |
| `task_history` | Shared | More data = better time estimates and flaky
detection |
| `cache_outputs` | Shared | Cache lives in one place, tracking should
too |
| `running_tasks` | **Per-worktree** | Each worktree runs tasks
independently — sharing would cause false conflicts |
### Migration
- Old `{machine_id}.db` files are simply ignored (Nx now looks for
`{machine_id}-v1.db`)
- No data migration — fresh DB on first use (same as today on version
bumps)
- Old files cleaned up on `nx reset` and automatically after 7 days of
inactivity
## Related Issue(s)
<!-- N/A - internal improvement -->
## Current Behavior
`INSERT OR REPLACE` is used in `task_details` and `cache_outputs`
tables. The bundled SQLite (`libsqlite3-sys`) is compiled with
`SQLITE_DEFAULT_FOREIGN_KEYS=1`, so FK constraints are enforced by
default. `INSERT OR REPLACE` does a DELETE + INSERT on PK conflict, and
the implicit DELETE on `task_details` fails when child rows exist in
`task_history` or `cache_outputs`:
```
NX DB transaction error: SqliteFailure(Error { code: ConstraintViolation, extended_code: 787 }, Some("FOREIGN KEY constraint failed"))
```
This happens because `record_task_details` is called multiple times with
the same hash across different code paths (`hashTask`, `hashTasks`,
`hashBatchTasks`), and by the second call, child rows already reference
that hash.
## Expected Behavior
Use `INSERT ... ON CONFLICT DO UPDATE` (upsert) which updates the
existing row in-place without deleting it. No DELETE means no FK
violation, while preserving the same idempotent behavior the code relies
on.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The patched jest resolver handles `nx/*` imports differently for unit
tests and e2e tests:
- **Unit tests**: Manual filesystem-based resolution with regex matching
and `fs.existsSync` checks to find `.ts` source files
- **E2e tests**: Falls through to `options.defaultResolver`, which
follows pnpm `workspace:*` symlinks to `packages/nx/` and resolves via
the package.json exports map — landing on `dist/*.js` files instead of
`.ts` source
This causes ~200 sandbox warnings for undeclared file reads from
`packages/nx/dist/` during e2e task execution, since those files aren't
listed as task inputs.
Additionally, `e2eInputs` in `nx.json` references
`{workspaceRoot}/jest.preset.js` (the root unit test preset) instead of
the actual e2e preset at `{workspaceRoot}/e2e/jest.preset.e2e.js`.
## Expected Behavior
Both unit and e2e tests resolve `nx/*` imports using the `@nx/nx-source`
custom export condition via `options.defaultResolver`. This leverages
the existing exports map in `packages/nx/package.json` to resolve to
`.ts` source files, eliminating:
- The manual filesystem resolution code for `nx/*` (regex matching,
`fs.existsSync` calls)
- The e2e-specific code path that fell through to compiled `.js` files
- Sandbox warnings for undeclared reads from `packages/nx/dist/`
The `e2eInputs` preset reference is corrected to point to the actual e2e
preset file.
## Related Issue(s)
<!-- No specific issue — this was discovered during sandbox warning
investigation -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
After the nodenext PR (#34111) added an `exports` map to
`packages/nx/package.json`, the nx-cloud light client (ocean) fails to
import `nx/src/native`.
The wildcard exports pattern `"./src/*"` resolves `nx/src/native` to
`./dist/src/native.js` — but the actual file is
`./dist/src/native/index.js` (it's a directory with an index file).
Node's exports map does literal `*` substitution and does **not**
perform CJS-style directory/index resolution.
The cloud client wraps all its nx imports in a single try/catch, so when
the native import fails, `getDbConnection` is never assigned either,
causing `getDbConnection is not a function` errors in CI.
This was not an issue with `nx@22.7.0-beta.1` because that version had
no `exports` map — Node fell back to normal CJS resolution which handles
directory/index lookups.
## Expected Behavior
`require('nx/src/native')` correctly resolves to
`dist/src/native/index.js` via an explicit export entry, and the cloud
client can load the native module and `getDbConnection` without errors.
## Related Issue(s)
N/A — discovered during version bump CI failure investigation.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Six e2e tests are failing on master due to a Cypress uncaught exception:
`[HMR] Hot Module Replacement is disabled`. The error originates from
the webpack `styles.js` bundle during the `before each` hook, causing
Cypress to abort the test suite. This appears to be triggered by an
external dependency update.
Failing tests:
- `e2e-nx:e2e-ci--src/workspace-legacy.test.ts`
-
`e2e-react:e2e-ci--src/module-federation/independent-deployability.webpack.test.ts`
-
`e2e-react:e2e-ci--src/module-federation/core-webpack-basic-host-remote-generation.test.ts`
-
`e2e-react:e2e-ci--src/module-federation/misc-rspack-interoperability.test.ts`
-
`e2e-react:e2e-ci--src/module-federation/dynamic-federation.webpack.test.ts`
-
`e2e-react:e2e-ci--src/module-federation/federate-module.webpack.test.ts`
## Expected Behavior
The flaky tests are skipped so that master CI is green while the root
cause (likely an external dependency update) is investigated separately.
## Related Issue(s)
N/A — CI stabilization fix.
## Current Behavior
`update-all-repos` only updates ocean and nx-console repositories. There
is no way to update just those two repos separately from the full set.
## Expected Behavior
- `update-all-repos` now includes all four repos: nx, ocean,
nx-examples, and nx-console
- A new `update-repos` target updates only ocean and nx-console (the
most commonly used subset)
## Related Issue(s)
Internal tooling improvement — no external issue.
## Current Behavior
When the globally installed Nx binary (`bin/nx.ts`) runs, it calls
`setupWorkspaceContext()` **before** determining whether it should hand
off to a local Nx installation. If the global Nx version differs from
the local version (e.g. global `22.7.0-beta.2` vs local
`22.7.0-beta.1`), `isNxVersionMismatch()` returns true,
`daemonClient.enabled()` returns false, and `setupWorkspaceContext()`
creates an in-process `WorkspaceContext` that locks the workspace data
directory.
When the local Nx then tries to start the daemon, it conflicts with this
lock and the daemon fails to start. This means running `nx reset`
followed by any command (e.g. `nx show projects`) results in the daemon
never auto-starting, falling back to in-process graph construction, or
erroring out entirely.
## Expected Behavior
The global bin should not set up a workspace context when it's about to
hand off to a local Nx installation. The local Nx will handle workspace
context setup itself.
`setupWorkspaceContext()` is now only called in the `isLocalInstall` and
`isNxCloudCommand` branches — skipped in the handoff path. This matches
the existing pattern established in #34914 for analytics and DB
connections.
## Related Issue(s)
<!-- No specific issue filed — discovered during development in the nx
repo -->
## Current Behavior
The command name (e.g., `build`, `test`, `generate`) is only sent as the
`dt` (document title) parameter on `page_view` events. All other
telemetry events like `run_completed` and `project_graph_computed` have
no command context, making it impossible to correlate them with the
command that triggered them in Google Analytics.
## Expected Behavior
All telemetry events include the `dt` parameter with the command name.
The Rust telemetry background thread stores the page title when a
`page_view` is received and automatically injects it into all subsequent
events in the same process.
## Related Issue(s)
N/A — internal analytics improvement
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
The native Rust import scanner mishandles TypeScript's `import X = Y.Z`
namespace alias syntax. When it encounters this pattern, it fails to
recognize it as a non-module statement and continues scanning forward
for string literals, treating the next one it finds as a module
specifier.
On case-insensitive filesystems (macOS default HFS+/APFS), this causes
phantom npm dependencies in the project graph. For example, `import
MyBar = Foo.Bar` followed by `case 'Open':` causes `npm:open` to appear
as a static dependency because `node_modules/Open` resolves to
`node_modules/open`.
This makes `@nx/dependency-checks` impossible to satisfy across
platforms — macOS and Linux disagree on whether the phantom package is
used.
## Expected Behavior
`import X = Y.Z` is a TypeScript namespace alias, not a module import.
The scanner should skip it entirely. Only actual module imports (`import
X = require('module')`, `import ... from 'module'`, etc.) should produce
dependency entries.
After this fix:
- `import X = Y.Z` is correctly identified as a namespace alias and
skipped
- `import X = require('module')` continues to work as a valid CommonJS
import
- String literals in switch/case statements and type aliases are no
longer misidentified as imports
## Related Issue(s)
Fixes#34644
Fix duplicate migrations keys as only the last migration was getting
picked up.
## Current Behavior
When running nx migrate only the module federation migration is picked
up and not updating the core angular packages.
## Expected Behavior
Both migrations should be added to migrations.json.
## Current Behavior
The TUI sidebar scrollbar thumb position is driven by the selected
task's index among tasks (`selected_task_index`). This means the
scrollbar can appear offset from the top even when `scroll_offset=0`
(i.e., the viewport is showing the very beginning of the list), because
the selected task might not be the first one. This makes it look like
there is content above that cannot be scrolled to.
<img width="1165" height="833" alt="Screenshot 2026-03-03 at 8 53 00 PM"
src="https://github.com/user-attachments/assets/43c695ad-c4a8-40c9-85b0-7677a8b83d12"
/>
Here, it looks scrolled down...but actually all the content is in view.
## Expected Behavior
The scrollbar should accurately represent which portion of the entry
list is currently in view. When at the top of the list
(`scroll_offset=0`), the thumb should be at the top. When scrolled to
the bottom, the thumb should be at the bottom.
## Related Issue(s)
N/A (discovered via visual inspection)
## Details
Switch the scrollbar from selection-based metrics to scroll-offset-based
metrics:
- `content_length` = total entries (including spacer rows)
- `viewport_content_length` = viewport height
- `position` = scroll offset
This ensures the scrollbar reflects the actual viewport position rather
than the selected task's position within the task list.
Also adds `total_entries` and `viewport_height` fields to
`ScrollMetrics` to make these values available without additional lock
acquisitions.
Co-authored-by: Amp <amp@ampcode.com>
## Current Behavior
`--parallel=N` doesn't cap discrete task concurrency when continuous
tasks exist. `getThreadCount` inflates the thread count to `N +
continuousCount`, and all threads are fungible — any thread picks up any
task. With `--parallel=1` and 2 continuous tasks, 3 threads run,
allowing up to 3 discrete tasks concurrently.
## Expected Behavior
`--parallel=N` caps discrete task concurrency to N. Continuous tasks get
dedicated threads that don't inflate the discrete limit.
### Changes
- **Two-pool thread model**: Split the unified thread pool into discrete
and continuous pools. Each pool runs its own loop and only picks up
matching tasks.
- **`getThreadPoolSize`** (renamed from `getThreadCount`): Returns `{
discrete, continuous, total }`. Discrete pool = `options.parallel`,
continuous pool = number of continuous tasks.
- **`executeDiscreteTaskLoop`**: Handles batches + discrete tasks only.
Uses slot-based groupIds via `closeGroup`/`openGroup`.
- **`executeContinuousTaskLoop`**: Handles continuous tasks only. Uses
counter-based groupIds (`parallel + n++`). Exits once all continuous
tasks have been started.
- **`nextTask(filter?)`** on `TasksSchedule`: Optional filter param to
dequeue only matching tasks. Scheduler stays unaware of pools/limits.
## Related Issue(s)
Fixes#34117Fixes#31494
## Current Behavior
`nx run-many` with TUI enabled crashes with `TypeError: Cannot read
properties of undefined (reading 'push')` in `tui-summary-life-cycle.ts`
when `appendTaskOutput` is called for a task whose output entry was
already cleaned up by `endTasks`.
## Expected Behavior
Late-arriving task output after `endTasks` has finalized the task is
silently discarded instead of crashing. The output is redundant since it
was already captured when the task completed.
## Related Issue(s)
Fixes#34677
## Current Behavior
- `splitTarget` causes issues when a project name has more than one
colon
- Project name substitution is triggered when a project depends on the
orginal name of a renamed project, even if the renamed project was
renamed before the dep was registered
That second one is hard to understand. Imagine the real scenario below:
- @nx/gradle was reading settings.gradle.kts and naming the root project
`nx`
- The package.json inference plugin renames it to `@nx/nx-source`
- The package.json inference plugin infers the `nx` project in
`packages/nx`
- The package.json inference plugin reads a config that has `dependsOn:
[nx:build]`
- The substitutors run, and update the dependsOn to
`@nx/nx-source:build`
## Expected Behavior
splitTarget follows the below precedence:
- If splitting a target string thats embedded in a specific project
configuration, targets on that project are preferred
- Targets belonging to the project with the longest name that is valid
from joining segments left to right
- Targets that have the longest name from joining segments are preferred
- Configuration is the remaining segments after picking off the longest
valid project containing a valid target.
Substitutors prefer registering by root if a project with a given name
exists, and only register by name if no project with that root exists
(e.g. if a plugin adds a dependsOn to a project that was inferred by a
later plugin, this would be common if a custom plugin is reading
project.json to get a name or smth).
## AI Summary
> # Branch Analysis: `fix/split-target-fixes` vs `master`
>
> ## Summary
>
> | Metric | Lines |
> |--------|------:|
> | **Total raw diff (added+removed, non-test)** | 2,840 + 1,221 = 4,061
|
> | **Lines that were just moved (file split)** | ~960 |
> | **Truly new/changed lines (non-test)** | ~570 |
> | **Test file changes (raw diff)** | 3,109 added / 2,242 removed |
>
> ## Commits
>
> | SHA | Message |
> |-----|---------|
> | `57ec87b3c4` | fix(core): split-target should handle projects with
colons in name better |
> | `af1ebc90ab` | fix(core): avoid renaming projects based on former
name if they were rooted when dep is drawn |
> | `b2fffc60c7` | cleanup(core): split project-configuration-utils into
focused modules |
> | `3bae3048d8` | fix(core): fixup name substitution manager |
>
> ## File Split: `project-configuration-utils.ts` -> 4 modules
>
> The original `project-configuration-utils.ts` (1,407 lines) was split
into focused modules. **~960 lines were moved as-is** (identical minus
whitespace/formatting) into the new files. The remaining changes are
actual logic modifications.
>
> | File | Total Lines | Moved from original | Truly new/changed |
> |------|------------:|--------------------:|------------------:|
> | `project-configuration-utils.ts` (remaining) | 444 | ~395 | ~49 |
> | `target-merging.ts` | 494 | ~430 | } |
> | `target-normalization.ts` | 282 | ~250 | } ~111 combined |
> | `project-nodes-manager.ts` | 365 | ~285 | } |
> | **Subtotal** | 1,585 | ~1,360 | ~160 |
>
> Additionally, ~39 lines were removed from the original and not moved
anywhere (dead code removal or refactored away).
>
> ## Actual Code Changes (non-test, excluding moved lines)
>
> ### Major changes
>
> | File | New | Removed | Net | Description |
> |------|----:|--------:|----:|-------------|
> | `split-target.ts` | ~211 | ~38 | +173 | New logic for handling
projects with colons in names |
> | `name-substitution-manager.ts` | ~152 | ~85 | +67 | Fix: avoid
renaming rooted projects based on former name |
> | Split files (combined, new logic only) | ~111 | — | +111 | New code
introduced during the split refactor |
> | `project-configuration-utils.ts` (new logic only) | ~49 | ~39 | +10
| Residual new code after split |
>
> ### Minor edits (import path updates, small fixes)
>
> | File | Added | Removed |
> |------|------:|--------:|
> | `parse-target-string.ts` | 7 | 1 |
> | `command-line/show/target.ts` | 11 | 7 |
> | `devkit-internals.ts` | 3 | 5 |
> | `tasks-runner/utils.ts` | 5 | 3 |
> | `build-project-graph.ts` | 2 | 4 |
> | `error-types.ts` | 2 | 4 |
> | `command-line/run/run-one.ts` | 2 | 1 |
> | `ngcli-adapter.ts` | 1 | 1 |
> | `convert-nx-executor.ts` | 1 | 1 |
> | `project-configuration.ts` (generators) | 1 | 1 |
> | `package-json.ts` | 1 | 1 |
> | `settings.gradle.kts` | 1 | 1 |
> | **Minor edits subtotal** | **37** | **30** |
>
> ### Other new files
>
> | File | Lines | Description |
> |------|------:|-------------|
> | `packages/devkit/CLAUDE.md` | 62 | Dev documentation |
> | `__fixtures__/merge-create-nodes-args.json` | 100 | Test fixture
data |
>
> ## Final Tally: True Non-Test Changes
>
> | Category | Lines |
> |----------|------:|
> | New logic in `split-target.ts` | ~211 |
> | New logic in `name-substitution-manager.ts` | ~152 |
> | New logic in split module files | ~111 |
> | New logic in remaining `project-configuration-utils.ts` | ~49 |
> | Minor import/path updates across 12 files | ~37 added / ~30 removed
|
> | New non-code files (CLAUDE.md, fixture JSON) | 162 |
> | **Total truly new/changed lines** | **~570 added, ~160 removed** |
> | Moved lines (file split, not real changes) | **~960** |
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
---------
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The `nx` package compiles its TypeScript output to
`../../dist/packages/nx/` (relative to the package root), which places
build artifacts outside the package directory at the repo root level
(`dist/packages/nx/`). This makes the package structure harder to reason
about, complicates the build pipeline, and doesn't align with how most
packages organize their output.
The package uses `"module": "commonjs"` with basic module resolution,
which limits future migration paths toward ESM.
## Expected Behavior
The `nx` package now builds to a local `dist/` directory within the
package itself (`packages/nx/dist/`). This is a cleaner, more standard
layout — like having your tools in your own toolbox instead of scattered
across the workshop.
### Key changes:
**Build configuration (`packages/nx/tsconfig.lib.json`):**
- `outDir` changed from `../../dist/packages/nx` to `dist` (local to
package)
- `module` changed to `nodenext` with `moduleResolution: nodenext`
- Updated `include` patterns to explicitly list source directories
**Package entry points (`packages/nx/package.json`):**
- `bin` paths updated: `./bin/nx.js` → `./dist/bin/nx.js`
- Added `"type": "commonjs"` explicitly
- Added comprehensive `exports` map with `@nx/nx-source` condition for
dev/test resolution back to TS source
- Added `postinstall` path update to `./dist/bin/post-install`
**Module resolution fixes:**
- Created `src/utils/handle-import.ts` — a CJS-first import utility that
falls back to ESM `import()` for ESM-only packages, providing a single
migration point for future ESM work
- Converted dynamic `await import()` calls to
`require(require.resolve())` pattern where needed to satisfy `nodenext`
extension requirements
- Plugin worker spawn path now uses correct `.ts`/`.js` extension based
on runtime context (source vs compiled)
**Test infrastructure:**
- Added custom `jest-resolver.js` for the `nx` package that resolves
`nx/...` imports using the `@nx/nx-source` exports condition, so tests
run against TS source
- Updated `jest.preset.js` with SWC transformer configuration
- Added chalk mock for test compatibility
**CI and tooling:**
- Conformance check updated to build `workspace-plugin` first (the Nx
Cloud runner lacks `@swc-node/register` for TS resolution)
- Conformance rule paths in `nx.json` now point to compiled
`dist/workspace-plugin/src/...` output
- Added `dist` to eslint ignore patterns to prevent linting compiled
output
- Added workspace-plugin build target and updated its dependencies
**Other fixes:**
- Various import path fixes across `create-nx-workspace`, gradle, and
other packages to work with `nodenext` resolution
- Updated e2e test paths to reference the new dist location
- Fixed `.gitignore` and `.npmignore` for the new output structure
## Related Issue(s)
Internal infrastructure improvement — no external issue.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: Coly010 <Coly010@users.noreply.github.com>
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
## Current Behavior
Framework generators append predefined configs (`flat/react`,
`flat/angular`, etc.) after `baseConfig` in the flat config array. In
flat config, later entries override earlier ones, so framework rules
override user root rules.
Additionally, the parser/plugins config entries in `flat/typescript`,
`flat/javascript`, and `flat/angular` have no `files` restriction,
applying the TypeScript parser to all files globally — including
`.html`, `.json`, and other non-TS/JS files. This was partially fixed in
PR #28381 (which scoped rules/extends) but the parser entries were
missed.
## Expected Behavior
Framework configs are inserted before `baseConfig`, giving user root
config higher priority. Root standalone projects (no `baseConfig`) are
unaffected.
Parser/plugins entries are scoped to their respective file types, so
`.html` files get the correct parser (Angular template parser, or ESLint
default) instead of the TypeScript parser.
## Changes
- Implement `checkBaseConfig` option in `addBlockToFlatConfigExport` to
insert before `...baseConfig` when present
- Framework generators (`@nx/react`, `@nx/angular`, `@nx/next`, etc.)
pass `checkBaseConfig: true`
- Scope parser entries in `flat/typescript` to `**/*.ts, **/*.tsx,
**/*.cts, **/*.mts`
- Scope parser entries in `flat/javascript` to `**/*.js, **/*.jsx,
**/*.cjs, **/*.mjs`
- Scope processor/plugins entry in `flat/angular` to `**/*.ts`
- Refactor `addPredefinedConfigToFlatLintConfig` to use an options
object for optional params
- Fix regex patterns in react-native and expo jest config templates to
use `[.]` instead of `\.` — avoids `no-useless-escape` lint errors and
fixes a latent bug where `\.` in a string literal matched any character
instead of a literal dot
## Related Issue(s)
Fixes#32923
## Current Behavior
When using Angular + Rspack + Module Federation, the
`NxModuleFederationPlugin` (Angular variant) sets `library: { type:
'module' }` on the Module Federation plugin config, which causes
`remoteEntry.js` to emit ESM `export` statements. However, the
compiler's `experiments.outputModule` and `output.module` flags are not
set, so the MF runtime tries to load the remote entry as a classic
script. This results in:
```
Uncaught SyntaxError: Unexpected token 'export' (at remoteEntry.js:48774:1)
```
This is especially broken during `nx serve` (dev server), because
`@nx/angular-rspack`'s `createConfig` only sets
`experiments.outputModule = true` for production builds, not dev server
builds.
## Expected Behavior
The Angular rspack `NxModuleFederationPlugin` should ensure
`experiments.outputModule = true` and `output.module = true` are set on
the compiler when using `library: { type: 'module' }`, so that Module
Federation works out of the box in both dev and production modes.
This aligns with how the Angular **webpack** MF config already handles
it (in `with-module-federation/angular/with-module-federation.ts`).
## Related Issue(s)
Fixes#34584Fixes#33992
## Current Behavior
The default `@nx/next:build` for nx-dev outputs to `dist/nx-dev/nx-dev`.
At runtime, Node.js can't resolve the `ai` package from that path
because `node_modules` is at the workspace root — causing
`ERR_MODULE_NOT_FOUND` on `/ai-chat`.
Additionally, `ui-video-courses` is missing `@nx/nx-dev-ui-icons` as a
dependency (introduced by #34669), causing a webpack compilation
failure.
## Expected Behavior
Build in-place to `nx-dev/nx-dev` (matching the existing Netlify config)
so `.next` stays close to `node_modules` and package resolution works.
This also simplifies config by removing the Netlify vs Vercel branching
in sitemap scripts and project.json configurations.
## Other Notes
This PR also:
- Redirects `/` to `/blog` since almost everything else, including
homepage, are in Framer.
- Uses `@nx/next/plugin` to infer targets now so we no longer use any
`@nx/next:*` executors.
- Cleans up `nx-dev` project config so it no longer has `serve-docs` and
other weird setup. It is purely just `nx dev nx-dev` or `nx build
nx-dev` or `nx start nx-dev` like a normal Next.js app.
- Removes extra `NETLIFY` env var checks to point outputs in different
places.
## Related Issue(s)
Closes DOC-418
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
## Current Behavior
Tutorials direct users to cloud.nx.app CTAs requiring GitHub account +
full cloud onboarding before the tutorial starts. CNW starts dropped to
~1,800/day weekday (target ~2,700). Self-healing CI content buried at
bottom of tutorials where only 15-20% of users scroll.
## Expected Behavior
- Tutorials use npx create-nx-workspace as primary path, cloud link kept
as secondary text link
- llm_only tags added to tell AI agents to use CLI only (they can't
handle the browser OAuth flow)
- Tutorial file trees, app names, and scopes updated to match what CNW
actually generates
- Self-healing CI extracted to standalone "Setting up CI" tutorial
- Nx Cloud page moved from Getting Started to Orchestration & CI
overview (redirect added)
- Sidebar labels converted to sentence case per style guide
- AI integrations moved before Editor setup in sidebar (higher traffic)
- Tutorials expanded by default in sidebar
- Intro page now shows CNW and nx init commands directly
## Notes
A follow-up to break tutorials down into smaller, more focused topics
will be the next step.
<img width="547" height="400" alt="image"
src="https://github.com/user-attachments/assets/b717ee54-0dc6-4c08-b1d3-0d80c9dce1df"
/>
## Related Issue(s)
Closes DOC-448
## Current Behavior
In TS solution workspaces, library generators pass `alwaysRun=true` to
`installPackagesTask` to ensure symlinks are created for new packages.
However, when the init generator already triggered an install (via
`addDependenciesToPackageJson`), the `alwaysRun` flag bypasses the cache
and forces a redundant second install.
## Expected Behavior
Only a single install should run per generator invocation. If install
already ran this cycle, subsequent `ensureInstall` calls should be
skipped since the previous install already picked up all filesystem
changes (including `pnpm-workspace.yaml` updates for symlinks).
## Fix
Renamed `alwaysRun` to `ensureInstall` in `installPackagesTask` and
simplified the install condition:
```typescript
if (packageJsonDiffers || (ensureInstall && !installAlreadyRan))
```
This handles both cases:
- **First library** (deps change): init callback runs install →
`ensureInstall` callback sees install already ran → skips. One install.
- **Second+ library** (no dep changes): init callbacks are no-ops →
`ensureInstall` callback sees no prior install → runs. One install.
## Related Issue(s)
<!-- No existing issue — discovered during investigation -->
## Current Behavior
The `dev.nx.gradle.project-graph` plugin is at version `0.1.15`.
## Expected Behavior
The `dev.nx.gradle.project-graph` plugin is bumped to version `0.1.16`,
with a corresponding Nx migration so users are automatically updated
when they migrate to `22.7.0-beta.2`.
## Related Issue(s)
N/A - routine version bump
---
### Changes
- Updated `gradleProjectGraphVersion` in
`packages/gradle/src/utils/versions.ts`
- Updated `version` in `packages/gradle/project-graph/build.gradle.kts`
- Created migration TS and MD files in
`packages/gradle/src/migrations/22-7-0/`
- Added migration entry in `packages/gradle/migrations.json`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Current Behavior
Workspace uses nx 22.7.0-beta.0 and related @nx/* packages at
22.7.0-beta.0.
## Expected Behavior
Workspace uses nx 22.7.0-beta.1 and related @nx/* packages at
22.7.0-beta.1.
## Related Issue(s)
N/A — routine version bump.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
On Windows, the Nx daemon runs as a detached background process with no
console. When child processes are spawned without `windowsHide: true`
(Node.js) or `CREATE_NO_WINDOW` (Rust/Win32), Windows allocates a new
visible console window for each subprocess. This causes command prompt
windows to flash on screen during:
- Project graph creation (daemon spawn, plugin workers)
- Task hashing (runtime hashers)
- NX Console extension detection (`code.cmd --list-extensions`, etc.)
- AI agent configuration checks (`git ls-remote`, npm install)
- Machine ID retrieval, package manager version detection, git
operations, and more
The issue is especially noticeable "after a little bit" following daemon
startup, because background operations like the NX Console status check
and AI agents configuration check kick off after the initial project
graph is computed.
## Expected Behavior
No console windows should flash on Windows. All child process spawns use
`windowsHide: true` (Node.js) or `CREATE_NO_WINDOW` (Rust) to suppress
console windows.
## Root Causes Found
Investigation using a child_process interceptor in the daemon revealed
multiple sources:
1. **Rust native `ide/install.rs`** — `Command::new("code.cmd")` calls
for NX Console extension detection/installation were missing
`CREATE_NO_WINDOW`
2. **Rust native `hash_runtime.rs`** — Had its own `CREATE_NO_WINDOW`
handling but was duplicated
3. **`nx@latest` temp install** — The daemon downloads `nx@latest` to a
temp directory for NX Console and AI agent checks. The install process
(`pnpm add -D nx@latest`) and the downloaded code's `git ls-remote`
calls run without `windowsHide: true`
4. **~120 Node.js `child_process` calls** — Various
`spawn`/`exec`/`execSync` calls across the codebase were missing
`windowsHide: true`
## Changes
### Node.js child_process fixes
- Set `windowsHide: true` on all `spawn`/`exec`/`execSync`/`spawnSync`
calls across the codebase (~120 files)
- Added custom ESLint rule `@nx/workspace/require-windows-hide` that
errors when any spawn/exec call is missing `windowsHide: true`
### Rust native fixes
- **New shared util `native/utils/command.rs`** with `create_command()`
and `create_shell_command()` that set `CREATE_NO_WINDOW` on Windows —
centralizes the pattern so future Rust code gets it right by default
- **`ide/install.rs`** — Use `create_command()` for `code.cmd` calls
(list-extensions, install-extension, version check)
- **`hash_runtime.rs`** — Replaced local `create_command_builder()` with
shared `create_shell_command()`
- **`machine_id/mod.rs`** — Updated to use shared
`create_shell_command()`
### Daemon background operation fixes
- **`handle-configure-ai-agents.ts`** — Now respects `NX_USE_LOCAL` env
var to skip downloading `nx@latest`, avoiding the pnpm install that
opens windows. Once these fixes ship in a release, the downloaded
`nx@latest` will also have the fixes.
### Inlined node-machine-id
- Replaced the `node-machine-id` npm package with an inlined
implementation in `machine-id-cache.ts` that uses `windowsHide: true`
- The original package used `exec`/`execSync` without `windowsHide`
## Related Issue(s)
Supersedes #34455
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
The `replace-removed-matcher-aliases` migration crashes the entire
migration runner when a project has a broken or misconfigured
`jest.config.ts`. This was discovered while upgrading nx-labs from Nx 21
to 22 via `nx migrate --run-migrations`.
The migration uses Jest's `readConfig()` and `Runtime.createContext()`
to resolve jest configs, but has no error handling around these calls.
If any project has issues like:
- A missing file referenced in the config (e.g. `.lib.swcrc` that was
renamed to `.swcrc`)
- A missing transform module (e.g. `@swc/jest` not installed)
- A missing preset file (e.g. `jest.preset.ts` instead of
`jest.preset.js`)
...the entire migration fails with an opaque error:
```
Error: Command failed: /var/folders/.../node_modules/.bin/nx _migrate --run-migrations
at checkExecSyncError (node:child_process:925:11)
status: 1,
stdout: null,
stderr: null
```
The actual errors are swallowed by the nested `execSync` call, making it
very hard to debug.
## Expected Behavior
The migration should skip projects with broken jest configs and continue
processing the rest of the workspace.
## Fix
Wrapped the `readConfig` / `Runtime.createContext` /
`SearchSource.getTestPaths` block in a try-catch that skips the failing
project. This matches the defensive pattern already used in
`packages/jest/src/plugins/plugin.ts` for similar jest config
resolution.
## Test Plan
- Added 3 tests covering: missing file reference, missing preset, and
verifying valid projects are still processed when a sibling project has
a broken config
- All existing tests continue to pass
<!-- 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 -->
The Gradle plugin's test class atomizer was incorrectly treating Kotlin
annotation class declarations as test targets, generating invalid test
tasks for them. This caused issues when projects defined custom
annotations alongside their test classes—the atomizer would attempt to
run annotation classes as tests.
## Expected Behavior
Both the AST-based parser and the regex fallback parser now skip
annotation classes, matching the existing behavior for data classes,
enum classes, sealed classes, and abstract classes. A new test suite
covers all annotation class exclusion scenarios for both parsers,
including files with multiple annotation classes and mixed
annotation/test class files.
## 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: lourw <lourw@users.noreply.github.com>
## Current Behavior
When multiple processes call `connectToNxDb()` concurrently (plugin
workers via `startAnalytics()`, daemon, main CLI), two bugs can corrupt
the workspace database:
**Bug 1: Lock file inode race.** `unlock_file()` deletes the lock file
after unlocking, allowing a subsequent `File::create()` to produce a new
file with a different inode. Two processes can hold "the lock"
simultaneously on different file objects, breaking mutual exclusion.
**Bug 2: Partial file cleanup on version mismatch/connection failure.**
The `reason` arm and `Err` arm in `initialize_db` call
`remove_file(db_path)` which only deletes `.db`, leaving stale `.db-wal`
and `.db-shm` on disk. The recursive `initialize_db` creates a fresh
`.db`, but SQLite detects the stale WAL (different inode salt) and
deletes it — destroying all data that existed only in the WAL.
Both bugs lead to:
```
Database file exists but has no metadata table.
```
## Expected Behavior
1. Lock file persists across lock/unlock cycles — all processes
serialize through the same inode
2. When DB recreation is needed, all auxiliary files (`.db`, `.db-wal`,
`.db-shm`) are cleaned up together via `remove_all_database_files`
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
When the daemon handles task hashing, the `NativeTaskHasherImpl` checked
`hasTaskInputSubscribers()` in the daemon process where no subscribers
exist, causing the native Rust hasher to skip input collection entirely.
This resulted in empty input arrays in IO tracing signals.
The fix passes collectInputs from the client process (where subscribers
are registered) through the daemon IPC to the hasher, so the daemon uses
the client's subscriber state instead of its own.
## Current Behavior
`nx init` has a special code path for Create React App (CRA) projects
that installs Vite-related dependencies incompatible with `@nx/vite`
when Vite 8 is resolved, causing failures for npm workspaces.
CRA is essentially unused for years so there's no point to keep it
around.
## Expected Behavior
CRA projects are no longer special-cased in `nx init`. They flow through
the normal npm repo path, which detects plugins like `@nx/vite` via the
standard `detectPlugins()` mechanism.
<img width="1279" height="448" alt="Screenshot 2026-03-18 at 2 34 56 PM"
src="https://github.com/user-attachments/assets/17da3c6d-8aef-450f-baaa-6014e03cf41f"
/>
<img width="1280" height="432" alt="Screenshot 2026-03-18 at 2 35 01 PM"
src="https://github.com/user-attachments/assets/e02ccda5-9faf-428f-878d-348e4cc44d74"
/>
<img width="1270" height="821" alt="Screenshot 2026-03-18 at 2 35 15 PM"
src="https://github.com/user-attachments/assets/fdd99af5-db7e-422b-be42-a817c9dcefa2"
/>
## Related Issue(s)
Closes NXC-4107
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Adds references to [Yarn catalog](https://yarnpkg.com/features/catalogs)
to dependency management documentation.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Related #34377
---------
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
## Current Behavior
Several issues in `nx migrate` registry fetching cause confusing errors:
- `packageRegistryPack` uses `pnpm pack` for pnpm users, but `pnpm pack`
only packs the local project (unlike `npm pack` which downloads remote
packages). This silently fails for all pnpm users, always falling back
to the slower install path.
- On Windows, `extractFileFromTarball` fails because `join('package',
migrationsFilePath)` produces backslash paths that don't match
forward-slash tarball entries.
- When the install fallback also fails,
`getPackageMigrationsUsingInstall` returns `{}` instead of throwing. The
missing `version` property (`undefined`) propagates through
`packageUpdates` and `collectedVersions`, producing `Fetching
nx@undefined`.
- The install fallback can fail on peer dependency conflicts since
`legacy-peer-deps` was removed as a default (PR #33014), even though the
install only needs files on disk (not a valid dependency tree).
- The `.catch()` in `fetchMigrations` swallows errors without logging,
making it impossible to diagnose registry fetch failures.
## Expected Behavior
- `packageRegistryPack` always uses `npm pack` since it's the only
package manager that supports downloading remote packages
- Tarball entry matching uses `joinPathFragments` to normalize paths
(forward slashes) for cross-platform compatibility
- `getPackageMigrationsUsingInstall` throws on failure with a
descriptive error instead of returning an empty object
- Install fallback sets `npm_config_legacy_peer_deps=true` since it only
needs files on disk, not a valid dependency tree
- Registry fetch errors are logged at verbose level
(`NX_VERBOSE_LOGGING=true`) for debuggability
## Related Issue(s)
Fixes#33135
## Current Behavior
When pressing Ctrl+C on continuous tasks, `postTasksExecution` never
fires because `process.exit()` in `running-tasks.ts` SIGINT handlers
kills the process before the orchestrator can run async cleanup and
lifecycle hooks.
## Expected Behavior
`postTasksExecution` fires reliably on Ctrl+C with complete task
results, without causing stale `RunningTasksService` DB entries that
block subsequent `nx` invocations with "Waiting for ... in another nx
process" messages.
## Technical Details
Re-applies #34623 (reverted in #34869) with fixes for the issues that
caused CI failures.
**Root cause of the revert**: Removing `process.exit()` from SIGINT
handlers left the nx process alive during async cleanup. The
`running_tasks` DB entry persisted with a still-alive PID, so new nx
processes saw it as a running task and hung.
**Fix 1 — Early DB cleanup**: Synchronously remove all owned DB entries
in the SIGINT handler *before* starting async cleanup. This replicates
the cleanup that `process.exit()` + Rust `Drop` previously provided —
from any external observer's perspective, the tasks are gone
immediately.
**Fix 2 — Always register onExit in startContinuousTask**: The original
PR added a guard that skipped `onExit` registration for initiating
tasks, assuming `executeNextBatchOfTasksUsingTaskSchedule` would handle
it. But `runContinuousTasks()` (used by DTE agents and Playwright) calls
`startContinuousTask` directly without going through `run()`. The
missing handler meant task exit was never processed — no DB cleanup, no
lifecycle hooks. This caused verdaccio (local-registry) to become
unreachable on DTE agents. The fix always registers `onExit` in
`startContinuousTask` and simplifies the
`executeNextBatchOfTasksUsingTaskSchedule` handler to only unblock the
thread.
Changes:
- Remove `process.exit()` from `running-tasks.ts` SIGINT handlers (lets
orchestrator run)
- Replace `cleanupDone` boolean with `cleanupPromise` (fixes
SIGINT/SIGTERM race)
- Set `stopRequested = true` for SIGTERM/SIGHUP (correct task
classification)
- Early synchronous DB cleanup in non-TUI SIGINT handler
- Always register `onExit` handler in `startContinuousTask` for both
initiating and non-initiating tasks
- Simplify initiating task handler in
`executeNextBatchOfTasksUsingTaskSchedule` to only call `res()`
## Current Behavior
The Angular devkit adapter (`ngcli-adapter.ts`) calls
`createProjectGraphAsync()` in multiple places, even though callers
(executors, generate command, migrate command) already have the project
graph available. This results in redundant requests to create the graph.
## Expected Behavior
Reuse the project graph from callers when available, falling back to
reading from cache (`readCachedProjectGraph()`) or
`createProjectGraphAsync()` only when necessary.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
There's not a great way to attach metadata to CLI commands to influence
rendering the markdown docs for them
## Expected Behavior
There's a metadata system currently used to express minimum support
version for the show target command
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
When calling `nxE2EPreset(__filename)` without passing the optional
`options` parameter, the function throws a runtime error because
`options.openHtmlReport` accesses a property on `undefined`. Other
property accesses in the function already use optional chaining
(`options?.testDir`, `options?.generateBlobReports`), but this one was
missed.
Additionally, the `openHtmlReport` property documents a default value of
`'on-failure'` via JSDoc, but that default was never actually applied in
the code.
## Expected Behavior
Calling `nxE2EPreset(__filename)` without the `options` argument works
without errors. The `openHtmlReport` option correctly falls back to
`'on-failure'` when not specified, matching the documented `@default` in
the interface.
## Related Issue(s)
N/A - Discovered when upgrading NX and we didn't pass options
## Current Behavior
The "Migrating Multiple Angular CLI Workspaces" page is a 26-line stub
with only a YouTube video. It doesn't mention `nx import` and contains
outdated guidance. The single-workspace migration guide (`angular.mdoc`)
also has outdated sections: a list of supported builders, a full CI
setup walkthrough, an irrelevant "From Nx Console" section, and a
reference to `karma.conf.js`.
## Expected Behavior
- The `angular-multiple.mdoc` page is removed with a redirect to the
main Angular migration guide.
- The main migration guide mentions `nx import` for consolidating
multiple Angular CLI projects.
- Outdated sections (Modified folder structure, Set up CI, From Nx
Console) are removed and replaced with concise links.
- The `nx-and-angular.mdoc` guide links to `nx import` docs instead of
the deleted page.
Preview:
https://deploy-preview-34913--nx-docs.netlify.app/docs/technologies/angular/migration/angular-multiple
(redirects to the updated page)
## Related Issue(s)
Closes DOC-419
## Current Behavior
The `@nx/eslint/plugin` uses `eslintConfigFiles[0]` to decide between
`FlatESLint` and `LegacyESLint`. Due to glob pattern ordering,
`.eslintrc.*` files sort before `eslint.config.*`, so a stray nested
`.eslintrc.json` (e.g., in `eslint-local-rules/`) causes the plugin to
pick `LegacyESLint` even when the root has a flat config — crashing with
"No ESLint configuration found" during project graph creation.
## Expected Behavior
The ESLint class is determined from the root config, matching ESLint's
own behavior where `find-up` from cwd decides the mode. Nested legacy
config files are irrelevant when a root flat config exists. When both
flat and legacy configs exist at root (mid-migration), flat config is
preferred.
## Related Issue(s)
Fixes#32110
## Current Behavior
The `convert-to-flat-config` generator silently skips project-level
`.eslintrc.json` files when `projectConfig.targets` is undefined (e.g.,
package.json-only projects in pnpm workspaces). The `@nx/eslint/plugin`
check is gated behind the targets check and never reached.
## Expected Behavior
Projects with `.eslintrc.json` are converted when `@nx/eslint/plugin` is
registered, even without explicit targets. When a project is skipped
because no ESLint lint target is detected, a warning is logged so users
know which projects were not converted and why.
## Related Issue(s)
Fixes#29458
## Current Behavior
The `convert-to-flat-config` generator wraps all plugin extends with
`FlatCompat`, including Nx-specific ones like `plugin:@nx/typescript`.
The output doesn't match what freshly generated projects produce
(`nx.configs['flat/typescript']`). The `@nx` plugin registration uses a
manual `{ plugins: { '@nx': nxEslintPlugin } }` block instead of
`flat/base`.
## Expected Behavior
Nx plugin extends are converted to native `nx.configs['flat/X']`
entries. The `@nx` plugin registration uses `flat/base`. `FlatCompat` is
only used for third-party plugins. The import variable is normalized to
`nx` to match fresh generation.
## Related Issue(s)
Fixes#31736
## Current Behavior
The `enforce-module-boundaries` rule only visits ESM AST nodes
(`ImportDeclaration`, `ImportExpression`, `ExportAllDeclaration`,
`ExportNamedDeclaration`). CommonJS `require()` calls bypass all
boundary checks entirely.
## Expected Behavior
`require()` and `require.resolve()` calls are detected and validated
against the same module boundary rules as ESM imports. Auto-fix is
skipped for `require()` nodes since they have no `specifiers`.
## Related Issue(s)
Fixes#34096
## Current Behavior
When CLI arguments containing shell metacharacters (like `|`, `&`, `$`,
`;`, `*`, etc.) are passed through Nx to underlying tasks, they are not
properly quoted, causing shell interpretation errors.
For example, running:
```bash
nx test app --grep="@tag1|@tag2"
```
Would fail with `/bin/sh: @smoke: command not found` because the pipe
character `|` was interpreted by the shell as a pipe operator instead of
being passed as a literal string to the underlying command.
Users had to use awkward double-quoting workarounds like
`--grep='"@tag1|@tag2"'` to get the expected behavior.
## Expected Behavior
CLI arguments containing shell metacharacters should be automatically
quoted before being passed to underlying commands, so that:
```bash
nx test app --grep="@tag1|@tag2"
```
Works correctly and passes `--grep="@tag1|@tag2"` to the underlying test
runner without shell interpretation.
## Related Issue(s)
Fixes#32305Fixes#26682
## Implementation Details
- Created a shared `needsShellQuoting()` utility in
`packages/nx/src/utils/shell-quoting.ts` that detects shell
metacharacters
- Updated `wrapArgIntoQuotesIfNeeded()` in `run-commands.impl.ts` to use
the shared utility
- Updated `stringShouldBeWrappedIntoQuotes()` in
`serialize-overrides-into-command-line.ts` to use the shared utility
- Fixed a bug where `arg.split('=')` would incorrectly split values
containing `=` (e.g., `--define=FOO=bar|baz`)
- Added proper escaping of embedded double quotes when wrapping values
- Added comprehensive test coverage
### Shell metacharacters now handled:
`|` `&` `;` `<` `>` `(` `)` `$` `` ` `` `\` `"` `'` `*` `?` `[` `]` `{`
`}` `~` `#` `!` and whitespace
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
## Current Behavior
When the globally installed Nx binary (`bin/nx.ts`) runs, it goes
through the following flow before determining whether to hand off to a
local Nx installation:
1. `ensureAnalyticsPreferenceSet()` — prompts user if analytics
preference not set
2. `startAnalytics()` — which calls `getDbConnection()` →
`connectToNxDb(directory, NX_VERSION)` using the **global** Nx version
and native bindings, then calls `initializeTelemetry(dbConnection, ...)`
to initialize telemetry
3. Sets `NX_ANALYTICS_SESSION_ID` env var
Then it checks which execution path to take:
- **`isNxCloudCommand`** — executes commands directly (no handoff)
- **`isLocalInstall`** — the global IS the local, calls `initLocal()`
(no handoff)
- **`localNx` exists** — hands off to the local Nx via
`require(localNx)`
In the handoff case, the local `bin/nx.ts` runs `main()` from scratch,
which calls `startAnalytics()` again. It sees `NX_ANALYTICS_SESSION_ID`
is already set and takes the "reuse session" shortcut — but telemetry
was already initialized with the wrong version's native bindings and DB
connection.
**Problems:**
- The global bin opens a DB connection with the **wrong `NX_VERSION`**
(global version, not local version) and **wrong native bindings**
- Analytics is initialized twice — once from global (incorrect), once
from local (correct but using session reuse path)
- The DB connection opened by the global bin is unnecessary since the
local Nx handles everything
## Expected Behavior
When the global bin is about to hand off to a local Nx installation, it
should **not** initialize analytics or open any DB connections. The
local Nx will handle analytics initialization correctly with its own
version and native bindings.
Analytics and DB connections should only be initialized in the two
branches where the global bin actually executes commands itself:
- `isNxCloudCommand` — needs analytics because it runs commands directly
- `isLocalInstall` — needs analytics because `initLocal()` doesn't
re-enter `bin/nx.ts`
## Related Issue(s)
<!-- Internal discovery during analytics work -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Visiting [nx.dev/changelog](https://nx.dev/changelog) returns an HTTP
500 Internal Server Error. The page builds successfully during
deployment (prerendered as SSG), but the Netlify server handler fails at
runtime when serving the page.
## Expected Behavior
The changelog page loads correctly, displaying Nx release history with
version timelines and any manually authored changelog content.
## Related Issue(s)
Fixes#34909
## Changes
- **`next.config.js`**: Add `outputFileTracingIncludes` for
`public/documentation/changelog/**` so the changelog content directory
is included in the Netlify serverless function bundle (Next.js file
tracing can't detect `readdirSync` with string paths)
- **`pages/changelog.tsx`**: Wrap `changeLogApi.getChangelogEntries()`
in try/catch for graceful degradation if the directory is unavailable
during on-demand rendering
- **`rewrite-framer-urls.ts`**: Add `/changelog` to the edge function
`excludedPath` config so the Framer proxy edge function is bypassed
entirely for changelog requests
- **`_redirects`**: Fix malformed redirect on line 55 — destination path
was missing a leading `/`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Current Behavior
After certain cache state transitions (e.g., tsconfig parsing cache warm
but targets cache cold), the `@nx/js/typescript` plugin falls back to
the `production` named input instead of deriving precise inputs from
tsconfig `include`/`exclude` paths. This results in broader cache
invalidation than necessary, and in projects with `allowJs` or
`resolveJsonModule`, the wrong file extensions are tracked as inputs.
Output inference is also affected when `emitDeclarationOnly` or
`declarationMap` are set.
Running `nx reset` restores correct behavior, but the issue recurs.
## Expected Behavior
Inferred inputs and outputs are consistent regardless of cache state —
always matching what the tsconfig actually specifies.
The root cause was the tsconfig parsing cache serialization
(`toAbsolutePaths`/`toRelativePaths`) dropping fields the plugin relies
on: `raw.include`, `raw.exclude`, `raw.files`, and `options.allowJs`,
`options.resolveJsonModule`, `options.emitDeclarationOnly`,
`options.declarationMap`. These are now preserved, and
`TSCONFIG_CACHE_VERSION` is bumped to invalidate stale caches.
## Current Behavior
On Windows, the TypeScript plugin produces `cwd` with backslash
separators (e.g., `cwd: "packages\\nx"`) for both the typecheck and
build targets. All paths in Nx targets should use `/` separators.
## Expected Behavior
The `cwd` option uses forward slashes on all platforms (e.g., `cwd:
"packages/nx"`).
## Related Issue(s)
Fixes NXC-4105
## Summary
- Self-healing auto-apply writes fix context to `.nx/self-healing/`, but
that directory was not in `.gitignore`. This adds it via a migration and
in the CAIA generator, following the same pattern as `.nx/polygraph`.
## Current Behavior
The `.nx/self-healing` directory is not gitignored. Users who run
self-healing auto-apply may accidentally commit generated fix context
files.
## Expected Behavior
`.nx/self-healing` is added to `.gitignore` automatically during `nx
migrate` (via a new migration) and when running the CAIA setup
generator.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Add X-Frame-Options and Content-Security-Policy frame-ancestors headers
to prevent clickjacking on nx.dev marketing and docs sites.
Fixes DOC-449
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Current Behavior
#34799 has some method names that could have been better
## Expected Behavior
The methods are named to signify they are notifications, not
instructions
## 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>
## Summary
- The upstream config repo (nx-ai-agents-config) now generates skills
into a shared `.agents/skills/` directory instead of separate per-agent
skills directories for codex, cursor, and gemini.
- Updated the `agentDirs` mapping so `.agents` is copied when any of
codex, cursor, or gemini are enabled, not just codex.
- Widened the `agent` field type from `Agent` to `Agent | Agent[]` to
support mapping a single directory to multiple agents.
## Key decisions
- Used `Agent | Agent[]` union type rather than always-array to minimize
changes to existing single-agent entries. The loop normalizes with
`Array.isArray` before checking.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
We want to capture more error and cancel events so that start events
match complete/error/cancel events. This PR ensures that more errors are
properly captured as `CnwError` rather than just `output.error` +
`process.exit(1)`;
- Invalid workpspace name (i.e. starting with a number) now throws
`CnwError` so we record them correctly
- Missing package manager is now captured as `CnwError` correctly
- SIGINT when workpspace is already created now send "cancel" event
Closes NXC-4095
## Current Behavior
Nx packages are on a previous version.
## Expected Behavior
All Nx packages updated to 22.6.0-rc.2.
## Related Issue(s)
N/A - routine version bump for RC testing.
This reverts commit 8d71d5b57b.
## Current Behavior
<!-- This is the behavior we have today -->
This generator causes unnecessary changes
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
The generator is removed for now and will be reintroduced when it is
refined.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
Batch mode documentation is scattered across individual technology pages
(TypeScript, Gradle, Maven, Jest) with no central guide explaining the
concept, how it works, or which executors support it.
## Expected Behavior
A new guide page at `guides/tasks--caching/batch-mode` provides a
technology-agnostic overview of batch mode, covering:
- What batch mode is and why it's faster
- How to enable it (`NX_BATCH_MODE=true` env var and `--batch` CLI arg)
- Which executors support it (with notes that Gradle/Maven have it on by
default)
- Caching and CI compatibility
Each technology-specific page now links back to the central guide for
the full explanation.
## Related Issue(s)
Fixes #DOC-420
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: barbados-clemens <barbados-clemens@users.noreply.github.com>
## Current Behavior
CNW flow matches v22.1.3 (restored in #34671): no template prompt is
shown, the cloud prompt uses simplified "Would you like remote caching?"
wording, banner variant is locked to 0, and preset flow connects to
cloud during workspace creation.
## Expected Behavior
Restore the template prompt and cloud prompts that were removed in
#34671:
- **Template prompt**: "Which starter do you want to use?" with 5
choices (Minimal, React, Angular, NPM Packages, Custom)
- **Cloud prompt**: `determineNxCloudV2` ("Connect to Nx Cloud?") for
preset flow when no `--nxCloud` CLI arg; existing CI provider prompt
when `--nxCloud` is explicitly provided
- **Banner**: Variant 2 (box banner) for standard Nx Cloud URLs, variant
0 for enterprise
- **Preset flow**: Deferred cloud connection (`nxCloud: 'skip'`) instead
of connecting during workspace creation
- **Push logic**: Push to GitHub for both `nxCloud === 'github'` and
`nxCloud === 'yes'`
- **Messages**: "Try the full Nx platform?" wording, GitHub repo link in
push messages, "Your remote cache setup is almost complete." title
Preserves all subsequent changes (analytics prompt from #34818,
SANDBOX_FAILED fix, .gitignore updates).
## Related Issue(s)
Fixes NXC-4096
## Current Behavior
Every process that initializes analytics opens its own DB connection to
get/create a session ID. This includes the CLI, the daemon, and **every
plugin worker**. In workspaces with many plugins, dozens of plugin
workers spawn simultaneously, each opening a DB connection and
querying/writing session metadata. This overwhelms the SQLite database
with concurrent connections and causes failures.
## How This Fixes It
The root cause is that plugin workers each independently open a DB
connection just to read the session ID. The fix eliminates this by
having the parent process (CLI or daemon) fetch the session ID once and
pass it to child processes via the `NX_ANALYTICS_SESSION_ID` environment
variable. Plugin workers inherit this env var and initialize telemetry
without touching the DB at all.
| Process | Before | After |
|---------|--------|-------|
| CLI | Opens DB connection | Opens DB connection (1x) |
| Daemon | Opens DB connection | Opens DB connection (1x) |
| Plugin worker (×N) | Each opens DB connection | Reads env var, **no DB
connection** |
In a workspace with 20 plugins, this reduces DB connections from 22 (CLI
+ daemon + 20 workers) down to 2 (CLI + daemon only).
## Two Initialization Paths
- **`initializeTelemetry(dbConnection, ...)`** — Used by CLI and daemon.
Gets/creates the session ID from the DB via a transaction, stores the
connection for persisting session refreshes on flush, and returns the
session ID so the caller can set it as an env var for child processes.
- **`initializeTelemetryWithSessionId(sessionId, ...)`** — Used by
plugin workers. Takes the session ID inherited from the parent process
env var. No DB connection, no DB queries.
## Session Refresh for Long-Lived Processes
The daemon is long-lived and could hold a stale session ID for hours.
The telemetry background thread now tracks activity and generates a new
session ID after 30 minutes of inactivity (matching the existing GA4
session timeout). When a session refreshes, the background thread
notifies the main thread via a channel, which persists the new session
to the DB in a transaction on flush.
## Other Changes
- Extracted `TelemetryOptions` struct to replace the long parameter list
in `TelemetryService::new`
- Moved `SESSION_TIMEOUT_SECS` to `constants.rs` so it can be shared
between modules
- Extracted `init_service` helper to deduplicate between the two init
paths
- Extracted `persist_session_to_db` helper that wraps both metadata
writes in a transaction
- Separated session ID retrieval (`get_or_create_session_id`) from
telemetry service initialization
## Related Issue(s)
Fixes database connection exhaustion when many plugin workers initialize
analytics simultaneously.
## Current Behavior
There is no documentation for Nx CLI telemetry, which was added in Nx
22.6.0. Users who are prompted to opt in have no reference page to learn
what data is collected or how to opt out.
## Expected Behavior
A dedicated telemetry reference page at `/reference/telemetry` explains
what is collected, what is not collected, and how to disable telemetry
via `nx.json`. The `nx.json` reference page also documents the
`analytics` property.
Changes:
- New page: `astro-docs/src/content/docs/reference/telemetry.mdoc`
- Sidebar entry added under Reference
- `analytics` property added to nx.json reference (expanded example +
new section)
## Related Issue(s)
Fixes DOC-446
## Current Behavior
The CODEOWNERS setup is getting in the way as core maintainers have
moved around and timezones and such...
## Expected Behavior
Anyone on the CLI reviewers team should be capable of recognizing PRs
they are capable of assessing, this eases the burden of increased PRs
from AI chatbots and timezones.
## Current Behavior
Only the astro-docs (nx-docs) Netlify app tracks server-side page views
via edge functions. AI tool and bot detection relies on fragile
User-Agent regex matching against a hardcoded list of known bot strings.
## Expected Behavior
Both nx-dev and nx-docs page views are tracked server-side via GA4,
using Netlify's built-in `Netlify-Agent-Category` header to distinguish
`ai-agent` from `crawler` traffic.
Tracking edge functions are consolidated into the nx-dev app
(`netlify/edge-functions/`) since all traffic flows through it before
being rewritten to nx-docs. Framer-proxied pages are tracked inline in
`rewrite-framer-urls.ts` because the proxy short-circuits
`context.next()`, preventing downstream edge functions from firing.
**Changes:**
- Moved `track-page-requests.ts` and `track-asset-requests.ts` from
`astro-docs/netlify/edge-functions/` to `netlify/edge-functions/`
- Replaced User-Agent regex with `Netlify-Agent-Category` header checks
in all tracking functions
- Added GA4 tracking to `rewrite-framer-urls.ts` for Framer-proxied
pages
## Related Issue(s)
Fixes DOC-445
## Current Behavior
New logic in the daemon can cancel an active graph creation, resulting
in worker shutdown not behaving as intended
## Expected Behavior
When the daemon aborts graph construction, the plugins still shut down
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
Projects scaffolded with `@nx/vite` or `@nx/vitest` generators use
`^4.0.0` for vitest v4 dependencies. Since vitest 4.1.0 (released Mar
12), its vite peer dep expanded to `^6.0.0 || ^7.0.0 || ^8.0.0-0`. Yarn
Classic's linker fails with `Invariant Violation: could not find a copy
of vite to link` when encountering this expanded OR range.
This breaks all e2e tests running with Yarn Classic (e2e-cypress,
e2e-eslint, e2e-jest, e2e-playwright, e2e-web, e2e-webpack on
Linux/yarn/20 combos).
## Expected Behavior
Scaffolded projects use `~4.0.x` tilde ranges for vitest v4, restricting
to patch updates only. This avoids pulling in vitest 4.1.0+ and the
problematic peer dep range, keeping Yarn Classic compatibility intact.
## Related Issue(s)
Fixes failing CI runs on yarn combos since Mar 12-13.
## Current Behavior
We accumulate `inputs`/`outputs`/`pids` and store them even if no
subscriber is subscribed, and then they hang around to notify late
subscribers that may never come
## Expected Behavior
`inputs`/`outputs`/`pids` are only sent to current subscribers
## AI Summary
This pull request optimizes how task input notifications are handled in
Nx by ensuring that expensive input collection and storage only occur
when there are active subscribers. This change prevents unnecessary
memory growth in long-lived processes, such as the Nx daemon, and
improves performance by avoiding redundant work.
Notification and input collection optimization:
* Added a `hasTaskInputSubscribers()` method to the `TaskIOService`
class, allowing the hasher to check if any input subscribers are
registered before collecting and notifying task inputs.
* Updated all hashing functions in `hash-task.ts` to only notify task
inputs if there are active subscribers, reducing unnecessary work and
memory usage.
[[1]](diffhunk://#diff-d061dc5551f692abad009b8284c719466cff2f0d3d19bd52e81b7921a9e543d6R57-R63)
[[2]](diffhunk://#diff-d061dc5551f692abad009b8284c719466cff2f0d3d19bd52e81b7921a9e543d6L114-R117)
[[3]](diffhunk://#diff-d061dc5551f692abad009b8284c719466cff2f0d3d19bd52e81b7921a9e543d6R172)
[[4]](diffhunk://#diff-d061dc5551f692abad009b8284c719466cff2f0d3d19bd52e81b7921a9e543d6L185-R188)
[[5]](diffhunk://#diff-d061dc5551f692abad009b8284c719466cff2f0d3d19bd52e81b7921a9e543d6L201-R204)
* Modified the native task hasher implementation
(`native-task-hasher-impl.ts` and Rust code) to conditionally collect
and return input data only when requested, minimizing overhead and
memory allocation.
[[1]](diffhunk://#diff-ddf992f97afbcd8b2206b8d1faab7e7f9571ecfe50e2ab8e3da104001f39f0c3L73-R80)
[[2]](diffhunk://#diff-ddf992f97afbcd8b2206b8d1faab7e7f9571ecfe50e2ab8e3da104001f39f0c3L88-R101)
[[3]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bR194-R200)
[[4]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bL226-R233)
[[5]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bR256)
[[6]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bL262-R273)
[[7]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bL288-R299)
[[8]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bR330-R352)
[[9]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bL348-R383)
[[10]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bL368-R407)
[[11]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bR432)
[[12]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bL417-R475)
[[13]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bL452-R489)
[[14]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bL461-R503)
[[15]](diffhunk://#diff-d81ca7875513ef544822c24671104ce52cd09409a41a796768aba507d87b3c0bR518)
Code cleanup and refactoring:
* Removed unused state and redundant code from `TaskIOService` related
to storing task-to-PID, task-to-input, and task-to-output mappings, as
well as unnecessary graph references and late subscriber emission logic.
[[1]](diffhunk://#diff-fc051b28c1ac25926033d157ec2278cc1cf2fcf7e16626eeedaf876c29fc9d62R42-R66)
[[2]](diffhunk://#diff-fc051b28c1ac25926033d157ec2278cc1cf2fcf7e16626eeedaf876c29fc9d62L91-L95)
[[3]](diffhunk://#diff-fc051b28c1ac25926033d157ec2278cc1cf2fcf7e16626eeedaf876c29fc9d62L104-L111)
* Updated imports and cleaned up constructor logic in
`task-io-service.ts` and `native-task-hasher-impl.ts` for clarity and
maintainability.
[[1]](diffhunk://#diff-ddf992f97afbcd8b2206b8d1faab7e7f9571ecfe50e2ab8e3da104001f39f0c3R16)
[[2]](diffhunk://#diff-fc051b28c1ac25926033d157ec2278cc1cf2fcf7e16626eeedaf876c29fc9d62L1-L2)
These changes collectively make task input tracking more efficient and
robust, especially in scenarios where Nx runs as a daemon or in
environments with no listeners for input notifications.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
## Current Behavior
When `detectPackageManager()` finds no lock file for bun, yarn, or pnpm,
it falls back to `detectInvokedPackageManager()` which checks
`npm_config_user_agent`. This causes misdetection when a workspace is
created with npm (has `package-lock.json`) but the parent process uses
pnpm — the detection picks up pnpm from the user agent instead of npm
from the lock file.
This has been causing **every nightly E2E run to fail since March 4th**
(when #34691 was merged), particularly in e2e-angular where `ng-add`
tests create npm workspaces but `installPackagesTask` incorrectly
invokes `pnpm install --no-frozen-lockfile`.
## Expected Behavior
`detectPackageManager()` should check for `package-lock.json` (npm's
lock file) before falling back to the invoking package manager
detection. This ensures that workspaces with a `package-lock.json` are
correctly identified as npm workspaces regardless of what package
manager invoked the current process.
## Related Issue(s)
Fixes the nightly E2E matrix failures in e2e-angular (`ng-add.test.ts`,
`plugin.test.ts`), e2e-nx-init, and e2e-workspace-create that have been
consistently failing since #34691 was merged.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This PR introduces the `deps-sync` generator which pairs with
`typescript-sync`generator to ensure internal dependencies are correctly
mapped via `devDependencies` of corresponding `package.json`.
## Current Behavior
When dependency to local package is created (via import for example) the
existing typescript-sync generator updates the tsconfig but package.json
is left untouched which might cause issues when referencing transitive
dependencies.
## Expected Behavior
The typescript-sync should be accompanied by deps-sync generator that
would update dependencies in package.json for workspaces.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
---------
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Add the continuous property to the nx show target output. Previously the
command showed cache and parallelism but not whether a target is
continuous.
Fixes NXC-4084
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
## Current Behavior
The Gradle plugin uses an in-memory `gradleCurrentConfigHash` variable
to decide whether to skip re-running `./gradlew nxProjectGraph`. Since
plugin workers shut down between graph computations, this variable
resets to `undefined` each time. The `??=` operator on the disk cache
read means it's only read once per worker lifetime, and the
`!gradleCurrentConfigHash` check always evaluates to `true` on fresh
workers — making the in-memory hash comparison dead code.
This means the caching logic works despite itself (via disk cache), but
is fragile and could cause unnecessary Gradle invocations or stale cache
hits if worker lifecycle assumptions change.
There was also another issue with hashing options when calculating the
project graph for nodes and dependencies. Options were not normalized
when hashing which you did not get deterministic hashing when calling
dependencies after createNodes.
## Expected Behavior
Always read the disk cache and compare hashes directly. If the hash
matches, skip running Gradle. If not, run Gradle and update the disk
cache. No in-memory state needed between worker restarts.
Options are normalized before hashing to ensure that regardless of
createNodes or dependencies, we will get a deterministic hash.
## Related Issue(s)
<!-- No specific issue — discovered during code review -->
---------
Co-authored-by: lourw <56288712+lourw@users.noreply.github.com>
## Current Behavior
When configuring AI agents via `nx polygraph`, a
`.claude/settings.local.json` file is created containing user-specific
settings. This file is not gitignored by Nx, so it can accidentally be
committed to the repository.
## Expected Behavior
`.claude/settings.local.json` should be gitignored by default, similar
to how `.claude/worktrees` is already handled.
## Changes
- Added a migration that adds `.claude/settings.local.json` to existing
workspaces' `.gitignore`
- Updated all three `.gitignore` templates so new workspaces include it
from the start
- Follows the same pattern as the existing
`add-claude-worktrees-to-git-ignore` migration
## Current Behavior
After #34623, when continuous tasks are killed (Ctrl+C), the nx process
stays alive during async cleanup instead of exiting immediately. This
causes the `RunningTasksService` SQLite entry to persist with a
still-alive PID.
When a new `nx` process starts during this cleanup window,
`is_task_running()` finds the stale entry, confirms the PID is still
alive (old process cleaning up), and creates a `SharedRunningTask` —
incorrectly printing:
```
Waiting for @nrwl/ocean:local-registry in another nx process
```
The root cause: #34623 removed `process.exit()` from SIGINT handlers in
`running-tasks.ts`, widening the window where the old process is alive
but the task is no longer actually running.
## Expected Behavior
After killing continuous tasks, a new `nx` invocation should not see
stale "Waiting for ... in another nx process" messages.
This reverts #34623 while a proper fix is designed that achieves both
goals: reliable `postTasksExecution` firing AND prompt DB cleanup.
## Related Issue(s)
Reverts #34623
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
When running nx commands outside of an Nx workspace (no nx.json), the
light client is downloaded to .nx/cache/cloud relative to process.cwd().
This creates an unwanted .nx folder in whatever directory the user
happens to be in.
## Expected Behavior
When outside an Nx workspace, the light client is downloaded to a temp
directory (os.tmpdir()/nx-cloud-client/hash) where hash is derived from
the NX_CLOUD_API URL. This avoids polluting arbitrary directories with
.nx folders while ensuring different cloud instances get separate
directories.
When inside an Nx workspace, behavior is unchanged.
## Current Behavior
The Maven plugin version is at 0.0.15.
## Expected Behavior
The Maven plugin version is bumped to 0.0.16 with a corresponding
migration entry for Nx 22.6.0-beta.14.
## Related Issue(s)
N/A - routine version bump.
### Changes
- Updated all pom.xml files to version 0.0.16
- Updated `mavenPluginVersion` constant in `versions.ts`
- Added migration entry in `migrations.json`
- Created migration file `update-pom-xml-version.ts` for 0.0.16
## Current Behavior
The `@nx/js/typescript` plugin adds dependency project `tsconfig.json`
files as inputs but doesn't resolve nested `projectReferences` within
those files. When `tsc -b` runs, it follows the full reference chain —
reading `tsconfig.lib.json`, `tsconfig.spec.json`,
`tsconfig.storybook.json`, etc. from dependencies — but these files
aren't declared as inputs, causing potential cache correctness issues.
## Expected Behavior
The plugin now walks the full project reference chain from external
dependencies and collects all distinct tsconfig relative paths (e.g.,
`tsconfig.lib.json`, `tsconfig.spec.json`, `cypress/tsconfig.json`).
These are emitted as `^{projectRoot}/...` input patterns, ensuring that
any tsconfig file read by `tsc -b` through the reference chain is
tracked as an input.
Key changes:
- Renamed `getExternalProjectReferenceConfigFiles` →
`getExternalProjectReferenceTsconfigPatterns` to reflect it now returns
input patterns instead of absolute paths
- Uses a worklist algorithm to traverse the reference chain
(breadth-first, cycle-safe)
- Collects relative paths per dependency project root, deduplicates, and
emits `^{projectRoot}/...` patterns
- For build targets, only tsconfig files reachable from the build
reference chain are included (not all refs from the solution tsconfig)
These flags were added while we tested changes to the task API and
streaming. They are no longer needed.
## Current Behavior
<!-- This is the behavior we have today -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
The Jest plugin infers `test` target inputs from the preset file path
but ignores other config properties that reference files — transforms,
setup files, module name mappers, reporters, watch plugins, etc. Changes
to those files don't invalidate the test cache.
## Expected Behavior
All Jest config properties that reference files are resolved and
included as task inputs, matching Jest's merge semantics:
- **Replaced** (config wins over preset): `resolver`, `globalSetup`,
`globalTeardown`, `snapshotResolver`, `snapshotSerializers`,
`testResultsProcessor`, `runner`, `reporters`, `watchPlugins`
- **Concatenated** (preset + config): `setupFiles`, `setupFilesAfterEnv`
- **Deep merged** (config keys win): `moduleNameMapper`, `transform`
Also handles `jest-runner-`/`jest-watch-` prefix resolution, `<rootDir>`
in preset values, and Windows path normalization.
Adds a `useJestResolver` option to control whether jest-resolve is used
for input resolution, decoupled from `disableJestRuntime`. By default,
inputs are resolved using path-based classification (fast, no filesystem
calls beyond what's already done). When `useJestResolver` is enabled,
jest-resolve is used instead, which follows symlinks and honors custom
`moduleDirectories`/`modulePaths` — more accurate for workspace-linked
packages but slower due to filesystem probing per resolved path.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
When pressing Ctrl+C on continuous tasks (e.g., `nx run app:serve`),
`postTasksExecution` never fires or fires with incomplete task results.
This affects any plugin or custom lifecycle hook relying on
`postTasksExecution` to perform cleanup, reporting, or post-run logic.
The issue manifests in non-TUI mode (`NX_TUI=false`) and in setups where
nx runs as a child of a package manager (pnpm/npm), which sends SIGTERM
shortly after SIGINT.
## Expected Behavior
`postTasksExecution` fires reliably on Ctrl+C with complete task
results, regardless of whether TUI is enabled or how the nx process is
invoked.
## Technical Details
Three independent issues caused the broken behavior:
**1. Initiating continuous task exits before orchestrator cleanup**
When the initiating task is continuous and exits with a signal code
(e.g., 130 from SIGINT), the `onExit` handler called
`process.exit(code)` synchronously — before the orchestrator's SIGINT
handler could run. Cleanup and `postTasksExecution` never executed.
Fixed by replacing `process.exit()` with proper task completion via
`handleContinuousTaskExit`, and ensuring initiating tasks are the sole
`onExit` callback (to avoid floating promises from
`exitCallbacks.forEach` not awaiting async callbacks).
**2. run-commands SIGINT handlers call `process.exit(130)`**
Every `nx:run-commands` task running in the main process registered a
SIGINT handler that called `process.exit(signalToCode('SIGINT'))`,
killing the process before the orchestrator's async cleanup could run.
Fixed by removing `process.exit()` from both SIGINT handlers in
`running-tasks.ts`. The `this.kill('SIGTERM')` call is kept for prompt
child termination. Cache-write safety is already guaranteed by
`postRunSteps` guards (`stopRequested`, `status !== 'stopped'`).
**3. `cleanup()` resolves prematurely on concurrent signals**
When pnpm/npm sends SIGTERM ~30ms after SIGINT, the SIGTERM handler saw
`cleanupDone = true` (set at the start of cleanup, before async work),
returned immediately, and its `.finally()` called `resolveStopPromise()`
before SIGINT's cleanup finished. `run()` returned with incomplete task
results.
Fixed by replacing the `cleanupDone` boolean with a stored promise.
Concurrent callers now await the same in-progress cleanup.
**Additional fixes:**
- SIGTERM/SIGHUP handlers now set `stopRequested = true` so externally
terminated tasks are correctly classified as `'interrupted'` rather than
`'fulfilled'`.
- Extracted shared exit-handling logic into `handleContinuousTaskExit`
to consolidate reason-determination between initiating and
non-initiating continuous tasks.
## Current Behavior
The analytics prompt (`ensureAnalyticsPreferenceSet()`) and
`startAnalytics()` run for all commands, including cloud commands like
`download-cloud-client`. When a cloud command runs outside an Nx
workspace, `saveAnalyticsPreference()` creates an almost-empty `nx.json`
(`{ "analytics": true }`) in whatever directory you happen to be in.
## Expected Behavior
Cloud commands skip the analytics prompt and `startAnalytics()`
entirely, since they may run without a workspace and there is no
appropriate `nx.json` to write to.
## Related Issue(s)
Fixes the issue where running `nx download-cloud-client` outside a
workspace creates a spurious `nx.json`.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Removes the `.nx/workflows/sandboxing-config.yaml` file, which is no
longer needed in the repository.
## Changes
- **Deleted** `.nx/workflows/sandboxing-config.yaml` — no CI pipelines
or workflow files reference this file directly
- The only existing mention is in `astro-docs/.../sandboxing.mdoc`,
which is purely instructional documentation guiding users to create
their own config; no update needed
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
> ## Goal
> Remove the workflow configuration file
`.nx/workflows/sandboxing-config.yaml` from the `nrwl/nx` repository.
>
> ## Context
> - Repository: `nrwl/nx`
> - Base branch: `master`
> - File to remove: `.nx/workflows/sandboxing-config.yaml`
> - Source URL:
https://github.com/nrwl/nx/blob/master/.nx/workflows/sandboxing-config.yaml
> - Commit OID (reference provided by user):
`5dd12046c3d5bc5919abdb176be2d11f16c8f943`
> - Current contents:
> ```yaml
> exclude-reads:
> - '**/node_modules/**'
> - .nx/**/*
> - .git/**/*
> - '**/package.json'
> - pnpm-workspace.yaml
> exclude-writes:
> - nx/**/*
> ```
>
> ## Requirements
> 1. Delete `.nx/workflows/sandboxing-config.yaml`.
> 2. Ensure there are no other required references to this file (e.g.,
in CI/workflows) that would break due to removal. If references exist,
update them accordingly.
> 3. Run the full validation suite prior to finalizing (`nx prepush`).
> 4. If validation fails, fix the issues by amending the current commit
(do not create additional commits solely for validation fixes).
>
> ## Deliverable
> Open a pull request that removes
`.nx/workflows/sandboxing-config.yaml` (and any necessary reference
updates), including a clear description of the change and validation
performed.
>
> The following is the prior conversation context from the user's chat
exploration (may be truncated):
>
> User: please open a pr to remove this file
>
> Assistant: I’m paused waiting for you to accept or reject the pull
request action in the confirmation dialog—once you respond there, I can
continue.
>
>
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
*This pull request was created from Copilot chat.*
>
<!-- START COPILOT CODING AGENT TIPS -->
---
💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FrozenPandaz <8104246+FrozenPandaz@users.noreply.github.com>
## Current Behavior
When using Maven plugins that fork executions (e.g. `maven-pmd-plugin`),
the Nx Maven batch executor crashes with
`java.lang.UnsupportedOperationException`. This happens because Kotlin's
`listOfNotNull()`, `mapNotNull()`, and `filter()` return immutable
lists, but Maven's `MojoExecutor.executeForkedExecutions` calls
`list.set()` on `session.projects`, which requires a mutable list.
## Expected Behavior
Maven plugins that fork executions (PMD, Checkstyle, etc.) should work
correctly with the Nx Maven batch executor. All lists assigned to
`session.projects` and `session.allProjects` are now wrapped with
`toMutableList()` to ensure Maven can mutate them as needed.
The fix is applied to both the Maven 3 and Maven 4 adapters.
## Related Issue(s)
Fixes#34758
Document how nx.dev is deployed on Netlify and how requests are routed
between Framer (marketing), Next.js (blog/courses), and Astro (docs).
Includes:
- Request flow diagram and explanation
- Edge function configuration
- _redirects file organization
- Environment variables reference
- Common debugging tasks
- How to add redirects and rewrites
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
## Current Behavior
The `@nx/gradle` plugin fails to parse the project graph when a Gradle
project name contains `.json` as a substring (e.g.
`org.acme.util.jsonutils`). The parsing logic in `processNxProjectGraph`
checks `includes('.json')` to find the JSON file path, but starts from
the task line itself. When the project name contains `.json`, the task
line matches immediately and gets used as a file path, causing an
`ENOENT` error.
## Expected Behavior
The plugin correctly skips the task line and finds the actual JSON file
path on the following line, regardless of whether the project name
contains `.json`.
Two changes:
1. Increment `index` after matching the task line to skip it before
searching for the JSON path
2. Use `endsWith('.json')` instead of `includes('.json')` for a more
precise match
## Related Issue(s)
Fixes#34768
## Summary
- Bumps `fork-ts-checker-webpack-plugin` from `7.2.13` to `9.1.0` in
root `package.json` and `packages/webpack/package.json`
- v7 depends on `memfs` v3 which uses a deprecated `fs.stat` API,
causing console warnings on Node 22+
- v9 uses updated `memfs` and has no breaking API changes for the
constructor pattern used by Nx
## Test plan
- [ ] Run Angular webpack app and verify no `fstat` deprecation warnings
- [ ] Run Node webpack build and verify no regressions
- [ ] Verify type checking still works via the plugin
Fixes#34404
## Summary
- `pathToKey` was incorrectly assigned
`this._options.devServerConfig.sslCert` instead of `sslKey` in all 4
Module Federation dev server plugins (Rspack, Rspack SSR, Angular,
Angular SSR)
- This caused SSL to break when using separate cert and key files
## Files Changed
-
`packages/module-federation/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-dev-server-plugin.ts`
-
`packages/module-federation/src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-ssr-dev-server-plugin.ts`
-
`packages/module-federation/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-dev-server-plugin.ts`
-
`packages/module-federation/src/plugins/nx-module-federation-plugin/angular/nx-module-federation-ssr-dev-server-plugin.ts`
## Test plan
- [ ] Verify SSL works with separate cert/key files in Module Federation
dev server (Rspack)
- [ ] Verify SSL works with separate cert/key files in Module Federation
dev server (Angular)
Fixes#34811
## Summary
- When native bindings fail to load (`IS_WASM = true`),
`runningTasksService` is `null`
- Two call sites in `task-orchestrator.ts` accessed it without null
checks, causing `Cannot read properties of null (reading
'addRunningTask')` during `nx serve`
- Added optional chaining (`?.`) at both sites, matching the existing
guard pattern already used elsewhere in the same file
## Test plan
- [ ] Run `nx serve` in an environment where native bindings are
unavailable (e.g. missing `@nx/nx-<platform>`)
- [ ] Verify no crash on `addRunningTask` or `removeRunningTask`
Fixes#34573
## Current Behavior
The Playwright plugin does not include `tsconfig*.json` files from
dependency projects as inputs. Playwright resolves these files when
running tests, leading to unexpected file reads that aren't tracked for
caching.
## Expected Behavior
When the `production` named input is used, the Playwright plugin infers
`^{projectRoot}/tsconfig*.json` as an input so dependency tsconfig files
are properly tracked. This is not needed for the `^default` branch since
it already includes all dependency files.
## Current Behavior
Running `npx nx@next download-cloud-client` outside an Nx workspace
fails with "The current directory isn't part of an Nx workspace" because
`download-cloud-client` is not in the `isNxCloudCommand` list in
`packages/nx/bin/nx.ts`. This means the process exits at the
`handleNoWorkspace` guard before ever reaching the handler fixed in
#34746.
## Expected Behavior
`download-cloud-client` runs successfully outside an Nx workspace, like
`login`, `logout`, `polygraph`, and other cloud commands that don't need
workspace context.
## Related Issue(s)
Follows up on #34728 and #34746 — `download-cloud-client` was added
before the cloud command bypass existed and was missed when the bypass
was introduced.
## Summary
- `expandWildcardTargetConfiguration` was explicitly copying only
`projects` and `dependencies` when expanding glob patterns in
`dependsOn` target names, dropping `params` and `options`
- This meant `"params": "forward"` (and `"options": "forward"`) had no
effect when the `dependsOn` target used a wildcard like `"target":
"build*"`
- Fix uses object spread (`...dependencyConfig`) to preserve all
properties from the original dependency config
## Bug
Given this `project.json`:
```json
{
"targets": {
"build-all": {
"executor": "nx:noop",
"dependsOn": [
{
"target": "build*",
"params": "forward"
}
]
}
}
}
```
Running `nx run project:build-all --myParam=value` would correctly
resolve the wildcard to match targets like `build`, `build:test`,
`build:prod`, etc. — but `--myParam=value` was never forwarded to those
targets because `params: "forward"` was dropped during expansion.
## Root Cause
In `expandWildcardTargetConfiguration`
(`packages/nx/src/tasks-runner/utils.ts`), the matched targets were
mapped with only `target`, `projects`, and `dependencies`:
```typescript
return matchingTargets.map((t) => ({
target: t,
projects: dependencyConfig.projects,
dependencies: dependencyConfig.dependencies,
// params and options were missing!
}));
```
## Fix
Use object spread to carry over all properties:
```typescript
return matchingTargets.map((t) => ({
...dependencyConfig,
target: t,
}));
```
## Test plan
- [x] Added test case verifying `params: "forward"` is preserved after
wildcard expansion
- [x] All 45 existing tests in `utils.spec.ts` continue to pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The change removes Nx Cloud routes from the list of excluded URL rewrite
paths in the Framer edge rewrite configuration. This means requests to
those Nx Cloud paths will now be eligible for the rewrite behavior
instead of being skipped.
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
---------
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Telemetry only reports event duration via the `measureAndTrack` helper,
which uses a `[track] ` prefix convention. No task-level metrics (count,
cache hits, project count) are reported.
## Expected Behavior
- New event dimensions: `taskCount`, `projectCount`, `cachedTaskCount`
available for telemetry events
- `TaskTelemetryLifeCycle` reports task execution metrics (duration,
task count, project count, cached task count) — only runs on the main
CLI process, not on DTE agents
- `performance.measure()` with `detail: { track: true, ... }` replaces
the `measureAndTrack` / `[track] ` prefix pattern
- Perf observer automatically forwards detail entries matching known GA4
dimension keys
- `reportEvent` simplified to a pass-through — callers use
`customDimensions` keys directly
- `customDimensions` and `EventParameters` exported for use by callers
## Current Behavior
The TUI help text format is currently malformed.
<img width="1181" height="191" alt="Screenshot 2026-03-07 at 11 13 40"
src="https://github.com/user-attachments/assets/e66316eb-600d-4456-920e-c9538c10e7e7"
/>
- The links overlap the plaintext
- Some of the plaintext has the link color
- The bullet points are on the same line
## Expected Behavior
Separation of links and plaintext with bullet points on separate lines.
### Changes
The text output of the TUI help section looks like this:
```txt
│ │
│ Thanks for using Nx! To get the most out of this terminal UI, please check out the docs: https://nx.dev/terminal-uiIf you would prefer to not use the TUI, you can disable it by: - Adding the `--no-tui` flag to your │
│ command.- Setting NX_TUI=false in your environment. │
│ If you are finding Nx useful, please consider giving it a star on GitHub, it means a lot: https://github.com/nrwl/nx │
│ │
```
This PR corrects the layout to be more readable:
```txt
│ │
│ Thanks for using Nx! To get the most out of this terminal UI, please check out the docs: https://nx.dev/terminal-ui │
│ │
│ If you would prefer to not use the TUI, you can disable it by: │
│ - Adding the `--no-tui` flag to your command. │
│ - Setting `NX_TUI=false` in your environment. │
│ │
│ If you are finding Nx useful, please consider giving it a star on GitHub, it means a lot: https://github.com/nrwl/nx │
│ │
```
### Notes
The help link `https://nx.dev/terminal-ui` doesn't exist at the moment.
## Current Behavior
When a user creates a new workspace with `create-nx-workspace`, they are
not asked about analytics. The analytics prompt only appears later on
the first `nx` command run, or via the 22.6.0 migration.
## Expected Behavior
Users are prompted to opt in or out of usage analytics during
`create-nx-workspace`, so the preference is set from the start. The
prompt matches the style of other prompts in the flow (autocomplete with
Yes/No choices).
- **Preset flow**: The `analytics` property is set via the workspace
generator's `createNxJson`, so `nx.json` is properly formatted by
prettier through `formatFiles(tree)`
- **Template flow**: The `analytics` property is written directly to
`nx.json` (matching the pattern used by `setNeverConnectToCloud`)
- Supports `--analytics` CLI flag for non-interactive usage
- Skips the prompt in CI and non-interactive environments (defaults to
`false`)
## Current Behavior
The Nx vs Turborepo comparison doc mixed setup complexity with advanced
features without a clear progression. Some sections were missing (code
generation comparison, cross-repo coordination, project graph
visualization). Images were in PNG format.
## Expected Behavior
- Progressive structure: starts with basics (onboarding, running tasks)
and moves to advanced capabilities (CI, AI, cross-repo); beats the
misconception that starting with Nx is more complex or you need to go
full in
- Overview table at top for quick comparison with links to sections (we
could collapse it if it gets too much 🤔)
- New sections: Running tasks (to emphasize how simple it is to run
tasks in an existing repo, again beating the misconceptions that are
around), Cross-repo coordination, CI throughput, Project graph
- Improved sections: Onboarding (incremental adoption story), Caching
(real benchmark configs), Code generation (acknowledges turbo gen)
- All images converted to AVIF
- Migration doc updated: removed beta/Nx 21 language for continuous
tasks
New docs:
- [comparison
doc](https://deploy-preview-34792--nx-docs.netlify.app/docs/guides/adopting-nx/nx-vs-turborepo)
- [migration doc (mostly
untouched)](https://deploy-preview-34792--nx-docs.netlify.app/docs/guides/adopting-nx/from-turborepo)
## Related Issue(s)
N/A - documentation improvement
## Current Behavior
- `hashBatchTasks` hashes tasks one-by-one via `Promise.all` +
`hashTask` (singular), making N separate native hasher calls.
- Batch cache resolution rebuilds the entire remaining task graph each
wave via `removeTasksFromTaskGraph`, which is O(tasks) per wave and can
crash if `graph.dependencies[id]` is undefined.
- `postRunSteps` for cached batch results is deferred until all cache
resolution completes.
- TUI batch groups appear below "Waiting for task..." placeholders
because their status is based on nested task statuses, which may not
have transitioned yet.
## Expected Behavior
- `hashBatchTasks` uses `hashTasks` (plural) to batch all tasks into a
single native hasher call.
- Batch cache resolution uses a new `walkTaskGraph` util that walks
topologically via in-degree counters — only visits direct dependents per
wave instead of rebuilding the graph.
- `postRunSteps` runs incrementally per wave during cache resolution.
- TUI batch groups are treated as in-progress by existence (since
`start_batch` was called), only moving to completed when all nested
tasks are done.
## Related Issue(s)
Fixes crash: `Cannot read properties of undefined (reading 'filter')` in
`removeIdsFromTaskGraph` during batch cache resolution.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
- Performance tracking is scattered across multiple files with separate
`PerformanceObserver` instances (daemon server, plugin worker, nx.ts)
- Only `createProjectGraphAsync` duration is reported to analytics via a
dedicated `reportProjectGraphCreationEvent` function
- The Rust telemetry `flush()` has a race condition where closing event
channels before sending the flush request can cause the background
thread to exit before processing the flush
- The `default(timeout)` branch in the background sender does nothing
(`continue`)
## Expected Behavior
- A single centralized `PerformanceObserver` in `perf-logging.ts`
handles all performance measure reporting
- Any `performance.measure()` call prefixed with `[track]` is
automatically reported to telemetry (prefix stripped from display/event
name)
- `reportPerfEvent(name, duration)` is a generic function that works for
any perf measure
- Daemon-aware logging: uses `serverLogger` when running in the daemon,
`console.log` otherwise
- The telemetry flush race condition is fixed by sending the flush
request before closing channels
- The background sender's `default(timeout)` branch properly drains and
sends batches
- Duplicate drain logic is extracted into `enqueue_event`,
`enqueue_page_view`, and `drain_channels` helpers
### Currently tracked measures:
- `createProjectGraphAsync` — full project graph build time
- `{plugin}:createNodes` — per-plugin node creation time
- `{plugin}:createDependencies` — per-plugin dependency creation time
## Related Issue(s)
N/A
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Nuxt and rspack E2E tests fail due to multiple issues:
1. `NODE_ENV=test` leaking from Jest into build subprocesses, causing
nuxt to skip type-checking
2. ESLint errors on compiled `.vue.js` files containing `__VLS_*`
identifiers
3. Rspack multi-compiler array config tests fail without `NODE_ENV`
4. Remix vite version incompatible with vitest
Nuxt build E2E still fails due to TS6304 (`composite: true` +
`declaration: false` conflict in non-TS-solution workspaces) — that test
is skipped pending a proper fix.
## Expected Behavior
Nuxt lint, rspack, and remix E2E tests pass. Nuxt build test is skipped
with a TODO until the composite tsconfig issue is resolved.
## Related Issue(s)
Fixes CI E2E failures for nuxt, rspack, and remix.
## Changes
### Environment fixes
- **Strip `NODE_ENV` from E2E subprocess env** — Jest sets
`NODE_ENV=test` which leaked into nuxt build, causing it to skip
type-checking. Stripped globally in `getStrippedEnvironmentVariables()`.
- **Strip AI agent env vars** (`CLAUDECODE`, `CURSOR_TRACE_ID`, etc.) —
prevents the test runner's environment from leaking into e2e
subprocesses.
- **Pass `NODE_ENV` explicitly for rspack array config tests** — rspack
multi-compiler builds need `NODE_ENV` to determine build mode; pass it
via `runCLI` env option.
### Nuxt fixes
- **Add `**/*.vue.js` to ESLint flat config ignores** — compiled Vue
files contain `__VLS_*` identifiers that trigger lint errors.
- **Skip nuxt build e2e test** — pending fix for TS6304
composite/declaration conflict in non-TS-solution workspaces.
### Remix fix
- **Bump vite from `^5.0.0` to `^6.0.0`** — vitest dropped vite 5
support.
### E2E infra
- **Always print E2E workspace directory** — removed `isVerbose()` gate
so the path is always logged.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The Okta SAML doc ends with a vague "Contact your developer productivity
engineer" message. It doesn't specify what information needs to be
exchanged between the customer and DPE to enable SAML and SCIM. This
section was present in the old combined `auth-saml.md` but was lost
during the migration to separate Okta/Azure docs in astro-docs.
## Expected Behavior
The doc now has a clear "Information to exchange with your DPE" section
that outlines:
- **From your DPE** (provided up front): Nx Cloud App URL, JWT token,
and Organization ID for SCIM setup
- **Send back to your DPE**: SAML certificate and SAML entry point URL
This matches the pattern already used in the Azure SAML doc and assumes
SCIM will be configured.
## Related Issue(s)
N/A
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: llwt <llwt@users.noreply.github.com>
## Current Behavior
The TUI debug pane (F12) opens but shows no log output. This is because
PR #34426 gated `tui_logger::init_logger()` behind `NX_TUI=true` in
`initialize_logger()`. However, `enable_logger()` uses `Once::call_once`
and is called from many places (WorkspaceContext, PseudoTerminal, etc.)
before yargs sets `process.env.NX_TUI = 'true'`. Since the first call
wins, the tui-logger layer is never registered and the debug pane stays
empty.
## Expected Behavior
The TUI debug pane (F12) displays log output when the TUI is active,
regardless of which code path calls `enable_logger()` first.
## Changes
- Always register `TuiTracingSubscriberLayer` in the global tracing
subscriber — it just buffers events with no thread overhead
- Defer `tui_logger::init_logger()` (which spawns the mover thread) to
the TUI lifecycle `__init`, where we know the TUI is actually in use
- Non-TUI contexts still avoid the background thread cost
<!-- 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 -->
We do not track changes to `libs.versions.toml` files
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Include `libs.versions.toml` files in the list of files that we track
when we hash. This ensures that changes to that file will trigger a
regeneration of the gradle project graph.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
- Remove relevant native files entries from `.nxignore`, causing them to
be missing inputs.
- Remove stale entry from `.nxignore` used to workaround an issue that
was already solved
## Current Behavior
The npm audit CI job fails with a critical vulnerability
(GHSA-r275-fr43-pm7q) in `simple-git < 3.29.0`, pulled in transitively
via `nuxt@3.17.6` → `@nuxt/devtools@2.6.2` → `simple-git@3.28.0`.
## Expected Behavior
The audit passes. Bumping `nuxt` from `^3.10.0` to `^3.21.1` pulls in
`@nuxt/devtools@3.2.3` → `simple-git@3.33.0`, which includes the fix.
## Related Issue(s)
Fixes the failing audit job:
https://github.com/nrwl/nx/actions/runs/22930179319/job/66549735267
## Current Behavior
The Maven plugin is at a previous version and needs to be updated.
## Expected Behavior
The Maven plugin version is bumped to `0.0.15`, with a corresponding
migration for users upgrading to Nx `22.6.0-beta.12`.
## Related Issue(s)
N/A
## Current Behavior
The Maven plugin only reports dependencies between workspace projects.
External dependencies (Spring, JUnit, etc.) from Maven Central are
invisible — `nx graph` doesn't show the full dependency picture, and Nx
can't invalidate cache when an external dependency changes.
## Expected Behavior
External Maven dependencies now appear as full-fidelity external nodes
in the Nx project graph, with dependency edges between them, hashes for
cache correctness, and `externalDependencies` inputs on targets.
### External Nodes
External nodes use the naming convention `maven:groupId:artifactId`
(e.g., `maven:org.springframework:spring-core`) with:
- Type `"maven"`
- `groupId` and `artifactId` as separate fields
- Declared version (or `"managed"` if inherited from a parent POM)
- SHA-1 hash read from Maven's `.sha1` sidecar files in
`~/.m2/repository`
### External-to-External Edges
Edges between external nodes are derived by parsing POMs from the local
Maven repository. For example, `spring-boot-starter-web` → `spring-web`
→ `spring-core`. This gives `nx graph` a complete picture of the
transitive dependency tree.
### Cache Invalidation via externalDependencies Inputs
Every cacheable target now includes `{"externalDependencies":
["maven:groupId:artifactId", ...]}` in its inputs. This means Nx
invalidates cache when a resolved artifact changes — important for
SNAPSHOTs and version ranges where the dependency can change without
`pom.xml` changing.
### Changes
**Kotlin (maven-plugin)**
- `NxProjectAnalyzerMojo.kt`: Changed ResolutionScope to COMPILE for
full transitive resolution. Added `generateExternalNodes()` with
deduplication and SHA-1 hash reading. Added `generateExternalEdges()`
via POM parsing from ~/.m2. Embedded external nodes in createNodesResult
tuples, added project-to-external and external-to-external dependency
edges.
- `NxProjectAnalyzer.kt`: Uses `project.artifacts` (transitive) instead
of `project.dependencies` (direct only) for external deps. Added
`artifactFile` field for hash lookup. Collects external dep names and
passes them to target factory.
- `NxTargetFactory.kt`: Accepts externalDependencies list, threads it
through all target creation methods, and adds `{"externalDependencies":
[...]}` to every cacheable target's inputs.
**TypeScript**
- `dependencies.ts`: External sources/targets with `maven:` prefix pass
through as-is instead of rootToProjectMap lookup.
- `types.ts`: Added `externalNodes` field to `MavenAnalysisData`.
## Related Issue(s)
<!-- Feature parity with Gradle plugin for external dependency support
-->
<!-- 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
`nx show *` doesn't default to JSON
## Expected Behavior
For agents, it defaults to JSON
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
The `@nx/js/typescript` plugin only infers `dependentTasksOutputFiles`
for `tsc --build` targets when external project references are detected.
Otherwise it falls back to `^production`, which tracks dependency source
files.
This is semantically incorrect — `tsc --build` resolves dependencies
through build artifacts (`.d.ts` and `.tsbuildinfo`), never source
files, regardless of reference type (external refs, internal refs, or
ad-hoc task dependencies).
## Expected Behavior
`dependentTasksOutputFiles` is always inferred for `tsc --build` targets
since that's what tsc actually reads. The `^production` fallback is
removed as it was a proxy that's no longer needed.
## Current Behavior
After #34446, batch tasks with `depsOutputs` inputs had their hashing
deferred until after execution. This meant the streaming `endTasks`
callback fired with `task.hash = undefined`, which Cloud/DTE rejects.
## Expected Behavior
All batch tasks always have a valid hash when `endTasks` is called.
Tasks with `depsOutputs` get a preliminary hash upfront (based on
whatever outputs are on disk), then are re-hashed after execution with
fresh outputs for correct cache storage.
### How it works
1. **Phase 1** now hashes ALL root tasks at each level (not just
cache-eligible ones). Ineligible tasks get a preliminary hash so the
streaming callback always has something valid to send.
2. **Phase 2** runs the batch, then clears and re-hashes all tasks that
ran — outputs are fresh on disk, so depsOutputs tasks get correct final
hashes.
3. The re-hash logic is consolidated into a single block after both code
paths (cache-enabled and cache-skipped).
## Related Issue(s)
Fixes the undefined hash regression from #34446
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
`@nx/webpack` specifies `"less": "^4.1.3"` as a dependency, which allows
`less@4.6.0` to be installed. However, `less@4.6.0` switched to ESM
(`"type": "module"`), and `less-loader@11.x` uses `require()` to load
it. This causes a runtime error:
```
class WebpackFileManager extends implementation.FileManager {
^
TypeError: Class extends value undefined is not a constructor or null
```
This breaks any React/webpack project that uses Less stylesheets (e.g.
the "should support global and css modules" e2e test).
## Expected Behavior
The `less` version range is capped to `>=4.1.3 <4.6.0`, preventing the
incompatible ESM-only version from being installed. Less stylesheets
compile correctly with webpack and less-loader.
## Related Issue(s)
N/A - discovered via e2e test failure
## Current Behavior
The gradle project graph plugin version is 0.1.14.
## Expected Behavior
The gradle project graph plugin version is bumped to 0.1.15 with a
corresponding migration.
## Related Issue(s)
N/A - Routine version bump.
Co-authored-by: Claude <noreply@anthropic.com>
<!-- 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
Gradle task inputs in Nx do not includ configuration files like
gradle.properties, gradle/wrapper/gradle-wrapper.jar, and
gradle/wrapper/gradle-wrapper.properties. When these files change, Nx's
cache does not invalidate, potentially causing builds to use stale
cached results despite configuration changes that affect build behavior.
## Expected Behavior
Gradle wrapper and properties files are now automatically included as
inputs for all Gradle tasks when they exist in the workspace. This
ensures that changes to Gradle version (via wrapper files) or build
configuration (via gradle.properties) properly invalidate Nx's task
cache, guaranteeing accurate incremental builds and cache hits.
## 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: lourw <lourw@users.noreply.github.com>
<!-- 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
When generating atomized CI test targets for Gradle projects, the
dependsOn entries do not respect targetNameOverrides or targetNamePrefix
configuration. This means that if a test task depends on other tasks
(like compileTestKotlin or classes), and those tasks have been renamed
via overrides or prefixed (e.g., gradle-compileTestKotlin), the
generated CI test targets reference the original, non-transformed task
names. This creates broken dependencies in the project graph.
## Expected Behavior
Atomized CI test targets now correctly apply both targetNameOverrides
and targetNamePrefix to their dependsOn entries. When a test task
depends on other tasks, the generated CI targets will reference the
properly transformed target names, ensuring dependency integrity
throughout the project graph. The fix passes these configuration
parameters through the entire CI target generation pipeline in
CiTargetsUtils.kt.
## 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: lourw <lourw@users.noreply.github.com>
## Current Behavior
When `nx.json` exists but contains a syntax error (e.g. invalid JSON),
the nx wrapper shows:
```
[NX]: The "nx.json" file is required when running the nx wrapper.
```
This is misleading because the file exists — it's just malformed.
## Expected Behavior
The nx wrapper now distinguishes between a missing `nx.json` and a
malformed one:
- **Missing file**: `[NX]: The "nx.json" file is required when running
the nx wrapper.`
- **Parse error**: `[NX]: Failed to parse "nx.json": <actual error>. See
...`
The existence check is done upfront before attempting to parse, so the
`catch` block only handles parse errors.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Current Behavior
The `@nx/js/typescript` plugin's `getInputs()` doesn't include tsconfig
files from external project references as task inputs. When `tsc
--build` follows a project reference to another Nx project (e.g.,
`../ui-common/tsconfig.lib.json`), it reads that tsconfig file, but the
dependency isn't declared. This means changes to an external reference's
tsconfig won't invalidate the dependent task's cache.
## Expected Behavior
External project reference config files (and their extended configs) are
declared as inputs for correct cache invalidation.
## Changes
- Replace `hasExternalProjectReferences` boolean check with
`getExternalProjectReferenceConfigFiles` that collects external ref
config file paths in a single traversal
- Add collected paths as inputs alongside `dependentTasksOutputFiles`
- Remove now-unused `hasExternalProjectReferences` (one traversal
instead of two when external refs exist)
## Current Behavior
The `@nx/js/typescript` plugin infers `dependentTasksOutputFiles:
'**/*.{d.ts,tsbuildinfo}'` as inputs for typecheck tasks, but only
tracks `.d.ts` outputs from **direct** dependencies. TypeScript project
references transitively read `.d.ts` files from the full dependency
chain, so when a project typechecks, it reads `.d.ts` outputs from
transitive deps too. These undeclared inputs can cause incorrect cache
hits.
## Expected Behavior
The inferred inputs for typecheck tasks include `.d.ts` and
`.tsbuildinfo` outputs from the entire transitive dependency graph by
setting `transitive: true` on the `dependentTasksOutputFiles` input.
## Current Behavior
Each `nx` CLI invocation generates a new random session ID for GA4
analytics. This means GA4 cannot correlate multiple commands from the
same user working session, making active user tracking inaccurate.
## Expected Behavior
Session IDs are persisted in the SQLite metadata table with a 30-minute
timeout. Consecutive `nx` commands within that window share the same
session ID, enabling accurate GA4 active user tracking. After 30 minutes
of inactivity, a new session is automatically created.
## Related Issue(s)
N/A — internal analytics improvement
## Current Behavior
The webpack legacy e2e test
(`e2e-webpack:e2e-ci--src/webpack.legacy.test.ts`) fails with a snapshot
mismatch because the `reportsDirectory` value changed from
`../coverage/app3224373` to `coverage/app3224373`.
## Expected Behavior
The snapshot should match the new `reportsDirectory` path format
introduced by #34720.
## Related Issue(s)
Fixes the e2e test breakage introduced by #34720 (`fix(vitest)!: resolve
reportsDirectory against workspace root`).
## Current Behavior
Nx CLI has no mechanism for collecting usage analytics, and users are
not prompted about their analytics preferences. There is no way for the
Nx team to understand which commands, generators, and features are most
used.
## Expected Behavior
This PR adds opt-in analytics collection to the Nx CLI with two main
components:
### 1. Analytics Prompt
Users are prompted for their analytics preference on first interactive
CLI run when `analytics` is not yet configured in `nx.json`:
- Only appears when `analytics` is undefined in `nx.json`
- Skipped in CI environments
- Skipped in non-interactive terminals (piped input/output)
- Stores the user's choice as a boolean (`true`/`false`) in the
`analytics` field of `nx.json`
- Defaults to `false` if the user cancels (Ctrl+C)
- Includes a migration (`update-22-6-0/enable-analytics-prompt`) for
existing workspaces
### 2. Analytics Collector
When analytics is enabled, the CLI collects usage data via a Rust-based
telemetry service and sends it to GA4. Data collected includes:
- **Commands run** (e.g. `build`, `test`, `generate`, `add`) — tracked
as page views
- **Command arguments** — with aggressive sanitization of sensitive
values (project names, file paths, URLs, credentials, free-form text are
all redacted; only boolean flags and safe enum values are preserved)
- **Generator and package names** for `nx add` and `nx generate` (as
custom dimensions)
- **Project graph creation duration**
- **Environment metadata**: Nx version, Node version, package manager,
OS, architecture, CI detection
### Workspace Identification
Each workspace is identified by a deterministic ID (used as the GA4
client ID) with the following priority:
1. **`nxCloudId`** (or `nxCloudAccessToken`) from `nx.json` — used
directly, most stable
2. **Git remote URL** (`git remote get-url origin`) — SHA-256 hashed for
privacy
3. **First commit SHA** (`git rev-list --max-parents=0 HEAD`) — used
directly as a fallback
Each user/machine is identified separately via `node-machine-id`.
### Privacy & Safety
- No project names, file paths, or other PII is collected
- Sensitive CLI arguments are redacted (see `SENSITIVE_ARGS_KEYS` list)
- Analytics is strictly opt-in (must be `true` in `nx.json`)
- Telemetry failures are silently ignored — never blocks or crashes the
CLI
- WASM builds are excluded (no native telemetry module available)
- The native telemetry functions are loaded with optional chaining to
prevent crashes when running against published Nx binaries that don't
include them yet
### Key Files
- `packages/nx/src/utils/analytics-prompt.ts` — Prompt logic and
workspace ID generation
- `packages/nx/src/analytics/analytics.ts` — Analytics collector, event
tracking, argument sanitization
- `packages/nx/src/native/telemetry/` — Rust telemetry service
(constants, service, mod)
- `packages/nx/src/utils/machine-id-cache.ts` — Machine ID for user
identification
- `packages/nx/src/migrations/update-22-6-0/enable-analytics-prompt.ts`
— Migration for existing workspaces
## Related Issue(s)
Closes NXC-3731
Closes NXC-3732
Closes NXC-3733
Closes NXC-3734
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: Coly010 <Coly010@users.noreply.github.com>
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
https://claude.ai/code/session_01BMRoUBzqhTQL6WrQFiBxnr
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
Co-authored-by: Claude <noreply@anthropic.com>
## Current Behavior
When `reportsDirectory` is configured with `{workspaceRoot}` token in
`nx.json` targetDefaults:
```json
"@nx/vitest:test": {
"options": {
"reportsDirectory": "{workspaceRoot}/coverage/{projectRoot}"
}
}
```
Coverage output lands in the wrong location. For example, with a project
at `apps/my-app`, coverage goes to `apps/my-app/coverage/apps/my-app/`
instead of the intended `coverage/apps/my-app/`.
## Expected Behavior
Coverage output should be written to
`<workspaceRoot>/coverage/apps/my-app/`.
## Root Cause
Nx's `resolveNxTokensInOptions` strips `{workspaceRoot}/` from option
values and replaces `{projectRoot}`, producing a workspace-root-relative
path (e.g. `coverage/apps/my-app`). The vitest executor then passed this
directly to vitest, which resolved it relative to the **project root** —
not the workspace root.
## Fix
Resolve non-absolute `reportsDirectory` paths against the workspace root
before passing them to vitest, so vitest writes coverage to the correct
location.
## Test Plan
- Added unit tests for the new `resolveReportsDirectory` helper
- Verified with a reproduction workspace that coverage now lands at the
correct path
## Current Behavior
The `@nx/eslint/plugin` infers a `lint` target for
`gradle-project-graph` because it contains a single `.ts` file
(`publish-maven.ts`), even though it's a Kotlin/Gradle project.
Similarly, the parent `gradle` project's `eslint .` scans into non-JS
sub-project directories unnecessarily.
## Expected Behavior
Non-JS Gradle sub-projects (`project-graph`, `batch-runner`) should not
have lint targets inferred by the ESLint plugin, and the parent `gradle`
project's lint should not scan into those directories.
## Changes
- Add `project-graph` and `batch-runner` to ESLint `ignorePatterns` in
`packages/gradle/.eslintrc.json`
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: leosvelperez <leosvelperez@users.noreply.github.com>
## Current Behavior
The Gradle plugin generates `dependsOn` entries using the shorthand
string format (e.g., `"projectName:taskName"`). This doesn't leverage
the full object syntax that Nx supports.
## Expected Behavior
`dependsOn` entries now use the object format:
- Same-project dependencies: `{ "target": "taskName" }`
- Cross-project dependencies: `{ "target": "taskName", "projects":
["proj1", "proj2"] }` with projects grouped by target name
This is more explicit, consistent with the CI targets code (which
already used object format), and enables the `projects` array for
grouping multiple project dependencies under a single target.
## Related Issue(s)
N/A - internal improvement
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
The `@nx/js/typescript` plugin sets `dependentTasksOutputFiles:
'**/*.d.ts'` for tsc tasks with external project references. This misses
`.tsbuildinfo` files that `tsc --build` reads from referenced projects
for incremental compilation, which can lead to incorrect cache hits.
## Expected Behavior
`dependentTasksOutputFiles` uses the glob `**/*.{d.ts,tsbuildinfo}`,
ensuring all files read by `tsc --build` from dependencies are tracked
as inputs for correct cache invalidation.
## Current Behavior
When the `dependency-checks` ESLint rule auto-fixes missing dependencies
in a project's `package.json`, it resolves the version from the root
`package.json` or falls back to the installed version from the project
graph. This inserts explicit version strings even when the workspace
uses catalogs, breaking same-version policy.
## Expected Behavior
The fixer now checks catalogs before falling back to installed versions.
When exactly one catalog entry for a missing dependency satisfies the
installed version, the fixer inserts `catalog:` (default catalog) or
`catalog:<name>` (named catalog) instead of an explicit version.
Fallback chain: root `package.json` → catalog lookup → installed
version.
Edge cases handled:
- Package in multiple catalogs but only one satisfies → uses that one
- Package in multiple catalogs and multiple satisfy → falls back to
installed version
- `file:` and other protocol-based versions → exact string comparison
instead of semver
- No catalog manager or no catalog definitions → falls back to installed
version
Also caches the catalog manager instance and catalog definitions per
lint run instead of re-creating them per function call.
## Current Behavior
In batch mode (Maven/Gradle), all task hashes are computed upfront in
`processScheduledBatch` before the batch executor runs any tasks. Tasks
with `dependentTasksOutputFiles` (aka `depsOutputs`) get hashed using
whatever dependency outputs happen to be on disk from a previous run.
This leads to:
- **False cache hits**: If a dependency's sources changed but its old
outputs are still on disk, the dependent task's hash matches a stale
cache entry and wrong results are served.
- **False cache misses**: On cold runs with no outputs on disk, the hash
is computed without dependency output content and never matches any
stored cache entry.
Non-batch mode doesn't have this problem because it uses lazy hashing —
tasks with `depsOutputs` are only hashed after their dependencies
complete and fresh outputs exist on disk.
## Expected Behavior
Batch mode hashes tasks topologically — each task is hashed only after
its dependencies have run and their outputs are on disk. This means
hashes are always computed against fresh outputs, eliminating both false
cache hits and false cache misses.
### How it works
`applyFromCacheOrRunBatch` now has two phases:
1. **Topological cache resolution** — Walk the **entire** batch task
graph level by level. At each level, partition root tasks into
cache-eligible vs ineligible. A task is **ineligible** for cache if it
has `depsOutputs` inputs AND any of its dependencies were not cached
(their outputs aren't on disk, so the hash would be wrong). Hash and
check cache for eligible tasks, then remove **all** roots from the graph
to expose the next level — even when some tasks are cache misses. This
ensures the walk continues past cache misses to find deeper cache hits.
2. **Run remaining tasks, then hash** — Rebuild a run graph from all
non-cached task IDs and run them through the batch executor. After the
batch completes, hash all tasks that ran. Since all outputs (including
from sibling batch tasks) are now fresh on disk, tasks with
`depsOutputs` get correct hashes on the first pass — no re-hash needed.
### Task history lifecycle fix
The batch streaming callback calls `endTasks` as tasks finish mid-batch,
but tasks haven't been hashed yet at that point (hash is deferred to
post-execution). Previously, `TaskHistoryLifeCycle` and
`LegacyTaskHistoryLifeCycle` eagerly snapshotted `task.hash` in
`endTasks`, which sent `undefined` to the native Rust layer causing a
"Missing field `hash`" crash.
**Fix:** Both lifecycles now store `TaskResult` references in `endTasks`
and defer building `TaskRun` objects until `endCommand`, when
`task.hash` is guaranteed to be set by the post-batch re-hash. The
streaming callback and `runBatch` return value also now use the original
task object reference (instead of spread copies) so that the hash
mutation from `hashBatchTasks` flows through to all stored references.
### Example: 3 tasks over 3 runs
Consider a batch with three tasks in a linear chain: **A → B → C**
- **Task A** — `lib:compile`. Inputs: source files only. No
`depsOutputs`.
- **Task B** — `app:compile`. Depends on A. Inputs: only `depsOutputs`
from Task A (e.g., `target/classes/**`). No source file inputs.
- **Task C** — `app:checkstyle`. Depends on B. Not cacheable.
Hash notation: `H(inputs…)` means the hash is a function of those
inputs.
---
#### Run 1 — Fresh (no outputs on disk, empty cache)
| Step | What happens |
|------|-------------|
| **Phase 1** | Roots = `[A]` (B depends on A, C depends on B). Hash A →
**H_A** |
| | Check cache: A = miss. A is added to `nonCachedTaskIds`. Remove all
roots. |
| **Phase 1, iter 2** | Roots = `[B]`. B has `depsOutputs` and A is
non-cached → B is **ineligible**. Added to `nonCachedTaskIds`. Remove
all roots. |
| **Phase 1, iter 3** | Roots = `[C]`. C has no `depsOutputs` →
eligible. Hash C → **H_C**. Cache miss. Added to `nonCachedTaskIds`.
Remove all roots. |
| **Phase 2** | Rebuild run graph from `nonCachedTaskIds` = {A, B, C}.
Run batch: all 3 tasks execute. A produces `target/classes/`. |
| | Hash all tasks post-execution: A → **H_A**, B → `H(A_outputs)` =
**H_B**, C → **H_C** |
| **Cache** | Store A as **H_A**, store B as **H_B**. C is not cached. |
> **Key:** B is hashed *after* the batch, when A's outputs already exist
on disk. The hash is correct on the first pass. C was still checked
against cache even though A and B were misses.
---
#### Run 2 — Warm (nothing changed, cache populated from Run 1)
| Step | What happens |
|------|-------------|
| **Phase 1, iter 1** | Roots = `[A]`. Hash A → **H_A** |
| | Check cache: A = **HIT** ✅ → restore `target/classes/` to disk. |
| **Phase 1, iter 2** | Roots = `[B]`. B has `depsOutputs` but A is
cached (not in `nonCachedTaskIds`) → B is **eligible**. Hash B →
`H(A_outputs)` = **H_B** (A's outputs just restored!) |
| | Check cache: B = **HIT** ✅ → restore B's outputs. |
| **Phase 1, iter 3** | Roots = `[C]`. Hash C → **H_C**. Not cacheable →
no hit. Added to `nonCachedTaskIds`. |
| **Phase 2** | Rebuild run graph from `nonCachedTaskIds` = {C}. Run
batch with just C. Hash C post-execution. |
> **Key:** Phase 1 restored A's outputs from cache *before* hashing B.
So B's hash matches Run 1's value → cache hit. The topological walk
peels the chain one level at a time: A → B → C.
---
#### Run 3 — Source changed (A's source modified, stale outputs from Run
2 still on disk)
| Step | What happens |
|------|-------------|
| **Phase 1, iter 1** | Roots = `[A]`. Hash A → `H(A_src')` = **H_A'**
(new hash!) |
| | Check cache: A = **miss** (H_A' not in cache). A added to
`nonCachedTaskIds`. |
| **Phase 1, iter 2** | Roots = `[B]`. B has `depsOutputs` and A is
non-cached → B is **ineligible**. Added to `nonCachedTaskIds`. |
| **Phase 1, iter 3** | Roots = `[C]`. C has no `depsOutputs` →
eligible. Hash C → **H_C**. Cache miss. Added to `nonCachedTaskIds`. |
| **Phase 2** | Run batch: all 3 tasks execute. A produces *new*
outputs. |
| | Hash all tasks post-execution: A → **H_A'**, B → `H(A_new_outputs)`
= **H_B'**, C → **H_C** |
| **Cache** | Store A as **H_A'**, store B as **H_B'**. C is not cached.
|
> **Key:** Because hashing happens after execution, B is always hashed
against A's *fresh* outputs. No stale hash, no re-hash needed. And C is
still checked against cache at every level, even when upstream tasks
miss.
---
#### Summary of hashes across runs
| Task | Run 1 (fresh) | Run 2 (warm) | Run 3 (src changed) |
|------|--------------|-------------|-------------------|
| **A** | miss → cache **H_A** | hit **H_A** ✅ | miss → cache **H_A'** |
| **B** | miss → post-exec hash & cache **H_B** | hit **H_B** ✅ | miss →
post-exec hash & cache **H_B'** |
| **C** | not cacheable → runs | not cacheable → runs | not cacheable →
runs |
### Maven plugin fixes
Several fixes to the Maven plugin to ensure correct batch behavior:
- **Propagate batch runner exit code failures**: Batch runner process
exit codes are now correctly propagated so task failures are reported
properly.
- **Use glob patterns for gitignored dependent task outputs**:
`depsOutputs` patterns like `target/classes` are now resolved using glob
patterns, fixing issues with `.gitignore`d output directories.
- **Fix inputs for `maven:test`**: Test task inputs now correctly
include test source files so hash changes when tests are modified.
- **Include test sources in `testCompile` task hash**: The `testCompile`
target now includes `src/test/java` in its inputs.
## Related Issue(s)
Related to #30949
This PR makes it much easier for everyone to contribute to our docs.
1. [Vale](https://vale.sh/) is installed via `mise` - This is our
automated editor.
2. Claude skill to invoke Vale and also follow
`astro-docs/STYLE_GUIDE.md` for things that Vale cannot pick up.
3. `CLAUDE.md` instruction to invoke the skill (2) whenever someone is
updating docs.
Demo: https://www.loom.com/share/415a9da056d3483da297fda61f7e7382
Remove the isNxCloudUsed guard that prevented the command from running
without an nx.json. Now gracefully falls back to default cloud URL when
not in an Nx workspace.
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Remove NX_CLOUD_IO_TRACING_DIRECTORY environment variable.
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
Running `nx migrate latest` updates all `@nx/*` packages except
`@nx/angular-rspack` and `@nx/angular-rspack-compiler`.
## Expected Behavior
`nx migrate latest` updates `@nx/angular-rspack` and
`@nx/angular-rspack-compiler` along with all other `@nx/*` packages.
## Related Issue(s)
Fixes#32772
## Current Behavior
The `getOutputs()` function in the `@nx/js/typescript` plugin derives
`.tsbuildinfo` filenames from `config.basenameNoExt` (the outer
`ConfigContext`, which always corresponds to `tsconfig.json`) instead of
the currently iterated internal project reference. This causes all
internal references to produce `dist/tsconfig.tsbuildinfo` as the output
path instead of the correct filenames like
`dist/tsconfig.lib.tsbuildinfo` and `dist/tsconfig.spec.tsbuildinfo`.
This leads to:
- Cache misses because Nx looks for `dist/tsconfig.tsbuildinfo` which
doesn't exist
- Missing actual `.tsbuildinfo` files from cache outputs
- Potential race conditions in parallel `tsc --build` invocations
## Expected Behavior
The `.tsbuildinfo` filename should be derived from each individual
tsconfig's file path. For a project with:
- `tsconfig.lib.json` → `dist/tsconfig.lib.tsbuildinfo`
- `tsconfig.spec.json` → `dist/tsconfig.spec.tsbuildinfo`
- `cypress/tsconfig.json` → `cypress/dist/tsconfig.tsbuildinfo`
The fix changes the loop in `getOutputs()` to iterate over entries (path
+ data pairs) so the basename can be correctly derived from each
tsconfig's file path. Also fixes the `outFile` and no-outDir branches
which had similar issues using the outer config parameter instead of the
loop variable.
## Related Issue(s)
Fixes#34737
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: vsavkin <vsavkin@users.noreply.github.com>
Rewrite README to reflect Nx's current capabilities: zero-config caching,
polyglot plugin system, CI distribution, AI-native tooling, and self-healing CI.
Remove stale badges (Gitter, Semantic Release). Point courses banner to nx.dev/courses.
## Current Behavior
When running `@nx/vitest:configuration` on an Angular project, the
generator always generates `import
'@analogjs/vitest-angular/setup-zone'` regardless of whether the app is
zoneless. Additionally, the setup file generation logic for Angular 21+
(which uses `setupTestBed()`) only exists in
`packages/angular/src/generators/utils/add-vitest.ts`
(`createAnalogSetupFile`), making the vitest generator not
self-contained.
## Expected Behavior
The vitest configuration generator should:
- Auto-detect whether an Angular project is zoneless (by checking
polyfills for apps, or zone.js dependency for libraries)
- Generate `setup-snapshots` instead of `setup-zone` for zoneless
projects
- Handle Angular 21+ `setupTestBed()` setup directly, without requiring
a separate function in the angular package
- Accept an explicit `zoneless` option to override auto-detection
## Related Issue(s)
Fixes#33983
## Current Behavior
The `resolveId` hook in `nxViteTsPaths` processes all import paths,
including Vite root-relative paths (e.g. `/src/test-setup.ts`). In Vite,
`/foo` means "relative to project root," but `nxViteTsPaths` resolves it
via tsconfig's `baseUrl` (workspace root), producing wrong paths.
In an Angular standalone workspace with Vitest + Analog, generating a
library and running `nx test test-lib` fails with `Error: Need to call
TestBed.initTestEnvironment() first` because the library's
`src/test-setup.ts` resolves to the root app's file instead. The Analog
Angular compiler never compiled that file, so the transform returns
empty content and `setupTestBed()` never runs.
## Expected Behavior
`nxViteTsPaths` should skip `/`-prefixed paths and let Vite's built-in
resolver handle them. These are filesystem paths (absolute or
root-relative), not TypeScript import specifiers. Library tests should
work correctly with their own `test-setup.ts`.
## Related Issue(s)
Fixes#34300
## Current Behavior
The TypeScript plugin's typecheck target inference was tied to
`tsconfig.json`, so users could not configure a different tsconfig file
name for typecheck inference.
## Expected Behavior
The plugin supports configuring `typecheck.configName`, which defaults
to `tsconfig.json`.
## Current Behavior
Running Nx Cloud commands (login, logout, polygraph, etc.) from outside
an Nx workspace fails with 'The current directory is not part of an Nx
workspace' because handleNoWorkspace exits before reaching the cloud
command handler. Additionally, polygraph was not in the isNxCloudCommand
list.
## Expected Behavior
Nx Cloud commands work regardless of whether you are inside an Nx
workspace, since they only delegate to the cloud client.
## Related Issue(s)
N/A
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Current Behavior
Plugin cache writes (`writeJsonFile`, `writeFileSync`) across the
codebase have inconsistent error handling:
- Some throw on failure, aborting project graph calculation entirely
- Some silently swallow errors, leaving corrupted cache files on disk
- No mechanism exists to recover from oversized or corrupted caches
- `nx-deps-cache.ts` retries 5 times then throws, crashing the graph
## Expected Behavior
- Plugin cache write failures **never** abort graph calculation — they
warn and continue
- A centralized `safeWritePluginCache` utility handles all hash-map
plugin caches with a 3-step strategy:
1. Attempt full write
2. On failure: evict oldest 50% of entries (LRU) and retry
3. On second failure: wipe cache file, log warning, return without
throwing
- `PluginCache<T>` class wraps cache data with a Proxy that
transparently tracks access order, enabling true LRU
eviction (not just insertion order)
- Access order is stored as a simple `string[]` array — front is oldest,
back is most recent
- Backward-compatible with 3 on-disk formats: legacy plain `Record`,
timestamp-based `{ entries, accessedAt }`, and
current `{ entries, accessOrder }`
- All plugin cache consumers migrated: package-json, js/lockfile,
dotnet, cypress, playwright, gradle, maven
- `nx-deps-cache.ts` changed from throw-after-retries to warn + cleanup
- New utilities exported via `@nx/devkit` internals for plugin authors
## Related Issue(s)
Fixes NXC-3833"
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
## Current Behavior
Several security-related issues reported:
1. **copy-webpack-plugin** (#34632): `@nx/webpack` and `@nx/next` pin
`copy-webpack-plugin@^10.2.4` which transitively depends on
`serialize-javascript@^6.0.1` (vulnerable) and `fast-glob` (supply-chain
risk).
2. **koa** (#34621): `@nx/module-federation` transitively pulls
`koa@3.0.3` via `@module-federation/dts-plugin`, which is vulnerable to
CVE-2026-27959 (Host Header Injection, fixed in koa 3.1.2).
3. **css-minimizer-webpack-plugin**: `@nx/webpack` pins `^5.0.0` which
also depends on vulnerable `serialize-javascript@^6.0.1`.
4. **@module-federation/enhanced**: Versions `<2.1.0` transitively
install vulnerable `koa` via `dts-plugin`.
5. **Next.js**: Versions `16.0.x` are vulnerable to GHSA-9g9p-9gw9-jx7f
(Image Optimizer DoS) and GHSA-5f7q-jpqc-wp7h (PPR Resume memory
consumption).
6. **minimatch** (#34701): User reports minimatch vulnerability, but the
Nx pnpm catalog already pins the patched version `10.2.4`. No code
change needed — users should delete their lockfile and reinstall.
## Expected Behavior
1. **copy-webpack-plugin** bumped to `^14.0.0` which uses
`serialize-javascript@^7.0.3` (patched). Added `noErrorOnMissing: true`
to all 3 copy-webpack-plugin usage sites to handle the v14 breaking
change where missing glob patterns now throw errors by default.
2. **css-minimizer-webpack-plugin** bumped to `^8.0.0` which uses
`serialize-javascript@^7.0.3` (patched).
3. **koa** bumped to `^3.1.2` in `@nx/node` versions.ts.
`@module-federation/dts-plugin@2.1.0` completely removes koa dependency.
4. **@module-federation/enhanced**, **runtime**, **sdk** bumped to
`^2.1.0` across all packages (`@nx/module-federation`, `@nx/react`,
`@nx/angular`, `@nx/rspack`). Added `noErrorOnMissing` fix for
`@module-federation/enhanced` 2.x `runtime-library-control.plugin.ts`
compatibility.
5. **Next.js** bumped to `~16.1.6` and `eslint-config-next` to
`^16.1.6`.
6. **minimatch** — no change needed, already resolved.
### Migrations added (22.6.0-beta.10)
- `@nx/module-federation`: Bump MF packages to `^2.1.0`
- `@nx/react`: Bump `@module-federation/enhanced` to `^2.1.0`
- `@nx/angular`: Bump `@module-federation/enhanced` to `^2.1.0`
- `@nx/node`: Bump `koa` to `^3.1.2`
- `@nx/next`: Bump `next` to `~16.1.6`
### Skipped
- **esbuild** (`<=0.24.2`, moderate severity, dev server only): Fix
requires breaking change jump from `^0.19.2` to `0.25+`. Will address
separately.
## Testing
Created a fresh Nx workspace with all affected plugins to verify `npm
audit` is clean after changes:
- `@nx/next` (nextapp)
- `@nx/webpack` (webpackapp)
- `@nx/rspack` (shell, remote1, remote2 via Module Federation)
- `@nx/module-federation` (shell + remotes with MF config)
- `@nx/react` (MF host/remotes)
- `@nx/node` + koa (api)
```
├── apps
│ ├── api # @nx/node (koa)
│ ├── nextapp # @nx/next
│ ├── remote1 # @nx/react + rspack + MF
│ ├── remote2 # @nx/react + rspack + MF
│ ├── shell # @nx/react + rspack + MF (host)
│ └── webpackapp # @nx/webpack
```
Post-change audit result — only remaining issue is esbuild (moderate,
skipped intentionally):
```
# npm audit report
esbuild <=0.24.2
Severity: moderate
esbuild enables any website to send any requests to the development server
and read the response - https://github.com/advisories/GHSA-67mh-4wv8-2f99
fix available via `npm audit fix --force`
Will install esbuild@0.27.3, which is a breaking change
1 moderate severity vulnerability
```
## Related Issue(s)
Fixes#34632Fixes#34621Fixes#34701
This addresses feedback on PR #34726 - .netlify directories are build
artifacts and should be in .gitignore rather than .nxignore. This also
generalizes the pattern from `astro-docs/.netlify` to `.netlify` to
cover the root-level `.netlify/static/documentation` directory that was
causing duplicate project detection.
Co-authored-by: Claude <noreply@anthropic.com>
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
This PR surfaces install errors instead of silently failing without
anything useful printed. It also records `needs_input` in the AX flow as
`cancelled` so we account for cases where AI agents exit without
recording either success, complete, or cancelled.
BEFORE:
<img width="1288" height="381" alt="image"
src="https://github.com/user-attachments/assets/44fe9642-764e-452b-90be-f30c4a230be5"
/>
AFTER:
<img width="1279" height="850" alt="Screenshot 2026-03-05 at 12 30
25 PM"
src="https://github.com/user-attachments/assets/24c96584-2790-4dcb-8404-7e221c50876c"
/>
## Notes
1. **Remove `--silent` from all PM install commands** — since
`execAndWait` uses `exec()` (captures output in memory, never shown to
terminal), `--silent` just suppressed error info for no benefit
2. **Increase `maxBuffer`** from default 1MB to 10MB to prevent process
being killed when PMs emit verbose output
3. **Fallback error message** when both stderr and stdout are empty —
includes exit code and log file path
4. **Structured sandbox error** with exit code, log file, and actionable
hint
5. **Record telemetry stat** for AI agent `needs_input` flow (was
previously missing)
6. **Migrate from deprecated `CreateNxWorkspaceError`** to `CnwError` in
`execAndWait`
## Related Issue(s)
Fixes NXC-4035
## Current Behavior
[The courses
page](https://deploy-preview-34669--nx-dev.netlify.app/courses) has a
hero section with title and subtitle but no mention of the YouTube
channel for one-off educational videos.
## Expected Behavior
A subtle callout link with a YouTube icon appears below the hero
subtitle, directing users to the Nx YouTube channel for one-off
educational videos.
## Related Issue(s)
N/A
## Current Behavior
No blog post covering the AX principles behind recent Nx CLI
improvements.
## Expected Behavior
New draft blog post (`docs/blog/2026-03-05-making-nx-agent-ready.md`)
covering:
- Why agentic experience (AX) matters for developer tools
- AX principles: context management, structured feedback, idempotency,
informative output
- The open question of human vs agent experience divergence
- A forward look at `nx connect` going agentic
The post is set to `draft: true` and scheduled for Thursday March 5th.
## Related Issue(s)
N/A — new content
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Juri <juri.strumpflohner@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: MaxKless <MaxKless@users.noreply.github.com>
## Current Behavior
When Claude Code creates worktrees under `.claude/worktrees/`, Nx picks
them up as workspace projects. This causes duplicate project errors that
break the project graph.
## Expected Behavior
`.claude/worktrees` is gitignored so worktree copies of the repo don't
interfere with Nx's project detection.
## Current Behavior
After the napi v2→v3 migration, running tasks with TUI enabled while Nx
Console is connected causes a panic: `there is no reactor running, must
be called from the context of a Tokio 1.x runtime`. This happens because
`end_command` is a sync NAPI callback that calls `end_running_tasks()`
which uses `tokio::spawn`, but napi v3 no longer wraps sync callbacks
with Tokio runtime context by default.
## Expected Behavior
Tasks complete without panic when Nx Console is connected and TUI is
enabled.
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Bump Nx version to 22.6.0-beta.8
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
On Windows, the `@nx/js` TypeScript plugin fails during project graph
creation with a path mismatch assertion error. The plugin uses
`path.join()` and `path.relative()` from Node's `path` module, which
produce backslash-separated paths on Windows. These are passed to
TypeScript's `readConfigFile` API, which has an internal inconsistency:
its parser normalizes paths to forward slashes for diagnostics but
retains the original (backslash) path on the source file, causing an
assertion failure when parse diagnostics are present.
Additionally, `posix.normalize()` was incorrectly relied upon to convert
backslashes to forward slashes — it only normalizes paths already in
POSIX format.
## Expected Behavior
The TypeScript plugin normalizes paths to POSIX format (forward slashes)
for TypeScript API compatibility and cache key consistency, working
correctly on both Windows and Unix systems.
## Related Issue(s)
Fixes#31232
## Current Behavior
When creating a new Angular standalone project with Vitest
(`create-nx-workspace` → Angular → Standalone), running tests fails
with:
```
✘ [ERROR] TS2304: Cannot find name 'Disposable'. [plugin angular-compiler]
node_modules/@vitest/spy/dist/index.d.ts:158:80:
158 │ ...tends Procedure | Constructable = Procedure> extends Disposable {
```
For standalone (root) projects, `getRootTsConfigFileName` returns
`tsconfig.json` — the same file as the project tsconfig.
`getNeededCompilerOptionOverrides` compares the file against itself,
sees `skipLibCheck: true` already matches, and strips it. The result:
`skipLibCheck` disappears from the final `tsconfig.json`.
## Expected Behavior
Standalone Angular projects should have `skipLibCheck: true` in their
`tsconfig.json` (matching what Angular CLI generates), preventing type
errors from third-party `.d.ts` files like `@vitest/spy`.
## Related Issue(s)
Fixes#34164
## Current Behavior
When using `generatePackageJson: true`, Nx generates a pruned
`pnpm-lock.yaml` that includes the `catalogs:` section from the root
lockfile. Since the dist folder doesn't include `pnpm-workspace.yaml`,
pnpm 10.24.0+ throws `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` during `pnpm
install --frozen-lockfile`.
## Expected Behavior
The pruned lockfile strips the `catalogs` section since the dist folder
has no `pnpm-workspace.yaml` to define them.
## Related Issue(s)
Fixes#34337
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
When `NX_PREFIX_OUTPUT=true` (e.g. `--output-style=stream`),
`nx:run-commands` tasks are forced out of the fast direct execution path
and into a slower forked process, because the direct path had no
mechanism to prefix output with project names.
## Expected Behavior
`nx:run-commands` tasks stay on the direct execution path even when
output prefixing is needed. The PTY's `quiet` mode suppresses direct
stdout writes, and the orchestrator intercepts output via `onOutput`
callbacks to prefix each line with the colored project name before
writing to stdout.
### Changes
- Allow `nx:run-commands` to use the direct execution path regardless of
prefix output setting
- When prefixing is enabled, suppress direct stream output and intercept
it via `onOutput` to add colored project-name prefixes
- Extract a shared `writePrefixedLines` utility used by both the direct
path and the existing `addPrefixTransformer` stream — eliminates
duplicated split/filter/prefix logic
- Use `os.EOL` instead of manual platform newline detection
- Hoist formatted prefix string outside the per-line callback for
efficiency
- Simplify boolean expressions (`streamOutput && !shouldPrefix` instead
of ternary, remove redundant guard)
## Related Issue(s)
N/A — performance improvement for streaming output mode.
## Current Behavior
Requests to `/.netlify/images?url=...` are intercepted by the Framer
proxy edge function and forwarded to Framer, returning broken images on
docs pages (e.g. sandboxing page).
## Expected Behavior
`/.netlify/*` requests pass through to Next.js, which rewrites them to
the astro-docs site where the actual images are hosted.
## Related Issue(s)
Fixes DOC-436
This PR adds a new feature page for sandboxing.
- What task sandboxing is (hermetic task execution with IO tracing)
- Why hermeticity matters for caching correctness, with concrete Vite
examples
- How to investigate sandbox violations in the Nx Cloud UI (with
annotated screenshots)
- How to inspect declared inputs/outputs with `nx show target` and `nx
show project`
- How to enable sandboxing (`NX_CLOUD_IO_TRACING_DIRECTORY`) and
configure path exclusions
The page is listed under Orchestration & CI in the sidebar.
Preview:
https://deploy-preview-34686--nx-docs.netlify.app/docs/features/ci-features/sandboxing
<img width="396" height="658" alt="image"
src="https://github.com/user-attachments/assets/96dc802a-eb1a-4b4a-9df4-bde846ed2775"
/>
## Related Issue(s)
Closes DOC-429
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
The sandboxing config only excludes reads for `node_modules`, `.nx`,
`.git`, and `package.json` paths. No write exclusions are configured,
and `pnpm-workspace.yaml` is not excluded from reads.
## Expected Behavior
- The `nx/**/*` directory is excluded from write sandboxing, allowing
write operations to the nx directory during CI workflows.
- `pnpm-workspace.yaml` is excluded from read sandboxing, similar to
other workspace config files.
## Related Issue(s)
N/A
This PR brings our CNW experience back to previous state.
## Current Behavior
The CNW prompt flow diverges from v22.1.3 in several ways:
- Shows "Which starter do you want to use?" template prompt
- Cloud prompt says "Try the full Nx platform?" instead of caching
question
- Preset flow uses simplified cloud prompt instead of CI provider →
caching fallback
- Completion message shows box banner with `accessToken=undefined`
manual URL
- Setup message includes `github.com/new` link not present in v22.1.3
## Expected Behavior
Human-visible CNW flow identical to v22.1.3 while preserving:
- NDJSON AI output (agentic experience)
- `--template` flag (works via CLI, not surfaced in prompts)
- Telemetry, error handling, AI agent detection
Changes:
- Skip template prompt, go straight to preset/stack flow
- Restore "Would you like remote caching?" with v22.1.3 wording
- Restore CI provider → caching fallback prompt chain for preset flow
- Pass actual nxCloud to createEmptyWorkspace so cloud token is real
- Restore v22.1.3 completion message ("Your remote cache is almost
complete.")
- Restore v22.1.3 getNxCloudInfo signature with rawNxCloud URL
visibility
- Restore "Nx Cloud has been set up successfully" spinner text
- Remove github.com/new link from push message
## Related Issue(s)
Closes NXC-4020
## Current Behavior
when you run things like `pnpm nx@latest init`, there might not be a
pnpm lockfile yet. So nx commands will detect the PM as `npm` by
default... even though the fact that the user is invoking the command
via `pnpm` is a strong signal that that's the PM they want to use.
## Expected Behavior
We fall back to detecting the invoking PM via env var if no lockfile
exists.
## Summary
- Read generated `config.toml` from `nx-ai-agents-config` repo as single
source of truth for Codex config format
- Deep-merge into user's existing `.codex/config.toml` using
`@ltd/j-toml` (already in monorepo)
- Adjust MCP args dynamically for Nx version (`["nx", "mcp"]` for ≥22,
`["nx-mcp"]` for <22), preserving extra user args
- Respect `multi_agent = false` if explicitly set by user
- Copy `.codex/agents/` subagent TOML files alongside existing
`.agents/skills/`
- Add unit tests (7) and e2e tests (4) for the new functionality
## Test plan
- [ ] Unit tests: `nx test nx -- --testPathPatterns set-up-ai-agents`
- [ ] E2e tests: `nx e2e e2e-nx -- --testPathPatterns
configure-ai-agents` (codex agent describe block)
- [ ] Verify codex config merges correctly with existing user config
- [ ] Verify `multi_agent = false` is not overwritten on re-run
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Current Behavior
The `extras.test.ts` e2e test is flaking on master because the snapshot
file expects `.github/workflows/ci.yml` in the "general" task inputs,
but `create-nx-workspace --no-interactive` no longer generates it.
PR #34332 accidentally updated the snapshot to include this file — the
author likely ran tests against a cached workspace from before PR #34616
fixed the CI workflow generation bug.
## Expected Behavior
The snapshot should not include `.github/workflows/ci.yml` since
`create-nx-workspace` correctly skips CI workflow generation in
non-interactive mode (fixed in PR #34616).
## Related Issue(s)
Fixes the `e2e-nx:e2e-ci--src/extras.test.ts` CI flakiness on master.
## Current Behavior
There's a bug currently where is a plugin returns a dependsOn dependency
or input that directly references another project by name, and a later
plugin renames that project, the dependsOn or input entry is left stale
and pointing at a now non-existent project.
## Expected Behavior
The old refs are kept up to date as the nodes get merged together
## AI Summary
This pull request introduces a new mechanism to handle project name
substitutions in the Nx project graph, ensuring that references to
project names in `inputs` and `dependsOn` blocks remain accurate even if
a plugin changes a project's name during graph construction. The main
addition is the `ProjectNameInNodePropsManager`, which tracks and
updates references when project names change. Several related
refactorings and improvements were made to integrate this manager into
the project configuration merging process.
**Project name substitution and consistency:**
* Added a new `ProjectNameInNodePropsManager` class to manage and apply
project name substitutions when project names change, ensuring that all
references in `inputs` and `dependsOn` blocks remain consistent.
* Integrated the `ProjectNameInNodePropsManager` into the
`mergeCreateNodesResults` function, registering substitutors for node
results, marking roots as dirty when names change, and applying
substitutions after merging.
[[1]](diffhunk://#diff-774ce1a4fd8ec0c898c989fb75e2d8c87a0055549bca680945bce50f717db117R518)
[[2]](diffhunk://#diff-774ce1a4fd8ec0c898c989fb75e2d8c87a0055549bca680945bce50f717db117L521-R551)
[[3]](diffhunk://#diff-774ce1a4fd8ec0c898c989fb75e2d8c87a0055549bca680945bce50f717db117R563-R571)
**API and function changes:**
* Modified `mergeProjectConfigurationIntoRootMap` to return an object
indicating whether a project name was changed, instead of just returning
void.
[[1]](diffhunk://#diff-774ce1a4fd8ec0c898c989fb75e2d8c87a0055549bca680945bce50f717db117L59-R62)
[[2]](diffhunk://#diff-774ce1a4fd8ec0c898c989fb75e2d8c87a0055549bca680945bce50f717db117R238-R244)
**Code organization and import cleanup:**
* Refactored imports in `project-configuration-utils.ts` for better
organization and to accommodate the new manager.
[[1]](diffhunk://#diff-774ce1a4fd8ec0c898c989fb75e2d8c87a0055549bca680945bce50f717db117L9-R25)
[[2]](diffhunk://#diff-774ce1a4fd8ec0c898c989fb75e2d8c87a0055549bca680945bce50f717db117R43-L43)
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
The clean-up PR inverted the reverse proxy logic such that only the
defined paths are passed to Next.js. The default is to return Framer
pages. Since we also remove the Nextjs redirect, we lose the `/docs` one
that was missing from `_redirects` file.
https://github.com/nrwl/nx/pull/34672
This PR adds the redirect to fix it again.
Bun includes extra consecutive async frames for async functions in call
stacks, causing `preventRecursionInGraphConstruction` to falsely detect
a recursive loop during normal project graph construction.
## Root Cause
`preventRecursionInGraphConstruction` uses `getCallSites().slice(2)` to
skip the top 2 frames (itself +
`buildProjectGraphAndSourceMapsWithoutDaemon`), then checks if
`buildProjectGraphAndSourceMapsWithoutDaemon` appears again in the
remaining frames.
In Bun, `buildProjectGraphAndSourceMapsWithoutDaemon` appears **twice
consecutively** due to async frame duplication — leaving one occurrence
after the slice, which incorrectly triggers the loop error.
**Node call stack (after `slice(2)`):**
```
#0 createProjectGraphAndSourceMapsAsync ← clean
#1 createProjectGraphAsync
#2 runOne
```
**Bun call stack (after `slice(2)`):**
```
#0 buildProjectGraphAndSourceMapsWithoutDaemon ← false positive!
#1 createProjectGraphAndSourceMapsAsync
#2 createProjectGraphAndSourceMapsAsync ← Bun duplicates async frames
#3 createProjectGraphAsync
```
## Fix
Since the call stack recursion check does not work reliably under Bun,
`preventRecursionInGraphConstruction` now returns early when running
under Bun, detected via `'Bun' in globalThis` — consistent with the
existing Bun runtime detection pattern used elsewhere in the codebase
(e.g., `isolated-plugin.ts`). The original `slice(2)` logic is preserved
unchanged for Node.js and other runtimes.
```ts
export function preventRecursionInGraphConstruction() {
// Bun's async stack traces include extra frames that cause false positives in the
// recursion check below, so we skip the check when running under Bun.
if ('Bun' in globalThis) {
return;
}
// ... existing Node.js check ...
}
```
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
----
*This section details on the original issue you should resolve*
<issue_title>getCallSites output differs between Node and Bun triggering
loop detection</issue_title>
<issue_description>### Current Behavior
Hey team,
I am trying to use Bun (1.3.5) instead of Node (v22.17.0) for running Nx
(v22.1.3) and bumped into the following error:
Command:
```bash
bunx --bun nx run api:build
```
Error:
```
NX Project graph construction cannot be performed due to a loop detected in the call stack. This can happen if 'createProjectGraphAsync' is called directly or indirectly during project graph construction.
To avoid this, you can add a check against "global.NX_GRAPH_CREATION" before calling "createProjectGraphAsync".
Call stack:
buildProjectGraphAndSourceMapsWithoutDaemon (/app/node_modules/.pnpm/nx@22.1.3_@swc-node+register@1.10.9_@swc+core@1.11.1_@swc+helpers@0.5.15__@swc+types@0.1.18_t_twtgkxomntuzxcyp4ewkmtxn2q/node_modules/nx/src/project-graph/project-graph.js:81:62)
createProjectGraphAndSourceMapsAsync (/app/node_modules/.pnpm/nx@22.1.3_@swc-node+register@1.10.9_@swc+core@1.11.1_@swc+helpers@0.5.15__@swc+types@0.1.18_t_twtgkxomntuzxcyp4ewkmtxn2q/node_modules/nx/src/project-graph/project-graph.js:274:31)
createProjectGraphAndSourceMapsAsync (/app/node_modules/.pnpm/nx@22.1.3_@swc-node+register@1.10.9_@swc+core@1.11.1_@swc+helpers@0.5.15__@swc+types@0.1.18_t_twtgkxomntuzxcyp4ewkmtxn2q/node_modules/nx/src/project-graph/project-graph.js:225:53)
createProjectGraphAsync (/app/node_modules/.pnpm/nx@22.1.3_@swc-node+register@1.10.9_@swc+core@1.11.1_@swc+helpers@0.5.15__@swc+types@0.1.18_t_twtgkxomntuzxcyp4ewkmtxn2q/node_modules/nx/src/project-graph/project-graph.js:222:45)
createProjectGraphAsync (/app/node_modules/.pnpm/nx@22.1.3_@swc-node+register@1.10.9_@swc+core@1.11.1_@swc+helpers@0.5.15__@swc+types@0.1.18_t_twtgkxomntuzxcyp4ewkmtxn2q/node_modules/nx/src/project-graph/project-graph.js:205:40)
runOne (/app/node_modules/.pnpm/nx@22.1.3_@swc-node+register@1.10.9_@swc+core@1.11.1_@swc+helpers@0.5.15__@swc+types@0.1.18_t_twtgkxomntuzxcyp4ewkmtxn2q/node_modules/nx/src/command-line/run/run-one.js:23:52)
runOne (/app/node_modules/.pnpm/nx@22.1.3_@swc-node+register@1.10.9_@swc+core@1.11.1_@swc+helpers@0.5.15__@swc+types@0.1.18_t_twtgkxomntuzxcyp4ewkmtxn2q/node_modules/nx/src/command-line/run/run-one.js:16:23)
Pass --verbose to see the stacktrace.
```
After some digging I found that the stack trace produced by
https://github.com/nrwl/nx/blob/691bb320ce1e9cc2872e1a1b364d3fdeb9e1ad0e/packages/nx/src/utils/call-sites.ts
differs between Node and Bun. When
https://github.com/nrwl/nx/blob/691bb320ce1e9cc2872e1a1b364d3fdeb9e1ad0e/packages/nx/src/project-graph/project-graph.ts#L422
is run, the produced function call tracing is:
Node (v22.17.0):
```
nrwl/nx#0 createProjectGraphAndSourceMapsAsync
nrwl/nx#1 createProjectGraphAsync
nrwl/nx#2 runOne
nrwl/nx#3 <anonymous>
nrwl/nx#4 <anonymous>
nrwl/nx#5 handleErrors
nrwl/nx#6 handler
```
Bun (1.3.5):
```
nrwl/nx#0 buildProjectGraphAndSourceMapsWithoutDaemon <- This entry causes Nx to detect a loop
nrwl/nx#1 createProjectGraphAndSourceMapsAsync
nrwl/nx#2 createProjectGraphAndSourceMapsAsync
nrwl/nx#3 createProjectGraphAsync
nrwl/nx#4 createProjectGraphAsync
nrwl/nx#5 runOne
nrwl/nx#6 runOne
```
This is not a Nx bug per-se, but wondering if this falls into the
efforts of supporting Bun into Nx (i.e.
https://nx.dev/blog/nx-19-5-adds-stackblitz-new-features-and-more#bun-and-pnpm-v9-support)?
I will cross post the above into the Bun repo too for input.
### Expected Behavior
Able to execute Nx commands with Bun
### GitHub Repo
_No response_
### Steps to Reproduce
1. Run bunx --bun nx run api:build
### Nx Report
```shell
NX_DAEMON=true bunx --bun nx --disableNxCache --disableRemoteCache --outputStyle dynamic-legacy report 1 ✘ 16:18:00
NX Report complete - copy this into the issue template
Node : 24.3.0
OS : darwin-arm64
Native Target : aarch64-macos
pnpm : 9.6.0
nx : 22.1.3
@nx/js : 22.1.3
@nx/jest : 22.1.3
@nx/eslint : 22.1.3
@nx/workspace : 22.1.3
@nx/cypress : 22.1.3
@nx/devkit : 22.1.3
@nx/esbuild : 22.1.3
@nx/eslint-plugin : 22.1.3
@nx/module-federation : 22.1.3
@nx/nest : 22.1.3
@nx/next : 22.1.3
@nx/node : 22.1.3
@nx/playwright : 22.1.3
@nx/plugin : 22.1.3
@nx/react : 22.1.3
@nx/rollup : 22.1.3
@nx/storybook : 22.1.3
@nx/vite : 22.1.3
@nx/vitest : 22.1.3
@nx/web : 22.1.3
@nx/webpack : 22.1.3
@nx/docker : 22.1.3
nx-cloud : 19.1.0
@nrwl/nx-cloud : 19.1.0
typescript : 5.7.3
---------------------------------------
Registered Plugins:
@nxlv/python
---------------------------------------
Community plu...
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixesnrwl/nx#33997
<!-- START COPILOT CODING AGENT TIPS -->
---
✨ Let Copilot coding agent [set things up for you](https://github.com/nrwl/nx/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.
---------
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: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: Copilot <Copilot@users.noreply.github.com>
## Current Behavior
Frequent write cache calls during tasks hashing results in delays
## Expected Behavior
Cache is validated but only written when changed
## AI Summary
This pull request introduces an optimization to the project graph cache
writing logic, reducing unnecessary disk writes when serving repeated
requests with unchanged graphs. The main change is the addition of a
mechanism to track the cache file's modification time and only write to
disk if the file has been externally modified or not written yet by the
current process.
Optimizations to cache writing:
* Added `writeCacheIfStale` function in `nx-deps-cache.ts` to prevent
redundant cache writes by checking the cache file's modification time
before writing. This function is now used in the daemon's graph
recomputation logic, replacing the previous unconditional write.
[[1]](diffhunk://#diff-82bd1a5a7b7320ffc3233470f191782c054bf69a696dc16001d2c4b1d0b04963R285-R312)
[[2]](diffhunk://#diff-d5bf3c66e62cac1884a071bf07fd1991320a3e62b07bfc03af3b9557b714c892L17-R17)
[[3]](diffhunk://#diff-d5bf3c66e62cac1884a071bf07fd1991320a3e62b07bfc03af3b9557b714c892L136-R149)
* Introduced `lastWrittenCacheMtimeMs` variable to track the last
successful write's modification time, updated after each cache write.
[[1]](diffhunk://#diff-82bd1a5a7b7320ffc3233470f191782c054bf69a696dc16001d2c4b1d0b04963R202-R208)
[[2]](diffhunk://#diff-82bd1a5a7b7320ffc3233470f191782c054bf69a696dc16001d2c4b1d0b04963R256-R261)
Codebase updates:
* Updated imports in `nx-deps-cache.ts` to include `statSync` for file
modification time checks.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
`{projectRoot}` and `{projectName}` tokens inside `{workspaceRoot}/...`
input patterns were silently never substituted in the native Rust
hasher, causing the glob to match zero files and those inputs to be
entirely excluded from the cache hash — a silent cache correctness bug.
## Root Cause
In `gather_self_inputs` (`hash_planner.rs`), workspace filesets (those
starting with `{workspaceRoot}/`) were forwarded as-is to
`HashInstruction::WorkspaceFileSet`. When `globs_from_workspace_globs`
later stripped `{workspaceRoot}/`, the remaining `{projectRoot}/**/*.go`
was matched literally — which never exists on disk.
## Changes
- **`hash_planner.rs`**: Before storing workspace filesets in
`HashInstruction::WorkspaceFileSet`, replace `{projectRoot}` and
`{projectName}` with their actual values from the project graph node.
- **`planner.spec.ts`**: Added a test verifying that a pattern like
`{workspaceRoot}/{projectRoot}/**/*.go` correctly resolves to
`{workspaceRoot}/libs/parent/**/*.go` in the hash plan.
```json
// nx.json — this pattern now works correctly
"namedInputs": {
"goSource": ["{workspaceRoot}/{projectRoot}/**/*.go"]
}
```
This pattern is documented as valid per the [Nx inputs
reference](https://nx.dev/docs/reference/inputs): `{projectRoot}` and
`{projectName}` can appear anywhere after the leading `{workspaceRoot}`.
> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `repo.gradle.org`
> - Triggering command: `/usr/lib/jvm/temurin-17-jdk-amd64/bin/java
/usr/lib/jvm/temurin-17-jdk-amd64/bin/java
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
--add-opens=java.prefs/java.util.prefs=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
--add-opens=java.prefs/java.util.prefs=ALL-UNNAMED
--add-opens=java.base/java.nio.charset=ALL-UNNAMED
--add-opens=java.base/java.net=ALL-UNNAMED
--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED
--add-opens=java.xml/javax.xml.namespace=ALL-UNNAMED
-XX:MaxMetaspaceSize=384m -XX:+HeapDumpOnOutOfMemoryError -Xms256m
-Xmx512m -Dfile.encoding=UTF-8 -Duser.country -Duser.language=en` (dns
block)
> - `staging.nx.app`
> - Triggering command:
`/home/REDACTED/work/_temp/ghcca-node/node/bin/node node
./bin/post-install` (dns block)
> - Triggering command: `/opt/hostedtoolcache/node/24.13.1/x64/bin/node
/opt/hostedtoolcache/node/24.13.1/x64/bin/node
/home/REDACTED/work/nx/nx/node_modules/.pnpm/nx@22.6.0-beta.5_@swc-node+register@1.11.1_@swc+core@1.15.10_@swc+helpers@0.5.18__@swc+_a827ebc424be037fc154d90301143d4e/node_modules/nx/src/project-graph/plugins/isolation/plugin-worker
/tmp/plugin4368-16-420.890618.sock @nx/enterprise-cloud` (dns block)
> - Triggering command: `/opt/hostedtoolcache/node/24.13.1/x64/bin/node
/opt/hostedtoolcache/node/24.13.1/x64/bin/node
/home/REDACTED/work/nx/nx/node_modules/.pnpm/nx@22.6.0-beta.5_@swc-node+register@1.11.1_@swc+core@1.15.10_@swc+helpers@0.5.18__@swc+_a827ebc424be037fc154d90301143d4e/node_modules/nx/src/project-graph/plugins/isolation/plugin-worker
/tmp/plugin5770-16-420.874901.sock @nx/enterprise-cloud` (dns block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/nrwl/nx/settings/copilot/coding_agent)
(admins only)
>
> </details>
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
>
> ----
>
> *This section details on the original issue you should resolve*
>
> <issue_title>{projectRoot} not interpolated inside {workspaceRoot}
input patterns in native hasher</issue_title>
> <issue_description>## Current Behavior
>
> When using a `{workspaceRoot}/{projectRoot}/**/*.go` pattern in target
inputs, the `{projectRoot}` token is **not interpolated** by the native
Rust hash planner. The pattern silently matches zero files, causing the
cache hash to exclude those files entirely.
>
> ```json
> // nx.json
> "namedInputs": {
> "gosourceUnfiltered": ["{workspaceRoot}/{projectRoot}/**/*.go"]
> }
> ```
>
> ```json
> // target config
> "format": {
> "inputs": ["gosourceUnfiltered", { "externalDependencies": [] }]
> }
> ```
>
> The hash plan for this target contains **zero `.go` file inputs**:
>
> ```
> Task: my-project:format:write
> Inputs:
> my-project:ProjectConfiguration
> my-project:TsConfig
> env:NX_CLOUD_ENCRYPTION_KEY
> file:nx.json
> file:.gitignore
> // no .go files!
> ```
>
> ## Root Cause
>
> In the Rust hash planner (`hash_planner.rs`), fileset inputs are
partitioned based on their prefix:
>
> ```rust
> .partition(|file_set| {
> file_set.starts_with("{projectRoot}/") ||
file_set.starts_with("!{projectRoot}/")
> });
> ```
>
> A pattern starting with `{workspaceRoot}/` is classified as a
**workspace fileset**. It then flows to `hash_workspace_files.rs` where
`{workspaceRoot}/` is stripped via `strip_prefix("{workspaceRoot}/")`,
leaving `{projectRoot}/**/*.go`. But `{projectRoot}` is **never
substituted** with the actual project root, so the glob literally tries
to match paths starting with `{projectRoot}/` — which don't exist.
>
> ## Expected Behavior
>
> Per the [Nx docs on inputs](https://nx.dev/docs/reference/inputs):
>
> > `{workspaceRoot}` should only appear in the beginning of an input
but **`{projectRoot}` and `{projectName}` can be specified later in the
input to interpolate the root or name of the project** into the input
location.
>
> The native hasher should interpolate `{projectRoot}` (and
`{projectName}`) within `{workspaceRoot}` patterns before glob matching.
After stripping `{workspaceRoot}/` and interpolating `{projectRoot}`,
the pattern should become e.g. `packages/shared/go/middleware/**/*.go`
and correctly match files.
>
> ## Impact
>
> This is a **silent cache correctness issue**: targets using this
pattern appear to work but their cache hash doesn't include the matched
files. Changes to those files won't invalidate the cache. There's no
warning or error emitted.
>
> ## Workaround
>
> Use the literal path instead of `{projectRoot}` inside workspace-level
patterns:
>
> ```js
> // In a createNodes plugin, instead of:
> inputs: ["{workspaceRoot}/{projectRoot}/**/*.go"]
> // Use:
> inputs: [`{workspaceRoot}/${actualProjectRoot}/**/*.go`]
> ```
>
> ## Related
>
> - nrwl/nx#34225 — Nested Project Files Excluded from Parent Project
Inputs (the reason `{workspaceRoot}/{projectRoot}` patterns are used in
the first place: to bypass project file filtering and include files from
nested sub-projects)
>
> ## Environment
>
> - **Nx version**: 22.6.0-beta.3
> - **OS**: macOS (Darwin 24.6.0)
> - **Package manager**: pnpm</issue_description>
>
> ## Comments on the Issue (you are @copilot in this section)
>
> <comments>
> </comments>
>
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixesnrwl/nx#34595
<!-- START COPILOT CODING AGENT TIPS -->
---
🔒 GitHub Advanced Security automatically protects Copilot coding agent
pull requests. You can protect all pull requests by enabling Advanced
Security for your repositories. [Learn more about Advanced
Security.](https://gh.io/cca-advanced-security)
---------
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: Jason Jean <jasonjean1993@gmail.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: Copilot <Copilot@users.noreply.github.com>
`nx show target inputs` returns empty results for any target with
`defaultConfiguration` set because
`HashPlanInspector.inspectTaskInputs()` keys results as
`project:target:config` (e.g. `my-app:build:local`), but the lookup was
using `project:target` — a guaranteed miss.
## Changes
- **`packages/nx/src/command-line/show/target.ts`**: In
`resolveInputFiles`, construct the plan lookup key using the explicitly
passed `configuration` first, then fall back to the target's
`defaultConfiguration`:
```ts
const targetConfig = graph.nodes[projectName]?.data?.targets?.[targetName];
const effectiveConfig = configuration ?? targetConfig?.defaultConfiguration;
const taskId = effectiveConfig
? `${projectName}:${targetName}:${effectiveConfig}`
: `${projectName}:${targetName}`;
```
If the computed `taskId` is not found in the hash plan, an error is
thrown instead of silently returning empty results.
- **`showTargetInputsHandler`**: Now extracts the configuration from the
target string (`project:target:config`) or the `-c`/`--configuration`
flag and forwards it to `resolveInputFiles`.
- **`ShowTargetInputsOptions`** (`command-object.ts`): Added
`configuration?: string` so the type correctly reflects the prop.
- **`packages/nx/src/command-line/show/target.spec.ts`**: Added tests
covering:
- Resolving input files when `defaultConfiguration` is set
- Preferring an explicit `configuration` over `defaultConfiguration`
- Throwing when the task ID is not found in the hash plan
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
>
> ----
>
> *This section details on the original issue you should resolve*
>
> <issue_title>nx show target inputs returns empty when target has
defaultConfiguration</issue_title>
> <issue_description>## Current Behavior
>
> `nx show target inputs <project>:<target> --json` returns no files for
any target that has a `defaultConfiguration` set (directly or via
`targetDefaults`).
>
> ```bash
> $ nx show target inputs card-api-lambda:build --json
> {
> "project": "card-api-lambda",
> "target": "build"
> }
> # Expected: files array with resolved input files
> ```
>
> The `--check` flag also reports files as not being inputs when they
should be:
>
> ```bash
> $ nx show target inputs card-api-lambda:build --check
packages/card/api/lambda/main.go
> ✗ packages/card/api/lambda/main.go is not an input for
card-api-lambda:build
> ```
>
> Targets **without** `defaultConfiguration` on the same project resolve
correctly:
>
> ```bash
> $ nx show target inputs card-api-lambda:generate-docs --json
> {
> "project": "card-api-lambda",
> "target": "generate-docs",
> "files": [ ".gitignore", "nx.json",
"packages/card/api/lambda/main.go", ... ]
> }
> ```
>
> ## Root Cause
>
> In `packages/nx/src/command-line/show/target.ts`, the
`resolveInputFiles` function constructs the lookup key as:
>
> ```js
> const taskId = `${projectName}:${targetName}`;
> ```
>
> But the native `HashPlanInspector.inspectTaskInputs()` returns results
keyed by the **full task ID including the default configuration**, e.g.
`card-api-lambda:build:local`.
>
> When a target has `defaultConfiguration: "local"`, the plan result is
keyed as `project:target:local`, but the lookup searches for
`project:target` — which doesn't exist — so it falls through to the
empty default `{ files: [], ... }`.
>
> Verified by calling `inspectTaskInputs` directly:
>
> ```
> === build ===
> Task IDs in result: card-api-lambda:generate-docs,
card-api-lambda:build:local
> # lookup for "card-api-lambda:build" → miss → empty
>
> === generate-docs ===
> Task IDs in result: card-api-lambda:generate-docs
> # lookup for "card-api-lambda:generate-docs" → hit → 142 files
> ```
>
> ## Suggested Fix
>
> The lookup key should account for the default configuration:
>
> ```js
> const defaultConfig =
graph.nodes[projectName]?.data?.targets?.[targetName]?.defaultConfiguration;
> const taskId = defaultConfig
> ? `${projectName}:${targetName}:${defaultConfig}`
> : `${projectName}:${targetName}`;
> ```
>
> ## Expected Behavior
>
> `nx show target inputs` should resolve and display input files
regardless of whether the target has a `defaultConfiguration`.
>
> ## Environment
>
> - **Nx version**: 22.6.0-beta.3
> - **OS**: macOS (Darwin 24.6.0)
> - **Node**: v22
> - **Package manager**: pnpm</issue_description>
>
> ## Comments on the Issue (you are @copilot in this section)
>
> <comments>
> </comments>
>
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixesnrwl/nx#34594
<!-- START COPILOT CODING AGENT TIPS -->
---
💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).
---------
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: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: Copilot <Copilot@users.noreply.github.com>
## Current Behavior
napi-derive v2 has a bug where the type definition temp file doesn't get
cleaned between builds with different profiles (release vs dev), causing
duplicate class declarations in `index.d.ts`.
## Expected Behavior
With napi-rs v3, the temp file handling is fixed — each crate gets its
own folder, preventing duplicate declarations across build profiles.
## Related Issue(s)
N/A — internal build tooling improvement.
## Changes
### Dependency upgrades
- **Cargo.toml**: `napi` 2.x → 3.8.3, `napi-derive` 2.x → 3.5.2,
`napi-build` 1.x → 2.3.1
- **package.json**: `@napi-rs/cli` → 3.5.1 (was 3.0.0-alpha.56)
### External data sharing (Arc pattern)
- Replaced raw-pointer `StoredExternal<T>` wrapper with napi-recommended
`Arc<T>` pattern for shared ownership
- Read-only data: `External::new(Arc::new(data))` at creation, `Arc<T>`
in struct fields
- Mutable data (DB): `External::new(Arc::new(Mutex::new(data)))`,
`Arc<Mutex<T>>` in struct fields
- Constructors take `&External<Arc<T>>`, clone the Arc;
`#[napi(ts_arg_type)]` preserves TS types
- Deleted `types/external_compat.rs` (86 lines of unsafe code removed)
### API migrations
- `ThreadsafeFunction`: Accept directly as napi parameters instead of
building from `Function<'_>`
- Return-only structs: Use `#[napi(object, object_from_js = false)]` for
structs with `External<T>` fields
- `env.create_object()` → `Object::new(&env)`, `JsObject` → `PromiseRaw`
for async returns
### Watcher fix
- Deferred `Watchexec::default()` creation from constructor to `watch()`
inside napi's `spawn()` block
- `Watchexec::default()` internally calls `tokio::spawn()` which
requires a Tokio runtime in TLS — the `#[napi(constructor)]` runs on the
JS main thread with no runtime context
- Changed `napi::tokio::spawn(...)` to `spawn(...)` (napi's
`bindgen_prelude::spawn` which uses a static runtime, works from any
thread)
- Store `watch_exec` as `Arc<Mutex<Option<Arc<Watchexec>>>>` for lazy
initialization
### TUI fix
- Same Tokio runtime issue: `tokio::spawn(async {})` placeholder in
`Tui::new()` panicked on JS main thread
- Changed `task` field from `JoinHandle<()>` to
`Option<JoinHandle<()>>`, initialized as `None`
- Moved `tui.start()` (which calls `tokio::spawn`) inside napi's
`spawn()` async block
- Changed `napi::tokio::spawn(...)` to `spawn(...)` in lifecycle.rs
### Platform-specific watcher improvements
- Gated non-macOS watcher functions with `#[cfg(not(target_os =
"macos"))]`
- Added `rlib` to `crate-type` in Cargo.toml for cargo test
compatibility
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
Several Rust native modules and one TS utility have unnecessary
allocations:
- `hash_planner.rs`: visited set uses `HashSet<String>` with
`to_string()` per insert; unnecessary `.collect::<Vec<_>>()` creating
temp vecs; no pre-allocation for dependency inputs
- `context.rs`: `update_files` allocates a String per map entry inside
retain loop
- `hash_workspace_files.rs`: `.clone()` before `.as_bytes()` — 2
needless heap allocs per file
- `find_matching_projects.rs`: collects HashMap keys into Vec + linear
search instead of O(1) lookup
- `validate_outputs.rs`: compiles regex on every call
- `project-configuration-utils.ts`: uses `JSON.parse(JSON.stringify())`
for deep cloning
## Expected Behavior
All unnecessary allocations removed:
- Borrow `&str` from project graph instead of cloning Strings into
visited set
- Use `Path::new()` once outside retain closure (also more correct —
component-boundary-aware matching)
- Call `.as_bytes()` directly without clone
- Use `HashMap::get_key_value()` for O(1) lookup
- Cache compiled regex with `LazyLock<Regex>`
- Use `structuredClone` (Node 18+) instead of JSON round-trip
See individual commits for detailed rationale per change.
## Current Behavior
When using `@nx/angular-rspack` with SCSS imports that reference
external assets (e.g., `flag-icon-css`), the `postcss-cli-resources`
plugin generates absolute filesystem paths in CSS `url()` values:
```css
.flag-icon-us{background-image:url(/some/project/path/dist/bug-demo/browser/media/us.2d0a1dd6.svg)}
```
This happens because `normalizeOutputPath()` makes `outputPath.media`
absolute, and this absolute path is passed directly as
`resourcesOutputPath` to `postcss-cli-resources`.
## Expected Behavior
CSS `url()` values contain relative paths:
```css
.flag-icon-us{background-image:url(media/us.2d0a1dd6.svg)}
```
## Related Issue(s)
Fixes#34092
## Current Behavior
CSS `url()` references to fonts and assets fail to resolve on Windows
with webpack, rspack, and angular-rspack builds. The
`postcss-cli-resources` plugin passes Windows absolute paths (e.g.
`E:\dev\project\font.woff2`) to `new URL(path, 'file:///')`, which
misinterprets the drive letter (`E:`) as a URL protocol, stripping it
from `pathname` and producing unresolvable paths like
`\dev\project\font.woff2`.
## Expected Behavior
CSS `url()` references to fonts, images, and other assets resolve
correctly on all platforms.
## Related Issue(s)
Fixes#33052
## Current Behavior
The Netlify edge function reads `NEXT_PUBLIC_FRAMER_REWRITES` env var to
determine which paths to proxy to Framer. As more pages move to Framer,
this growing allowlist becomes cumbersome to maintain.
## Expected Behavior
The edge function now proxies all requests to Framer by default. Only
paths explicitly listed in `nextjsPaths` and `excludedPath` are served
by Next.js. This removes the need for the `NEXT_PUBLIC_FRAMER_REWRITES`
env var (should be removed from Netlify dashboard manually).
Deleted 25 page files (pages router + app router) that are now served by
Framer: homepage, 404, enterprise/*, contact/*, solutions/*, community,
company, customers, nx-cloud, partners, brands, careers, java, react,
remote-cache, resources, webinar.
**Next.js paths kept:** `/blog/*`, `/courses/*`, `/pricing`, `/podcast`,
`/ai-chat`, `/changelog`, `/resources-library`, `/whitepaper-fast-ci`,
`/500`, `/api/*`, `/docs/*`
**Manual follow-up:** Remove `NEXT_PUBLIC_FRAMER_REWRITES` env var from
Netlify dashboard.
## Related Issue(s)
Closes DOC-431
## Current Behavior
10 `nx.dev` URLs return 404 — broken links in CLI output, graph UI, and
cloud UI. Additionally:
- ~20 wildcard redirect rules silently broken because `:slug*` was never
converted to Netlify's `*`/`:splat` syntax
- Specific `/getting-started/` rules ordered after the wildcard
catch-all, sending users to the generic intro page instead of the
correct page (e.g., `/getting-started/editor-setup`)
- Redirect chain breaks where intermediate targets (e.g.,
`/nx-api/powerpack-*-cache` → `/nx-api/*-cache`) had no rule
## Expected Behavior
All documented `nx.dev` URLs resolve correctly. `_redirects` is the sole
source of truth — no more JS generator pipeline.
### Changes
1. **Fixed `_redirects`**:
- Converted all `:slug*` to `*`/`:splat` (Netlify syntax)
- Reordered specific `/getting-started/` rules before the wildcard
catch-all
- Added 13 new redirect rules for broken 404 URLs
2. **Fixed `astro-docs/netlify.toml`**:
- Added 3 redirects for `/docs/` path typos (trailing-s, wrong path)
3. **Deleted legacy files** (-2,248 lines):
- `redirect-rules.js`
- `redirect-rules-docs-to-astro.js`
- `redirect-rules.spec.js`
- `scripts/generate-netlify-redirects.mjs`
## Related Issue(s)
Fixes DOC-428
## Current Behavior
When running Gradle batch tasks (e.g. `nx build my-gradle-project
--no-tui`), the terminal shows no task output. The Gradle build output
is captured into `ByteArrayOutputStream` for JSON results but never
forwarded to the terminal. Users can only see the output in Nx Cloud
task results.
## Expected Behavior
Gradle batch task output (build logs, test results, etc.) should be
visible in the terminal in real-time as the batch executes.
## Changes
- **`TeeOutputStream.kt`** (new): An `OutputStream` that writes to two
destinations simultaneously — captures output for JSON results while
also forwarding to `System.err` for terminal display.
- **`GradleRunner.kt`**: Wrap `setStandardOutput` and `setStandardError`
with `TeeOutputStream` to tee into `System.err` in both
`runBuildLauncher` and `runTestLauncher`.
- **`gradle-batch.impl.ts`**: Replace `PseudoTerminal` (which swallowed
all output with `quiet: true`) with `execSync` using `stdio: ['pipe',
'pipe', 'inherit']` so stderr flows directly to the terminal.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
The `build-base` target in `nx.json` has a manual `inputs` override of
`["production", "^production"]` which shadows the more accurate inputs
inferred by the `@nx/js/typescript` plugin.
## Expected Behavior
Let the `@nx/js/typescript` plugin infer the correct inputs for
`build-base` tasks, providing more accurate cache invalidation based on
the actual tsconfig project references.
## Lint Rule Changes
Removing the broad `inputs` override causes the `@nx/dependency-checks`
lint rule to flag a few dependencies as "unused" across three packages.
This happens because the rule uses the build target's inputs to
determine which files to scan for imports, and the narrower tsc-inferred
inputs don't cover certain files:
- **`packages/nx`**: `@napi-rs/wasm-runtime` — used in
`nx.wasi-browser.js`, a `.js` file outside tsconfig scope
- **`packages/angular`**: `@angular-devkit/core` — only referenced as a
string (for `ensurePackage()`, migrations config) and is a
`peerDependency`, not directly imported at runtime
- **`packages/angular-rspack-compiler`**: `semver` — used in
`patch/patch-angular-build.js`, a `.js` patch file outside tsconfig
scope
These are all legitimate dependencies. Adding the `.js` files to
tsconfig would require `allowJs` and pull non-TS scripts into the build
pipeline unnecessarily. Adding them to `ignoredDependencies` in each
package's `.eslintrc.json` is the correct fix.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
The vitest executor ignores `reporter`/`reporters` set in target options
(project.json). Both forms leak through as passthrough CLI args, causing
vitest's `resolveConfig` to override the `reporters` array (which
includes NxReporter), making the executor hang indefinitely.
Additionally, when the vite config uses `reporter` (singular), the
config value always overrides the `reporters` array passed as inline
options — again removing NxReporter.
## Expected Behavior
Target-level `reporter`/`reporters` options take priority over
config-file values. NxReporter is always preserved in the final
reporters array regardless of configuration source.
Priority chain: target `reporter` (singular) > target `reporters`
(plural) > config `reporter` (singular) > config `reporters` (plural).
## Related Issue(s)
Fixes#34495
This PR updates the guide for integrated -> TS solution setup with
additional information. It also adds `% llm_copy_prompt %}` and `{%
llm_only %}` components to provide instructions to the AI agent to
perform the migration.
Preview:
https://deploy-preview-34646--nx-docs.netlify.app/docs/technologies/typescript/guides/switch-to-workspaces-project-references
## Current Behavior
The migration guide covers basic steps but is missing several nuances
discovered in the ocean repo's convert-ts-solution generator and through
end-to-end validation against a real workspace.
## Expected Behavior
The guide covers all steps needed for a successful migration, including:
- .gitignore updates for out-tsc/dist/test-output artifacts
- Order of operations (install before removing paths)
- Root tsconfig cleanup (remove paths entirely, baseUrl, rootDir)
- Package.json self-export and nested path alias strategies
- Update build targets (remove @nx/js:tsc for non-buildable libs)
- Import path updates after package renaming
- Bundler config updates (webpack auto-detect, Jest resolver,
Vite/Vitest)
- Edge cases: circular deps, e2e projects, non-standard locations,
tsconfig include/exclude patterns
Validated end-to-end against a test workspace with 5 libs and 2 React
apps (webpack+jest, vite+vitest) across 2 clean iterations.
## Screenshots
<img width="810" height="458" alt="Screenshot 2026-02-27 at 7 31 16 AM"
src="https://github.com/user-attachments/assets/80ad87db-6ebe-4f54-9995-65f2ee2c46f1"
/>
<img width="784" height="735" alt="Screenshot 2026-02-27 at 7 31 20 AM"
src="https://github.com/user-attachments/assets/a0a7fe26-7d7d-4225-94bb-f82e5fc49178"
/>
<img width="897" height="161" alt="Screenshot 2026-02-27 at 7 31 39 AM"
src="https://github.com/user-attachments/assets/3b410092-5fb4-420d-9909-ee83c34ff0d6"
/>
## Related Issue(s)
NXC-2950
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
## Current Behavior
Searching for CLI commands like `nx watch` on the docs site does not
surface the [Nx Commands reference
page](https://nx.dev/docs/reference/nx-commands) on the first page of
results, even when filtering by "References."
Root causes:
- **Term saturation**: "nx" appears 150+ times on the CLI reference page
(in every heading, usage block, and example), causing it to saturate and
contribute almost nothing to ranking differentiation.
- **Page length penalty**: The current `pageLength: 0.5` setting
actively penalizes long pages—the CLI reference is one of the longest on
the site.
- **No weight boost**: The CLI page had no `weight` set, while
generators/executors pages already get `weight: 2.0`.
## Expected Behavior
Searching for `nx watch`, `nx run-many`, or other CLI commands should
surface the Nx Commands reference page prominently in results.
This PR applies two quick-win tuning changes:
1. **`weight: 4`** on the CLI commands page entry — gives body text ~16×
impact (quadratic scaling), making it competitive with shorter pages
that mention commands incidentally. include the command name in the sub
headers for more improvement in relevancy search without impacting other
pages
2. **`termSaturation: 1.2`** (down from default 1.4) — makes highly
repeated terms like "nx" saturate faster so that the differentiating
term (e.g. "watch") carries more relative weight.
## Related Issue(s)
Addresses
[DOC-401](https://linear.app/nxdev/issue/DOC-401/investigate-boosting-cli-command-reference-pages-in-search)
## Current Behavior
When running `pnpm nx-release --local false`, the `nx release version`
step modifies `package.json` files for `angular-rspack`,
`angular-rspack-compiler`, `dotnet`, and `maven` (bumping versions and
resolving `workspace:*` protocols). The local release path (`--local
false`, non-CI) exits early before reaching the reset logic that the CI
path uses, leaving unstaged changes behind.
## Expected Behavior
After the release script completes (or is interrupted), the source
`package.json` files should be restored to their original state. The
version bumps are only needed in `dist/` for publishing — the source
files should stay at `0.0.1` with `workspace:*` protocols.
This PR:
- Extracts a shared `resetPackageJsons()` function used by both the
local and CI code paths
- Wraps the local release steps in `try/finally` so files are restored
even on errors
- Adds a `SIGINT` handler so files are restored on ctrl+C
Add sandboxing-config.yaml to exclude node_modules from reads in Nx
Cloud workflow sandboxing.
## Current Behavior
No sandboxing configuration exists for Nx Cloud workflows.
## Expected Behavior
Nx Cloud workflows use sandboxing config that excludes `node_modules/**`
from reads.
## Related Issue(s)
N/A
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
## Current Behavior
In `internalCreateNodesV2`, the ESLint plugin creates a **new `ESLint`
instance per child project root** to check whether the project has
non-ignored lintable files via `isPathIgnored`:
```ts
const eslint = new ESLint({
cwd: join(context.workspaceRoot, projectRoot),
});
for (const file of lintableFilesPerProjectRoot.get(projectRoot) ?? []) {
if (!(await eslint.isPathIgnored(...))) { ... }
}
```
In large monorepos, this means hundreds or thousands of ESLint
instantiations during graph calculation. Each instantiation involves
loading and resolving the ESLint configuration, which is the dominant
cost in the plugin.
## Expected Behavior
A single ESLint instance should be shared across all child projects
under the same ESLint config directory. Since `isPathIgnored` receives
**absolute paths**, the instance's `cwd` does not affect the result — it
only needs to resolve the correct config, which is the same for all
children of a given config root.
## Changes
* In `internalCreateNodesV2`: create one lazily-initialized shared
ESLint instance per config directory (the `configDir`), reused across
all child project roots
* Projects that have their own `.eslintignore` file fall back to a
per-project ESLint instance, preserving correct behavior for ESLint v8
(which resolves `.eslintignore` relative to `cwd`)
## Benchmark
Measured on a real monorepo with **1,609 projects** and a single root
ESLint config.
| | Before | After | Change |
|---|---|---|---|
| Cold run (avg of 3) | **~18,700ms** | **~7,300ms** | **-61%** |
| ESLint instances created | 1,457 | 8 | **-99.5%** |
### Breakdown
| Version | Run 1 | Run 2 | Run 3 |
|---|---|---|---|
| Baseline (upstream) | 23,482ms | 16,238ms | 16,389ms |
| With shared instance | 7,473ms | 7,012ms | 7,515ms |
### Methodology
* `NX_DAEMON=false` to prevent daemon caching
* ESLint plugin hash cache cleared between each cold run
* `performance.now()` instrumentation around `internalCreateNodesV2`
* Each measurement repeated 3 times
* **Verified same number of lint targets** (1,473) before and after — no
behavior change
### Why `configDir === '.'` is not excluded
The earlier revision excluded root-level ESLint configs (`configDir ===
'.'`) from the optimization as a conservative safety measure. However,
benchmarking showed this **completely disables the optimization** for
the most common monorepo setup (single root ESLint config), dropping
1,456 of 1,457 instances back to per-project instantiation with zero
measurable improvement.
The per-project `.eslintignore` check already handles the ESLint v8
concern: projects with their own `.eslintignore` still get dedicated
instances, while all others share one instance whose `cwd` correctly
resolves the root config and root `.eslintignore`.
## Related
This is the `createNodesV2` (Nx plugin) code path. The change does not
affect ESLint execution itself — only the graph calculation phase.
When latest >= next, the build-migrations script now preserves the
existing x-prompt and requires fields from the pre-release entry, rather
than skipping them, and adjusts the requires upper bound to the stable
version.
## Current Behavior
The Maven batch runner uses a parallel thread pool to execute tasks.
When multiple tasks are independent roots in the task graph, they get
picked up by separate threads simultaneously. Both threads call
`invoke()` on the same shared `CachingResidentMavenInvoker` (Maven 4) or
`CachingMaven3Invoker` (Maven 3) instance concurrently.
Maven 4's `LookupInvoker.invoke()` is not thread-safe — it
snapshots/restores `System.getProperties()` and the thread context
classloader in a `finally` block, and all concurrent invocations share
the same resident `MavenContext`. This can cause deadlocks in Maven's
internal session machinery.
## Expected Behavior
Maven executions through the shared invoker are serialized via
`@Synchronized`, preventing concurrent access to non-thread-safe Maven
internals. The parallel thread pool still handles task graph management,
build state recording, and result emission concurrently.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Current Behavior
When running `nx release` with Git remotes configured as **canonical SSH
URLs**, the repository slug is extracted incorrectly.
Examples of affected remotes include:
- `ssh://git@ssh.github.com:443/org/repo.git`
- `ssh://git@gitlab.company.com:2222/group/subgroup/repo.git`
In these cases, the existing regex-based logic misinterprets the port
number as part of the repository path (for example: `443/org`). This
causes the release creation request to be built with an invalid
repository slug and results in `404 Not Found` errors from the GitHub or
GitLab APIs.
## Expected Behavior
`nx release` should correctly extract the repository slug from all valid
Git remote URL formats, regardless of whether the remote uses HTTPS,
SCP-style SSH, or fully qualified SSH URLs with explicit ports.
Valid examples should resolve to the correct slug:
- `ssh://git@ssh.github.com:443/org/repo.git` => `org/repo`
- `ssh://git@gitlab.company.com:2222/group/subgroup/repo.git`=>
`group/subgroup/repo`
Users should not need to modify their Git remote configuration in order
for `nx release` to work correctly.
## What’s Changed
- Introduced a shared utility
([`extractRepoSlug`](https://github.com/nrwl/nx/pull/31684/changes#diff-799188c178b8a084e86dbe9063ec88a7c324212a8ef79729777f56e4bf7f455cR29-R60))
to consistently extract repository slugs from Git remote URLs.
- Replaced provider-specific, regex-based parsing in:
- `GithubRemoteReleaseClient`
- `GitLabRemoteReleaseClient`
- Added support for:
- HTTPS remotes
- SCP-style SSH remotes (`git@host:org/repo.git`)
- Fully qualified SSH URLs with custom ports
- Arbitrarily nested GitLab group and subgroup paths
- Self-hosted GitHub and GitLab instances via hostname matching
- Added comprehensive unit tests covering valid and invalid URL formats
for both providers.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes#31682
## Current Behavior
The Nx native module (Rust cdylib loaded by Node.js) uses the system
allocator. The daemon process retains a large RSS footprint after the
initial project graph build, even though most of that memory is no
longer in use. On macOS and Linux, the system allocator doesn't
aggressively return freed pages to the OS.
## Expected Behavior
The daemon's steady-state RSS drops significantly after graph build by
using jemalloc with tuned page purge timers. Peak RSS and wall time are
unaffected.
## Changes
Adds [tikv-jemallocator](https://github.com/tikv/jemallocator) as the
global allocator on Linux and macOS, with two compile-time settings:
- **`dirty_decay_ms:1000`** — returns freed pages to the OS after 1s
instead of the default 10s. Tuned to Nx's phase-separated workload
(graph build → idle → task execution), where transitions happen every
~30-60s. Benchmarked against 5s and 10s — both too slow to purge between
phases.
- **`muzzy_decay_ms:0`** — skips the lazy purge phase (`MADV_FREE`) and
goes straight to `MADV_DONTNEED`. Required on macOS and Linux ≥ 4.5
where `MADV_FREE` doesn't actually reduce RSS.
Windows and WASI continue using the system allocator. Windows is
excluded because `tikv-jemalloc-sys` fails to build with MSVC (spaces in
`cl.exe` path break the autoconf configure script). Tracked upstream in
[tikv/jemallocator#99](https://github.com/tikv/jemallocator/pull/99).
### Other Settings Considered
Tested narenas reduction, tcache_max, extent fit tuning, background
threads, and decay timer values. Only the decay timer configuration
improved steady-state RSS without wall time regression.
closed#32190
## Current Behavior
eslint crashes when tsconfig.base.json path includes a * and you have an
import going to that project
## Expected Behavior
The plugin shouldn't crash and it should auto fix to a working import
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
https://github.com/nrwl/nx/issues/32190Fixes#32190
## Current Behavior
The `replace-removed-matcher-aliases` migration uses `tsquery.replace()`
which reprints the entire AST through TypeScript's Printer. This causes
two problems:
1. **Syntax corruption**: Valid TypeScript files are mangled:
- Destructuring patterns: `{ result }` becomes `{result}:`
- Arrow functions: missing opening braces
- Nested callbacks: collapsed/merged code blocks
2. **Unnecessary file changes**: Every test file is written back to disk
even when no matchers are replaced. This triggers `formatFiles()` to
reformat unchanged files, creating large whitespace-only diffs. In large
codebases, this can result in hundreds or thousands of files being
modified unnecessarily, making the migration PR difficult to review.
**Why I care a Lot**
I was running this on a multi-million-LOC monorepo and ran into two
issues:
* I got ~10k modified files with whitespace-only changes from the
removal of newlines. These changes couldn't be fixed with Prettier
because it didn't care about the number of newlines, so the diff was
unmergeable.
* I got ~8 files with malformed Typescript, causing commit hooks, CI,
etc. to fail without manual intervention.
## Expected Behavior
The migration should:
1. Only replace the deprecated matcher names (e.g., `toBeCalled` →
`toHaveBeenCalled`)
2. Preserve all surrounding code exactly as written
5. Only touch files that actually contain deprecated matchers
## Solution
Replace AST-reprinting with surgical text replacement:
- Use `tsquery.query()` to find matching AST nodes
- Collect text positions (start/end) for each node to replace
- Apply replacements in reverse order using string slicing
- Only write files that actually changed
This pattern is already used successfully in other Nx migrations (e.g.,
`rename-cy-exec-code-property.ts` in the Cypress package).
**Additional improvements:**
- Single AST parse with regex selector vs. 11 separate passes
- Quick string check skips parsing files without deprecated matchers
- New regression test covers complex patterns that triggered corruption
## Related Issue(s)
Fixes#32062
---------
Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
## Current Behavior
E2E tests may timeout without clear error messages, making it difficult
to diagnose test failures.
## Expected Behavior
E2E test utilities should provide better timeout handling and logging to
help diagnose test failures.
## Changes
- Add timeout handling to e2e test utilities (`runCLI` and
`runLernaCLI`)
- Add command logging to track execution time
- Improve timeout error messages with process output
- Bump cache bust value
## Related Issue(s)
CI stability and debugging improvements
<!-- 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
`@nx/eslint` relies on ESLint internals that changed in ESLint v10
(`use-at-your-own-risk`), which causes failures.
It looks like https://github.com/nrwl/nx/pull/24632 originally attempted
to use `loadESLint()` which would've been forward compatible with v10,
but it was later removed in https://github.com/nrwl/nx/pull/27404 in
favor of the `use-at-your-own-risk` import.
## Expected Behavior
`@nx/eslint` supports ESLint v10.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes#34415
<!-- 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 -->
If you specify both the `all` and `initialRun` options when running `nx
watch`, `initialRun` have no effect.
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
The command should be called once at the beginning even if there are no
file changes.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes#32281
## Current Behavior
nx-maven-plugin 0.0.12 is also changing the base directory along with
the pom file when the plugins like flatten-maven-plugin /
maven-shade-plugin produces pomFile in different directory other than
the base directory
## Expected Behavior
plugin should only update the pom file and not the base directory
location
## Related Issue(s)
Fixes#34181https://github.com/mojohaus/flatten-maven-plugin/issues/50
Co-authored-by: anurag.ag <anuragagarwal561994@users.noreply.github.com>
## Current Behavior
Technology introduction pages have inconsistent or missing version
requirements information. Some pages have no Requirements section,
others use ad-hoc formats (asides, bullet lists under Prerequisites),
and page titles follow different naming conventions ("Overview of the Nx
X Plugin", "Nx X Plugin Overview", "Introduction - X", etc.).
## Expected Behavior
Every technology introduction page now has a standardized Requirements
section with:
- A version support table using consistent semver range format
- Code-formatted package names in the `Package` column
- An intro sentence identifying the Nx plugin (e.g. "The `@nx/react`
plugin supports the following package versions.")
- A note linking to [code generation docs](/docs/features/generate-code)
for auto-installed packages
- Consistent **"X Plugin for Nx"** page title pattern across all intro
pages
Additional changes:
- Deleted the standalone Node.js/TypeScript compatibility page, inlining
its content into the respective plugin intro pages
- Created a proper introduction page for Angular Rsbuild (previously
linked directly to `createConfig` API reference)
- Added Requirements tables to Java, Gradle, and Maven pages with system
dependency versions
- Updated sidebar links and redirects for removed/moved pages
- Applied style guide fixes across all edited pages (removed "allows you
to", "easily", product possessives, etc.)
## Related Issue(s)
Fixes DOC-423
## Current Behavior
After #34580, `determineNxCloudV2()` returns `'skip'` in non-interactive
mode, but the caller remaps it to `nxCloud = 'yes'` with
`skipCloudConnect = true`. This causes `setupCI()` to run and generate
`.github/workflows/ci.yml` in new workspaces — which didn't happen
before.
This breaks the `extras.test.ts` e2e snapshot test because
`.github/workflows/ci.yml` now appears in the expanded default task
inputs.
## Expected Behavior
Keep `nxCloud = 'skip'` when the cloud choice is `'skip'`, which
prevents CI file generation in non-interactive mode. This restores the
behavior prior to #34580.
## Related Issue(s)
Fixes the `extras.test.ts` e2e snapshot failure introduced by #34580.
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Bump project graph plugin version.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Current Behavior
`build_glob_set` recompiles identical glob pattern sets on every call,
even when the same set of patterns has been compiled before.
## Expected Behavior
Compiled `NxGlobSet` instances are cached in a static `DashMap` keyed by
sorted glob strings. Repeated calls with the same patterns return a
shared `Arc<NxGlobSet>` instead of recompiling. Profiling `nx run-many
-t build lint test --parallel 8` in the Nx repo measured 95.6% cache hit
rate (9,758 of 10,202 calls) with only 444 unique pattern sets, reducing
hashing-phase CPU by ~40%.
## Current Behavior
All 1,200+ redirect rules are processed by the Next.js serverless
function via the `redirects()` config in `next.config.js`. Every
redirect request requires a cold start of the serverless function, which
contributed to the 10-minute outage reported in DOC-415.
## Expected Behavior
Redirects are handled at the Netlify CDN edge via a plain `_redirects`
file, which is faster and doesn't depend on the Next.js serverless
function being healthy.
- Converted all redirect rules from `redirect-rules.js` and
`redirect-rules-docs-to-astro.js` into Netlify `_redirects` format
(1,231 rules)
- Expanded Next.js regex group patterns (e.g. `/(l|latest)/...`) into
individual Netlify rules since Netlify doesn't support regex
- Converted `:path*` wildcards to Netlify `*`/`:splat` syntax
- Rewrites (Astro docs proxy) remain in `next.config.js` as they require
server-side processing
- Original JS redirect files kept for reference (can be removed in
follow-up)
## Related Issue(s)
Fixes DOC-415
## Current Behavior
`TargetProjectLocator.findProjectFromImport` stores `null` for
unresolved imports using a bare `importExpr` key, but
`findNpmProjectFromImport` looks up cache entries using
`${packageName}__${dirPath}`. The key mismatch means repeated lookups
for the same import+directory re-run the full resolution waterfall
(typescript + require.resolve) instead of returning the cached `null`.
## Expected Behavior
Store `null` for unresolved imports using the same
`${packageName}__${dirPath}` key that `findNpmProjectFromImport` uses
for lookups. Repeated lookups for already-failed imports skip the
expensive resolution steps.
Also removes an unused cache write for builtin module imports as a minor
cleanup.
## Current Behavior
After migrating from chalk to picocolors (#34305), `FORCE_COLOR=0` no
longer disables colors. picocolors checks `!!env.FORCE_COLOR`, and since
`!!"0"` is `true` in JavaScript, it treats `FORCE_COLOR=0` as "enable
colors."
This breaks CI environments and tools like Homebrew that set
`FORCE_COLOR=0` to get plain text output.
## Expected Behavior
`FORCE_COLOR=0` should disable ANSI color output, matching the previous
chalk behavior and the [FORCE_COLOR spec](https://force-color.org/).
## Related Issue(s)
Fixes#34387
Upstream issue filed:
https://github.com/alexeyraspopov/picocolors/issues/100
## Current Behavior
When processing Gradle tasks, Nx tracks dependent task output files by
recording individual file paths for each output. This can lead to
incorrect cache invalidation behavior since we are prefixing the paths
unnecessarily. We will therefore never match.
## Expected Behavior
Nx now consolidates dependent task output files using glob patterns
based on file extensions (e.g., **/*.jar, **/*.class). This focuses on
the types of files produced rather than their specific paths. The
approach groups all output files by extension and generates a single
glob pattern per extension, reducing the complexity of input tracking
while maintaining correctness.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #Q-247
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
The dependsOn of atomized tasks should match the base non-atomized task.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes Q-174
## Current Behavior
The CNW cloud prompt is locked to auto-select deferred connection
(CLOUD-4255), always generating a short URL but never writing nxCloudId
to nx.json. Users have no explicit choice.
## Expected Behavior
The cloud prompt now offers three explicit choices:
- Yes: connect now, generate nxCloudId in nx.json, show strong
completion message
- Skip for now: deferred connection (no nxCloudId), still show short URL
and update README
- No: full opt-out, set neverConnectToCloud: true in nx.json, no URL, no
README update, no cloud messaging
**CLI args:** `--nxCloud=skip`, `--nxCloud=never` (new),
`--nxCloud=yes`. Non-interactive defaults to skip.
Closes CLOUD-4242
## Current Behavior
PR #34523 fixed the macOS file watcher issue (#34522) but did not
include comprehensive test coverage.
## Expected Behavior
Tests should verify that the fix works correctly and catch any future
regressions.
## Related Issue(s)
Adds test coverage for #34523 and #34522
---
## Changes
**TypeScript integration test**
(`packages/nx/src/native/tests/watcher.spec.ts`):
Added **"should detect file changes in large directory structures"** - a
comprehensive integration test that:
1. Creates 10,000+ directories simulating a monorepo-scale workspace
2. Starts a real `Watcher` instance
3. Creates and modifies files deep in the directory tree
4. Verifies that file change events are actually delivered
This test validates the actual behavior users care about - that file
watching works reliably in large repos - rather than testing
implementation details. It would catch any regression where events fail
to be delivered at scale.
## Testing
TypeScript integration test validates the actual bug fix - that file
events are delivered reliably in large directory structures with 10,000+
directories.
<!-- 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
Given the following Vite config:
```ts
import { defineConfig } from 'vite';
export default defineConfig((config) => {
console.log(config);
return {};
});
```
`npx nx preview` logs:
```ts
{
mode: 'development',
command: 'build',
isSsrBuild: false,
isPreview: false
}
```
## Expected Behavior
`npx nx preview` should log:
```ts
{
mode: 'development',
command: 'build',
isSsrBuild: false,
isPreview: true
}
```
## Related Issue(s)
https://github.com/vitejs/vite/issues/15694
Fixes #
## Current Behavior
`BatchProcess` accumulates all stdout/stderr output in
`terminalOutputChunks` and exposes it via `getTerminalOutput()`, but
nothing ever calls `getTerminalOutput()`. The accumulated strings are
unique allocations (created via `chunk.toString()`), not shared with the
output callbacks or `process.stdout.write`.
For verbose batched tasks (e.g., Maven/Gradle with hundreds of tasks),
this can hold tens to hundreds of MB for the entire batch duration.
## Expected Behavior
Remove the dead accumulation code. stdout/stderr chunks are still
forwarded to `process.stdout`/`process.stderr` and output callbacks as
before — only the unused storage is removed.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
When generating a library with vitest as the test runner and a non-vite
bundler (e.g. `tsc`), the js library generator creates two config files:
- `vitest.config.mts` (from the vitest `configurationGenerator`) with
`root: __dirname`
- `vite.config.ts` (from a second `createOrEditViteConfig` call) with
`root: import.meta.dirname`
The redundant `vite.config.ts` uses ESM-only `import.meta.dirname`
syntax, which causes TS1470 when the project targets CommonJS output:
```
vite.config.ts:5:9 - error TS1470: The 'import.meta' meta-property is not allowed in files which will build into CommonJS output.
5 root: import.meta.dirname,
~~~~~~~~~~~
```
## Expected Behavior
Only `vitest.config.mts` should be generated. The vitest
`configurationGenerator` already handles creating the correct config
file with `root: __dirname`. The second `createOrEditViteConfig` call
from `@nx/vite` was redundant and produced the conflicting file.
## Related Issue(s)
Fixes#34399
## Current Behavior
Since Nx 22.5.0, the daemon's native file watcher silently drops all
file change events on macOS in large monorepos (~5,250+ watched
directories). `nx watch`, `nx serve`, and any daemon-dependent file
watching is broken.
The root cause is that #34329 switched all watched paths to
`WatchedPath::non_recursive()`. On macOS, the `notify` crate uses
**kqueue** for non-recursive watches instead of **FSEvents**. kqueue
silently fails at scale due to vnode table pressure (`kern.num_vnodes ==
kern.maxvnodes`), causing the daemon to never detect file changes.
This is a **scale-dependent** bug: it works fine in small workspaces
(~30 directories) but breaks silently in large ones.
| | **Nx 22.4.5** | **Nx 22.5.0+** |
|---|---|---|
| **Small repo (~30 dirs)** | Works (FSEvents) | Works (~30 kqueue
watches) |
| **Large repo (~5,250+ dirs)** | Works (FSEvents) | **Broken** (kqueue
silently drops all events) |
## Expected Behavior
The macOS file watcher should detect file creates, modifications, and
deletions at any scale, matching the behavior of Nx 22.4.x.
## Fix
Use platform-conditional watch modes:
- **macOS:** Single recursive watch on the workspace root (uses FSEvents
natively)
- **Linux/Windows:** Non-recursive per-directory watches (preserves the
#33781 inotify fix)
On macOS, FSEvents handles recursive watching from a single root path,
so directory enumeration and dynamic registration are skipped entirely.
This also improves daemon startup time on macOS from ~10 minutes to <1
second in a 354-project monorepo.
### What changed in `watcher.rs`
1. **Initial pathset:** On macOS, watch only the root directory
recursively via FSEvents instead of enumerating all directories for
non-recursive kqueue watches.
2. **Dynamic directory registration (`on_action`):** Wrapped in
`#[cfg(not(target_os = "macos"))]` since FSEvents already watches the
full tree.
Linux/Windows behavior is completely unchanged.
### Why the event filter is fine as-is
We verified that with recursive FSEvents watches, macOS emits specific
`FileEventKind` variants (`Create(File)`, `Modify(Data(Content))`,
`Remove(File)`, `Modify(Name(Any))`) that the current
`watch_filterer.rs` already handles correctly. Zero events were rejected
by the catch-all. The `Modify(Any)` / `Create(Any)` variants are kqueue
artifacts that are not needed with FSEvents.
### Why kqueue fails silently
Apple's [File System Events Programming
Guide](https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/FSEvents_ProgGuide/KernelQueues/KernelQueues.html)
explicitly recommends FSEvents over kqueue for large hierarchies: *"If
you are monitoring a large hierarchy of content, you should use file
system events instead."* kqueue requires `open(path, O_EVTONLY)` per
watched directory. Under vnode table pressure, the kernel recycles
vnodes with kqueue watches attached without notifying the watcher. There
is no error, no partial delivery, and no diagnostic signal.
## Tested on
- macOS 26.3 (Tahoe), Apple Silicon (arm64), APFS
- 354-project pnpm monorepo (~19,865 non-ignored directories)
- Verified: file modifications, file creates, and file deletes all
detected
- Daemon init time: ~10 min (with enumeration) -> <1s (with root-only
FSEvents watch)
## Related Issue(s)
Fixes#34522
Co-authored-by: Amp <amp@ampcode.com>
## Current Behavior
When nx release runs with docker-configured projects (either via
explicit config or
@nx/docker plugin inference), git tags are created with the literal
string {version}
instead of the actual version number (e.g., v{version} instead of
v1.0.6, or
app-3@{version} instead of app-3@1.0.0).
This happens because:
1. If ANY project in a release group has docker config,
preferDockerVersion is auto-set to
true for the ENTIRE group
2. createGitTagValues() then blindly selects
projectVersionData.dockerVersion, which is
null for non-docker projects (or projects with no changes)
3. The interpolate() function receives null for {version} and returns
the literal
placeholder unchanged
Commit messages are unaffected because createCommitMessageValues() only
uses newVersion and
already guards against null. The changelog code (changelog.ts:1117-1121)
also already has
the correct null-safe pattern.
## Expected Behavior
When preferDockerVersion is true but dockerVersion is null, git tags
should fall back to
using newVersion instead of producing literal {version} placeholders.
When both versions
are null, no tag should be created.
For mixed release groups (some projects have docker config, some don't),
the auto-enable
logic should use 'both' mode instead of true, which already has proper
null-safe checks for
each version type.
## Changes
- shared.ts: Added null-safe fallback (??) in createGitTagValues() for
both independent and
fixed group code paths, plus a guard to skip tag creation when both
versions are null
- config.ts: Refined auto-enable logic to check whether ALL or only SOME
projects have
docker config — mixed groups now get 'both' mode instead of true
- shared.spec.ts: Added 5 test cases covering null version fallback
scenarios for fixed
groups, independent groups, both-null, reverse fallback, and mixed
groups
## Related Issue(s)
Fixes#34382Fixes#33890Fixes#34391
## Current Behavior
The "7. Thriving Community" section on
`nx.dev/docs/guides/adopting-nx/from-turborepo` displayed an outdated Nx
download statistic (~5 million downloads per week).
## Expected Behavior
The "7. Thriving Community" section on
`nx.dev/docs/guides/adopting-nx/from-turborepo` now reflects the latest
Nx download statistic (~9 million downloads per week).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: MaxKless <MaxKless@users.noreply.github.com>
When we optimized the `process.env` values to not embed the full object
unnecessarily, we also regressed in cases where users do use
`process.env` instead of `process.env["NX_PUBLIC_FOO"]`.
## Current behavior
Users cannot use `process.env` and must access each key individuall.
Although the serializing the full object can bloat bundle sizes, we also
don't want to break existing apps unnecessarily.
## Expected behavior
Existing apps should continue to work as usual.
## Related issues
Fixes#34279
## Current Behavior
When running `@nx/js:prune-lockfile` on a monorepo with transitive
dependencies that have multiple versions where neither version is
reachable from a direct dependency in package.json, the executor throws:
```
NX An error occurred while creating pruned lockfile
Original error: Cannot read properties of undefined (reading 'name')
TypeError: Cannot read properties of undefined (reading 'name')
at switchNodeToHoisted (node_modules/nx/src/plugins/js/lock-file/project-graph-pruning.js:165:31)
```
## Expected Behavior
The lockfile pruning should complete without crashing, even when some
transitive dependencies cannot be traced back to a direct dependency.
## Root Cause
In `rehoistNodes()`, when there are multiple nested nodes for a package,
the code finds the "closest" node by computing `pathLengthToIncoming()`
for each. However, when none of the nested nodes have a path to any
direct dependency in package.json, `pathLengthToIncoming()` returns
`undefined` for all of them. Since `undefined < Infinity` is `false` in
JavaScript, `closest` remains `undefined`, and then
`switchNodeToHoisted(undefined, ...)` crashes.
## Fix
Add a guard to only call `switchNodeToHoisted()` when a closest node was
actually found:
```typescript
if (closest) {
switchNodeToHoisted(closest, builder, invBuilder);
}
```
This allows the pruning to continue - the nested nodes simply won't be
rehoisted if no closest node can be determined.
## Related Issue
Fixes#34322
## Test Added
Added a unit test that verifies `rehoistNodes()` doesn't crash when
nested nodes have no path to package.json dependencies.
## Current Behavior
When importing a `package.json` file in an Angular application built
with `@nx/angular-rspack`, the build fails with a Babel syntax error if
the `package.json` contains `@angular/*` dependencies:
```
SyntaxError: /path/to/package.json: Missing semicolon. (2:10)
1 | {
> 2 | "name": "@org/app",
| ^
3 | "version": "4.0.2",
4 | "dependencies": {
5 | "@angular/platform-browser": "20.3.7",
```
This happens because the `JS_ALL_EXT_REGEX` pattern
`/\.[cm]?(js)[^x]?\??/` incorrectly matches `.json` files. When the JSON
file content contains `@angular` strings, the
`angular-partial-transform-loader` attempts to process it through Babel,
which fails because JSON is not valid JavaScript.
**Root cause:** The regex `[^x]?` (optional character that is NOT 'x')
allows `.json` to match because 'o' is not 'x'.
## Expected Behavior
- `.json` files should NOT match `JS_ALL_EXT_REGEX` or
`TS_ALL_EXT_REGEX`
- Importing `package.json` in Angular applications should work correctly
- All existing matches for `.js`, `.jsx`, `.mjs`, `.cjs` (and TypeScript
equivalents) should continue to work
## Related Issue(s)
https://github.com/nrwl/nx/issues/32649
## Current Behavior
Documentation bugfix on
https://nx.dev/docs/concepts/typescript-project-linking#set-up-typescript-project-references
In the section about setting up TypeScript project references, the
documentation currently states:
"Each project's tsconfig.lib.json file extends the project's
tsconfig.json file and adds references to the tsconfig.lib.json files of
project dependencies."
## Expected Behavior
In a standard Nx workspace configuration, tsconfig.lib.json extends the
workspace-level tsconfig.base.json, not the project-level tsconfig.json
(and the example provided just after is correct).
Suggested correction:
"Each project's tsconfig.lib.json file extends the workspace
tsconfig.base.json file and adds references to the tsconfig.lib.json
files of project dependencies."
## Related Issue(s)
Fixes #34118
---------
Co-authored-by: Aude Planchamp <aude.planchamp@ekino.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
When a plugin worker process exits unexpectedly, the exit handler
previously sent synthetic `loadResult` messages to all pending response
handlers. If any handler was waiting for a different result type (e.g.
`createNodesResult`), the type validation would reject with a confusing
"Expected createNodesResult, got loadResult" error instead of surfacing
the actual cause.
Split response handlers into `onMessage` / `onError` callbacks so the
exit handler can reject each pending promise directly with a clear
"Plugin worker exited unexpectedly" error.
Also use unique transaction IDs for `load` messages (via `generateTxId`)
to avoid potential handler overwrites during worker restarts.
Fixes#34564
## Summary
Testing CI behavior with continuous assignment disabled and cache bust
set to 4.
This is part of investigating flakiness potentially related to
continuous assignment in CI.
## Test plan
- Monitor CI execution behavior
- Compare with other test branches (bust=2, bust=3)
## Current Behavior
Documentation pages across Getting Started, How Nx Works, and Platform
Features sections contain:
1. Duplicated content — mental-model.mdoc has a ~70-line caching section
and a ~20-line DTE section that are near-verbatim
copies of how-caching-works.mdoc and distribute-task-execution.mdoc
respectively. remote-cache.mdoc re-explains local caching
in its intro instead of linking to the canonical page.
2. Missing cross-reference links — Key concepts like "affected command",
"remote cache", "project graph", and "task pipeline
configuration" are mentioned without linking to their dedicated pages.
3. Style guide violations — Trust-undermining words ("simply", "just",
"straightforward"), anti-AI phrases ("Let's take",
"Whether you're..."), product possessives ("Nx's"), customer perspective
issues ("allows you to"), and em dashes appear
across Getting Started and How Nx Works pages.
## Expected Behavior
1. Content consolidation — mental-model.mdoc is trimmed by ~85 lines,
keeping the concept + images and linking to dedicated
pages for details. remote-cache.mdoc intro references the canonical
caching page. publish-conformance-rules-to-nx-cloud.mdoc
deduplicates its intro. maintain-typescript-monorepos.mdoc shortens its
inferred tasks re-explanation.
2. Cross-reference links added — First-mention links for affected,
remote cache, computation caching, task pipeline
configuration (in mental-model) and project graph (in self-healing-ci).
3. Style guide compliance — 18 fixes across 10 Getting Started and How
Nx Works pages, removing banned phrases and aligning
with the new STYLE_GUIDE.md.
4. Sidebar improvements — Cache Task Results added after Run Tasks in
Platform Features; Maintain TypeScript Monorepos moved
to first in KB > TypeScript.
## Pages changed
```
┌─────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Section │ Pages │
├─────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Getting Started │ intro, index, nx-cloud, ai-setup, start-with-existing-project │
├─────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ How Nx Works │ mental-model, how-caching-works, task-pipeline-configuration, nx-plugins, nx-daemon │
├─────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Features │ remote-cache, self-healing-ci, maintain-typescript-monorepos, cache-task-results (sidebar only) │
├─────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Enterprise │ publish-conformance-rules-to-nx-cloud │
└─────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────┘
```
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
### 1. Continuous tasks missing from `postTasksExecution` hook
When running continuous tasks (e.g., `nx serve app`) and stopping them
with Ctrl+C, the `postTasksExecution` lifecycle hook does not include
them in `taskResults`. This breaks plugins that rely on post-run
statistics (e.g., uploading task stats to DataDog).
### 2. Confusing TUI status when sibling continuous task exits
When multiple continuous tasks run together and one exits unexpectedly,
its sibling is marked as "failed" even though it was intentionally
terminated/stopped by the task orchestrator.
## Expected Behavior
1. All tasks, including continuous ones, are included in `taskResults`
for the `postTasksExecution` hook
2. Continuous tasks that are intentionally stopped (because dependent
tasks completed or during graceful shutdown) report as `success` with
`Stopped` display status
3. Continuous tasks that exit unexpectedly (crash) report as `failure`
4. TUI summary shows correct status: success when all tasks completed
successfully, square icon for stopped tasks
## Related Issue(s)
Fixes https://github.com/nrwl/nx/issues/33561
Supersedes:
- https://github.com/nrwl/nx/pull/33562
- https://github.com/nrwl/nx/pull/34132
Add NX_CLOUD_IO_TRACING_DIRECTORY environment variable.
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes Q-245
## Current Behavior
No documentation exists explaining the concept of synthetic monorepos —
how they bridge polyrepo and monorepo setups by connecting separate
repositories into a unified dependency graph.
https://deploy-preview-34565--nx-docs.netlify.app/docs/concepts/synthetic-monorepos
## Expected Behavior
New concept page under "How Nx Works" that explains:
- What synthetic monorepos are (unified graph across separate repos
without moving code)
- Why they matter for humans (visibility, cross-repo coordination) and
AI agents (seeing beyond repo boundaries)
- What they provide (cross-repo graph, actionable tooling, AI agent
enablement)
- How they serve as a gradual entry point toward deeper monorepo
adoption
## Related Issue(s)
N/A — new documentation page based on existing content from webinars and
internal knowledge.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: juristr <juristr@users.noreply.github.com>
## Current Behavior
When users hit the `generatePackageJson: true` error with TS Solution
Setup, the error tells them to "unset the option" but gives no guidance
on the replacement workflow.
## Expected Behavior
The error message now includes a link to the pruning guide at
https://nx.dev/docs/technologies/node/guides/deploying-node-projects so
users can immediately find the migration steps.
## Related Issue(s)
Related #30146
When plugin isolation is off, concurrent createNodesV2 invocations share
the same module instance. The module-level mutable `cache` variable
caused
invocation A's `finally` block to null it out while invocation B was
still
reading from it, resulting in "Cannot read properties of null (reading
'configContexts')".
Replace the shared mutable `cache` with a Symbol-keyed Map so each
invocation gets its own isolated cache. The tsconfig disk cache is
shared
across invocations with an idempotent initialization guard.
CLOSES NXC-3971
## Summary
- Updated version of the classic "Misconceptions about Monorepos"
article
- New sections on AI compatibility, scaling strategies (affected,
caching, distribution, atomization), and `@nx/owners`
- Custom SVG diagrams for project graph illustrations (replacing old
Medium images)
- Authors: Victor Savkin, Juri Strumpflohner
## Test plan
- [ ] Verify blog post renders correctly on preview
- [ ] Check all images load (SVGs + avif)
- [ ] Verify internal doc links resolve
- [ ] Check TOC renders properly
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: juristr <juristr@users.noreply.github.com>
## Current Behavior
folks had to type in `nx-cloud apply-locally`
## Expected Behavior
now `nx apply-locally` works
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
### Current Behavior
nx list <plugin> shows generator/executor names and descriptions in text
format only. It does not show where the plugin or its
generators/executors are located on disk, and there is no
machine-readable output option.
### Expected Behavior
- nx list --json outputs all local and installed plugins with their
paths and capability types
- nx list <plugin> --json outputs detailed structured JSON including
resolved paths to each generator/executor implementation and schema
- nx list <plugin> (text mode) now also shows the plugin's root path
## Current Behavior
In TS Solution Setup, the esbuild executor forces `runTypeCheck` even
when `skipTypeCheck: true` and `declaration: false`, due to the `||
options.isTsSolutionSetup` condition in `esbuild.impl.ts` (lines 139-140
and 195).
When `declaration: false`, this type check runs in `noEmit` mode with
`ignoreDiagnostics: true` — making it **completely pointless** (no
declarations emitted, no diagnostics reported). Its only observable
effect is writing a poisoned 19-byte tsbuildinfo file that causes race
conditions with `tsc --build`.
## Expected Behavior
When `skipTypeCheck: true` and `declaration: false`, the esbuild
executor should not run type checking at all. The `isTsSolutionSetup`
override should only force type checking when declarations actually need
to be generated.
## Fix
### Primary: Skip unnecessary type check (`esbuild.impl.ts`)
```diff
// Non-watch mode (line 195)
- if (!options.skipTypeCheck || options.isTsSolutionSetup) {
+ if (!options.skipTypeCheck || (options.isTsSolutionSetup && options.declaration)) {
// Watch mode (lines 139-140)
- options.isTsSolutionSetup
+ (options.isTsSolutionSetup && options.declaration)
```
Only force type checking in TS Solution Setup when declarations need to
be generated. This eliminates the pointless type check entirely.
### Defense-in-depth: Prevent tsbuildinfo in `noEmit` mode
(`run-type-check.ts`)
```diff
- : { noEmit: true };
+ : { noEmit: true, composite: false };
```
Setting `composite: false` alongside `noEmit: true` prevents TypeScript
from writing tsbuildinfo files, protecting against this class of bug
from any caller of `runTypeCheck`.
## Why This is Safe
| Scenario | Before | After |
|----------|--------|-------|
| `skipTypeCheck: false`, `declaration: false`, `isTsSolutionSetup:
true` | Runs type check (noEmit) | Still runs (`!false \|\| ...` = true)
|
| `skipTypeCheck: false`, `declaration: true`, `isTsSolutionSetup: true`
| Runs type check (emitDeclarationOnly) | Still runs |
| `skipTypeCheck: true`, `declaration: true`, `isTsSolutionSetup: true`
| normalize.ts overrides skipTypeCheck to false; runs type check | Still
runs (same normalization) |
| **`skipTypeCheck: true`, `declaration: false`, `isTsSolutionSetup:
true`** | **Runs pointless type check (noEmit + ignoreDiagnostics),
writes poisoned tsbuildinfo** | **Skipped entirely** |
The only behavior change is in the last row — the case where the type
check was doing nothing useful but causing harm.
## Related Issue(s)
Fixes#34492
---------
Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
## Current Behavior
Users migrating to Nx 20's TS Solution Setup lose `generatePackageJson`
support and have no documentation on the replacement prune workflow
(`prune-lockfile`, `copy-workspace-modules`). The error message tells
them to "unset the option" but doesn't explain what to do instead.
## Expected Behavior
A dedicated guide at
`/docs/technologies/node/guides/deploying-node-projects` covers the full
prune workflow: when to use pruning vs bundling, target configuration,
Dockerfile setup, and step-by-step migration from `generatePackageJson`.
Also updated the existing bundling guide to match the same structure
(intro table, cross-links, style guide compliance). The two articles are
sister guides covering the two ways to deploy Node.js apps: bundle
everything into a single file, or prune dependencies for a
`node_modules`-based install.
Cross-links added from the bundling guide and ci-deployment guide.
## Related Issue(s)
Closes#30146
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
## Current Behavior
The `checkAllBranchesWhen` option is documented as type `string` with a
minimal description, which does not match the actual implementation.
## Expected Behavior
Document the correct type (`boolean | string[]`) and explain the default
branch resolution behavior, the three value modes (true, false,
string[]), and when this option is useful.
## Related Issue(s)
Closes DOC-414
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
## Current Behavior
When multiple Nx processes (task hasher, daemon, workers) access the
SQLite database concurrently, the `NxDbConnection::transaction()` method
only retries the BEGIN step using the `retry_db_operation_when_busy!`
macro. Operations executed inside the transaction and the COMMIT are not
retried, so if the database is busy during those steps, the task crashes
with:
```
Error: DB transaction operation error: SqliteFailure(Error { code: DatabaseBusy, extended_code: 5 }, Some("database is locked"))
```
This is particularly common during parallel task hashing with continuous
tasks, where `TaskDetails.recordTaskDetails()` and `RunningTasksService`
compete for write access.
## Expected Behavior
The entire transaction (begin, execute, commit) is retried as a single
unit when any step encounters a `DatabaseBusy` error. If the database is
busy during the operation or commit, the transaction is automatically
rolled back (via drop) and retried with the same exponential backoff
used everywhere else.
## Related Issue(s)
<!-- No public issue linked -->
## Current Behavior
The NPM audit CI job is failing due to a critical XSS vulnerability
(CVE-2026-25896) in `fast-xml-parser` version 4.5.3.
## Expected Behavior
The NPM audit should pass with no critical vulnerabilities.
## Related Issue(s)
Fixes the failing NPM audit CI run:
https://github.com/nrwl/nx/actions/runs/22288455713
---
This PR upgrades `fast-xml-parser` from `^4.2.7` to `^5.3.7` to address
GHSA-m7jm-9gc2-mpf2, a critical XSS vulnerability that allows entity
encoding bypass via regex injection in DOCTYPE entity names.
The package is only used in
`scripts/documentation/internal-link-checker.ts` for parsing XML
sitemaps, so the risk of this upgrade is low.
## Current Behavior
Batch mode is binary:
- `--batch` flag → batch ALL executors that support it
- No flag → batch NOTHING
This means users of gradle/maven must always remember to pass `--batch`
to get the performance benefits.
## Expected Behavior
Plugin authors can now set `preferBatch: true` in their executor config
to indicate batch mode should be used by default. Users can still
opt-out with `--no-batch`.
Three states:
- `--batch` → batch everything
- `--no-batch` → batch nothing
- (not specified) → use each executor's `preferBatch` preference
| `--batch` flag | `preferBatch` | Result |
|----------------|---------------|--------|
| `true` | any | Batch |
| `false` | any | No batch |
| not set | `true` | Batch |
| not set | `false`/undefined | No batch |
## Changes
- Added `preferBatch?: boolean` to `ExecutorJsonEntryConfig` and
`ExecutorConfig` interfaces
- Updated `--batch` default from `false` to `undefined` to allow
`preferBatch` to decide
- Modified batch scheduling logic to respect `preferBatch`
- Enabled `preferBatch: true` for gradle and maven executors
- Added 5 unit tests covering all `preferBatch` scenarios
## Related Issue(s)
<!-- Link any related issues here -->
PR #30826 introduced a fallback definition for `process.env`:
```ts
{ 'process.env': '{}' }
```
Since `DefinePlugin` performs raw textual replacement, this can generate
invalid JavaScript when user code accesses environment variables via dot
notation:
```ts
process.env.SOME_KEY
```
becomes:
```js
{}.SOME_KEY
```
`{}` is parsed as a block statement (not an object literal), resulting
in:
> Unexpected token: punc (.)
This PR updates the fallback to a parenthesized object literal:
```ts
{ 'process.env': '({})' }
```
which produces valid output:
```js
({}).SOME_KEY
```
This preserves the intended bundle-size optimization while ensuring
syntactically correct output for standard `process.env.X` access
patterns.
## Related Issue(s)
Refs #30826Fixes#34460
//CC @Coly010 @coolassassin
## Current Behavior
`nx import` relies on interactive prompts (enquirer) and spinners (ora)
for user interaction. AI agents cannot parse this output or respond to
prompts, making `nx import` unusable in agent workflows.
## Expected Behavior
When `isAiAgent()` is true, `nx import` now:
- Skips all interactive prompts and spinners
- Emits NDJSON progress to stdout (`starting`, `cloning`, `filtering`,
`merging`, `detecting-plugins`, `complete`)
- Returns structured `needs_input` when required args are missing (all
at once to minimize round-trips)
- Returns structured `needs_input` for plugin selection when `--plugins`
flag is not provided
- Returns structured success/error results with hints and next steps
- Supports new `--plugins` flag (`skip`/`all`/comma-separated list)
Shared AI output types extracted from `init` into
`packages/nx/src/command-line/ai/ai-output.ts` for reuse across
commands.
## Current Behavior
Running `configure-ai-agents` overwrites the entire `source` object in
`extraKnownMarketplaces['nx-claude-plugins']`, removing any user-added
properties like `ref`.
## Expected Behavior
User-added source properties (e.g. `ref`) are preserved, while `source`
and `repo` are always set to the correct values.
The nx-welcome component inline styles were always using CSS/SCSS syntax
(with braces and semicolons) regardless of the selected style option.
When --style=sass is chosen, the component now correctly uses SASS
indented syntax (no braces or semicolons) matching the expected
behavior for the .sass file format.
Fixes#33489
## Current Behavior
When `configure-ai-agents` is invoked from within an AI agent (e.g.
Claude Code), it either shows an interactive multi-select prompt (which
the agent can't interact with) or requires `--agents` and
`--no-interactive` flags to work correctly. This makes the experience
awkward when AI agents call the command as part of workspace setup.
## Expected Behavior
When an AI agent is detected (via environment variables like
`CLAUDECODE`), the command now:
1. **Auto-configures the detected agent** if it's not yet configured,
partially configured, or outdated — no prompts needed
2. **Auto-updates any other outdated agents** alongside the detected one
3. **Reports non-configured agents** with a suggested `nx
configure-ai-agents --agents ...` command
4. **Reports up-to-date status** if the detected agent is already fully
configured
When `--agents` is explicitly passed, detection is ignored entirely
(existing behavior preserved). `--check` mode also works with detection
— it checks the detected agent plus all other configured agents.
Additionally:
- Strips AI agent detection env vars (`CLAUDECODE`, `CLAUDE_CODE`,
`OPENCODE`, `GEMINI_CLI`, etc.) from e2e subprocess environments to
prevent the test runner's environment from leaking into tests
- Fixes e2e tests to use `AGENTS.md` (not `GEMINI.md`) for gemini
assertions, matching what the gemini generator actually creates for
fresh installations
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
- Cypress `start-dev-server.ts` file creates a port lock file next to
the source code
- Native temp DB files created by tests are not properly ignored
- Astro config timestamp file is not ignored
These all trigger watch file change events, which cause the project
graph to be recomputed unnecessarily.
## Expected Behavior
Output files should not trigger watch file change events. The project
graph should not be recomputed unnecessarily.
## Current Behavior
Several Nx packages directly depend on a minimatch version with a
high-severity vulnerability
(https://github.com/advisories/GHSA-3ppc-4f35-3m26).
## Expected Behavior
Several Nx packages should depend directly on a minimatch version that
does not include the reported high-severity vulnerability.
Note: unsafe `minimatch` versions can still be pulled in transitively.
Upstream deps need to be updated, and then we need to update the Nx
packages to newer versions.
## Related Issue(s)
Fixes#34507
---------
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
## Current Behavior
Users specifying directory paths in `inputs` without a trailing slash or
glob pattern find that files are not matched. For example,
`{projectRoot}/src` does not match any files, while `outputs` allows
naked directory paths without issue.
## Expected Behavior
Documentation clearly explains that directory paths in `inputs` require
a trailing slash or glob pattern:
```jsonc
{
"inputs": [
"{projectRoot}/src/", // ✓ Works (trailing slash)
"{projectRoot}/src/**/*", // ✓ Works (glob pattern)
"{projectRoot}/src" // ✗ Does NOT work
]
}
```
### Changes
- **Reference doc** (`reference/inputs.mdoc`): Added "Directory Paths"
section explaining the requirement with examples
- **Guide** (`configure-inputs.mdoc`): Added callout warning at top
alerting users to this behavior
- Both docs note the difference from `outputs`, which do support naked
directory paths
## Related Issue(s)
Fixes
https://linear.app/nxdev/issue/NXC-2102/clarify-trailing-slash-requirement-for-inputs-in-directory-paths
Co-authored-by: Steven Nance <steven@nrwl.io>
## Current Behavior
When creating a new workspace using `create-nx-workspace` with the
"custom" preset flow, an `nxCloudId` is generated and added to
`nx.json`. This happens even though the onboarding flow is supposed to
handle Cloud setup separately via a short URL.
## Expected Behavior
New workspaces created via `create-nx-workspace` should not have
`nxCloudId` set in `nx.json`. Instead, a short URL is provided for users
to finish Cloud onboarding on their own. The `nxCloud: 'skip'` option is
now passed for the custom flow to prevent the ID from being generated.
E2E tests are updated to verify that `nxCloudId` is undefined in the
generated `nx.json` across all workspace presets.
## Related Issue(s)
N/A - internal fix for workspace creation behavior.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
`--help` on commands that hit yargs help are hanging
## Expected Behavior
It doesn't hang. This contains a quick fix in adding the process.exit
call, but also adds the unref needed to maintain previous working
behavior. We'll need to investigate long term if additional areas keep
commands alive, but adding this unref theoretically allows removing the
process.exit calls from `nx show`
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
---------
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
## Current Behavior
14 e2e test suites were disabled (`xdescribe`) due to an ESM import
issue in `@microsoft/api-extractor@7.57.0` (see
https://github.com/qmhc/unplugin-dts/issues/461).
## Expected Behavior
With the upstream issue resolved, all 14 e2e test suites are re-enabled
(`describe`) and should pass normally.
## Related Issue(s)
Reverts #34516
## Current Behavior
Running `nx test` for Next.js projects causes Jest to hang with:
```
Jest did not exit one second after the test run has completed.
```
This happens because `next/jest` loads `next.config.js`, which calls
`withNx` → `createProjectGraphAsync()`. The daemon client socket
connection is left open, keeping the Node.js event loop alive and
preventing Jest from exiting. Non-Next.js projects are unaffected since
they don't trigger this code path.
## Expected Behavior
Jest exits cleanly after tests complete for Next.js projects, without
needing `forceExit: true`.
## Fix
Pass `resetDaemonClient: true` to `createProjectGraphAsync()` in
`packages/next/plugins/with-nx.ts`. This tells the project graph
function to call `daemonClient.reset()` after fetching the graph, which
closes the socket and allows Jest to exit.
### Verification
| Scenario | Before | After |
|----------|--------|-------|
| `nx test next-app` | Hangs | Exits cleanly |
| `NX_DAEMON=false nx test next-app` | Exits cleanly | Exits cleanly |
| Direct `npx jest` | Exits cleanly | Exits cleanly |
| Non-Next.js `nx test react-lib` | Exits cleanly | Exits cleanly |
## Related Issue(s)
Fixes#32880
## Current Behavior
The Maven plugin is on version `0.0.13`.
## Expected Behavior
The Maven plugin is bumped to version `0.0.14`, with a migration
generated for Nx `22.6.0-beta.1`.
## Current Behavior
Agents are silent and hard to diagnose
## Expected Behavior
Agents should print debug logs without making all of Nx print debug logs
## Current Behavior
14 e2e tests are failing across master with "Failed to process project
graph" errors. The root cause is `@microsoft/api-extractor@7.57.0` which
has a broken ESM export (`ConsoleMessageId`). When `@nx/vite/plugin` or
`@nx/vitest` plugins load `vite.config.mts` files, they transitively
import api-extractor which crashes.
## Expected Behavior
Broken e2e tests are disabled via `xdescribe` so they no longer block
CI. Tests should be re-enabled once the upstream api-extractor ESM issue
is fixed.
## Disabled Tests
| Project | Test File |
|---------|-----------|
| e2e-vite | `vite.test.ts`, `vite-legacy.test.ts`,
`vite-ts-solution.test.ts` |
| e2e-vue | `vue.test.ts`, `vue-legacy.test.ts`,
`vue-ts-solution.test.ts` |
| e2e-js | `js-ts-solution.test.ts` |
| e2e-web | `web-vite.test.ts` |
| e2e-react | `react-vite.test.ts`, `react-ts-solution.test.ts` |
| e2e-next | `next-ts-solutions.test.ts` |
| e2e-release | `release-publishable-libraries.test.ts`,
`release-publishable-libraries-ts-solution.test.ts` |
| e2e-storybook | `storybook-nested.test.ts` |
## Related Issue(s)
Upstream: https://github.com/qmhc/unplugin-dts/issues/461
## Current Behavior
The `maven-shade-plugin` at version 3.5.0 intermittently fails on CI
with:
```
Could not replace original artifact with shaded artifact!
```
This is a file-locking race condition where the plugin fails to
atomically replace the original JAR with the shaded JAR.
## Expected Behavior
Upgrading to 3.6.0 resolves the intermittent CI failures by using
improved file-handling logic with better retry behavior during the
artifact replacement step.
## Related Issue(s)
N/A - fixes intermittent CI flakiness in `maven-batch-runner` builds.
Closes#19779
<!-- 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` -->
## Current Behavior
When targeting Windows the resulting binary (nx.dll) dynamically links
against Microsoft Visual C++ runtime (msvcrt140.dll). This means nx
won't be able to run on Windows systems without this runtime installed.
## Expected Behavior
I'd like to avoid this dependency by linking the runtime statically into
the nx binary. (This is also how e.g. cargo.exe for Windows is built.)
## Related Issue(s)
Fixes#19779
---------
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com>
## Current Behavior
Terminal output in the task runner is accumulated via repeated string
concatenation (`terminalOutput += chunk`). Each `+=` on a growing string
causes V8 to allocate a new, larger string and copy the old contents,
resulting in O(n²) allocation behavior for tasks with large output.
Additionally, `PseudoTtyProcess.onExit` didn't pass `terminalOutput` to
its callbacks, forcing callers like `TaskOrchestrator` to duplicate
output accumulation logic with a separate `onOutput` listener.
## Expected Behavior
- Terminal output is collected in `string[]` arrays and joined once at
the end, reducing intermediate allocations from O(n²) to O(n)
- `PseudoTtyProcess.onExit` now passes `terminalOutput` as a second
argument, matching the signature of other `RunningTask` implementations
- `TaskOrchestrator` no longer needs a special code path for
`PseudoTtyProcess` — unified `onExit` handling for all task types
- `tui-summary-life-cycle` accumulates output in chunks during execution
and stores the finalized string on task completion, allowing chunk
arrays to be GC'd
- `SeriallyRunningTasks` and `RunningNodeProcess` similarly switched to
chunk-based accumulation
- `BatchProcess` and `NodeChildProcessWithNonDirectOutput` lazily join
and cache their terminal output
Work on making a tech intro pages more consistent with each other and
focus on "answering the 80%" for the given technology.
Focusing on
- Angular
- Maven/Gradle
- react
- TS
- Vite
- Vitest/Jest
The changes are based around answering the following, where each
"category" of page might have a different set of depth for the answer.
1. Why do I want to use this plugin?
- Plugins are considered fully optional and are aimed at providing
better DX for a technology, such as inferred setup, generators,
migrations.
- some plugins (like TSC) might have special call outs in some of this,
but generally the same for all plugins.
2. How do I use this plugin in my workspace?
- also pretty commonly the "same" for all plugins in terms of "setup"
- where they differ is mostly for frameworks, e.g. Angular, React.
You're looking at setting up a project to use these tools
- For Build/Test tools you're looking at adding to an existing project,
or converting from one to another.
- Build/Test tools are "means to an end", so should callout if the goal
is tool + framework in a "new" context point to the framework based
plugin page. Otherwise, show adding to an existing project like React
project.
3. What do I need to know about using this plugin?
- understanding finer details of a plugin options, e.g. buildable &
publishable
- extra generators for the plugin. e.g. "convert-to-swc"
- generally I like the idea of having a "CI considerations" where we
talk about CI setups that can help, e.g. options or batch mode etc.
closes DOC-407
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: barbados-clemens <barbados-clemens@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
navigation of the breadcrumbs could lead to confusing state since they
were based around the folder structure.
Breadcrumbs are now based around the sidebar structure so they match the
hierarchy of content.
Note: I left the existing index file based route pages in place in case
there are any links people have booked marked/linked to in other
locations. these will get cleaned up when we finally rewrite all the
URLs to their new content locations
## Current Behavior
When file changes arrive rapidly, the daemon triggers multiple
concurrent project graph recomputations that all run to completion —
wasting CPU/memory on redundant work and returning stale results.
Additionally, after processing file changes, the daemon clears all
tracked files indiscriminately. Files that changed mid-recomputation are
silently lost and never reflected in the project graph until another
unrelated file change arrives.
## Expected Behavior
Stale recomputations detect when a newer one has started and exit early,
chaining to the newer promise so callers always get the freshest result.
File change tracking now uses versioned maps. Each batch of file watcher
events gets a unique version, and only files matching the snapshotted
version are cleared after processing. Files that changed
mid-recomputation are preserved and picked up by the next cycle.
## Current Behavior
Two categories of e2e CI failures were observed in run
https://github.com/nrwl/nx/actions/runs/22127884259:
1. **`e2e-nx-init` and `e2e-js` fail on Node 22.12.0** with:
```
error eslint-visitor-keys@5.0.0: The engine "node" is incompatible with
this module.
Expected version "^20.19.0 || ^22.13.0 || >=24". Got "22.12.0"
```
Node 22.12.0 is one minor version short of the `^22.13.0` range required
by `eslint-visitor-keys@5.0.0`.
2. **`e2e-nx` tests fail because `[isolated-plugin]` / `[plugin-worker]`
verbose messages leak into captured stdout**, causing:
- `JSON.parse(runCLI('show project --json'))` to throw `SyntaxError:
Unexpected token 'i', "[isolated-p"...`
- `expect(runCLI('show projects')).toEqual('')` to fail with worker
spawn noise
- The `@nx/workspace:infer-targets` test to unexpectedly find
`@nx/remix` in output (from a worker spawn message)
Root cause: in `isolated-plugin.ts`, the plugin worker's stdout was
piped directly to `process.stdout`, so `[plugin-worker]` verbose
messages written by the worker ended up in the stdout captured by
`runCLI` in e2e tests.
## Expected Behavior
1. The CI matrix uses a Node 22.x version that satisfies `^22.13.0`.
2. Plugin worker verbose/diagnostic messages go to `process.stderr` (not
`process.stdout`), so they don't contaminate output captured by `runCLI`
in e2e tests. Both worker stdout and stderr now pipe to
`process.stderr`, and the max listener bump is consolidated to `+2` on
stderr.
## Related Issue(s)
N/A — identified from CI run
https://github.com/nrwl/nx/actions/runs/22127884259
## Current Behavior
When running in maven 4 batch mode, the build state is recorded only
after the full batch is done.
This means that nx caching records the state of a task before build
state is recorded to disk.
When running another maven task that depends on this partially recorded
cache, the build state file is missing and we get errors.
## Expected Behavior
build state should be recorded after every task is done and before nx
caching can kick in. This way we can ensure that nx cache is correct.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Current Behavior
When running generators or migrations, Nx automatically skips Prettier
formatting if no root Prettier config is detected (added in #30426).
However, there's no way to explicitly skip Prettier formatting when a
config IS present but the user wants to bypass it for specific
operations.
This can be needed when:
- When running something like Oxfmt that may treat prettier slightly
differently, even with the same config (this is the main thing I ran
into)
- Running migrations where Prettier reformatting causes unintended side
effects (e.g., breaking `eslint-disable` comments)
- Temporarily disabling formatting for debugging purposes
- Using a formatter that coexists with Prettier in the workspace but
should take precedence for certain files
## Expected Behavior
Users can set `NX_SKIP_FORMAT=true` to explicitly skip Prettier
formatting in generators and migrations, regardless of whether Prettier
is configured. TSConfig path sorting (controlled by
`sortRootTsconfigPaths` or `NX_FORMAT_SORT_TSCONFIG_PATHS`) continues to
work independently.
```bash
NX_SKIP_FORMAT=true nx migrate --run-migrations
NX_SKIP_FORMAT=true nx g @nx/react:app my-app
```
## Related Issue(s)
Related to #30403 and #30426. This enhancement adds explicit user
control for cases where auto-detection of Prettier configuration isn't
sufficient.
## Current Behavior
When publish fails due to missing OTP code, its not clear as a user who
is using the top level command what to do next.
## Expected Behavior
Add the --otp flag to the top-level `nx release` command so users can
provide a one-time password for 2FA-enabled registries when running the
full release orchestration (version + changelog + publish).
When publish fails due to an expired or missing OTP (EOTP error),
display a helpful warning listing affected projects and the exact
command to re-run the publish step in isolation with a new OTP.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
Codex has only basic MCP/AGENTS.md support right now. Also, because it
used to have only global-level config files, we had some extra logic
around configuring that.
We want Codex to get the latest skills too (they don't support custom
subagents yet, though) and use project-level config files that they now
support.
After running nx build (or any task), the daemon now shows a hint if
your AI agent configuration is outdated: "Your AI agent configuration is
outdated. Run nx configure-ai-agents to update."
The daemon computes and caches the full agent configuration status
(fully configured, outdated, partially configured, non-configured) using
latest Nx from npm, so the check is always against the newest available
configuration. Running nx configure-ai-agents resets the daemon's cache
so the message disappears on the next build.
Key changes
- Daemon agent status endpoint: New GET_CONFIGURE_AI_AGENTS_STATUS /
RESET_CONFIGURE_AI_AGENTS_STATUS message types. The handler fires off
computation in the background and returns immediately (never blocks the
request). Results are cached for the daemon's lifetime.
- Shared latest-nx module: Extracted the "install nx@latest to tmp"
logic from nx-console-operations into daemon/server/latest-nx.ts so both
Nx Console and AI agents handlers share a single cached installation.
Includes a race-condition guard (in-flight promise deduplication).
- Post-task outdated hint: run-command.ts queries the daemon after task
execution and prints a single dim line if agents are outdated.
- Daemon reset from configure-ai-agents: The CLI sets NX_DAEMON=false
for configure-ai-agents, so we bypass daemonClient.enabled() and use
isServerAvailable() directly to reach an already-running daemon. The
socket is closed in a finally block so the process exits cleanly.
- Async editor detection (Rust): Made isEditorInstalled,
canInstallNxConsoleForEditor, installNxConsole, and related napi
functions async so they run on the libuv thread pool instead of blocking
Node's event loop. This prevents the daemon from stalling for ~3.5s when
checking editor extensions.
- output.logRawLine: New helper that prints a single line without the NX
prefix.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
We want to minimize prompts for people and agents. But we also want to
help them by setting up nx config for them so their agents can work
optimally.
If they're executing `nx init` or `create-nx-workspace` from within an
agent, it's a reasonable assumption that they'll want the best AI config
for that specific agent - so we set it up for them.
## Current Behavior
The `nx watch` file watcher uses the `ignore-files` and
`watchexec-filterer-ignore` crates to handle `.gitignore` matching.
These crates use a trie-based approach that has a bug with
path-component matching — certain gitignore patterns (e.g., prefix
patterns) don't match correctly, causing files that should be ignored to
trigger unnecessary watch events.
## Expected Behavior
Gitignore patterns are matched correctly using per-directory `Gitignore`
instances from the `ignore` crate — the same crate already used by the
file walker. Each `.gitignore` file is scoped to its directory, and
matching is done deepest-first so that nested gitignores take priority —
matching standard git behavior.
### What changed
- Replaced `ignore-files` + `watchexec-filterer-ignore` with direct use
of `ignore::gitignore::{Gitignore, GitignoreBuilder}`, aligning the
watcher with the approach already used by the file walker
- Each `.gitignore` is now compiled as a standalone instance tied to its
parent directory
- Gitignore evaluation walks deepest-first; first match wins
- `.nxignore` matching now uses `matched_path_or_any_parents` for
correct ancestor checking
- `create_filter` is now synchronous (no longer `async`) since the new
approach doesn't need async I/O
- Removed 2 crate dependencies (`ignore-files`,
`watchexec-filterer-ignore`)
## Current Behavior
When a project-level `tsconfig.json` (e.g., `apps/aurora/tsconfig.json`)
inherits `paths` via `extends` from `tsconfig.base.json` at the
workspace root and no explicit `baseUrl` is set, Nx incorrectly resolves
`./`-prefixed path mappings relative to the project tsconfig directory
instead of the workspace root where the paths were defined.
This causes errors when loading TypeScript config files (e.g.,
`rspack.config.ts`) that import workspace libraries using path aliases:
NX Cannot find module './libs/plugins/rspack/src'
`@swc-node/register`'s `readDefaultTsConfig` auto-sets `baseUrl` to
`dirname(tsConfigPath)` (the project directory) when not explicitly
configured, causing SWC to rewrite imports to incorrect relative paths
during transpilation.
## Expected Behavior
Path aliases defined in `tsconfig.base.json` (e.g.,
`"@trellis/plugins/rspack": ["./libs/plugins/rspack/src/index.ts"]`)
should resolve relative to the workspace root when no `baseUrl` is
configured.
This is needed so that when using `tsgo` and needing to prefix all paths
with `./` (no more `baseUrl` allowed) the paths are still resolved from
the right spot.
I tested this fix against our codebase on the branch I was trying to
switch to tsgo on and it seemed to work.
## Related Issue(s)
Fixes
https://discord.com/channels/1143497901675401286/1471627045694865581
## Current Behavior
CONTRIBUTING.md contains an outdated "How to Get Started Video" section
and references Stack Overflow for general questions.
## Expected Behavior
Remove outdated video section and point users to the Discord community
instead of Stack Overflow for general questions.
## Related Issue(s)
N/A
When switching from inline mode to full-screen TUI (or during window
resize), the PTY resize operation reparsed ALL raw terminal output
through a new vt100 parser synchronously on the event loop. For tasks
with large output, this caused a noticeable hang.
Add `resize_async()` which moves the expensive reparse to a background
thread using a snapshot-and-replay pattern:
1. Quick snapshot of raw output (brief read lock)
2. Expensive reparse on background thread (no locks held)
3. Quick swap with replay of any new output (brief write lock)
A generation counter prevents stale resizes from overwriting newer ones.
Also combine two separate O(n) scrollback processing calls in inline
mode into a single pass.
## Current Behavior
Copyright year shows 2017-2025 and README uses older tagline.
## Expected Behavior
Copyright reflects current year 2026 and README uses updated repository
description.
## Changes
- **LICENSE**: Updated copyright year from `2017-2025` to `2017-2026`
- **README.md**: Replaced heading and description
- New heading: "The Monorepo Platform that amplifies both developers and
AI agents. Nx optimizes your builds, scales your CI, and fixes failed
PRs automatically. Ship in half the time."
- Removed redundant description line below heading
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
> ## Update License and README
>
> Please make the following changes:
>
> 1. **Update LICENSE file**: Change the copyright year from `2017-2025`
to `2017-2026`
> - File: `LICENSE`
> - Line 3: Update `Copyright (c) 2017-2025 Narwhal Technologies Inc.`
to `Copyright (c) 2017-2026 Narwhal Technologies Inc.`
>
> 2. **Update README.md description**: Replace the current description
with the repository's official description
> - File: `README.md`
> - Line 22: Change the heading from `# Smart Monorepos · Fast Builds`
to `# The Monorepo Platform that amplifies both developers and AI
agents. Nx optimizes your builds, scales your CI, and fixes failed PRs
automatically. Ship in half the time.`
> - Line 24: Remove the current description line: `Get to green PRs in
half the time. Nx optimizes your builds, scales your CI, and fixes
failed PRs. Built for developers and AI agents.`
>
> The new README should have the repository description as the main
heading, followed immediately by the "Create a new Nx workspace with"
section.
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
*This pull request was created from Copilot chat.*
>
<!-- START COPILOT CODING AGENT TIPS -->
---
💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FrozenPandaz <8104246+FrozenPandaz@users.noreply.github.com>
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
## Current Behavior
`tui_logger::init_logger()` and `TuiTracingSubscriberLayer` are
initialized unconditionally in `initialize_logger()`. This spawns a
background thread, causing unnecessary allocation churn and increased
processing.
## Expected Behavior
`tui_logger` is only initialized when `NX_TUI=true`, avoiding the
background thread and allocation overhead for all non-TUI contexts.
## Current Behavior
We have ~1,657 redirect rules across `redirect-rules.js` and
`redirect-rules-docs-to-astro.js`, getting close to Netlify's limit and
we need room for more as the Astro migration continues.
## Expected Behavior
Reduced to **1,139 rules** (~31% reduction) by:
- Resolving duplicate/conflicting source paths across sections
- Flattening multi-hop redirect chains to point directly to final
destinations
- Consolidating groups of individual rules into wildcard patterns
(tutorials, CLI, helm, concepts, recipes, etc.)
- Removing old 2022-era sections (`schemaUrls`, `overviewUrls`,
`packagesIndexes`, `packagesDocuments`) whose destinations chain 3-5
hops deep and are long superseded by newer redirects
- made sure old links in nx code base still have redirects (will update
in future PR)
Build, tests, and internal link check all pass with no issues.
## Related Issue(s)
Fixes DOC-403
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: barbados-clemens <barbados-clemens@users.noreply.github.com>
## Current Behavior
When AI agents (Claude Code, Cursor, Windsurf, etc.) run `nx init`, the
command works but:
- Uses interactive prompts that AI agents can't handle
- Outputs human-readable text that AI agents must parse
- Doesn't provide structured progress or error information
## Expected Behavior
When `nx init` detects an AI agent (via environment variables like
`CLAUDE_CODE=1`), it should:
- Skip interactive prompts and use sensible defaults
- Output structured NDJSON for progress updates, success, and errors
- Include detailed context for AI agents to understand and fix issues
## Changes
This PR adds agentic mode to `nx init`:
### `nx init` Changes
- Detect AI agents via `isAiAgent()` native function
- Auto-defaults: `interactive=false`, `nxCloud=false`, auto-detect `.nx`
installation
- NDJSON output with `type: progress|success|error`
- Error logs written to `.nx/ai-errors/` with full context
- Cursor restoration escape sequence skipped for AI agents (prevents
NDJSON corruption)
### Output Format
```jsonl
{"type":"progress","step":"starting","message":"Initializing Nx..."}
{"type":"success","nxVersion":"22.5.0","projectsDetected":1,"pluginsInstalled":["@nx/vite"]}
```
Or on error:
```jsonl
{"type":"error","message":"Failed to install","code":"INSTALL_ERROR","errorLogPath":".nx/ai-errors/nx-init-error-2025-01-15T10-30-00.log"}
```
## Current Behavior
we pull from latest all the time even if the current version is already
latest
## Expected Behavior
we can skip this extra work sometimes
## Current Behavior
Pages proxied from Framer contain canonical URLs pointing to the Framer
domain (`ready-knowledge-238309.framer.app`), causing duplicate indexing
issues in search engines.
## Expected Behavior
Canonical URLs and other references in Framer-proxied pages now point to
`nx.dev`, ensuring proper SEO indexing.
### Implementation
Consolidated all Framer logic into a single Netlify edge function
(`rewrite-framer-urls.ts`) that:
1. Checks if the request path matches a Framer-proxied path (using
`FRAMER_REWRITES` env var)
2. Fetches directly from Framer (using `FRAMER_URL` env var)
3. Rewrites all Framer URLs to `nx.dev` in the HTML response (handles
`<link rel="canonical">`, `og:url`, etc.)
4. For non-Framer paths, passes through to Next.js
The edge function uses the `accept: ['text/html']` config to only run on
HTML requests, matching the pattern from `track-page-requests.ts` in
astro-docs.
The Next.js middleware has been removed since all Framer routing is now
handled by the edge function.
### Environment Variables
The edge function expects these env vars in Netlify (already added
previously):
- `NEXT_PUBLIC_FRAMER_URL`: e.g.,
`https://ready-knowledge-238309.framer.app`
- `NEXT_PUBLIC_FRAMER_REWRITES`: comma-separated list of paths, e.g.,
`/pricing,/enterprise`
## Demo
1. Go to https://deploy-preview-34445--nx-dev.netlify.app/
2. View source and look for canonical
3. See it is nx.dev not framer domain
<img width="1347" height="161" alt="image"
src="https://github.com/user-attachments/assets/d4a515da-e39f-41cb-a7b4-668fe0bedbbd"
/>
## Related Issue(s)
Closes CLOUD-4148
## Current Behavior
The `release` function returned by `createAPI` has a return type of
`Promise<NxReleaseVersionResult | number>`. The `| number` union member
is inaccurate since the function always returns
`NxReleaseVersionResult`, which can mislead consumers of the
programmatic API.
## Expected Behavior
The return type is narrowed to `Promise<NxReleaseVersionResult>`,
accurately reflecting what the function actually returns and giving API
consumers correct type information.
Co-authored-by: Andreas Hörnicke <andreas.hoernicke@contentful.com>
Blog post draft about the evolution from MCP tools to agent skills.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: MaxKless <MaxKless@users.noreply.github.com>
Co-authored-by: Juri Strumpflohner <juri.strumpflohner@gmail.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
## Current Behavior
`^` and `dependencies: true` only work for fileset inputs
## Expected Behavior
Adds support for inputs of the form `^{projectRoot}/**/*.ts` as
syntactic sugar for specifying a fileset input that should be collected
from dependency projects.
Previously, only named inputs could use the `^` prefix to include
dependencies (e.g., `^production`). Now filesets can also use this
syntax directly without needing to define a named input first.
Examples:
- `^{projectRoot}/**/*.ts` - include .ts files from all dependencies
- `^{workspaceRoot}/tools/**/*` - include workspace tools from
dependencies
- `{ fileset: '{projectRoot}/**/*.ts', dependencies: true }` - object
form
Detection is deterministic: if the string after `^` starts with
`{projectRoot}` or `{workspaceRoot}`, it's treated as a dependency
fileset; otherwise, it's treated as a named input reference.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
- In some `nx run-many` executions, terminal output can appear staggered
or visually misaligned instead of updating cleanly in place.
- For `run-many` cases that end up running a single task (especially
when TUI is not active), the spinner/status line can be rendered twice.
## Expected Behavior
- Dynamic terminal output updates should remain stable and aligned, with
clean in-place refreshes.
- Single-task `run-many` should display a single spinner/status line
with no duplicate rendering.
Add uncaughtException handler for ERR_USE_AFTER_CLOSE to prevent
ugly stack trace when pressing Ctrl+C during enquirer prompts
(Node 24 stricter readline behavior). Matches existing pattern
used in nx init and create-nx-workspace.
## Current Behavior
The publish workflow uses `sudo npm install -g npm@11.5.2` which was
added in #34409. This causes issues with OIDC token permissions in the
release pipeline since `sudo` runs as a different user context.
## Expected Behavior
The publish workflow should use `npm install -g npm@11.5.2` without
`sudo`, matching the standard approach used elsewhere and avoiding
permission context issues during release.
## Related Issue(s)
Reverts #34409
## Current Behavior
The publish workflow uses `addnab/docker-run-action@v3` which is based
on `docker:20.10` (Docker API 1.41). GitHub's `ubuntu-24.04` runners now
ship Docker Engine 28.x which requires minimum API version 1.44, causing
all 4 Linux Docker builds to fail:
```
docker: Error response from daemon: client version 1.41 is too old. Minimum supported API version is 1.44, please upgrade your client to a newer version.
```
Failed run: https://github.com/nrwl/nx/actions/runs/21961139962
## Expected Behavior
Linux Docker builds (x86_64-gnu, x86_64-musl, aarch64-gnu, aarch64-musl)
complete successfully using the host's modern Docker CLI.
https://github.com/nrwl/nx/actions/runs/21996143819
## Related Issue(s)
The `addnab/docker-run-action` repo is abandoned (last release March
2021, last commit May 2021) with open issues about this exact problem.
## Current Behavior
There's a chance that windows can falsely flag our native binaries as a
threat. We do not use the shellapi feature from winapi.
## Expected Behavior
We hope that removing this API doesn't break things, and the threat
messaging goes away
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #https://github.com/nrwl/nx/issues/34186
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Pressing `[1]` or `[2]` on a task that's already pinned to that slot
**focuses** the pane instead of unpinning it. This means there's no way
to unpin a single pane via keyboard — you can only nuke everything with
`[0]`.
This regression was introduced in #34175, which fixed a real problem:
pressing Enter on an already-pinned task would unpin it, leaving focus
on an invisible pane (a ghost pane — you're staring at nothing but the
TUI thinks you're looking at output). The fix was to make the "already
pinned to this pane" branch focus instead of unpin. The problem is that
`[1]`, `[2]`, and Enter all flowed through the same function
(`assign_current_task_to_pane`), so changing behavior for Enter changed
it for everyone. One lock got swapped out, and every door started
behaving the same way.
## Expected Behavior
`[1]` and `[2]` are **toggles**: press once to pin, press again to
unpin. Enter is a **display** action: show me this task's output and put
my cursor there — if it's already visible somewhere, just take me to it.
After this change:
- **`[1]` / `[2]`** on an already-pinned task → unpins it (pane
disappears, layout adjusts, focus returns to task list if no panes
remain)
- **Enter** on an already-pinned task → focuses whichever pane it's in
(even if it's in pane 2 and you'd normally expect pane 1)
- **Init** (startup restore of pinned tasks) → pure assignment, no
toggling, no focusing
## Approach
The old code had one function trying to serve three masters. Rather than
adding a flag parameter (`should_toggle: bool`) — which would just be a
boolean that lies about its intentions at every call site — the function
was split along the actual semantic boundaries:
| Function | Used by | "Already pinned here" behavior |
|---|---|---|
| `toggle_current_task_in_pane` | `[1]` / `[2]` keys | **Unpin** (toggle
off) |
| `assign_current_task_to_pane` | `init()` | No-op (task is where it
should be) |
| `display_and_focus_current_task_in_terminal_pane` | Enter key |
**Focus** the existing pane |
The shared logic — exiting spacebar mode, moving a task between panes,
fresh-pinning — lives in two small helpers (`exit_spacebar_and_pin`,
`move_or_pin_selection`) that both `toggle` and `assign` delegate to.
The only code that differs is the "what do we do when it's already
here?" branch, which is exactly the part that *should* differ.
**Why not keep one function with a mode parameter?** Because the three
behaviors aren't variations of the same action — they're genuinely
different user intents. A toggle is "I changed my mind." A focus is
"Take me there." An assignment is "Put this here." Encoding that as an
enum parameter just moves the branching somewhere less obvious and makes
the call sites harder to read. The function names now document the
intent at the point of use, and there's no shared state to accidentally
couple.
**Why does Enter check all panes, not just pane 0?** Because if you
pinned a task to pane 2 via `[2]` and then press Enter on it, the least
surprising thing is to jump to where it already lives — not to silently
duplicate it into pane 1 or ignore you. The task is already on screen;
Enter means "show me."
## Related Issue(s)
Fixes the regression introduced by #34175.
## Current Behavior
CNW (Create Nx Workspace) has A/B testing logic that randomly selects
between variants 0, 1, and 2 for the Nx Cloud connection flow. Each
variant shows different prompts and banners.
## Expected Behavior
Lock in variant 2 as the permanent behavior:
- **No cloud prompt** - users are not asked about Nx Cloud during
workspace creation
- **Deferred connection** - no `nxCloudId` is written to `nx.json` (uses
`skipCloudConnect: true`)
- **Variant 2 banner** - shows "Enable remote caching and automatic
fixes when CI fails" with a link to complete setup later
### Changes
- Simplified `ab-testing.ts` - removed caching, random selection;
`getFlowVariant()` always returns `'2'`
- `shouldShowCloudPrompt()` always returns `false`
- `determineNxCloudV2()` returns `'github'` with `skipCloudConnect:
true` for deferred connection
- Removed variant 1 banner logic from `messages.ts`
- Updated tests to reflect the locked-in behavior
## Demo
https://www.loom.com/share/7f688eed6052428cbe91dd9db837cbbd
## Related Issue(s)
Closes CLOUD-4255
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Simplified form targeting by replacing the global `reactHubspotForm` ID
with a deterministic `targetId` that incorporates portal, form, and
calendly IDs. This improves scalability and avoids potential ID
collisions.
## Current Behavior
When banner content changes in Framer, the nx-docs and nx-dev sites need
to be manually redeployed to pick up the new content.
## Expected Behavior
A scheduled workflow monitors the banner URL every 15 minutes and
automatically triggers Netlify production deploys when content changes.
## How it works
1. Fetches `BANNER_URL` content (from repository variable)
2. Computes SHA256 hash
3. Compares to cached hash from previous run
4. If different → triggers both Netlify deploys, updates cache
5. If unchanged → no-op
## Required Setup
1. **Repository variable** (`Settings → Secrets and variables → Actions
→ Variables`):
- `BANNER_URL` = Framer banner API URL
2. **Repository secret** (`Settings → Secrets and variables → Actions →
Secrets`):
- `NETLIFY_AUTH_TOKEN` = Netlify personal access token
## Related Issue(s)
Fixes DOC-405
<!-- 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
Negation patterns are ignored in plugin configuration for the `include`
and `exclude` properties.
## Expected Behavior
- Negation patterns should work in the same way that they do for other
`include`/`exclude` configurations
**Example: Excluding all e2e projects except one**
```jsonc
// nx.json
{
"plugins": [
{
"plugin": "@nx/jest/plugin",
"exclude": ["**/*-e2e/**/*", "!**/toolkit-workspace-e2e/**/*"],
},
],
}
```
This will exclude all e2e projects except `toolkit-workspace-e2e`.
**Example: Including packages except legacy ones**
```jsonc
// nx.json
{
"plugins": [
{
"plugin": "@nx/vite/plugin",
"include": ["packages/**/*", "!packages/legacy/**/*"],
},
],
}
```
**How negation patterns work:**
- Patterns are processed in order from first to last
- A pattern starting with `!` removes files from the match set
- A pattern without `!` adds files to the match set
- The last matching pattern determines if a file is included
- If the first pattern is a negation, all files are matched initially
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
## Current Behavior
There is currently no plugin.md file for Gradle.
Other plugin.md files can be improved
## Expected Behavior
Add plugin.md file for Gradle to aid with verification with Agents.
Add plugin.md file for Vite for workspaces that have not migrated to
@nx/vitest.
## Related Issue(s)
CLOSES NXC-3843
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: Coly010 <Coly010@users.noreply.github.com>
## Current Behavior
The Playwright executor does not support configuring a custom cache
directory for Playwright's internal cache (browser binaries, etc.).
Users who need to control where Playwright stores its cache, for example
in CI environments with specific disk constraints like not being able to
DTE tasks or shared caching setups, have no way to set this through the
executor configuration.
## Expected Behavior
A new `cacheDir` option is available on the Playwright executor. When
provided, it sets the `PWTEST_CACHE_DIR` environment variable on the
forked Playwright process, allowing users to control where Playwright
stores its internal cache.
```json
{
"targets": {
"e2e": {
"executor": "@nx/playwright:playwright",
"options": {
"cacheDir": "/tmp/playwright-cache"
}
}
}
}
```
## Related Issue(s)
Replaces #34397
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Current Behavior
The SECURITY.md file does not clarify what types of reports should be
sent to the security email, leading to reports about outdated
dependencies and vulnerability scanner output.
## Expected Behavior
The file now clarifies that the security email is for demonstrable,
verified vulnerabilities in the Nx codebase itself, not for:
- Outdated dependency reports
- Dependencies with CVEs that don't directly affect Nx
- General vulnerability scanner output
## Related Issue(s)
Fixes NXC-3898
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Current Behavior
The `npm install -g npm@11.5.2` step in the publish workflow fails with
`EACCES: permission denied, mkdir '/usr/local/share/man/man5'` on newer
GitHub Actions runner images.
## Expected Behavior
The global npm install step completes successfully regardless of runner
image permissions on `/usr/local/share/man/`.
## Related Issue(s)
This is a known issue with GitHub Actions runners:
https://github.com/actions/runner-images/issues/9644
Add isSandbox() utility that checks for sandbox environment variables
(SANDBOX_RUNTIME, GEMINI_SANDBOX, CODEX_SANDBOX, CURSOR_SANDBOX) and
use it to disable the daemon and plugin isolation in sandbox
environments.
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## 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: meeroslav <meeroslav@users.noreply.github.com>
## Current Behavior
Running ai agents in sandbox mode causes issues with Nx's daemon and
plugin isolation
## Expected Behavior
Running ai agents in sandbox mode should work
## Related Issue(s)
CLOSES NXA-828
## Current Behavior
We do not include NxVersion when creating short urls.
## Expected Behavior
Include NxVersion when creating short urls.
## Related Issue(s)
CLOSES NXC-3879
When cache outputs include both glob patterns and directory patterns
containing symlinks, the cache restore fails with EEXIST (os error 17).
This happens because `fs_extra::remove_items` silently skips dangling
symlinks (since `is_dir()`/`is_file()` follow links and return false),
leaving stale symlinks that cause `symlink()` to fail.
The fix makes symlink creation idempotent by checking for and removing
any existing symlink at the destination before creating a new one, using
`symlink_metadata()` which correctly detects dangling symlinks.
Fixes#34013
## Current Behavior
The Maven plugin's `createNodes` and `createDependencies` functions both
independently compute a hash of all pom.xml directories, then use that
hash to look up cached Maven analysis data from disk. When Maven
projects have `<includes>` or `<excludes>` in their plugin config, the
hash can differ between the two calls, causing `createDependencies` to
fail to find the data that `createNodes` stored.
## Expected Behavior
`createDependencies` reliably receives the Maven analysis data from
`createNodes` regardless of hash differences, by reading it from a
module-level variable instead of re-hashing and looking it up from disk.
This matches the pattern already used by the Gradle plugin
(`getCurrentGradleReport`).
## Related Issue(s)
## Current Behavior
Runtime cache keys could be nondeterministic because the order of
environment variables varied, leading to inconsistent cache hits across
runs.
## Expected Behavior
Runtime cache keys are deterministic regardless of the insertion order
of env variables, improving cache stability.
## Current Behavior
Cycles in the task graph could remove unrelated `continuousDependencies`
when the cycle exists only in `dependencies`, leading to missing
continuous task edges.
## Expected Behavior
Cycle removal only removes the specific cyclic edge from the list where
it appears, preserving unrelated continuous dependencies.
## Current Behavior
When running Nx tasks in CI environments (e.g., Buildkite) where the
host's /tmp is mounted to containers, intermittent EADDRINUSE errors
occur in PseudoIPCServer.init(). This happens because:
1. PseudoIPCServer doesn't clean up its Unix socket file before calling
listen()
2. ForkedProcessTaskRunner.createPseudoTerminal() instantiates
PseudoTerminal directly instead of using the createPseudoTerminal()
helper, bypassing shutdown callback registration
When a new container starts with the same PID as a previous run (PID
recycling), it generates the same socket path and hits EADDRINUSE
because the stale socket file still exists.
## Expected Behavior
No EADDRINUSE errors should occur. The PseudoIPCServer should
defensively remove any stale socket file before attempting to listen,
similar to how the daemon server handles this.
## Related Issue(s)
Fixes#34233
Streamlined analytics tracking by removing Cookiebot and direct GA
(gtag.js) integrations. Consolidated event logging through GTM's
dataLayer for consistency and maintenance simplicity.
## Current Behavior
Batch IDs are generated in two places: the task scheduler uses an
incremental counter (`executorName N`) while the forked process task
runner generates its own using the process PID (`executorName-pid`).
This means the batch ID registered in metrics doesn't match the one used
everywhere else.
## Expected Behavior
Batch IDs are only created by the task scheduler. The forked process
task runner uses the scheduler-assigned ID to ensure consistency across
the system.
## Current Behavior
right now if users modify their mcp params like `--minimal`, we will
override them on `configure-ai-agents`
## Expected Behavior
We want to bring users up to latest without overriding their valid
configurations
## Current Behavior
The Bun package manager config uses `--frozen-lockfile` for
`updateLockFile`:
```typescript
updateLockFile: 'bun install --frozen-lockfile',
```
However, `--frozen-lockfile` **prevents** changes to the lockfile,
causing `nx release` to fail when trying to update the lockfile after
version bumps.
## Expected Behavior
Use `--lockfile-only` which generates/updates the lockfile without
installing dependencies:
```typescript
updateLockFile: 'bun install --lockfile-only',
```
This is consistent with other package managers:
- npm: `npm install --package-lock-only`
- pnpm: `pnpm install --lockfile-only`
- yarn berry: `yarn install --mode update-lockfile`
## Background
When Bun support was added in PR #22602 (April 2024), `--lockfile-only`
didn't exist in Bun. Bun has since added this flag.
Closes#34344
Co-authored-by: Kai Gritun <kai@kaigritun.com>
## Current Behavior
Several GitHub Actions workflows hardcode pnpm version `10.11.1`, while
`package.json` specifies `pnpm@10.28.2` in the `packageManager` field.
This causes CI failures with:
```
Error: Multiple versions of pnpm specified:
- version 10.11.1 in the GitHub Action config with the key "version"
- version pnpm@10.28.2 in the package.json with the key "packageManager"
```
## Expected Behavior
All pnpm version references across CI workflows should match the
`packageManager` field in `package.json` (`10.28.2`).
## Related Issue(s)
N/A — Fixing CI breakage from version mismatch.
## Changes
Updated pnpm version from `10.11.1` → `10.28.2` in:
- `.github/workflows/npm-audit.yml` — `pnpm/action-setup` version
- `.github/workflows/publish.yml` — `PNPM_VERSION` env var and FreeBSD
install
- `.github/workflows/issue-notifier.yml` — `pnpm/action-setup` version
- `.github/workflows/generate-embeddings.yml` — `pnpm/action-setup`
version
## Current Behavior
The Windows build (`aarch64-pc-windows-msvc`) fails to compile with:
```
error[E0433]: failed to resolve: use of undeclared type `FileType`
--> packages\nx\src\native\watch\types.rs:128:55
```
The `FileType` type is used inside a `#[cfg(target_os = "windows")]`
block but was not imported.
## Expected Behavior
The Windows build compiles successfully. The `FileType` import is scoped
inside the `#[cfg(target_os = "windows")]` block (matching the existing
pattern in the macOS block) so there are no unused imports on any
platform.
## Related Issue(s)
N/A — build breakage discovered during CI publish workflow.
## Current Behavior
The daemon's file watcher uses watchexec 3.0.1 which hardcodes
`RecursiveMode::Recursive` when registering inotify watches. This means
**every** directory gets an inotify watch — including all of
`node_modules`, `.git`, and other ignored trees.
On a typical workspace with a large `node_modules`, this can consume
thousands of inotify watches, eating kernel memory and CPU. The
`WatchFilterer` only filters **events** after watches are already
registered — the watches themselves are never prevented.
## Expected Behavior
Only non-ignored directories (workspace source code) get inotify
watches. Ignored directories like `node_modules`, `.git`, `.nx/cache`,
`.nx/workspace-data`, and `.yarn/cache` are skipped entirely at the
watch registration level.
This dramatically reduces:
- **inotify watch count** (from thousands to hundreds)
- **Memory usage** (each watch consumes kernel memory)
- **CPU overhead** (fewer watches = less kernel bookkeeping)
### How it works
- Upgraded watchexec 3.0.1 → 8.0.1 which supports
`WatchedPath::non_recursive()`
- Added `create_watch_walker()` using `ignore::WalkBuilder` (same
pattern as `walker.rs`) to enumerate only non-ignored directories
- Each directory is watched with `NonRecursive` mode — like putting
security cameras only in the rooms you care about instead of every room
in the building
- New directories created at runtime are dynamically added to the watch
set via the `on_action` handler
- Event-level filtering via `WatchFilterer` is unchanged — same behavior
for gitignore/nxignore patterns
### macOS Support for Dynamic Directory Registration
The initial implementation worked on Linux and Windows but failed tests
on macOS because macOS FSEvents doesn't always provide the same
`FileEventKind` tags as Linux inotify or Windows ReadDirectoryChangesW.
**Three changes to support macOS:**
1. **watcher.rs**: On macOS, check all events for directory creation
(not just events with specific FileEventKind tags) and verify via
filesystem
2. **types.rs**: Filter directory events from JavaScript callbacks on
macOS (similar to Windows behavior)
3. **watch_filterer.rs**: Allow macOS directory events (`Create(Folder)`
and `Modify(Metadata)`) through the filter so the action handler can
register them
All changes use `#[cfg(target_os = "macos")]` for compile-time
conditional compilation, so Linux/Windows behavior is completely
unchanged and there's zero runtime overhead.
### Additional notes
- Pinned `serde` to `<1.0.220` because serde 1.0.220+ moved `__private`
to `serde_core`, breaking `swc_common 0.31.22`
- No TypeScript changes — the napi interface is identical
- `watch_filterer.rs`, `types.rs`, `utils.rs` required no changes (APIs
are compatible)
## Related Issue(s)
Fixes https://github.com/nrwl/nx/issues/33781
Fixes https://github.com/nrwl/nx-console/issues/2468
<!-- No specific issue linked yet -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Re-enabling tests and putting back kotlin e2e tests for gradle.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
Flaky task detection warns about all tasks that have different exit
codes for the same hash, including non-cacheable tasks. This is
misleading because the flaky task warning message points users to Nx
Cloud's flaky task retry feature, which is only relevant for cached
tasks.
## Expected Behavior
Flaky task detection should only consider tasks where `task.cache ===
true`, making the warning more meaningful and avoiding noise for
non-cacheable tasks.
## Related Issue(s)
N/A - Internal improvement
## Changes Made
###
`packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle.ts`
1. Added `cacheable: boolean` to the `TaskRun` interface
2. In `endTasks`, now tracks `cacheable: taskResult.task.cache === true`
for each task
3. In `endCommand`, filters to only check flaky tasks among cacheable
tasks
###
`packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle-old.ts`
1. Added `cacheableHashes: Set<string>` to track which task hashes are
cacheable
2. In `endTasks`, tracks cacheable tasks by adding their hash to the set
3. In `endCommand`, only checks for flaky tasks among cacheable task
hashes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
A new version compatibility table is added to help users understand
which versions of the Gradle plugin work with which versions of the Nx
plugin. The targetNamePrefix configuration option is now documented with
an explanation of its use case in polyglot workspaces where target name
collisions may occur.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Gradle plugin to 0.1.12
## 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>
<!-- 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 -->
- No way to run the batch executor in debug mode
- Any flags passed into the nx gradle batch command get forwarded into
the `gradlew` command.
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
- Allow for an env variable to be set for debug flags so that the batch
runner jar can be run with a debugger hooked in.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #NXC-3797
---------
Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
## Current Behavior
In some resize/background scenarios, the TUI could crash while rendering
output panes, displaying a "Scrollbar area is empty" message.
## Expected Behavior
The TUI remains stable during resizes and backgrounding. Output panes no
longer crash when a scrollbar would render in an invalid area.
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
<!-- 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 Nx processes Gradle subprojects, task dependencies reference the
wrong project names. For a subproject structure like :app or :lib:core,
the generated task dependencies use only the simple project name (app or
core) instead of the full build tree path (:app or :lib:core). This
causes dependent tasks to be generated with incorrect project
references, breaking the task graph for multi-project Gradle builds.
## Expected Behavior
Task dependencies should use the full Gradle build tree path for
subprojects. When a task in :app depends on a task in :lib, the
dependency should be correctly referenced as :lib:taskName.
The fix introduces a getNxProjectName() utility function that correctly
resolves the Nx project name based on the Gradle project's
buildTreePath, and applies it consistently across all dependency
resolution logic in ProjectUtils.kt and
TaskUtils.kt. New tests verify the fix works for both single and nested
subproject structures.
Also removed --rerun-tasks from the batch and non batch runners, we
found that during parallel task executions with the non batch runner,
the flag would cause cache conflicts that would fail tasks.
## 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>
## Current Behavior
When a task has outputs at different path depths, some outputs may not
be tracked. This causes:
- Deleted output files not being detected
- Cache restoration being skipped with message "existing outputs match
the cache, left as is"
- Files not being restored even though they exist in cache
## Expected Behavior
All task outputs are tracked regardless of their path depth, ensuring:
- Deleted outputs are correctly detected
- Cache restoration happens when outputs are missing
## Current Behavior
When a task output directory contains a nested `.gitignore` that hides
its contents, Nx can treat the outputs as already present and skip
restoring them from cache. This can result in generated files being
missing from disk, even though the cache entry is valid.
## Expected Behavior
Nx should restore cached outputs regardless of ignore rules inside the
output directory.
## Related Issue(s)
Fixes#32620
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Disabling Gradle e2e tests until foojay toolchain service back online.
Ensures that gradle within the Nx repo uses mise to download java
toolchain, but gradle workspaces within the e2e environments download
their own toolchain.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #Q-175
AI agents are detected via environment variables (CLAUDECODE, OPENCODE)
and receive NDJSON streaming output, non-interactive mode, structured
JSON results with explicit GitHub setup instructions.
Related NXC-3628
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Add documentation for batch mode. Remove references to removed custom
overrides for intTest.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
<!-- 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 -->
The only way to download the cloud client is to run a specific cloud
command or a task with cloud configured.
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
A new command is added where we only download the cloud client with:
```
nx download-cloud-client
```
## 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>
add guide to clarify various ways to bundle a node app for different
bundlers
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: barbados-clemens <barbados-clemens@users.noreply.github.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
## Current Behavior
In the TUI, when any standalone task finishes,
`handle_standalone_task_finished` unconditionally switches the user's
selection to another in-progress task — even if the finished task wasn't
the one the user had selected. This causes the selection to jump
unexpectedly while the user is watching a different task.
## Expected Behavior
Selection should only change when the task the user is actively viewing
finishes. If an unrelated background task finishes, the user's selection
should remain on whatever they chose.
## Related Issue(s)
N/A — discovered during development testing.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
NX_NATIVE_LOGGING is hardcoded and can't be customized on the daemon
server
## Expected Behavior
Log settings can be customized by changing them in the env of the first
command to spawn the daemon
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
we were including native binaries in the final netlify function build
for nextjs which was fine until we reach the limit of 250mb causing a
failure to upload the function (AWS imposed lambda limit)
Now we strip out any deps we know we don't need for the app which are
dev deps and not runtime required.
## Current Behavior
- Canary releases calculate their base version by incrementing the minor
of `nx@latest`, or using `nx@next` major when majors differ
- PR releases always use `0.0.0` as their base version (e.g.
`0.0.0-pr-1234-abc1234`)
- This means canary and PR versions don't clearly relate to the current
beta release line
## Expected Behavior
- Both canary and PR releases derive their base version directly from
`nx@next`
- If next is `22.5.0-beta.5`, then:
- Canary: `22.5.0-canary.20260204-abc1234`
- PR: `22.5.0-pr.1234.abc1234`
- All prerelease channels now share the same base version, making it
clear which release line they belong to
## Related Issue(s)
N/A - internal improvement to release infrastructure
## Current Behavior
The Maven plugin version is `0.0.12` across all pom.xml files and the
versions.ts constant. The `bump-maven-version` generator does not update
the `batch-runner-adapters` pom files, causing version mismatches.
## Expected Behavior
The Maven plugin version is bumped to `0.0.13` in **all** pom.xml files
(including batch-runner-adapters), with a migration created for users
upgrading to Nx `22.5.0-beta.4`. The bump generator now includes the
batch-runner-adapters pom files so future bumps won't miss them.
### Changes
- Updated version from `0.0.12` to `0.0.13` in all pom.xml files (root,
maven, maven-plugin, shared, batch-runner, batch-runner-adapters,
maven3-adapter, maven4-adapter)
- Updated `mavenPluginVersion` constant in
`packages/maven/src/utils/versions.ts`
- Added `update-0-0-13` migration entry in
`packages/maven/migrations.json` targeting Nx `22.5.0-beta.4`
- Created migration file
`packages/maven/src/migrations/0-0-13/update-pom-xml-version.ts`
- Fixed `bump-maven-version` generator to include
`batch-runner-adapters` pom files
## Current Behavior
The FreeBSD build in CI can fail silently or with unclear error messages
when:
- Disk space runs low during the build process
- The build command fails without proper error propagation
- Unnecessary files consume valuable disk space
## Expected Behavior
With these changes:
- Additional disk space is freed by removing docs/astro-docs/nx-dev
directories before building
- Build exit codes are properly captured and propagated
- Disk usage is logged after the build completes for debugging purposes
- Build failures are clearly reported with explicit error messages
This improves reliability and makes it easier to diagnose issues when
they occur.
## Related Issue(s)
<!-- No specific issue, general CI improvement -->
We're getting requests to `favicon.svg.md` that are being tracked,
ignore these. Also for the server page views, we should only count them
if `text/html` is in the accept header. Browsers will send these, and AI
agents, curl, etc. do not. This allows us to compare browser traffic vs
AI/curl traffic more accurately.
## Current Behavior
When a client disconnects while the daemon is writing a response, a
`socket.write` call triggers an EPIPE error. The old error handler used
`console.error`, which caused the error to propagate through
`respondWithErrorAndExit` and crash the daemon process via
`process.exit(1)`. The client would then see an `internalDaemonError`
and permanently disable the daemon via `markDaemonAsDisabled`, requiring
`nx reset` to recover.
Additionally, disconnected sockets were not cleaned up from the file
watcher and project graph listener registries on socket error events,
only on `close` events. This left a window where the daemon could
attempt to write to dead sockets during notifications.
## Expected Behavior
When a client disconnects mid-response:
- The `socket.write` callback logs the error gracefully via
`serverLogger` instead of `console.error`
- The daemon process stays alive and continues serving other clients
- The `socket.on('error')` handler cleans up registered file watcher and
project graph listener sockets immediately, matching the existing
`close` handler behavior
- The daemon is never permanently disabled due to EPIPE errors
## Related Issue(s)
<!-- No linked issue -->
### Current Behavior
The nx configure-ai-agents command output is minimal - just "AI agents
set up successfully" with a
bullet list of agent names. Users don't understand what was actually
configured (plugin vs MCP,
skills, which files were created/modified).
### Expected Behavior
Clearer feedback about what gets configured for each agent:
Selection prompt improvements:
- Agents needing updates show (update available) tag
- Footer always shows result state: what will be configured
- Agent-specific descriptions (e.g., "Installs Nx plugin (MCP + skills +
agents). Updates
CLAUDE.md.")
Post-configuration output:
- Compact summary per agent showing what was set up
- Example: Claude Code: Nx plugin (MCP + skills + agents) + CLAUDE.md
Claude .mcp.json cleanup:
- When configuring Claude, removes nx-mcp from .mcp.json since it's now
handled by the plugin
- Deletes the file entirely if nx-mcp was the only entry
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
This PR fixes two issues:
1. When the README changes are amended, there's an edge case where we
don't have a commit to amend (e.g. `--skipGit`), and this fails the
entire CNW flow.
2. When user opts out of Cloud, we strip the entire `<!-- BEGIN:
nx-cloud -->` block in README rather than just the comments and leaving
the content.
Closes NXC-3812
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Current Behavior
There's not an easy to use service to track PIDs being registered to nx
tasks
## Expected Behavior
There's a service to track this stuff
## Related Issue(s)
## Current Behavior
The `getNpmPackageVersion` function in
`packages/workspace/src/generators/utils/get-npm-package-version.ts`
uses `execSync` with direct string interpolation of the `packageName`
parameter. When a user runs `create-nx-workspace` with a custom
`--preset` value that doesn't match a known preset, the value flows
unsanitized into a shell command:
```js
execSync(`npm view ${packageName}... version --json`)
```
This allows arbitrary command execution via shell metacharacters (e.g.,
`--preset='pkg$(malicious command)'`).
## Expected Behavior
User-supplied package names are validated against a strict npm package
name regex before being passed to any shell command. The function now
uses `execFileSync` with an args array instead of `execSync` with string
interpolation, providing defense in depth:
1. **Input validation** — rejects anything that isn't a valid npm
package name
2. **Safe execution** — arguments are passed as an array so Node.js
handles escaping, rather than concatenating into a raw shell string
## Current Behavior
Given a project name like `:foo`, you can run tasks like `nx test foo`
(note `foo` vs `:foo`), but passing --help throws an error
## Expected Behavior
`--help` works the same with the shortname vs full name
## 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>
## Current Behavior
- `pom.xml` is only included as an input when a mojo uses default inputs
- Mojos with specific input configurations (like
`maven-compiler-plugin:compile`) don't get `pom.xml` in their inputs
- Parent `pom.xml` files aren't tracked as inputs
This can lead to stale cache hits when:
1. `pom.xml` changes but a mojo has specific input config
2. A parent `pom.xml` changes (affecting inherited properties,
dependency versions, plugin config)
## Expected Behavior
- Every target should include its own `pom.xml` as an input
- Every target should include ancestor `pom.xml` files (within the
workspace) as inputs
- Cache should invalidate when any relevant `pom.xml` changes
## Related Issue(s)
N/A - discovered during code review
## Changes
- **CacheConfig.kt**: Removed `pom.xml` from `defaultInputs` (now always
added explicitly)
- **MojoAnalyzer.kt**: Added `workspaceRoot` parameter and logic to walk
up the parent chain, adding all in-workspace ancestor `pom.xml` files as
inputs
- **NxProjectAnalyzerMojo.kt**: Pass `workspaceRoot` to `MojoAnalyzer`
## Current Behavior
When the daemon dies while processing a request, the reconnect logic
adds the retry back to the promise-based queue. However, the original
request is still blocked in the queue waiting for a response that will
never come (the socket is dead). This creates a deadlock:
1. Original request (`fn1`) is blocked awaiting a promise that will
never resolve
2. Retry request (`fn2`) is queued but can't execute until `fn1`
completes
3. `fn1` can't complete because it's waiting on the dead socket
## Expected Behavior
When reconnecting after daemon death, the retry should resolve the
pending promise that the original queue entry is waiting on, allowing
the queue to proceed normally.
## Related Issue(s)
<!-- No specific issue, discovered during development -->
## Solution
Instead of re-queuing the retry through `sendToDaemonViaQueue` (which
adds to the end of the queue), we now call `sendMessageToDaemon`
directly. This resolves the pending promise that `fn1` is waiting on,
allowing it to complete naturally and the queue to proceed.
Also removed the now-unused `decrementQueueCounter` method from
`PromisedBasedQueue`.
## Current Behavior
Only markdown and text file requests are tracked server-side via the
`track-asset-requests` edge function. HTML page views are not tracked on
the server, missing requests from AI tools and curl.
## Expected Behavior
Track all doc page views server-side with a new edge function that:
- Sends `server_page_view` events to GA4 with `content_type` param to
differentiate HTML/markdown/text
- Uses Netlify's `excludedPath` config for efficient path filtering
(zero compute for excluded paths)
- Skips non-HTML requests via Accept header check
### Changes
| File | Change |
|------|--------|
| `track-page-requests.ts` | **NEW** - Edge function for HTML page view
tracking on `/docs/*` |
| `track-asset-requests.ts` | Changed event name to `server_page_view`,
added `content_type` param |
| `add-link-headers.ts` | Refactored to use `excludedPath` config
instead of runtime path checks |
| `netlify.toml` | Added edge function declaration for
`track-page-requests` |
### GA Event Schema
```javascript
{
name: 'server_page_view',
params: {
content_type: 'text/html' | 'text/markdown' | 'text/plain',
file_extension: '.html' | '.md' | '.txt',
is_ai_tool: 'true' | 'false',
// ... other params
}
}
```
## Other Notes
This PR also removes the edge function entries from `netlify.toml` since
it's auto detected from `astro-docs/netlify/edge-functions`. This makes
all the configuration in the actual `.ts` file, not duplicated in the
`netlify.toml` file.
## Related Issue(s)
Closes DOC-395
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
<!-- 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 -->
The Gradle executor accepts a taskName option that *should not* contain
multiple space-separated tasks. When multiple tasks are provided, the
batch runner misinterprets the space-separated string as containing
project names rather than treating it as a single task argument, leading
to execution errors and confusion.
This only occurs if the taskName is manually overridden and should not
occur when task names are generated by the project graph plugin.
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
The Gradle executor now validates that taskName contains only a single
task without spaces. If multiple tasks are passed, it throws a clear
error message: "Task '[taskName]' contains spaces. Only a single Gradle
task is allowed per executor invocation." This prevents the batch runner
from misinterpreting the task name and provides immediate feedback to
users about the correct usage.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
<!-- 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
When running Gradle tasks in batch mode with atomized targets, if the
same task appears multiple times in the output (which happens when tasks
are atomized and executed separately), the batch runner only captures
the output from the last execution. Previous executions' output gets
overwritten because the splitOutputPerTask function replaces the entire
output for each task name it encounters.
## Expected Behavior
All output from a task should be preserved, even when the task appears
multiple times in the batch output. When the same task name is
encountered multiple times, the outputs should be concatenated rather
than replaced, ensuring developers can see the complete execution
history for atomized targets.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Plan for fixing .NET incremental builds documentation
- [x] Update the "Target dependencies" section to remove `restore` from
the `dependsOn` array
- [x] Add explanation about why `restore` cannot be run through Nx for
solutions using custom frameworks
- [x] Improve explanation clarity based on review feedback
- [x] Verify the "Target configuration" section is consistent with the
changes
- [x] Complete code review and address feedback
- [x] Run security checks (no issues found)
- [x] Address PR feedback: Use JSX `<Aside>` component with import
statement instead of Markdoc tag
## Summary
Successfully fixed the documentation issue in the .NET incremental
builds guide. The changes made:
1. **Removed `restore` from the build target's `dependsOn` array** - The
documentation now correctly shows `"dependsOn": ["^build"]` instead of
`"dependsOn": ["restore", "^build"]`, matching the actual implementation
in the plugin code.
2. **Added a clear explanation** - Included an aside box explaining why
`restore` is not in the `dependsOn` array: because Nx requires NuGet
package restoration to be completed before running any tasks, and
including it would create a circular dependency.
3. **Verified consistency** - Checked that the "Target configuration"
section already showed the correct configuration, ensuring all
documentation is now consistent.
4. **Used correct Starlight component** - Changed from Markdoc `{% aside
%}` tag to JSX `<Aside>` component with proper import statement per
Starlight documentation standards.
The changes align with the actual implementation in
`packages/dotnet/analyzer/Utilities/TargetBuilder.Build.cs` where the
build target's `dependsOn` is set to `[$"^{targetName}"]` (line 56).
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
>
> ----
>
> *This section details on the original issue you should resolve*
>
> <issue_title>docs(dotnet): implied conflict in dependsOn of inferred
build task</issue_title>
> <issue_description>### Documentation issue
>
> <!-- (Update "[ ]" to "[x]" to check a box) -->
>
> - [ ] Reporting a typo
> - [ ] Reporting a documentation bug
> - [ ] Documentation improvement
> - [x] Documentation feedback
>
> <!--
> If your issue is not regarding the documentation, please choose an
issue type:
> https://github.com/nrwl/nx/issues/new/choose
> -->
>
> ### Is there a specific documentation page you are reporting?
>
>
https://nx.dev/docs/technologies/dotnet/guides/incremental-builds#target-dependencies
>
> ### Additional context or description
>
> The code sample provides in this doc includes `"dependsOn":
["restore", "^build"]`, but the automatically inferred `build` target
from this plugin does not actually include the `restore` target in the
dependsOn array. I assume this is by design? The docs seem to confuse it
a bit.
> </issue_description>
>
> <agent_instructions>Remove the `restore` target from the dependsOn
block. Add a small explanation that we can't run restore through Nx
because Nx requires restore to have been completed prior to running
tasks if the solution uses a custom framework</agent_instructions>
>
> ## Comments on the Issue (you are @copilot in this section)
>
> <comments>
> </comments>
>
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixesnrwl/nx#34150
<!-- START COPILOT CODING AGENT TIPS -->
---
💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: AgentEnder <6933928+AgentEnder@users.noreply.github.com>
## Current Behavior
When socket data chunks split a multibyte UTF-8 character (e.g., CJK
characters like Korean, Chinese, Japanese) at an arbitrary byte
boundary, `Buffer.toString()` decodes incomplete byte sequences as
replacement characters (�), causing message corruption.
This can occur when:
- File paths contain non-ASCII characters
- Project names include multibyte characters
- Any JSON message contains international text
## Expected Behavior
Multibyte UTF-8 characters should be properly decoded even when split
across multiple socket data chunks. The fix uses Node.js `StringDecoder`
which buffers incomplete multibyte sequences until the remaining bytes
arrive.
## Related Issue(s)
Fixes socket message corruption for paths/names containing multibyte
characters.
- fix(nx-dev): always link headers regardless of mdoc or markdown
content source (generated vs static file)
- fix(nx-dev): make option/property columns in table linkable
- the table column header is matched on `options`, `option`,
`properties`, and property` (case insensitive)
https://github.com/user-attachments/assets/7250b9d5-1030-4ebc-9e21-0a05f295bbf5
Note bc mdoc and `renderMarkdown` go through 2 different rendering
pipelines, this logic must bc within the markdoc config and rehype
(markdown) processing logic. tried to shared logic where I could
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Current Behavior
After completing the CNW (Create Nx Workspace) flow with Nx Cloud, users
see a plain text completion message with a link to finish setup.
## Expected Behavior
Users now see one of four completion message variants controlled by
`NX_CNW_FLOW_VARIANT`:
- **Variant 0**: Plain link (control) - always used for enterprise URLs
- **Variant 1**: "Try the full Nx platform" decorative ASCII banner
- **Variant 2**: "Unlock 70% faster CI" decorative ASCII banner
- **Variant 3**: "Reclaim your team's focus" decorative ASCII banner
Key changes:
- Added enterprise URL detection (non-standard Nx Cloud URLs always get
variant 0)
- Locked the cloud prompt to always show "Try the full Nx platform?" (no
longer varies by flow variant)
- Flow variant now only affects the completion banner, not the prompt
- Added `snapshot.nx.app` to standard Nx Cloud hosts
- Removed variant 2 auto-connect behavior (all variants now prompt)
## Screenshots
Variant 0:
<img width="1392" height="1065" alt="variant0"
src="https://github.com/user-attachments/assets/0b18686e-1481-4fc0-995e-1577052887ff"
/>
Variant 1:
<img width="1392" height="1065" alt="variant1"
src="https://github.com/user-attachments/assets/e5909e2e-e1d8-4d04-9721-ba6186d06891"
/>
Variant 2:
<img width="1392" height="1065" alt="variant2"
src="https://github.com/user-attachments/assets/7e9f819f-e3c0-44d7-9760-0cab1d5dd9ac"
/>
Variant 3:
<img width="1392" height="1065" alt="variant3"
src="https://github.com/user-attachments/assets/83f0499f-d807-4dc8-9390-c7eec93590a9"
/>
## Related Issue(s)
Closes CLOUD-4147
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Enable Gradle executor to run tasks in batch mode in CI.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
We're showing over 8 GB of memory usage on Netlify, and 11+ GB on
Agents. Let's test out a few ways to reduce the memory footprint.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Preload vitest/node ESM module early in
buildViteTargets/buildVitestTargets
functions before parallel processing occurs. This prevents the
ERR_INTERNAL_ASSERTION error that occurs when multiple vitest.config
files
are processed in parallel on Node 24+.
Fixes#34028Fixes#33091
## Current Behavior
The `runCommandUntil` e2e utility function waits indefinitely for the
expected output to appear. If the output never appears (e.g., server
fails to start, different output format, port conflict), the test hangs
forever, causing CI jobs to run for hours before being killed.
## Expected Behavior
The function should timeout after a configurable duration and fail with
a clear error message showing what output was received.
## Related Issue(s)
Fixes hanging e2e tests observed in CI (e.g.,
`e2e-node:e2e-ci--src/node-server.test.ts` hung for 1h 21m).
## Changes
- Added optional `timeout` parameter to `runCommandUntil` opts (default:
5 seconds)
- On timeout: kills the process, logs the collected output, and rejects
with a clear error
- Existing call sites work unchanged; tests needing more startup time
can pass `{ timeout: 30000 }`
---------
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: FrozenPandaz <8104246+FrozenPandaz@users.noreply.github.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
1. The `check-links` task cache inputs only included `sitemap.xml` (the
index
file) and `sitemap-index.xml`, but not the actual `sitemap-0.xml` files
that
contain the URL data. This meant that when pages were added or removed,
the
cache wasn't properly invalidated - the check-links task would return a
cached "passing" result even when broken links existed.
2. The `/launch-nx` page was removed in #34183 but one link in
`astro-docs/src/content/docs/reference/Nx Cloud/release-notes.mdoc`
still
pointed to it. This link was masked by being in the `validate-links.ts`
ignore list.
## Expected Behavior
1. The `check-links` task cache is invalidated when sitemap URLs change
by
using glob patterns (`sitemap*.xml`) to include all sitemap files.
2. All links point to valid pages. The `/launch-nx` link now redirects
to
`/blog/launch-nx-week-recap`.
## Changes
- **astro-docs/release-notes.mdoc**: Updated `/launch-nx` link to
`/blog/launch-nx-week-recap`
- **astro-docs/validate-links.ts**: Removed `/launch-nx` from ignore
list (no longer needed)
- **nx-dev/project.json**: Fixed cache inputs to use `sitemap*.xml` glob
patterns
## Related Issue(s)
Fixes DOC-385
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Current Behavior
With Vite now providing additional options (environments etc) for
framework authors, vite.config files can be much more simple for the
user.
However, this often assumes that the `root` property will be set and
provided during `Vite CLI` invocation.
When we run `resolveConfig` to determine inputs and outputs, we do not
set this `root` and expect the user to have it in their vite config
file.
For some plugins/frameworks such as Tanstack Start - this causes the
plugin to error.
The `isBuildable` conditions is also not inclusive enough and can skip
projects that should be marked as buildable.
## Expected Behavior
Ensure that sophisticated vite plugins are supported with Nx
## Related Issue(s)
CLOSES NXC-3637
## Current Behavior
Template-generated workspaces use a generic link in the README instead
of a per-workspace short link for Nx Cloud setup.
## Expected Behavior
When users opt into Nx Cloud (or are auto-connected via variant 2), the
template README is updated with a personalized connect URL section that
helps them finish setting up their workspace.
---
BEFORE:
<img width="762" height="460" alt="image"
src="https://github.com/user-attachments/assets/58900071-1727-49d1-aa19-279c488b5037"
/>
AFTER:
<img width="1032" height="599" alt="image"
src="https://github.com/user-attachments/assets/a6e3a122-5807-4ba2-90dd-441e41a3280e"
/>
---
## Related Issue(s)
Closes NXC-3783
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
deno worker.stdout is a Readable/Writeable. To provide better deno
support an error should not be thrown
<!-- 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 -->
If `worker.stdout` or `worker.stderr` are not instanceof Socket then nx
throws an error. This is problematic in Deno where `stdout` and `stderr`
are Readable/Writable and not Socket.
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
`startupPluginWorker` function should work in Deno runtime
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
- https://github.com/denoland/deno/issues/31961
- https://github.com/oven-sh/bun/issues/26505
---------
Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com>
## Current Behavior
Plugin workers occasionally fall over during the start up steps.
## Expected Behavior
Improves some issues with the error handling when loading plugin workers
and adds some more logs to help understand what's went wrong here.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Remove redundantly placed period and make error message construction
more readable when facing AggregateError
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes NXC-3766
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
<!-- 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 -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
--rerun is not tooling api compatible and therefore will break usage of
the batch executor. Replaced the flag with --rerun-tasks.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
The Maven batch runner bundles Maven 4 classes at compile time, which:
- Creates a large JAR file (~50+ MB)
- Only works with Maven 4
- Has classloader conflicts with Maven's own SLF4J
## Expected Behavior
The batch runner loads Maven classes at runtime from `MAVEN_HOME`,
which:
- Creates a small JAR (~2 MB) with no bundled Maven dependencies
- Works with both Maven 3.x and Maven 4.x
- Avoids classloader conflicts by isolating Maven in its own ClassRealm
- Outputs clean Maven-style logs (`[INFO]`, `[WARNING]`, etc.)
## Implementation
### Architecture
```
batch-runner.jar (NO Maven dependencies)
├── MavenClassRealm → Loads Maven JARs from MAVEN_HOME at runtime
├── ResidentMavenExecutor → Maven 4 executor (reflection-based)
├── CachingMaven3Invoker → Maven 3 executor (reflection-based)
└── nx-maven-adapters/ → Pre-compiled adapter JARs (embedded as resources)
├── batch-runner-adapters-maven3.jar
└── batch-runner-adapter-maven4.jar
```
### Key Changes
1. **Removed compile-time Maven dependencies** from batch-runner module
2. **Created batch-runner-adapters** modules for Maven 3 and Maven 4
specific code
3. **Implemented MavenClassRealm** to load Maven JARs from MAVEN_HOME at
runtime
4. **Implemented reflection-based executors** that load adapter JARs
into ClassRealm
5. **Fixed SLF4J logging** with System.out redirection for clean
Maven-style output
6. **Added shared module** for BuildStateManager, BuildStateApplier, and
BuildStateRecorder
### Benefits
- **Version agnostic**: Same JAR works with Maven 3.x and 4.x
- **Graph caching**: Project dependency graph built once, reused across
tasks
- **Build state persistence**: compile → package → install works
correctly
- **No classloader conflicts**: Maven's classes isolated in their own
ClassRealm
- **Clean output**: Standard Maven log format without SLF4J noise
## Related Issue(s)
N/A - Internal refactoring for better Maven version support
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: FrozenPandaz <FrozenPandaz@users.noreply.github.com>
## Current Behavior
LLMs and CLI tools must explicitly request the `.md` URL suffix to get
raw markdown content from documentation pages.
## Expected Behavior
When a client requests a docs page with `Accept: text/markdown` header,
the edge function rewrites to serve the `.md` version directly (no
redirect). This enables LLM tools to get markdown content by requesting
the standard URL.
Behavior:
- `Accept: text/markdown` → serves .md content (via rewrite, no
redirect)
- Default (browsers) → serves HTML with Link headers (unchanged)
Examples:
```
curl -H 'Accept: text/markdown' https://deploy-preview-34239--nx-docs.netlify.app/docs/getting-started/intro
curl -H 'Accept: text/markdown' https://deploy-preview-34239--nx-docs.netlify.app/docs/getting-started/tutorials/angular-monorepo-tutorial
```
Uses Netlify Edge Function rewrite (returns URL object) instead of
redirect for single-request response that works with all HTTP clients.
## Related Issue(s)
Closes DOC-389
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Current Behavior
broken links
## Expected Behavior
links aren't broken any more and are updated to expected pages from docs
page.
## Related Issue(s)
Fixes DOC-391
This PR adds:
1. `llms-full.txt` that is a full copy of our docs in markdown.
2. HTTP `Link` headers to our docs HTML pages so that they point to the
`.md` (markdown) version, and also to `llms.txt` and `llms-full.txt`.
The `llms-full.txt` is currently at 2.7 MB, which is much less than
other sites that are up to 5MB or more.
<img width="569" height="35" alt="Screenshot 2026-01-27 at 12 11 10 PM"
src="https://github.com/user-attachments/assets/61be02b4-2813-4c39-951c-d831af83e823"
/>
First 100 lines of `llms-full.txt`:
````
# Nx Documentation
> Complete Nx documentation compiled into a single file for LLM consumption.
Nx is a powerful, open source, technology-agnostic build platform designed to efficiently manage codebases of any scale. From small single projects to large enterprise monorepos, Nx provides intelligent task execution, caching, and CI optimization.
This file was generated from 503 documentation pages.
Individual pages are available at: https://nx.dev/docs/{slug}.md
# Quickstart
---
<!-- source: https://nx.dev/docs/quickstart.md -->
## Quickstart with Nx
Get up and running with Nx in just a few minutes by following these simple steps.
{% steps %}
1. Install the Nx CLI
Installing Nx globally is **optional** - you can use `npx` to run Nx commands without installing it globally, especially if you're working with Node.js projects.
{% tabs syncKey="install-method" %}
{% tabitem label="npm" %}
```shell
npm add --global nx
```
**Note:** You can also use Yarn, pnpm, or Bun
{% /tabitem %}
{% tabitem label="Homebrew (macOS, Linux)" %}
```shell
brew install nx
```
{% /tabitem %}
{% tabitem label="Chocolatey (Windows)" %}
```shell
choco install nx
```
{% /tabitem %}
{% tabitem label="apt (Ubuntu)" %}
```shell
sudo add-apt-repository ppa:nrwl/nx
sudo apt update
sudo apt install nx
```
{% /tabitem %}
{% /tabs %}
2. Start fresh or add to existing project
For JavaScript-based projects you can **start with a new workspace** using the following command:
```shell
npx create-nx-workspace@latest
```
**Add to an existing project: (recommended also for non-JS projects)**
```shell
npx nx@latest init
```
**Get the complete experience:**
For a fully integrated development workflow with AI-powered CI features, [start directly from Nx Cloud](https://cloud.nx.app/get-started).
Learn more: [Start New Project](/docs/getting-started/start-new-project) • [Add to Existing](/docs/getting-started/start-with-existing-project) • [Complete Nx Experience](https://cloud.nx.app/get-started)
3. Run Your First Commands
Nx provides powerful task execution with built-in caching. Here are some essential commands:
**Run a task for a single project:**
```shell
nx build my-app
nx test my-lib
```
**Run tasks for multiple projects:**
```shell
nx run-many -t build test lint
```
Learn more: [Run Tasks](/docs/features/run-tasks) • [Cache Task Results](/docs/features/cache-task-results)
4. What's next?
Now that you've experienced the Nx basics, choose how you want to continue:
````
## Related Issue(s)
Closes DOC-236
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
This design refresh emphasizes the contrast variant aesthetic across all
hero sections, pricing cards, and primary call-to-actions.
- Change 47 instances of variant="primary" to variant="contrast"
- Update ui-courses to use variant="secondary" for GitHub link
- Prefer high-contrast inverted style for primary CTAs
- Maintain proper visual hierarchy with secondary actions
- Replace all slate-* classes with zinc-* equivalents (1,158 instances)
- Replace all sky-* classes with blue-* equivalents (210 instances)
- Update opacity variants, gradients, rings, and borders
- Maintain full dark mode compatibility
## Current Behavior
If something is in `package.json#dependencies`, we still suggest it to
be `nx add`-ed during `nx import`
## Expected Behavior
If a plugin is already installed, we don't suggest it anymore
`@nx/web:app` generator is incorrectly calling `createOrEditViteConfig`
when bundler != vite and unitTestRunner = vitest.
Ensure it is using the correct file
## Current Behavior
The migration for svgr requires using file-loader which is unmaintained.
## Expected Behavior
Use asset/resource instead of file-loader
## Related Issue(s)
CLOSES NXC-3667
## Summary
- When `/tmp` is mounted with `noexec`, loading native modules from the
cache fails silently and causes Nx to hang indefinitely
- This adds a fallback to load from `node_modules` when permission
errors occur
## Problem
Users with `/tmp` mounted with `noexec` (a common security hardening
practice) experience Nx hanging forever, even for simple commands like
`nx --version`.
The root cause:
1. Nx copies native `.node` files to `/tmp` to avoid Windows file
locking issues
2. On `noexec` mounts, execution fails with `EACCES`/`EPERM`
3. The error wasn't caught, leading to broken native bindings and
infinite loops
## Solution
Catch permission errors when loading from the cache and fall back to the
original `node_modules` location. This:
- Works automatically without user config
- Preserves Windows file locking fix (only falls back when needed)
- No error messages for users
Closes#33991
<!-- 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 -->
Schema validation (done, for instance, when calling an executor) fails
when an option has value "null" and schema accepts null values. I had it
in a custom executor for `nx-release-publish`, that understands that
`nxReleaseVersionData` is implicitly passed, so I define its schema:
```json
"newVersion": {
"type": ["string", "null"],
"description": "The new version of the project, null if no changes detected"
}
```
My code calls `getReleaseClient().releaseVersion(options)`, which gets
me a `projectsVersionData` object with version info. It contains `null`
values (allowed). I then pass it, and ends up in:
```typescript
// nx/src/tasks-runner/task-orchestrator.ts:531-539
const combinedOptions = combineOptionsForExecutor(
task.overrides, // ← Contains nxReleaseVersionData with null values
task.target.configuration,
targetConfiguration,
schema, // ← Schema from executor
task.target.project,
relativeCwd,
isVerbose
);
```
which fails inside:
```typescript
// nx/src/utils/params.js:126-201
function validateObject(opts, schema, definitions) {
// Line 191-200: Iterate through all properties
Object.keys(opts).forEach((p) => {
validateProperty(
p, // "nxReleaseVersionData"
opts[p], // { foo: { newVersion: null, ... }}
(schema.properties ?? {})[p], // schema for nxReleaseVersionData
definitions
);
});
}
```
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR -->
`null` values should be considered, as they are valid in JSON schemas. It was probably not considered, because we never think that `typeof null === "object"`, but it's unfortunately the case.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is merged. -->
I will create one
Fixes https://github.com/nrwl/nx/issues/34169
Transpiling through SWC or ts-node is slow compared to the native type
stripping that Node.js provides.
This PR adds `NX_PREFER_NODE_STRIP_TYPES` to allow users to use Node.js
built-in TypeScript support. There are some features that need
transpilation that won't work with type stripping:
- Enum declarations
- namespace with runtime code
- legacy module with runtime code
- parameter properties
- path aliases
See: https://nodejs.org/api/typescript.html#full-typescript-support
The speed-up is significant. My test workspace went from 22s to ~2s to
compute from cold cache.
Demo: https://www.loom.com/share/ce1db29e501b46d58109ffeec8a7a649
In the future we should enable this by default, and users have to turn
it off to use SWC/ts-node.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
Right now GA only collects page views. This PR allow us to see which
`.md` files are being used. There are mostly useful for AI agents to
fetch without using too many tokens. We want to track usage so we can
see what techniques to guide agents actually work.
## Related Issue(s)
Closes DOC-386
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This PR makes it so Cloud commands like `npx nx record` and `npx nx
fix-ci` still work without `nxCloudId`. We'll log a warning so that
`ci.yml` using these commands will still work. The warning let's users
know that these do not work without being connected.
Closes #NXC-3753
<!-- 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
Commit linting does not mention requirement for commit message to be
lowercase.
<!-- This is the behavior we have today -->
## Expected Behavior
Hook message should instruct user to use all lowercase for commit
message.
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
<!-- 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
Self-healing docs only reference being supported for GitHub, Azure, and
GitLab.
<!-- This is the behavior we have today -->
## Expected Behavior
We should show instructions for all currently supported vcs providers,
including Bitbucket.
<!-- This is the behavior we should expect with the changes in this PR
-->
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
Resizing the TUI while in inline view kinda breaks things. Its
unfortunate, I'm not sure there's a ton to be done, but this PR explores
some solutions
## Expected Behavior
The TUI is less sensitive to resize events with inline mode
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
The create-nx-workspace (CNW) command includes A/B testing variant 1
which skips the cloud prompt under certain conditions.
## Expected Behavior
Revert to the previous behavior where the cloud prompt flow is
consistent without the A/B testing variant.
## Related Issue(s)
This reverts commit 2039a5e119 from PR
#34106.
## Current Behavior
The internal link checker reports 3 broken links pointing to
`/launch-nx`:
- `/blog/2024-02-05-nx-18-project-crystal.md`
- `/blog/2024-02-15-launch-week-recap.md`
- `/changelog/18_0_0.md`
The `/launch-nx` page was a temporary page for the Nx 18 launch event in
February 2024 and no longer exists.
## Expected Behavior
All internal links should point to valid pages. Links to the old launch
page now redirect to the Launch Nx Week recap blog post.
## Related Issue(s)
Fixes the internal link checker errors.
## Current Behavior
There's a hard to reproduce hang that happens occasionally when running
the TUI
## Expected Behavior
We think this should fix it
## 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>
The `banner.json` was committed as a fallback if we're not using Framer
to control the banner yet on nx.dev. Now that it is verified we can
remove the committed file.
Closes DOC-381
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Previously, `nx reset --onlyDaemon` would only stop the daemon process
but not clean up the daemon files in `.nx/workspace-data/d`. This change
ensures the daemon workspace data directory is also removed when using
the `--onlyDaemon` flag, consistent with the behavior of a full reset.
Co-authored-by: MaxKless <MaxKless@users.noreply.github.com>
This PR updates the releases page so v22 is included.
Closes #DOC-382
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
1. When running a task with dependencies (e.g., `nx serve app` where app
depends on app2:serve), the initiating task might not be selected on
startup. Additionally, the auto-select logic could switch selection to
the initiating task at any time when it started running - even minutes
later - which felt "random" to the user.
2. When pressing Enter on an already-pinned task, it would unpin the
task, causing the pane to disappear while focus remained on it
(invisible-but-focused state).
## Expected Behavior
1. The initiating task (the one the user actually requested) should be
selected during init, and selection should never unexpectedly change
later when tasks start.
2. Pressing Enter on an already-pinned task should focus the pane, not
unpin it.
## Changes
- **Select initiating task during init**: Moved initiating task
selection to `init()` in app.rs. This only applies in `RunOne` mode
since in `RunMany` there's no single initiating task to prioritize.
Removed the "switch to initiating task" logic from `start_tasks()` in
tasks_list.rs.
- **Focus pane on Enter**: Changed behavior so pressing Enter on an
already-pinned task focuses the pane instead of unpinning it.
## Related Issue(s)
N/A - discovered during TUI testing
# Current Behavior
The `beforeCompile` hook is registered inside the `watchRun` hook,
causing a new handler to be added on every recompilation. This leads to
handler accumulation, where setup operations (building static remotes,
starting file server, starting proxies) are triggered multiple times
during watch mode.
# Expected Behavior
Hooks should be registered once, outside of other hooks, to prevent
accumulation. Setup operations should only run once, not on every
recompilation.
# Related Issue(s)
Fixes https://github.com/nrwl/nx/issues/34141
# Current Behavior
1. **Performance bottleneck**: `statsValue.toJson()` is called with no
options, causing full serialization of all stats data on every build.
This is expensive and unnecessary when only budget checking is needed.
2. **Redundant work**: Budget checking code runs even when no budgets
are configured or when targeting server platform.
3. **User stats config ignored**: Custom stats configuration provided
via `rspackConfigOverrides` is not respected by the stats logger.
4. **Double serialization**: `rspackStatsLogger` calls `stats.toJson()`
without passing the stats options, ignoring user preferences.
# Expected Behavior
1. Only serialize what's needed for budget checking (`assets` and
`chunks`), significantly reducing overhead.
2. Early exit when budgets are not configured or on server platform,
skipping expensive `toJson()` entirely.
3. User's stats configuration is merged with defaults and respected
throughout the build output.
4. `rspackStatsLogger` uses the provided `statOptions` when serializing
stats.
# Related Issue(s)
Fixes https://github.com/nrwl/nx/issues/34145
## Current Behavior
CNW always shows the "Try the full Nx platform?" prompt and connects to
Nx Cloud to generate an onboarding URL with a token.
## Expected Behavior
For A/B testing variant 1:
- Skip cloud prompt
- Skip connectToNxCloudForTemplate() - no nxCloudId in nx.json
- Skip readNxCloudToken() - no misleading spinner
- Use GitHub flow for URL generation (accessToken: null)
- Show github.com/new hint when user hasn't pushed
Also fixes:
- Expired cache file bug: now deletes with unlinkSync() instead of
ignoring, which caused 50-50 randomization after 1-week expiry
- Adds variant-X to short URL meta property for cloud analytics
## Related Issue(s)
Closes NXC-3628
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Current Behavior
`getNxRequirePaths` returns paths in the order `[root,
getNxInstallationPath(root)]`, which means the workspace root is checked
first when resolving modules.
## Expected Behavior
The nx installation path (`.nx/installation`) should be prioritized and
checked first before falling back to the workspace root. This ensures
that modules from the nx installation directory take precedence.
## Related Issue(s)
N/A
# Current Behavior
The Angular Rspack compiler's `ComponentStylesheetBundler` does not
receive Tailwind or PostCSS configuration. This means Tailwind
directives (like `@apply`, `@tailwind`) in component stylesheets are not
processed, resulting in broken styles.
# Expected Behavior
Component stylesheets should support Tailwind CSS and PostCSS
configurations, matching the behavior of the standard Angular CLI build
process.
# Related Issue(s)
https://github.com/nrwl/nx/issues/34098
# Current Behavior
1. **Handler accumulation**: The `compilation`, `beforeCompile`, and
`done` hooks are registered inside `watchRun`, causing new handlers to
be added on every rebuild cycle. This leads to performance degradation
and duplicate operations during watch mode.
2. **Double rebuilds**: Rapid filesystem events (e.g., editor
backup/swap files) trigger multiple rebuilds because there's no
aggregation timeout configured.
3. **No watchOptions configuration**: Users cannot customize watcher
behavior (aggregateTimeout, ignored patterns, etc.).
# Expected Behavior
1. Hooks should be registered once outside of `watchRun` to prevent
accumulation. Shared state is used to pass data between watch cycles and
compilation hooks.
2. A default `aggregateTimeout: 50` batches rapid filesystem events to
prevent double rebuilds.
3. Users can provide custom `watchOptions` to configure watcher
behavior, with user options taking precedence over defaults.
# Related Issue(s)
https://github.com/nrwl/nx/issues/34142#issuecomment-3767571208
## Current Behavior
New task processes show 0% CPU on their first measurement because no
baseline exists. Accurate readings only appear on the second collection
cycle (~1s later).
## Expected Behavior
New task processes get accurate CPU readings on their first measurement.
The collector establishes CPU baselines for newly registered processes
~250ms before collection, giving `sysinfo` enough time to calculate
accurate CPU deltas.
## Technical Details: Baselining & Collection Flow
The collection loop runs in 4 phases:
```
T=0ms T=750ms T=1000ms T=1750ms T=2000ms
| | | | |
v v v v v
Collect → Sleep(750ms) → Baseline → Sleep(250ms) → Collect → ...
```
1. **Collect**: Refresh all processes and gather metrics
2. **Post-collection sleep**: Wait until baseline time (interval -
250ms)
3. **Baseline**: Bulk CPU refresh for newly registered PIDs (if any)
4. **Pre-collection sleep**: Wait 250ms for accurate CPU delta
calculation
## Current Behavior
if inline tui init fails, we panic
## Expected Behavior
If inline tui init fails, inline mode is disabled. We show the reason
its disabled when someone tries to use it.
## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
Fixes #
## Current Behavior
When exiting the TUI (especially via Ctrl+C), escape sequences leak to
the terminal:
```
^[]11;rgb:2121/2121/2121^[\^[[55;1R^[[?62;22;52c
```
This happens because the TUI queries terminal background color via OSC
11 to detect dark/light mode. The terminal responds with an escape
sequence, but if the program exits before fully consuming the response,
it appears in the terminal output.
## Expected Behavior
Clean terminal state after TUI exits, with no escape sequence artifacts.
## Related Issue(s)
N/A - discovered during development
## Solution
Added `drain_stdin()` function that polls and consumes any pending
terminal events before disabling raw mode. This clears any lingering OSC
responses (like the background color query response) before the terminal
is restored.
```rust
fn drain_stdin() {
use std::time::Duration;
while crossterm::event::poll(Duration::from_millis(5)).unwrap_or(false) {
let _ = crossterm::event::read();
}
}
```
The 5ms timeout is long enough to catch pending responses but short
enough not to noticeably delay exit.
## Current Behavior
When the daemon encounters a project graph error during task hashing, it
extracts the partial project graph from the error and continues hashing
tasks. This can produce incorrect hashes since the graph is incomplete.
## Expected Behavior
The error should be thrown immediately, preventing any hashing attempts
with an invalid project graph. This ensures we don't produce incorrect
task hashes that could lead to cache issues.
## Related Issue(s)
N/A - Bug fix discovered during development
## Current Behavior
The `rollup-plugin-postcss` has not released a new version in 4 years.
The deps it depends on are outdated and starting to cause problems with
peer-dep conflicts.
## Expected Behavior
Recreate the plugin within the `@nx/rollup` package to maintain the
functionality/behaviour and manage dependencies ourselves.
## Related Issue(s)
Closes NXC-3644
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
Co-authored-by: Coly010 <Coly010@users.noreply.github.com>
## Current Behavior
Running `nx test` with Vitest browser mode fails with error:
> "The browser configuration must have a 'name' property"
Array configs like `browser.instances` and `reporters` get duplicated,
breaking tests.
## Expected Behavior
Vitest browser mode and array configurations work correctly without
duplication.
## Related Issue(s)
Fixes#33591
## Current Behavior
Nx's AI agent detection currently identifies Claude Code, Repl.it, and
Cursor AI agents via environment variables, but does not detect
OpenCode.
## Expected Behavior
Nx should also detect when running under OpenCode AI agent by checking
for the `OPENCODE` environment variable, which OpenCode sets to `1` when
active.
## Related Issue(s)
N/A - Feature addition to improve AI agent detection coverage.
## Changes
- Added `is_opencode_ai()` function in
`packages/nx/src/native/utils/ai.rs`
- Updated `is_ai_agent()` to include OpenCode detection
- Added corresponding unit tests
## Current Behavior
For optional packages that are not installed when using yarn, we
currently add the package version to the key for the hash. NPM and PNPM
do not do this.
The results in inconsistent hashes for package dependencies when running
in different environments. For example, trying to use the cache created
in CI on linux on a mac where native dependencies are used.
**yarn on arm mac**
_note how the key has the version in it for the linux and x64 versions
that are not installed_
```
$ nx test foo | grep @nx/nx-
...
"npm:@nx/nx-darwin-x64@22.3.3": "14042642002999097748",
"npm:@nx/nx-linux-x64-gnu@22.3.3": "12169496858981304476",
"npm:@nx/nx-darwin-arm64": "1683411334940043113",
```
**npm on arm mac**
```
$ nx test foo | grep @nx/nx-
...
"npm:@nx/nx-darwin-x64": "14042642002999097748",
"npm:@nx/nx-darwin-arm64": "1683411334940043113",
"9980946580833020728",
"npm:@nx/nx-linux-x64-gnu": "12169496858981304476",
```
**pnpm on arm mac**
```
$ nx test foo | grep @nx/nx-
...
"npm:@nx/nx-darwin-arm64": "1683411334940043113",
"npm:@nx/nx-darwin-x64": "14042642002999097748",
"npm:@nx/nx-linux-x64-gnu": "12169496858981304476",
```
## Expected Behavior
Optional dependencies should be handled the same way from a hashing
perspective as installed dependencies.
**yarn on arm mac**
```
$ nx test foo | grep @nx/nx-
...
"npm:@nx/nx-darwin-x64": "14042642002999097748",
"npm:@nx/nx-linux-x64-gnu": "12169496858981304476",
"npm:@nx/nx-darwin-arm64": "1683411334940043113",
```
On test repo reduces the
`nx/js/dependencies-and-lockfile:createDependencies`:
- from `10506ms`
- to `3665ms`
## Current Behavior
<!-- This is the behavior we have today -->
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
## 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>
## Current Behavior
CPU metrics collection can report inaccurate values where:
- Individual processes show inflated CPU usage
- Total CPU aggregation across all processes exceeds the system's
maximum available CPU
- This leads to confusing and misleading metrics data
## Expected Behavior
CPU metrics accurately reflect actual resource usage:
- Process CPU values are accurate
- Total CPU aggregation stays within system limits
- Metrics data is reliable and trustworthy
### Additional Notes
- **Root cause**: When registering a new process, we established a CPU
baseline by refreshing only that single process via `sysinfo`.
Internally, `sysinfo` calculates CPU% as `(process_cpu_time_delta /
wall_time_delta) * 100`. Refreshing a single process updates the wall
time reference but leaves the CPU time baselines of other processes
unchanged. In the next metrics collection, these other processes appear
to have consumed their CPU time over a shorter wall time period (based
on the last baseline), resulting in inflated percentages (e.g., 200%+
for single-threaded processes).
- This PR also improves initialization performance by only loading
necessary system data (processes, CPU, memory) instead of all system
information
- Upgrades `sysinfo` dependency to v0.37.2, which includes upstream CPU
measurement improvements.
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
## Current Behavior
Marketing pages (homepage, /react, /java, etc.) do not track scroll
depth. Only docs pages have scroll tracking via the ScrollableContent
component.
## Expected Behavior
Marketing pages now track scroll depth and fire scroll_0, scroll_25,
scroll_50, scroll_75, scroll_90 events to Google Analytics, matching the
existing docs page behavior.
## Related Issue(s)
Closes DOC-376
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Current Behavior
The inline tui runs some terminal escape codes to check cursor position,
these break when stdin isn't a tty (like in a git hook)
## Expected Behavior
The inline tui is disabled if stdin isn't a tty
## 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>
## Current Behavior
The `migrations.json` file in the `@nx/maven` package is not included in
the `files` array in `package.json`. This means when the package is
published to npm, the migrations file is not included, preventing users
from running migrations.
## Expected Behavior
The `migrations.json` file should be included in the published package
so that Nx can discover and run migrations when users upgrade.
## Related Issue(s)
N/A - discovered during development
Exclude some handwritten files from the native build outputs. When those
files are updated in isolation, the build can replace them with stale
cached outputs. This is because they are not inputs of the native
builds, but are incorrectly stored as outputs of the native builds.
- run:echo "We are in the process of transitioning from Circle CI to GitHub Actions. For details about your build results, consult github actions build logs."
"check violations", when given a sandbox report JSON file or URL to investigate,
or when the user pastes a staging.nx.app sandbox-report URL. Also trigger when
discussing unexpected reads/writes in Nx task execution. Guides structured
investigation of why tasks read/write undeclared files, determines root causes,
and recommends fixes.
argument-hint: '<sandbox-report.json or URL> [--filter <file|pattern|list>]'
allowed-tools: Bash, Read, Grep, Glob
---
# Diagnose Sandbox Report
## Overview
Sandbox violations occur when an Nx task reads files not declared as inputs or writes files not declared as outputs.
**Unexpected reads** are one of:
1.**Missing input** (most likely) — the process legitimately needs this file. Understand what the process does and why the access makes sense, then declare it as an input.
2.**Potential sandboxing gap** (last resort) — the access is irrelevant to correctness and should be filtered/ignored by the sandbox. Only conclude this after exhausting every possibility for it being a missing input.
**Unexpected writes** follow the same logic:
1.**Missing output** (most likely) — the process legitimately produces this file.
2.**Potential sandboxing gap** (last resort) — same as above.
The default assumption is that an unexpected access IS a missing declaration. The investigation's job is to understand WHY the process accesses the file — not to find reasons it shouldn't.
## Critical Rules
1.**NEVER read the sandbox report JSON directly** — these files are too large for the Read tool (50K+ tokens). Do NOT use `Read`, `cat`, `head`, `python3`, or `jq` on the raw report. All report parsing is handled by the script.
2.**ALWAYS run the context-gathering script as the very first step** — no manual parsing, no ad-hoc python/jq on the report file. The script does everything deterministically.
3. If the script fails, **report the error and stop**. Do not attempt manual parsing as a fallback.
4.**Identify the inferring plugin BEFORE proposing any fix** — check `inference.plugin` in the script output or run `jq '.targets.<target>.metadata' <detail-file>`. Fixing the wrong plugin wastes entire investigation rounds.
5.**Verify hypotheses empirically before committing to them** — see Principle 4 and the Phase 2 instrumentation guidance.
## Workflow
### Phase 0: Input
User provides one of:
- Path to a sandbox report JSON file
- A URL to a sandbox report — pass it directly to the script, it handles downloading
- A task ID + CIPE URL (fetch report via MCP if available)
- Inline violation data
If a task ID is provided but no report, ask the user for the report file.
**Filtering**: Most invocations will focus on specific files, not the entire report. The user may specify:
- A single file: `e2e.log`
- A comma-separated list: `apps/nx-cloud/e2e.log,apps/nx-cloud/build/client/assets/main.js`
- A glob pattern: `*.tsbuildinfo`, `apps/nx-cloud/build/**`
- A directory prefix: `apps/nx-cloud/build/client/assets`
When the user specifies files to focus on, pass them via `--filter` to the script. When they don't specify a filter and the report has many violations, summarize the groupings (by directory, extension) and ask which group(s) to investigate first rather than trying to investigate everything at once.
### Phase 1: Deterministic Pre-Processing
Run the context-gathering script **immediately** — this is the first tool call after reading the user's input.
Call it exactly as shown — do NOT append `2>&1` or `2>/dev/null` (the script manages its own stderr internally). Run in the **foreground** (no `run_in_background`) with a **3-minute timeout** — reports can be large and the script runs the task + multiple nx commands:
```bash
npx tsx ${CLAUDE_SKILL_DIR}/scripts/gather-sandbox-context.ts <report.json or URL> [--filter <pattern>][--workspace <path>]
```
Pass `--filter` when the user wants to focus on specific files or patterns. The script filters violations before all downstream processing (grouping, validation, classification), so the output only contains relevant data.
The script produces two outputs:
**stdout** (~3-5KB compact brief) — everything needed to start investigating:
-`summary`: violation counts (total, filtered, confirmed vs undeclared)
-`undeclaredFiles`: the actual file paths that are true violations
-`grouping`: violations grouped by directory and extension
-`commands`: processes with violations (pid, cmd, executable, arguments, counts) — no full file lists
-`classificationSummary`: counts per category (cross-project, build artifacts, config files, etc.)
-`crossProjectDependencyCheck`: whether cross-project file owners are in the task's dependency chain
-`staleDeclarations`: grouped analysis of expectedInputsNotRead / expectedOutputsNotWritten
-`dependentTasksOutputFiles`: extracted from target inputs config and named inputs — shows what dep output globs are declared (critical for cross-project violations)
-`executorInfo`: executor name and resolved source path in `node_modules` — read this file to understand how the tool is invoked
-`checkSample`: results of `--check` on up to 5 undeclared files (catches false positives early)
Read the brief output — it has everything to start. Use `jq` on the detail file only when you need to drill into specific sections. When querying the detail file, use the structure above — do not guess the schema. Do NOT use Python, ad-hoc scripts, or the Read tool on the detail file — only `jq`.
For reports with many violations, use `--filter` to narrow scope. When investigating without a filter, use the `grouping` data to identify patterns and prioritize — don't try to trace every file individually.
If `summary.undeclaredReads` and `summary.undeclaredWrites` are both 0, all violations were resolved by the script's validation against resolved inputs/outputs. Report this to the user — no further investigation needed.
The `commands` array pre-parses each process — use `executable` and `arguments` to identify the tool without re-parsing `cmd`. When many files share the same root cause, group them under one finding using a glob pattern or count (e.g., "88 `.d.ts` files matching `packages/nx/dist/**/*.d.ts`").
### Phase 2: Command Analysis — the core investigation
**This is the most important phase.** The goal is to determine with 100% certainty why each process reads or writes each violated file. Do not classify violations from file names or paths alone — trace the actual causal chain from command → config → file access.
#### Step 1: Understand the command
The brief's `commands` array pre-parses each process. Use the `executable` and `arguments` fields directly — don't re-parse `cmd`. Identify:
- The tool (from `executable`)
- The arguments (target files/dirs, config flags, extensions — from `arguments`)
- The working directory (from executor options or project root)
#### Step 2: Trace why the command accesses each violated file
For each violated file, establish the **exact causal chain** that leads the command to read or write it. The approach is the same regardless of tool:
1. Identify the tool's config file (usually in the project root or workspace root)
2. Read the config and trace file references: `includes`, `extends`, `presets`, entry points, plugins
3. Follow the reference chain until you can explain exactly why the violated file is accessed
- **Directory traversal**: tool scans a directory for matching files and reads everything, including files it won't process (e.g., jest-haste-map scanning `.next/`, eslint reading `.d.ts` alongside `.ts`)
- **Dependency resolution**: tool resolves imports/requires and follows the dependency graph to files outside the project (e.g., esbuild/vite/webpack resolving workspace packages to their dist outputs)
- **Plugin/transformer loading**: tool loads plugins or transformers that read additional files (e.g., ts-jest loading tsconfig for TypeScript compilation)
For any tool, read its source code in `node_modules` to understand its file discovery behavior. Don't assume — trace the actual code.
**You must be able to explain the full path:** e.g., "eslint loads `.eslintrc.json` → configures `@typescript-eslint/parser` → parser resolves `parserOptions.project` → walks up to find `tsconfig.json` → reads it." If you can't trace the full path, keep investigating — do not guess.
**When theoretical analysis is inconclusive, verify empirically.** For difficult cases, instrument `node_modules` with interceptors to capture real stack traces. For example, patch `fs.readFileSync` in the tool's entry point to log stack traces when the violated file is accessed. A confirmed stack trace is worth more than multiple rounds of code reading.
#### Step 3: Confirm the violation with `--check`
**This step is mandatory — do not skip it.** The script already runs `--check` on a sample of up to 5 undeclared files (see `checkSample` in the brief). Review those results first — if the sample files are confirmed as inputs/outputs, the corresponding violations are false positives.
For files not in the sample, use the pre-generated commands from `verificationCommands` in the brief:
```bash
npx nx show target inputs <project>:<target> --check <violated-read-files>
npx nx show target outputs <project>:<target> --check <violated-write-files>
```
If the commands fail because output files don't exist (e.g., the script's task run timed out), run the task first with `verificationCommands.runTask`.
If `--check` shows the file IS already an input/output, the violation is a false positive from the script's static analysis. If it confirms the file is NOT an input/output, proceed to classification.
#### Step 4: Classify
With the causal chain established and the violation confirmed, classify into one of these categories:
1.**Missing input/output** (most common) — the process legitimately needs this file. Understand why:
- **Direct dependency** — the tool needs this file to do its job (e.g., tsc reads referenced tsconfigs, eslint loads config chain)
- **Transitive dependency** — a config file references another file that references this one (e.g., jest preset → resolver → module). Trace the full chain.
- **Directory traversal side effect** — the tool reads all files in a directory even if it only processes some (e.g., eslint reads `.d.ts` files while linting `.ts`). Still a legitimate access from the tool's perspective.
2.**Bad tool configuration** — the tool accesses a file it shouldn't because its scope is too broad. The fix is fixing the tool's config, NOT adding an input. Investigate:
- Is the command targeting too broad a directory? (e.g., `eslint .` instead of `eslint src/`)
- Is a config file missing ignore/exclude rules? (e.g., eslint processing a file type it should skip)
- Is a plugin inferring a target for a project that doesn't match? (e.g., eslint target on a non-JS project)
- Is an env var causing the tool to behave differently?
3.**Potential sandboxing gap** (last resort) — the access is genuinely irrelevant to correctness (PID files, temp sockets, dev server logs that no task consumes). Only conclude this after exhausting categories 1 and 2.
### Phase 3: Deep Investigation
For violations that aren't immediately obvious, investigate further:
#### If the target is inferred by a plugin
1. Identify which plugin from `inference.plugin` in the brief output, or `nx show project --json` metadata
2. Read the plugin's `createNodesV2` implementation to understand inference logic
3. Determine if this project should have this target at all
4. Check if the plugin has `include`/`exclude` patterns in `nx.json` that should filter this project
5.**Check for input override layers** — `project.json`, `package.json`, or `nx.json``targetDefaults` may override plugin-inferred inputs, rendering plugin-level fixes invisible. Check all three before concluding a plugin fix is sufficient.
#### If violations come from a subprocess
1. Trace the process tree: which parent spawned the subprocess?
2. Why does the subprocess exist? (dev server for e2e, worker thread, build tool subprocess)
3. What environment does the subprocess inherit? (env vars, cwd)
4. Does the subprocess access files in a different project's directory?
#### If violations involve config file reference chains
1. Read the config file (jest.config, tsconfig, .eslintrc)
2. Trace all file references: `preset`, `extends`, `references`, `setupFiles`, `resolver`, `moduleNameMapper`, `transform`, etc.
4. Determine which referenced files are not declared as task inputs
#### If violations involve dependency task outputs
1. Check `dependsOn` to understand task dependency chain
2. Check `dependentTasksOutputFiles` glob pattern — is it too narrow?
3. Compare the glob against actual file types the tool reads from dependencies (e.g., `**/*.d.ts` missing `.tsbuildinfo`)
#### Generalizability analysis
After diagnosing the root cause, determine scope:
1. Is this violation specific to this project, or does it affect all projects using this tool/plugin?
2. What conditions trigger it? (specific config, specific tool version, specific project structure)
3. Should the fix be per-project (declarative input) or systemic (plugin improvement)?
4. If the plugin can be made smarter to infer the correct inputs, that's preferable to manual declarations.
### Phase 4: Output
**You MUST present findings using the structured format below before proceeding to any implementation discussion.** Do not use free-form narrative — the structure ensures completeness and makes findings reviewable.
Why: {why this access is irrelevant to correctness}
Evidence: {proof that categories 1-2 were exhausted}
### [INVESTIGATE] {short description}
Files: {file list or pattern}
Notes: {what's known, what needs more info}
Question: {what to ask the user or team}
## Stale Declarations
expectedInputsNotRead: {count and details if relevant}
expectedOutputsNotWritten: {count and details if relevant}
## Verification Plan
For each fix, provide the exact commands to verify:
1. Run the task so output files exist on disk: `npx nx <target> <project> --skip-nx-cache`
2. Check each violation file is now an input: `npx nx show target <project>:<target> inputs --check <space-separated files>`
3. For plugin-level fixes: build the plugin, patch node_modules, then verify with steps 1-2
```
## Principles
1.**Missing declaration is the default.** Most unexpected accesses are legitimate — the process needs the file, it just wasn't declared. Start from this assumption and investigate to understand WHY the access happens.
2.**The command is the unit of analysis.** Don't classify files in isolation. Understand what the command does and whether each file access makes sense given that command's purpose.
3.**Trace the full chain.** Plugin inference → target config → executor → command → file access. The root cause is often several layers removed from the symptom.
4.**Empirical over theoretical.** When code analysis produces a hypothesis, verify it before acting. Instrument `node_modules`, capture stack traces, run with debug flags. Wrong theories waste entire investigation rounds.
5.**Be thorough.** Read plugin source code, config files, executor implementations. Don't guess based on file names alone.
6.**Potential sandboxing gaps are last resort.** Only conclude this after exhausting missing declaration and bad tool config. The access must be genuinely irrelevant to correctness.
7.**Verify claims about Nx behavior in source code.** Any assertion about how Nx works must be traced to the actual implementation. Do not reason from theory or assumptions.
8.**Prefer systemic fixes over per-project declarations.** If a plugin can be improved to infer correct inputs for all projects, that's better than adding manual input declarations to each project.
## Delegating to Subagents
When the investigation is complex and requires parallel research, you can delegate to subagents. Follow this pattern:
1.**Run the context-gathering script yourself first.** The brief output (~3-5KB) is the shared context all subagents need.
2.**Include the brief output in each subagent prompt** along with the specific question to investigate. Subagents should NOT run the script again or try to parse the raw report.
3.**Give subagents the detail file path** so they can `jq` specific sections (process tree, resolved inputs, etc.) without re-running the script.
4.**Each subagent should answer one focused question**, e.g., "Why does PID 12345 (eslint) read `tsconfig.base.json`? Trace the full causal chain from the eslint config."
5.**Subagents must still follow the skill principles** — trace full causal chains, verify empirically, use `--check`, don't guess from file names. Include these instructions in the subagent prompt.
6.**Synthesize subagent results yourself** using the structured Phase 4 output format. Do not delegate the final classification.
## Reference
For the sandbox report data model and field definitions, see `references/data-model.md`.
You are migrating an Nx monorepo package from building to `../../dist/packages/<name>` to building locally to `packages/<name>/dist/`. This matches the pattern already used by `nx` and `devkit`.
## Argument
The user provides a package name (e.g., `js`, `webpack`, `angular`). The package lives at `packages/<name>/`.
## Steps
### 0. Preflight: check `workspace:*` deps for unmigrated packages
Read `packages/<name>/package.json` and list every `workspace:*` dep (in `dependencies`, `devDependencies`, `peerDependencies`).
For each such dep, look at the target package's `project.json`. If it does **not** override `release.version.manifestRootsToUpdate` to `["packages/{projectName}"]`, that target package is still on the old layout. You **must** migrate those packages too (apply this skill to each), in the same PR.
**Why:** With `preserveLocalDependencyProtocols: true` (the new pattern), `nx release version` does not substitute `workspace:*` in your manifest. At publish time, pnpm resolves `workspace:*` by reading the target's _source_`packages/<dep>/package.json`. The default `manifestRootsToUpdate: ["dist/packages/{projectName}"]` only bumps the dist copy, so pnpm picks up the unbumped source `0.0.1` and publishes your package with a dep on a version that does not exist in the registry. Local registry installs then fail with `ERR_PNPM_NO_MATCHING_VERSION`.
A `workspace:*` dep on a still-on-old-layout package is a hard blocker — migrate it before continuing.
### 1. Read current state
Read these files for the target package:
-`packages/<name>/package.json`
-`packages/<name>/project.json`
-`packages/<name>/tsconfig.lib.json`
-`packages/<name>/tsconfig.spec.json` (if exists)
-`packages/<name>/.eslintrc.json` (if exists)
-`packages/<name>/assets.json` (if exists)
-`packages/<name>/.npmignore` (if exists)
-`packages/<name>/.gitignore` (if exists)
Also read the reference implementations:
-`packages/devkit/tsconfig.lib.json`
-`packages/devkit/package.json`
-`packages/devkit/project.json`
-`packages/devkit/.npmignore`
Run `pnpm nx show target <name>:build-base` to see the inferred build target.
Run `pnpm nx show target <name>:build` to see the full build target.
### 2. Identify entry points
Look at the package's root `.ts` files and any existing `exports` field. Common entry points:
-`index.ts` (main)
-`testing.ts`
-`internal.ts`
-`ngcli-adapter.ts`
- Any other `.ts` files at the package root that re-export from `src/`
Also check for `migrations.json` and `generators.json`/`executors.json` — these need exports entries too.
### 3. Update `tsconfig.lib.json`
Transform from the old pattern to the new pattern:
**Important**: Adjust `include` based on the package's actual structure. If the package has directories like `bin/`, `plugins/`, etc. at the root level (like `nx` does), include those too.
### 4. Update `tsconfig.spec.json` (if exists)
Change `outDir` from `../../dist/packages/<name>/spec` to `dist/spec`.
### 5. Update `package.json`
Key changes:
- Add `"type": "commonjs"` near the top (after `private`)
- Change `"main"` to `"./dist/index.js"`
- Change `"types"` to `"./dist/index.d.ts"`
- Add `"typesVersions"` for backwards compatibility with `moduleResolution: "node"` consumers
- Add `"exports"` map with entries for each entry point
Each export entry follows this pattern:
```json
"./entry-name":{
"@nx/nx-source":"./entry-name.ts",
"types":"./entry-name.d.ts",
"default":"./dist/entry-name.js"
}
```
The main entry (`.`) uses `./index.ts`, `./index.d.ts`, `./dist/index.js`.
Always include:
```json
"./package.json":"./package.json"
```
Include `"./migrations.json": "./migrations.json"` if the package has migrations.
**Note**: The `@nx/nx-source` condition is a custom condition used for source-level resolution within the workspace (so other packages import from source, not dist).
Add a `typesVersions` field for consumers using `moduleResolution: "node"` (which doesn't read `exports`):
```json
"typesVersions":{
"*":{
"testing":["dist/testing.d.ts"],
"ngcli-adapter":["dist/ngcli-adapter.d.ts"]
}
}
```
Add an entry for each subpath export (excluding `.`, `./package.json`, and `./migrations.json`).
Do **not** override `build-base.outputs` in `project.json`. The `@nx/js/typescript` plugin reads `outDir` and `tsBuildInfoFile` from `tsconfig.lib.json` and infers the correct outputs (including the tsbuildinfo and the full set of file extensions). A hand-written override is almost always less complete than the inferred set.
If the package already has a hand-written `build-base.outputs` array, **delete it** — don't try to patch it. An incomplete override that omits `dist/tsconfig.tsbuildinfo` causes a sandbox violation in _every consumer_ that has a TypeScript project reference to this package: their `tsc --build` reads the referenced project's `.tsbuildinfo`, but `dependentTasksOutputFiles` can only collect it if this package declares it as an output.
Verify the inferred outputs include the tsbuildinfo:
```bash
pnpm nx show project <name> --json | jq '.targets["build-base"].outputs'
# Must include "{projectRoot}/dist/tsconfig.tsbuildinfo"
```
Update the existing `build` target's `outputs` if they reference `{workspaceRoot}/dist/packages/<name>` — they should now reference `{projectRoot}/dist/`.
Also update `dependsOn` in the `build` target: replace `"^build"` with `"^build"` if it isn't already, and make sure `"build-base"` is listed.
### 7. Update eslint config
Add `dist` to the ignores. For flat config (`eslint.config.mjs`):
```js
{ignores:['**/__fixtures__/**','dist']},
```
For legacy `.eslintrc.json`:
```json
"ignorePatterns":["!**/*","node_modules","dist"]
```
Do **not** add `*.d.ts` or `**/*.d.ts` — the base config already ignores `**/dist`, and `tsconfig.lib.json` (Step 4) sends all generated `.d.ts` files into `dist`, so they're already out of scope. Hand-authored `.d.ts` files in `src/` (e.g. `schema.d.ts`) generally don't need ignoring.
### 8. Update `assets.json` (if exists)
Change `outDir` from `"dist/packages/<name>"` to `"packages/<name>/dist"`.
### 9. Add `files` field to `package.json`
Instead of using `.npmignore`, add a `"files"` field to `package.json` (matching the `nx` package pattern). Remove `.npmignore` if it exists.
```json
"files":[
"dist",
"!dist/tsconfig.tsbuildinfo",
"migrations.json"
]
```
Adjust based on the package's needs:
- Add `"executors.json"` and/or `"generators.json"` if the package has them
- Add any other non-TS files that need to be published
- npm always includes `package.json` and `README.md` automatically — no need to list them
### 10. Rename README.md and update build command
If the package has a `README.md` at its root and uses the `copy-readme.js` script in its build target:
1. Rename `README.md` to `readme-template.md` (`git mv`)
2. Update the build command to pass explicit paths:
3. Update the build target `outputs` to `["{projectRoot}/README.md"]`
The script's default behavior reads `packages/<name>/README.md` and writes to `dist/packages/<name>/README.md` — both wrong for the new layout. Passing explicit args fixes both.
### 11. Update root `.gitignore`
Under the section that lists generated README files (look for `packages/nx/README.md`), add:
```
packages/<name>/README.md
```
The generated README is written next to source (not into `dist/`), so it needs its own ignore.
Do **not** add a `packages/<name>/**/*.d.ts` rule. The root `.gitignore` already has a top-level `dist` entry that ignores every `dist/` directory in the repo — and `tsconfig.lib.json` (Step 4) sets `declarationDir: "dist"`, so all generated `.d.ts` files land there. Adding a package-wide `**/*.d.ts` rule plus `!` re-includes for hand-authored `.d.ts` files (like committed `schema.d.ts` source files) is redundant defense-in-depth.
### 12. Update docs generation paths
Check `astro-docs/src/plugins/utils/` for any code that references `.d.ts` files from the package. The docs generation reads `.d.ts` entry points to build API reference pages. Paths that previously pointed to `dist/packages/<name>/foo.d.ts` (workspace root dist) or `packages/<name>/foo.d.ts` (package root) now need to point to `packages/<name>/dist/foo.d.ts`.
For example, `devkit-generation.ts` had to be updated to look for `packages/devkit/dist/index.d.ts` instead of `packages/devkit/index.d.ts`.
### 13. Update `scripts/nx-release.ts`
Two things to do here:
1. **Add the package to `packagesToReset`.** That array (around `scripts/nx-release.ts:76`) is the snapshot/restore list — every package whose source `package.json` gets mutated by `nx release` (because it now publishes from `packages/<name>/` directly, not `dist/packages/<name>/`) must be in this list. Otherwise the release will leave `packages/<name>/package.json` dirty in the working tree after running. **Easy to forget — and there is no test that catches it.**
2. **Update any package-specific paths.** If the package has special release handling (like devkit's `hackFixForDevkitPeerDependencies`), update any paths from `./dist/packages/<name>/` to `./packages/<name>/`.
### 14. Update imports across the workspace
Search for imports from `@nx/<name>/src/` across all other packages. These internal imports need to be updated:
- If the imported thing is re-exported through a public entry point (index.ts, internal.ts, etc.), update the import to use that entry point
- If not, consider adding it to `internal.ts` or the appropriate entry point
### 14b. (Optional) Lock down `./src/*` and route internal consumers through `./internal`
When you ship the migration, the package's `exports` map exposes everything under `./src/*` if you keep the wildcard. That's a 100s-of-symbols-wide semi-private surface that pins the implementation layout forever — consumers (first-party and external) can reach into any source file. The cleaner long-term shape, matching `@nx/devkit`/`@nx/workspace`, is to drop the wildcard and route internal consumers through a single curated `./internal` entry. Skip this step if you'd rather defer (e.g. the package has very heavy internal usage and you'd prefer a smaller PR), but plan a follow-up.
#### When to lock down vs defer
- **Lock down in the same PR** if internal subpath imports number in the low hundreds AND the package isn't `workspace:*`-pinned by other not-yet-migrated packages whose dist code would crash at runtime against the older published version (see "Published-version mismatch" below).
- **Defer to a follow-up PR** if the inventory is huge OR if dist-output code in other workspace packages depends on the OLD `./src/*` paths and those packages can't be migrated to local-dist yet. Lock down only once the immediate runtime-resolution surface is contained.
#### Step-by-step
**1. Inventory the subpath imports.** Scan for `from '@nx/<name>/src/...'`, plus runtime `require()`, dynamic `import()`, and `jest`/`vi.mock`-family calls:
Compile a `subpath → set-of-imported-symbols` map. About 30 distinct subpaths and 60 symbols is typical for a package the size of `@nx/js`.
**2. Identify runtime-string-resolved subpaths.** Some subpaths are referenced by _string default values_ the nx runtime resolves later (not static imports). The classic example: `packages/nx/src/command-line/release/config/config.ts` has `DEFAULT_VERSION_ACTIONS_PATH = '@nx/js/src/release/version-actions'`. These strings are also baked into pre-existing user `nx.json` files and you cannot rewrite them via a migration. **Keep those exact subpaths as explicit non-wildcard entries in the exports map** (not under `./internal`), and have the migration skip rewriting them.
```bash
# Search for string-default usages of the subpath in nx core
**3. Build `packages/<name>/internal.ts` at the package ROOT** (not inside `src/`, to mirror `@nx/devkit/internal`). Re-export every symbol callers reach for via `@nx/<name>/src/*`, BUT only symbols not already exported from `packages/<name>/src/index.ts`. Anything already public stays public — the migration sends those callers to `@nx/<name>`, not `@nx/<name>/internal`.
To compute the public set:
```bash
grep -E "^export " packages/<name>/src/index.ts
```
…and recursively expand any `export *` lines. The "public-export reachability" calculation is fiddly enough that a small Python script with a recursive expand is worth it (see PR #35538 commit history for an example).
Curate the new file:
```ts
// Semi-private surface for first-party Nx packages.
//
// External plugins should NOT import from here — this entry is curated for
// internal consumers and may change without semver protection. Mirrors
// `@nx/devkit/internal`.
// Re-exports of nx-source internals (need `no-restricted-imports` overrides).
Also strip `src/*` glob entries from `typesVersions`. Replace with explicit non-wildcard entries that mirror the kept exports.
**5. Codemod consumers in two passes.** Mechanical sed-style first, then a smarter split:
```bash
# Pass 1: every `from '@nx/<name>/src/...'` → `from '@nx/<name>/internal'`,
# except the preserved subpaths from step 2.
# (Use a Python/TS script — sed is fine for the simple cases too.)
```
```bash
# Pass 2: split mixed imports. Any line like
# import { libraryGenerator, ensureTypescript } from '@nx/<name>/internal';
# where `libraryGenerator` is publicly exported from `src/index.ts` becomes:
# import { libraryGenerator } from '@nx/<name>';
# import { ensureTypescript } from '@nx/<name>/internal';
```
Also handle these non-static cases:
- `jest.mock('@nx/<name>/src/...', ...)` and `jest.requireActual(...)` — same rewrite. The whole mock surface is now `@nx/<name>/internal`, so `...jest.requireActual('@nx/<name>/internal')` spreads more than the original site mocked, but that's fine in practice.
- Runtime `require('@nx/<name>/src/...')` — same rewrite.
- Template-string fixtures inside `.spec.ts` files — careful! Don't let your codemod rewrite literal `from "@nx/<name>/internal"` substrings that _test_ the migration (it'll flip quote style and break the test). Either skip `*.spec.ts` files containing fixtures, or operate at AST level.
**6. Collapse duplicate imports.** After the two-pass codemod, many files end up with two `import { ... } from '@nx/<name>/internal'` lines (or two `from '@nx/<name>'`). Run a third pass to merge same-source-same-`type`-prefix imports:
```python
# Match lines (anchored): `^import [type ]{ ... } from '@nx/<name>[/internal]';$`
# Group by (is_type_only, source). For each group with >1 entry: keep the first
# occurrence's position, merge the named bindings (dedupe), delete the others.
# Don't merge across type/non-type — the semantics differ.
```
**7. Public-symbol audit.** After splitting, `internal.ts` must not re-export anything already exported from `src/index.ts`. If it does, namespace consumers (`import * as shared from '@nx/<name>/internal'`) will see only the curated set and `shared.publiclyExportedSymbol` becomes `undefined`. Cross-check:
```bash
# Symbols in internal.ts that are ALSO in the recursive index.ts export set
# are a bug. Remove them from internal.ts. The codemod from step 5 should
# have already routed their callers to `@nx/<name>`, but verify nothing is
# left pointing at `@nx/<name>/internal` for these.
```
The three load-bearing patterns to verify:
- `import * as shared from '@nx/<name>/internal'` followed by `shared.publicSymbol` — fix by changing source to `@nx/<name>`.
- Runtime `const shared = require('@nx/<name>/internal')` followed by `shared.publicSymbol` — same fix.
- Named imports of public symbols from `@nx/<name>/internal` — already split by step 5; verify nothing slipped through.
**8. Ship a migration.** Add `packages/<name>/src/migrations/update-<version>/rewrite-<name>-internal-subpath-imports.ts` based on the workspace `move-typescript-compilation-import` template. It needs to handle:
- Static `import [type] { ... } from '@nx/<name>/src/<anything>'`
- `export [type] { ... } from '@nx/<name>/src/<anything>'`
- Dynamic `import('@nx/<name>/src/<anything>')`
- `require('@nx/<name>/src/<anything>')`
- `jest.mock|unmock|doMock|dontMock|requireActual|requireMock|importActual|importMock(...)` and the `vi.` equivalents
**Route by symbol, not blindly to `./internal`.** Some symbols reachable via `@nx/<name>/src/*` are _public_ — they're exported from `packages/<name>/src/index.ts` and ship on the main `@nx/<name>` entry. A migration that rewrites every `@nx/<name>/src/*` import to `@nx/<name>/internal` silently breaks any consumer importing a public symbol that way, because `internal.ts` deliberately does **not** re-export public symbols (step 7). Instead:
- Hard-code the public symbol set (the recursively-expanded `export`s of `src/index.ts`) in the migration.
- For a **named** `import`/`export` declaration, partition the named bindings: public symbols go to `@nx/<name>`, the rest to `@nx/<name>/internal`. Classify an `orig as alias` binding by `orig`. If both groups are non-empty, replace the single declaration with two — one per target — preserving any `import type` / `export type` modifier.
- A **namespace** import (`import * as ns`), a **default** import, `export *`, every **call expression** (`require`, dynamic `import`, `jest.mock` family), and `typeof import('...')`**type queries** (`ImportTypeNode`) reference the module as a whole and can't be symbol-split — route them to `@nx/<name>/internal`.
Skip the preserved subpaths from step 2 (e.g. `@nx/<name>/src/release/version-actions`). Use `ts.createSourceFile` for AST-based detection so you don't rewrite literals inside comments or template strings.
**Don't forget `typeof import('...')`.** It parses as an `ImportTypeNode`, not a `CallExpression`, so it's a separate AST branch from the `require`/dynamic-`import` handling. Real-world consumers use the idiom `const m = require('@nx/<name>/src/x') as typeof import('@nx/<name>/src/x')` to get a typed runtime `require` — if the codemod only rewrites the runtime arg, the type arg stays pointing at the now-removed `./src/*` wildcard and the consumer fails to type-check. Handle it explicitly: walk `ImportTypeNode`s and rewrite `node.argument.literal` when the string starts with `@nx/<name>/src/`.
Register in `packages/<name>/migrations.json` with `version: <current beta>`. The description should state the routing rule: named public-symbol imports/exports go to `@nx/<name>`, everything else to `@nx/<name>/internal`.
Add a spec covering: public-symbol import (→ `@nx/<name>`), internal-symbol import (→ `@nx/<name>/internal`), mixed import split into two, aliased bindings classified by original name, type-only split, `export { ... } from` (public / internal / mixed), `export *`, namespace import, **default import**, single-quoted, double-quoted, deep subpath, `.js` extension, `require()`, dynamic `import()`, **`typeof import()` type queries (→ `/internal`)**, **a `<typeof import()>require()` cast in tandem** (catches the regression where the runtime arg gets rewritten but the type arg doesn't), the **full** jest mock family (`it.each` over `MOCK_HELPER_METHODS`), the **full** vi mock family, **`jest.mock('...', factory)` with a factory argument**, a non-mock `jest.*` call left alone, an import + `jest.mock` in the same file, preserved subpaths, non-`@nx/<name>` imports, unrelated string literals inside comments. Make sure every entry in `PUBLIC_SYMBOLS` and every entry in `MOCK_HELPER_METHODS` is exercised at least once — drift from hardcoded sets is the most likely silent regression.
**9. Watch for the published-version-mismatch gotcha in example/test builds.**
The workspace's root `node_modules/@nx/<name>` is the _published_ version (root `package.json` pins it to a real release tag, not `workspace:*`). When code at `dist/packages/<X>/...` does `require('@nx/<name>/internal')` at runtime, Node walks up from `dist/` and finds workspace-root `node_modules/@nx/<name>` — the published copy. If that version was released BEFORE this PR, it has no `internal.js` and resolution fails.
This bites specifically for examples or e2e flows that load `dist/packages/<other-package>/...` artifacts (e.g. an angular-rspack module-federation example that monkey-patches `Module._resolveFilename` to redirect `@nx/<other-package>` to dist). If the other-package's dist code does `require('@nx/<name>/internal')`, you'll hit this.
Two fixes:
- **(Preferred, if applicable.)** Migrate the _other_ package to local-dist too. Then its built code lives at `packages/<other>/dist/...`, walks up to `packages/<other>/node_modules/@nx/<name>` (a workspace symlink to source), and resolution finds the new `internal.js` because workspace source has it.
- **(Band-aid for the in-between window.)** If migrating the other package is out of scope, extend the example's existing request-path patch to also redirect `@nx/<name>/internal` to the workspace source `packages/<name>/dist/internal`. Document it as a temporary measure tied to the same TODO that exists for the other-package redirect.
Search aggressively for this pattern after step 8:
If `nx affected` fails on a single example test with `Cannot find module '@nx/<name>/internal'`, that's step 9 — extend the example's request-path patch.
If `nx affected` fails on a package with `TS2339: Property 'foo' does not exist on type 'typeof import(".../internal")'`, that's step 7 — a `shared.publicSymbol` call survived. Find it (`grep -rn 'shared\.<symbol>' packages/`) and rewrite the namespace source to `@nx/<name>`.
### 15. Audit `require('../../package.json')` (or similar relative paths to the package.json)
Search for `require\(['"]\.\..*package\.json` inside `packages/<name>/src/`. Any TS source file that reads the package's own `package.json` via a relative path is a **layout-fragility bug** that this migration triggers:
- Before migration: source `packages/<name>/src/utils/versions.ts` → built `dist/packages/<name>/src/utils/versions.js`. `'../../package.json'` resolves to `dist/packages/<name>/package.json` (which the old build path copied there).
- After migration: source unchanged → built `packages/<name>/dist/src/utils/versions.js`. `'../../package.json'` now resolves to `packages/<name>/dist/package.json` — **doesn't exist**. Every consumer that pulls in `nxVersion`/`NX_VERSION`/etc. crashes at module-load time with `Cannot find module '../../package.json'`. This breaks e2e tests broadly because most generators load `versions.ts`.
**Fix**: replace the relative path with a **package-name self-reference**, using the dynamic `join()` form so eslint's `@nx/enforce-module-boundaries` doesn't trip on it:
A literal `require('@nx/<name>/package.json')` works at runtime but trips `enforce-module-boundaries`'s `noSelfCircularDependencies` check — the rule statically pattern-matches self-imports and fires before checking whether the import resolves to a non-main entry. The dynamic `join()` form is opaque to the static check, matches `@nx/devkit`'s established pattern, and resolves to the same path at runtime.
Node resolves `@nx/<name>/package.json` via `node_modules` (workspace symlink in dev, real install in published), and the package.json's `exports` map already declares `./package.json` (you ensured this in Step 5). Works identically in source and dist contexts.
Reference implementations:
-`packages/nx/src/utils/versions.ts` — `require('nx/package.json').version` (works because `nx` is the project's own name; the static rule's entry-point check is lenient for the top-level `nx` package specifically)
Search for `ensurePackage\(['"]@nx/` inside `packages/<name>/src/`. For every match, look at the next 5–20 lines for a `await import('@nx/<other>/...')` pulling from the same package. This pattern is **broken** under `nodenext`:
- Before migration: `module: commonjs` made TypeScript downlevel `await import('@nx/<other>')` to `Promise.resolve(require('@nx/<other>'))`. The synchronous `require()` honors `Module._initPaths`, which is exactly where `ensurePackage` registers the on-demand temp install. Resolution succeeds.
- After migration: `module: nodenext` preserves `import()` as a true ESM dynamic import. ESM resolution **ignores**`Module._initPaths` — it walks up `node_modules` from the importing file's location only. The temp install lives in a different temp dir, so the import fails with `Cannot find package '@nx/<other>'`.
**Fix**: replace the dynamic import with a synchronous `require()`. The `ensurePackage` side effect makes it findable via `_initPaths`, and `require()` honors that:
Collapse multiple successive `await import()`s of the same module into one `require()` destructuring while you're at it.
This was the source of the M2 e2e regressions (Playwright/Web/React generators crashed at `Cannot find package '@nx/eslint'` from `ignore-vite-temp-files.js` and `ignore-vitest-temp-files.js`). One-line failure mode, but it can sit hidden in any code path that the unit-test suite doesn't exercise — only the published-then-installed flow exposes it. Audit every `ensurePackage` callsite.
### 16. Preserve `add-extra-dependencies` if the package has one
`scripts/add-dependency-to-build.js` is a release-time hack that injects an extra dep into the **published**`package.json` (e.g., it adds `nx` to `@nx/workspace`'s `dependencies`). It is **not dead code** — without it the transitive resolution chain breaks for downstream consumers.
Concretely: created workspaces depend on `@nx/js`, which transitively depends on `@nx/workspace`. When the fork in `generate-preset.ts` runs `nx g @nx/workspace:preset`, Node's `require.resolve('@nx/workspace/package.json')` only finds the transitively-installed package because pnpm hoists `nx` along with `@nx/workspace` into `.pnpm/node_modules/` — and `nx` is hoisted there only because the **published**`@nx/workspace/package.json` declares it as a regular dependency. Drop that injection and the fork in the new workspace fails with `Unable to resolve @nx/workspace:preset` → `unable to find tsconfig.base.json`.
When migrating a package that has the `add-extra-dependencies` target:
1.**Keep** the target in `packages/<name>/project.json`.
2.**Update**`scripts/add-dependency-to-build.js`: change the `pkgPath` from `../dist/packages/<package>/package.json` to `../packages/<package>/package.json` (the source manifest is now the published manifest under the local-dist layout).
3.**Keep** the `pnpm nx run-many -t add-extra-dependencies --parallel 8` invocations in `scripts/nx-release.ts` (both the GitHub-release path and the local-publish path) — they fire between `runNxReleaseVersion` and `nx run nx:expand-deps`.
4. Confirm the snapshot/reset list (`packagesToReset`) covers this package so the injection is undone after publish.
If the package does not have the target, leave the script and the run-many calls alone — they no-op for any project without the target.
### 17. Verify
Run:
```bash
pnpm nx run-many -t test,build,lint -p <name>
```
Then:
```bash
pnpm nx affected -t build,test,lint
```
### Summary of the pattern
The core idea is simple: instead of building to a shared `dist/packages/<name>/` at the workspace root, each package builds to its own `packages/<name>/dist/`. The `exports` map with `@nx/nx-source` condition lets workspace packages resolve to `.ts` source files during development, while external consumers get the built `.js` from `dist/`. This is like giving each package its own "output mailbox" instead of sharing one big mailbox.
| Linear milestone "Multi-version supported across plugins" (project NXC-4072) | What's wrong per plugin, the resolved support window, open human decisions. Per-plugin tasks NXC-4381..NXC-4410 (P1–P29). |
| This skill | How to implement the canonical shape, code-level anti-patterns, gotchas, findings doc shape (no-task case). |
The skill is the gap-closer: it accepts a Linear task, parses it, drives
the fix. When no task exists for the plugin, fix mode runs discovery in
Phase 1–2 and produces a findings doc that mirrors a Linear task body —
so the user can file it as a new task before proceeding.
**Reference PRs (the canonical shape):**
-`#35587` — `@nx/angular` — merged. Set the precedent. Introduced
`throwForUnsupportedVersion`.
-`#35642` — `@nx/playwright` — merged. Generalized the shared helpers
into `@nx/devkit/internal`. Established executor / runtime feature-
gating.
-`#35670` — `@nx/cypress` — merged. Added `excludeGenerators` to the
parameterized test helper.
-`#35671` — `@nx/vitest` — open at time of writing. Demonstrates
"drop phantom peer-range claim" and "declared floor < effective floor"
patterns.
Before citing any PR by number, verify state — these go stale:
| `multi-version-compliance <NXC-XXXX>` | Fix (primary) | Fetch task, surface findings + decisions in Phase 2, wait for user OK before Phase 3 edits. |
| `multi-version-compliance` (no arg) | Ask for task ID | Prompt for NXC-XXXX. |
| `multi-version-compliance @nx/<plugin>` (bare plugin) | Fix (task lookup) | Look up the per-plugin task in milestone NXC-4072. If found, confirm with user and enter fix mode. If not found, run discovery in Phase 1–2 (rubric against code), present findings, suggest filing as a new task before any edits. |
| `multi-version-compliance review #<N>` | Review | Fetch PR, derive Linear task from branch name if possible, compare diff vs. task findings (or run pure code-level review if no task). |
**Stop-after-Phase-2 (audit-equivalent):** if you want findings without
edits, decline to approve at the end of Phase 2. The skill stops, no
branch, no commits.
**On a branch matching `nxc-NNNN` with no explicit arg:** before
asking the user, suggest "Use NXC-NNNN?" inferred from the branch name.
## Linear-fetching protocol
Before any code-level work in Linear-driven mode, the skill MUST:
1.**Check Linear MCP availability.** If `mcp__linear-server__get_issue`
isn't available (MCP server not installed / not connected), tell the
user and fall through to the no-task discovery path (fix mode Phase 1
step 2). Don't pretend to fetch.
2.**Fetch the task.**`mcp__linear-server__get_issue id="NXC-XXXX"`.
If the call errors (invalid ID, network), halt and ask the user to
verify the ID.
3.**Verify shape.** Confirm:
- Title matches `[multi-version][P##] \`@nx/<plugin>\` — multi-version support compliance`(per-plugin) or`[multi-version][W#] ...` (cross-cutting). If the pattern doesn't match, halt and ask the user to confirm this is the right task.
- Status. `Done` → ask whether re-audit or follow-up. `Canceled` → halt and ask.
4.**Read description sections.** Every per-plugin task has:
- **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).
`chore-not-fix-non-prod.md`). Don't enforce or flag these from this
skill — defer to whatever the user's conventions resolve to at PR time.
## Findings doc template (Phase 2 output, used when no Linear task exists)
When fix mode hits the no-task case (Phase 1 step 2), produce
`tmp/<plugin>-findings.md` shaped to mirror a Linear task body so the
user can file it as a new task in milestone NXC-4072 before proceeding
to Phase 3.
For plugins managing multiple primary packages, repeat the install-map
/ decisions / findings bullets per primary.
```md
# @nx/<plugin> — multi-version support compliance findings
> No Linear task in milestone NXC-4072. This doc is filing-ready —
> create the task with this body before proceeding to fix.
## Plugin
- Path: packages/<plugin>
- Upstream support: <official policy if any, else "no formal LTS">
- peerDep declarations: <list>
- Per-major install (`<file>` branches on installed `<package>` major):
- v<N-1>: <constants>
- v<N>: <constants> (default)
- Paired secondaries: <list of ecosystem-locked siblings>
## Needs human decision
1. <decision 1 — e.g., raise floor to vN.0.0 vs keep current>
2. <decision 2 — e.g., drop ^1.0.0 from peer (no v1 install lane)>
## Findings
- **(high) <one-line summary>** [file:line]
_Suggested fix_: <one-line>
- **(medium) ...**
- **(low) ...**
## Verification checklist (A–F)
### A. Support window declarations
- [ ] peerDep ranges match the support window
- [ ] Version map / runtime branching covers every supported major
- [ ] Every third-party package the plugin **invokes at runtime** has a peerDep entry. "Invokes" = TS import/`require` OR executor spawns its CLI binary OR inferred plugin emits a target whose `command` invokes its CLI (look for `externalDependencies: ['<pkg>']` in emitted target inputs). Packages the generator installs for the user to consume independently (ESLint plugins loaded by the user's eslintrc, `@types/*`) don't need peer-declaration.
- [ ] Peers needed only when a user opts into a specific surface (executor opt-in, inferred plugin gated on config file presence, opt-in preset) are declared **optional** via `peerDependenciesMeta: { "<pkg>": { "optional": true } }`. Required-non-optional peers are reserved for packages every workspace using the plugin needs.
- [ ]`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.
- Inline `clean(declared) ?? coerce(declared)` chain at a generator entry point — duplicates `getDeclaredPackageVersion`.
- Open-coded tree-branch in `getInstalled<Pkg>Version(tree?)`: `getDependencyVersionFromPackageJson(tree, pkg)` + an `installedVersion === 'latest' || installedVersion === 'next'` check + a `clean(...) ?? coerce(...)?.version ?? null` chain. The dist-tag list and the normalization chain are both centralized in `getDeclaredPackageVersion` (via `NON_SEMVER_DIST_TAGS` / `normalizeSemver`). A local copy silently misses new entries if `NON_SEMVER_DIST_TAGS` grows.
**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)`.
For the `getInstalled<Pkg>Version(tree?)` wrapper specifically:
**Omit the third arg by default.** It conflates "missing" with "dist tag" — both fall back to the supplied fresh-install constant. That's almost never what the caller wants; consumers that need a `?? latest` fallback should encode it at the call site, not globally in the helper (rspack/rsbuild precedent). See `canonical-shape.md` §"Dist-tag semantics — third arg".
**Reference:** Compliant — `packages/cypress/src/utils/assert-supported-cypress-version.ts` (7 lines); `packages/cypress/src/utils/versions.ts``getInstalledCypressVersion` (no third arg); `packages/rspack/src/utils/version-utils.ts` and `packages/rsbuild/src/utils/version-utils.ts` (same shape). 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
**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.
## 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
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
constcypress=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):**
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`
**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(newRegExp(`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
**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+:
**Why wrong:** A workspace on v2 will pass the floor assert (peer + versionMap claim support), then crash at runtime with `getRelevantTestSpecifications is not a function`. The peer is lying.
**Do instead:** Raise the floor to the lowest major where every called third-party API exists. Drop the now-unsupported entries from `versionMap`, `peerDependencies`, and the per-major version aliases (if any). The `assert-supported-<pkg>-version.spec.ts` sub-floor test now covers the dropped major.
**Reference:** Open PR `#35671`'s second commit (`fix(testing): drop vitest v2 support from @nx/vitest`) — originally proposed a v2 floor matching the lowest install lane, then raised to v3 after audit caught the `getRelevantTestSpecifications` usage. Inspect via `gh pr view 35671 --repo nrwl/nx --json commits`.
## 17. Creating a branch during Phase 1–2 (discovery / read-only)
**Looks like:**`git checkout -b <some-branch>` before the user has approved Phase 3 edits.
**Why wrong:** Phase 1–2 produces findings, not commits. Creating a branch creates pressure to commit something. Any working artifact (e.g., `tmp/<plugin>-findings.md` for the no-task case) goes in `tmp/` (gitignored) — for the user to read and scope from, not to commit.
**Do instead:** Run Phase 1–2 on the current branch (typically `master`). Output to `tmp/<plugin>-findings.md` if you wrote one. Branch creation belongs in Phase 3, after explicit user approval to proceed with edits.
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.
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 {
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.
### 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.
**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 {
return versionMap[vitestMajorVersion as CompatVersions] ?? latestVersions;
}
```
### The `getInstalled<Pkg>Version(tree?)` helper
Optional `tree` parameter — with tree, reads declared from `package.json` via `getDeclaredPackageVersion` (handles dist-tag normalization, semver cleaning); without tree, routes through `getInstalledPackageVersion` from `@nx/devkit/internal` (FS resolution via `getNxRequirePaths()`).
**Do not open-code the tree-branch.** `getDeclaredPackageVersion` already centralizes the dist-tag list (`isNonSemverDistTag`) and the `clean(v) ?? coerce(v)?.version ?? null` chain (`normalizeSemver`). Local re-implementations drift when devkit's constants change. See `anti-patterns.md` §1.
```ts
import { type Tree } from '@nx/devkit';
import {
getDeclaredPackageVersion,
getInstalledPackageVersion,
} from '@nx/devkit/internal';
import { major } from 'semver';
export function getInstalledVitestVersion(tree?: Tree): string | null {
if (!tree) {
return getInstalledPackageVersion('vitest');
}
return getDeclaredPackageVersion(tree, 'vitest');
}
export function getInstalledVitestMajorVersion(tree?: Tree): number | null {
#### Dist-tag semantics — third arg (`latestKnownVersion`)
`getDeclaredPackageVersion(tree, pkg, latestKnownVersion?)`'s third arg falls back to `normalizeSemver(latestKnownVersion)` whenever the declared range can't be normalized to semver — both "package missing from `package.json`" AND "package declared as a dist tag (`latest` / `next`)". The helper does not distinguish the two cases.
**Default to omitting the third arg.** The wrapper returns `null` for both "missing" and "dist tag"; consumers that want `?? latestVersions` semantics should encode it at the call site (rspack/rsbuild precedent), not globally in the helper. Passing the third arg makes the helper claim the package is "installed at the fresh-install constant" even when nothing is declared — which silently disables init generators' "add the package" branches.
When a consumer specifically needs to distinguish "missing" from "dist tag", use `getDependencyVersionFromPackageJson` from `@nx/devkit` to inspect the raw declared string.
## 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) {
### 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.
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`.)
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`.
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).
`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`.
- 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`.
| `@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';
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
all-generators-enforce-floor.spec.ts # NEW — parameterized
generators/
<each>/
<each>.ts # assert as first statement
init/
schema.json # keepExistingVersions default: true
init.ts # keepExistingVersions ?? true
executors/ # feature-gate via getInstalledPackageVersion
plugins/ # same
migrations.json # tighten / remove `requires` gates per audit
package.json # align peer; add "semver": "catalog:" if newly used
```
---
# Code-level verification (review-mode lens)
In review mode, walk these markers against the diff.
Output rules:
- **Inline categories are `[blocker]` and `[non-blocker]` only.** No "open question," "ask," or other ad-hoc tags. Author-directed questions emerge from non-blocker findings and surface in the closing "Open questions for author" block.
- **For each blocker / non-blocker:** anchor at `file:line` and cite which reference PR / file demonstrates the correct pattern. Cross-reference `anti-patterns.md` when the finding matches a numbered pattern.
- **Sections without findings get a single summary line**, not a per-file enumeration. `"Pass — all 7 generator entries assert at first statement"` is right; listing seven file:lines is wrong. Reviewer time is spent on actionable items; passing checks should not eat reading budget.
- **Produce the verdict block** at the end (see §"Verdict template"). The block is the skimmable index — produce it, don't substitute a free-form summary.
Scope:
- **In scope:** the diff's code, configs, schemas, migrations, and **in-codebase documentation that describes runtime behavior** (e.g., `.mdoc` / `.md` files under `astro-docs/` or `docs/` that claim how the plugin behaves). A docs claim that contradicts the code is a correctness issue and belongs here.
- **Out of scope:** PR title, PR body shape, commit message format, related-issues section, branch naming. Defer to the user's PR/commit conventions (loaded globally from `~/.claude/memory/workflow/git/`). Don't flag PR/commit shape in this skill's review output.
## 1. Peer dep & install constants
- [blocker] `package.json` peer dep range matches the install lanes implemented in `versions.ts`. No phantom version claims. → If `versionMap` has no v1 entry, peer must not list `^1.0.0`. Reference correction: `#35671` (`@nx/vitest`). Anti-pattern: §8.
- [blocker] **Declared floor matches the effective floor.** Grep every `import` / `require` from the third-party package in plugin code. If any imported API only exists at version >N, the declared floor must be >=N. Anti-pattern: §16. Reference correction: `#35671` second commit raised vitest from v2 to v3 after catching a `getRelevantTestSpecifications` import (v3+ only).
- [blocker] Fresh-install constant exposes the **full feature surface**, not just the peer floor. → Playwright peer stayed `^1.36.0` but fresh-install moved to `^1.37.0` because blob reporter + `merge-reports` CLI both require 1.37. Reference: `#35642` `packages/playwright/src/utils/versions.ts`.
- [blocker] No per-major version aliases (`<pkg>V3Version`, `<pkg>V4Version`, etc.) alongside a `versionMap` — pick one source of truth. Anti-pattern: §15. Reference: `#35671`'s third commit dropped these aliases.
- [blocker] `versions.ts` exports `minSupportedXVersion = 'X.Y.Z'` as a plain string (no caret, no range markers). The wrapper passes this verbatim to `assertSupportedPackageVersion`.
- [blocker] `versionMap[major]` lookup is `versionMap[major] ?? latestVersions`. No `switch + throw default:` or other above-ceiling throw. Anti-pattern: §2. Reference correction: `#35670` (`@nx/cypress` `versions()` rewrite).
- [blocker] Every third-party package the **plugin invokes at runtime** has a `peerDependencies` entry. "Invokes" covers: (a) TypeScript `import`/`require`, (b) executor spawning the package's CLI binary, (c) inferred-plugin (`createNodes`/`createNodesV2`) emitting a target whose `command` invokes the package's CLI (the `externalDependencies: ['<pkg>']` field on such targets confirms the dependency). See §"Peer dep alignment" for the full categorization.
- **Don't flag** packages the plugin's generator installs into the user's workspace for the user to consume independently, with no plugin codepath invoking them (e.g., ESLint plugins like `eslint-plugin-cypress` that ESLint loads from the user's eslintrc; `@types/*` packages).
- Plugins flagged at time of writing for actually-invoked packages without a peer entry: `@nx/webpack`, `@nx/rollup`, `@nx/angular-rspack-compiler` (primary listed under `dependencies`); `@nx/jest`, `@nx/nest`, `@nx/module-federation`, `@nx/react`, `@nx/vue`, `@nx/expo`, `@nx/react-native`, `@nx/node`, `@nx/js` (verify per plugin — TS imports, binary spawns, AND inferred-plugin emitted commands all count).
- [blocker] Peers that are only used when the user opts into a specific surface (executor opt-in, inferred plugin gated on config file presence, opt-in preset) are declared **optional** via `peerDependenciesMeta: { "<pkg>": { "optional": true } }`. Pattern is established across reference plugins — `@playwright/test`, `cypress`, `vitest`, `vite`, `@angular/build`, `ng-packagr` are all optional. Required-non-optional peers (`@angular-devkit/core`, `rxjs` in `@nx/angular`) are reserved for packages every workspace using the plugin needs. See §"Required vs. optional peer".
- [non-blocker] If the PR introduces `semver` usage in the plugin, `package.json` `dependencies` lists `"semver": "catalog:"`. Reference: `#35642` added it to `packages/playwright/package.json`.
## 2. Generator entry points
- [blocker] **Every** entry in `generators.json` has its working function calling `assertSupported<Pkg>Version(tree)` as the **first statement** — before any tree reads, writes, or sub-generator calls. For wrapper/internal-split plugins (cypress, playwright): assert is in `*Internal`. For single-function generators (angular): in the function itself. Anti-pattern: §14.
- [blocker] Plugin wrapper file `assert-supported-<pkg>-version.ts` imports `assertSupportedPackageVersion` from `@nx/devkit/internal`. No direct call to `throwForUnsupportedVersion`. No bespoke `throwBelowFloor` / `throwAboveWindow` / `assertVersion` / local `cleanVersion = clean(v) ?? coerce(v)?.version` helpers (use `normalizeSemver` / `getInstalledPackageVersion` / `getDeclaredPackageVersion`). Anti-pattern: §1. Concrete example: PR `#35676` introduces a local `cleanVersion` and `getInstalledRsbuildVersionRuntime` — both already exist as shared helpers.
- [blocker] If `all-generators-enforce-floor.spec.ts` uses `excludeGenerators`, each excluded name has a code comment explaining why the generator must run sub-floor (e.g., `migrate-to-cypress-11` lifts v8–v10 workspaces onto v11).
- [non-blocker] Double-assert chains (`configurationInternal` calls `initInternal`, both assert) are OK. Idempotent. Don't refactor away.
## 3. Generator outputs
- [blocker] Templates the generator writes (project files, configs, schemas) compile and run on every major in the support window. Verify with a quick mental walk: for each template referenced from the generator, identify any per-major-version conditional and confirm it's accurate.
- [blocker] Generated `project.json` target shape (executor, options, schema) is valid on every supported major. If the executor's option schema differs across the support window, the generator branches or uses the union shape.
- [blocker] Default option values are valid on every supported major. A default that's only valid above a specific major must be conditional.
- [blocker] Version map covers every managed third-party dep. If the runtime later branches on a sibling's version (e.g., `@vitest/ui`), the version map must have an entry for that sibling per major — no gaps where the generator picks a constant the runtime then can't reconcile.
- [blocker] Generator schema accepts the **union of options across the support window**. Options removed in a newer major still validate at schema level (with description-notice); runtime throws when inapplicable on the installed major. See `gotchas.md` §"Schema-level deprecated-option stubs with runtime throws".
- [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.
- [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.
- [blocker] Executor code that invokes a CLI subcommand or uses an API introduced after the floor calls `getInstalledPackageVersion('<pkg>')` + `lt(installed, threshold)` from `semver` and throws a clear "requires >= X.Y.Z (the version that introduced …)" message. Reference: `packages/playwright/src/executors/merge-reports/merge-reports.impl.ts` (`#35642`).
- [blocker] Preset / config builders that auto-inject feature-version-coupled config skip injection silently when installed < threshold, and only throw when the user **explicitly** opted in on an unsupported version. Reference: `packages/playwright/src/utils/preset.ts` (`#35642` — `generateBlobReports` logic).
- [blocker] **Inferred plugins** (`createNodes`/`createNodesV2`) parse configs across every major in the support window. The plugin emits the same target shape regardless of the installed major (or branches if shapes diverge). Don't hardcode helper imports against one major.
- [blocker] **Above-ceiling is silent fallthrough.** No warn, no throw, no branch. Anti-pattern: §2.
- [non-blocker] Executors don't enforce the plugin floor. Floor enforcement is generator-only. Don't suggest adding an executor-level floor assert unless the user asks.
- [non-blocker] `require('<pkg>')` for optional peers should live inside the function body, after version detection. Anti-pattern: §9.
## 7. Tests
- [blocker] `all-generators-enforce-floor.spec.ts` exists at `packages/<plugin>/src/utils/all-generators-enforce-floor.spec.ts`, calls `assertGeneratorsEnforceVersionFloor` from `@nx/devkit/internal-testing-utils`. This is the parameterized spec that exercises every generator's floor assert.
- [blocker] `subFloorVersion` is a semver range where `lt(coerce(it).version, floor)` is true. No pre-release identifiers. Reference values: `~18.2.0` (angular, v19 floor), `~12.17.0` (cypress, v13 floor), `~1.35.0` (playwright, v1.36 floor).
- [non-blocker] Plugins establishing the pattern (`#35587` angular) ship a `assert-supported-<pkg>-version.spec.ts` with the 5 canonical cases (sub-floor / fresh-install / `latest` / `next` / in-range). The underlying `assertSupportedPackageVersion` already has full coverage in `@nx/devkit`, so the per-plugin spec largely re-tests the shared helper. Useful for symmetry across the PR series but not required — don't block on missing.
- [non-blocker] Runtime/executor feature-gate throw tests are nice-to-have, not required — reference PRs (`#35587`, `#35642`, `#35670`) do not have them today.
- [non-blocker] Error message matching uses substring (`toThrow('Unsupported version of \`<pkg>\` detected')`) instead of hand-rolled `RegExp`. Anti-pattern: §13.
- [non-blocker] FS-side helper migrated to `getInstalledPackageVersion`; tree-side helper may stay inline. The two helpers' `null` vs. fallback semantics differ. Reference: `#35670` `packages/cypress/src/utils/versions.ts` rewrite.
## Open questions to raise (when missing from the PR / Linear task)
1. **Floor:** deliberate raise from the previous declared peer, or matching the existing peer? If raise: do sub-floor users get a `packageJsonUpdates` bridge or manual bump?
2. **Peer-range tightening:** dropping a major because there's no install lane (legitimate, `#35671` pattern) or because tests fail (regression risk — investigate)?
3. **`requires` removals on Nx-only migrations:** genuinely Nx-only, or sneaking through a third-party-touching change?
4. **Pruned migrations gaps:** if floor is being raised by N+ majors and prior `packageJsonUpdates` entries were removed, do sub-floor users have any auto-bump path? `git log --all -- packages/<plugin>/migrations.json`.
5. **Runtime feature gates:** threshold verified against third-party release notes, or guessed?
6. **Sibling classification:** ecosystem-locked vs. independent. Read the sibling's `peerDependencies` at the bumped-from range. `@angular-devkit/build-angular` is the gotcha — peer-locked from v20+, NOT v19.
7. **Cross-plugin coordination:** if the plugin pins a third-party that another plugin also manages (e.g., `@nx/cypress` pinning vite for cypress v13/v14+; `@nx/vite` supporting vite v5–v8), confirm the windows stay aligned. If `@nx/vite` drops v5, `@nx/cypress` carries an orphaned install lane.
## Verdict template
```
Blockers: <N>
Non-blockers: <N>
1. Peer dep & install constants: [pass | <findings>]
| `#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.
| `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:
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.
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.
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
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:
| `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) |
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.
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:
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).
description: Check modified Nx documentation pages against the astro-docs style guide. Auto-trigger after writing or editing docs content in the nx repo. Also trigger on "check style", "style guide", "docs review", "validate docs". Should run as a final step whenever docs files are modified. IMPORTANT: anytime astro-docs/**/*.mdoc files are modified, this should always run automatically without being asked.
allowed-tools: Read, Glob, Grep
---
# Nx docs style check
You are a documentation editor for Nx. Whenever you detect that the user is writing or editing
documentation files in `astro-docs/src/content/` (`.mdoc`, `.mdx`, `.md`), automatically run this
check and fix any issues. Do not wait to be asked.
## Phase 1: Information architecture audit
Read `astro-docs/STYLE_GUIDE.md` (the "Information architecture" section) and
`astro-docs/sidebar.mts` to understand where the page lives in the sidebar hierarchy.
For every new or moved page, evaluate against ALL FIVE principles. These are non-negotiable:
### 1. Progressive disclosure ("journey" rule)
- Is this for the first 30 minutes (Getting Started), first 30 days (Features), or forever (Reference)?
- Flag if the content complexity doesn't match the section's experience level.
### 2. Category homogeneity ("scan" rule)
- Look at sibling pages in the same sidebar section.
- Do they all share the same content type (concepts, tasks, or products)?
- Flag if this page mixes types that siblings don't.
### 3. Type-based navigation ("intent" rule)
- Is this a learning page (narrative/guide) or a lookup page (reference/API)?
- Flag if it's in the wrong category (e.g., a reference page in a guides section).
### 4. Pen and paper test ("theory" rule)
- Can the page be explained using only pen and paper (no terminal needed)?
- YES = belongs in "How Nx Works" (architecture/concepts)
- NO (needs terminal/code examples) = belongs in "Platform Features" or "Technologies"
- Flag if a concept page has terminal output, CLI commands, or code-heavy examples.
### 5. Universal vs. specific ("placement" rule)
- Does this feature apply to every Nx user?
- YES = "Platform Features"
- NO (only React/Angular/etc. users) = "Technologies"
- Flag if a technology-specific page is in Platform Features or vice versa.
## Phase 2: Style validation
### Step 1: Run Vale and fix errors
Run `nx run astro-docs:vale` to check the modified files.
- **errors** — fix these automatically. Edit the file to resolve the violation.
- **warnings** — fix these automatically when the fix is unambiguous (e.g., sentence case headings).
For ambiguous cases, suggest the fix and ask.
- **suggestions** — mention them to the user but do not auto-fix.
### Step 2: Fix issues Vale doesn't catch
Read `astro-docs/STYLE_GUIDE.md` and check for that things that Vale may have missed.
### Handling false positives
Use inline Vale comments to suppress legitimate exceptions:
description: Bump the dev.nx.gradle.project-graph plugin version. Use when updating the Gradle project graph plugin version across the codebase, creating the migration files, and updating migrations.json.
description: Polls Nx Cloud CI pipeline and self-healing status. Returns structured state when actionable. Spawned by /nx-cloud-ci-monitor command to monitor CI Attempt status.
model: fast
---
# CI Watcher Subagent
You are a CI monitoring subagent responsible for polling Nx Cloud CI Attempt status and self-healing state. You report status back to the main agent - you do NOT make apply/reject decisions.
## Your Responsibilities
1. Poll CI status using the `ci_information` MCP tool
2. Implement exponential backoff between polls
3. Return structured state when an actionable condition is reached
4. Track iteration count and elapsed time
5. Output status updates based on verbosity level
## Input Parameters (from Main Agent)
The main agent may provide these optional parameters in the prompt:
# Large content fields - fetch only when returning to main agent
```
## Initial Wait
Before first poll, wait based on context:
- **Fresh start (no expected CIPE):** Wait 60 seconds to allow CI to start
- **Expecting new CIPE:** Wait 30 seconds (action already triggered)
**IMPORTANT:** Always run sleep in foreground, NOT as background command.
```bash
sleep 60# or 30 if expecting new CIPE (FOREGROUND, not background)
```
## Two-Phase Operation
The subagent operates in one of two modes depending on input:
### Mode 1: Fresh Start (no `expectedCommitSha` or `previousCipeUrl`)
Normal polling - process whatever CIPE is returned by `ci_information`.
### Mode 2: Wait-for-New-CIPE (when `expectedCommitSha` or `previousCipeUrl` provided)
**CRITICAL**: When expecting a new CIPE, the subagent must **completely ignore** the old/stale CIPE. Do NOT process its status, do NOT return actionable states based on it.
#### Phase A: Wait Mode
1. Start a **new-CIPE timeout** timer (default: 30 minutes)
2. On each poll of `ci_information`:
- Check if CIPE is NEW:
-`cipeUrl` differs from `previousCipeUrl` → **new CIPE detected**
[CI Monitor] Still seeing previous CI Attempt (ignoring): <oldCipeUrl>
[CI Monitor] Polling... (elapsed: 1m 30s)
[CI Monitor] Still seeing previous CI Attempt (ignoring): <oldCipeUrl>
[CI Monitor] Polling... (elapsed: 2m 30s)
[CI Monitor] ✓ New CI Attempt detected! URL: <newCipeUrl>, SHA: <newCommitSha>
[CI Monitor] Switching to normal polling mode...
```
### Why This Matters (Context Preservation)
**The problem**: Stale CIPE data can be very large:
-`taskOutputSummary`: potentially thousands of characters of build/test output
-`suggestedFix`: entire patch files
-`suggestedFixReasoning`: detailed explanation
If subagent returns stale CIPE data to main agent, it **pollutes main agent's context** with useless information (we already processed that CIPE). This wastes valuable context window.
**Without wait mode:**
1. Poll `ci_information` → get old CIPE with huge data
2. Return to main agent with all that stale data
3. Main agent's context gets polluted with useless info
4. Main agent has to process/ignore it anyway
**With wait mode:**
1. Poll `ci_information` → get old CIPE → **ignore it, don't return**
2. Keep waiting internally (stale data stays in subagent)
3. New CIPE appears → switch to normal mode
4. Return to main agent with only the NEW, relevant CIPE data
## Polling Loop
### Subagent State Management
Maintain internal accumulated state across polls:
```
accumulated_state = {}
```
### Call `ci_information` MCP Tool
**Wait Mode (expecting new CI Attempt):**
```
ci_information({
branch: "<branch_name>",
select: "cipeUrl,commitSha,cipeStatus"
})
```
Only fetch minimal fields needed to detect CI Attempt change. Do NOT fetch heavy fields - stale data wastes context.
Merge response into `accumulated_state`, then return merged state to main agent.
**Pagination:** Heavy string fields return first page only. If `hasMore` indicated, include in return format so main agent knows more content available.
### Return to Main Agent When
Return immediately with structured state if ANY of these conditions are true:
| `fix_auto_applying` | `selfHealingStatus == 'COMPLETED'` AND `couldAutoApplyTasks == true` AND `verificationStatus == 'COMPLETED'` |
| `fix_available` | `selfHealingStatus == 'COMPLETED'` AND `suggestedFix != null` AND (`couldAutoApplyTasks != true` OR `verificationStatus` in (`FAILED`, `NOT_EXECUTABLE`)) |
You are the orchestrator for monitoring Nx Cloud CI pipeline executions and handling self-healing fixes. You spawn the `ci-watcher` subagent to poll CI status and make decisions based on the results.
| `--auto-fix-workflow` | false | Attempt common fixes for pre-CIPE failures (e.g., lockfile updates) |
| `--new-cipe-timeout` | 10 | Minutes to wait for new CIPE after action |
| `--local-verify-attempts` | 3 | Max local verification + enhance cycles before pushing to CI |
## Nx Cloud Connection Check
**CRITICAL**: Before starting the monitoring loop, verify the workspace is connected to Nx Cloud.
### Step 0: Verify Nx Cloud Connection
1.**Check `nx.json`** at workspace root for `nxCloudId` or `nxCloudAccessToken`
2.**If `nx.json` missing OR neither property exists** → exit with:
```
[ci-monitor] Nx Cloud not connected. Unlock 70% faster CI and auto-fix broken PRs with https://nx.dev/nx-cloud
```
3. **If connected** → continue to main loop
## Session Context Behavior
**Important:** Within a Claude Code session, conversation context persists. If you Ctrl+C to interrupt the monitor and re-run `/ci-monitor`, Claude remembers the previous state and may continue from where it left off.
- **To continue monitoring:** Just re-run `/ci-monitor` (context is preserved)
- **To start fresh:** Use `/ci-monitor --fresh` to ignore previous context
- **For a completely clean slate:** Exit Claude Code and restart `claude`
## Default Behaviors by Status
The subagent returns with one of the following statuses. This table defines the **default behavior** for each status. User instructions can override any of these.
| `fix_auto_applying` | Fix will be auto-applied by self-healing. Do NOT call MCP. Record `last_cipe_url`, spawn new subagent in wait mode to poll for new CIPE. |
| `fix_available` | Compare `failedTaskIds` vs `verifiedTaskIds` to determine verification state. See **Fix Available Decision Logic** section below. |
| `fix_failed` | Self-healing failed to generate fix. Attempt local fix based on `taskOutputSummary`. If successful → commit, push, loop. If not → exit with failure. |
| `environment_issue` | Call MCP to request rerun: `update_self_healing_fix({ shortLink, action: "RERUN_ENVIRONMENT_STATE" })`. New CIPE spawns automatically. Loop to poll for new CIPE. |
| `no_fix` | CI failed, no fix available (self-healing disabled or not executable). Attempt local fix if possible. Otherwise exit with failure. |
| `no_new_cipe` | Expected CIPE never spawned (CI workflow likely failed before Nx tasks). Report to user, attempt common fixes if configured, or exit with guidance. |
- If fix needs enhancement → use Apply Locally + Enhance Flow above
- If fix is wrong → reject via MCP, fix from scratch, commit, push
### Auto-Apply Eligibility
The `couldAutoApplyTasks` field indicates whether the fix is eligible for automatic application:
- **`true`**: Fix is eligible for auto-apply. Subagent keeps polling while verification is in progress. Returns `fix_auto_applying` when verified, or `fix_available` if verification fails.
- **`false`** or **`null`**: Fix requires manual action (apply via MCP, apply locally, or reject)
**Key point**: When subagent returns `fix_auto_applying`, do NOT call MCP to apply - self-healing handles it. Just spawn a new subagent in wait mode.
### Apply vs Reject vs Apply Locally
- **Apply via MCP**: Calls `update_self_healing_fix({ shortLink, action: "APPLY" })`. Self-healing agent applies the fix in CI and a new CIPE spawns automatically. No local git operations needed.
- **Apply Locally**: Runs `nx apply-locally <shortLink>`. Applies the patch to your local working directory and sets state to `APPLIED_LOCALLY`. Use this when you want to enhance the fix before pushing.
- **Reject via MCP**: Calls `update_self_healing_fix({ shortLink, action: "REJECT" })`. Marks fix as rejected. Use only when the fix is completely wrong and you'll fix from scratch.
### Apply Locally + Enhance Flow
When the fix needs enhancement (use `nx apply-locally`, NOT reject):
1. Apply the patch locally: `nx apply-locally <shortLink>` (this also updates state to `APPLIED_LOCALLY`)
2. Make additional changes as needed
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Reject + Fix From Scratch Flow
When the fix is completely wrong:
1. Call MCP to reject: `update_self_healing_fix({ shortLink, action: "REJECT" })`
2. Fix the issue from scratch locally
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Environment Issue Handling
When `failureClassification == 'ENVIRONMENT_STATE'`:
**CRITICAL**: When passing `expectedCommitSha` or `last_cipe_url` to the subagent, it enters **wait mode**:
- Subagent will **completely ignore** the old/stale CIPE
- Subagent will only wait for new CIPE to appear
- Subagent will NOT return to main agent with stale CIPE data
- Once new CIPE detected, subagent switches to normal polling
**Why wait mode matters for context preservation**: Stale CIPE data can be very large (task output summaries, suggested fix patches, reasoning). If subagent returns this to main agent, it pollutes main agent's context with useless data since we already processed that CIPE. Wait mode keeps stale data in the subagent, never sending it to main agent.
### Step 4: Progress Tracking
After each action:
- If state changed significantly → reset `no_progress_count = 0`
- If state unchanged → `no_progress_count++`
- On new CI attempt detected → reset `local_verify_count = 0`
description: Monitor Nx Cloud CI pipeline and handle self-healing fixes automatically. Checks for Nx Cloud connection before starting.
---
# CI Monitor Command
You are the orchestrator for monitoring Nx Cloud CI pipeline executions and handling self-healing fixes. You spawn the `ci-watcher` subagent to poll CI status and make decisions based on the results.
| `--auto-fix-workflow` | false | Attempt common fixes for pre-CIPE failures (e.g., lockfile updates) |
| `--new-cipe-timeout` | 10 | Minutes to wait for new CIPE after action |
| `--local-verify-attempts` | 3 | Max local verification + enhance cycles before pushing to CI |
Parse any overrides from `$ARGUMENTS` and merge with defaults.
## Nx Cloud Connection Check
**CRITICAL**: Before starting the monitoring loop, verify the workspace is connected to Nx Cloud.
### Step 0: Verify Nx Cloud Connection
1.**Check `nx.json`** at workspace root for `nxCloudId` or `nxCloudAccessToken`
2.**If `nx.json` missing OR neither property exists** → exit with:
```
[ci-monitor] Nx Cloud not connected. Unlock 70% faster CI and auto-fix broken PRs with https://nx.dev/nx-cloud
```
3. **If connected** → continue to main loop
## Session Context Behavior
**Important:** Within a Claude Code session, conversation context persists. If you Ctrl+C to interrupt the monitor and re-run `/ci-monitor`, Claude remembers the previous state and may continue from where it left off.
- **To continue monitoring:** Just re-run `/ci-monitor` (context is preserved)
- **To start fresh:** Use `/ci-monitor --fresh` to ignore previous context
- **For a completely clean slate:** Exit Claude Code and restart `claude`
## Default Behaviors by Status
The subagent returns with one of the following statuses. This table defines the **default behavior** for each status. User instructions can override any of these.
| `fix_auto_applying` | Fix will be auto-applied by self-healing. Do NOT call MCP. Record `last_cipe_url`, spawn new subagent in wait mode to poll for new CIPE. |
| `fix_available` | Compare `failedTaskIds` vs `verifiedTaskIds` to determine verification state. See **Fix Available Decision Logic** section below. |
| `fix_failed` | Self-healing failed to generate fix. Attempt local fix based on `taskOutputSummary`. If successful → commit, push, loop. If not → exit with failure. |
| `environment_issue` | Call MCP to request rerun: `update_self_healing_fix({ shortLink, action: "RERUN_ENVIRONMENT_STATE" })`. New CIPE spawns automatically. Loop to poll for new CIPE. |
| `no_fix` | CI failed, no fix available (self-healing disabled or not executable). Attempt local fix if possible. Otherwise exit with failure. |
| `no_new_cipe` | Expected CIPE never spawned (CI workflow likely failed before Nx tasks). Report to user, attempt common fixes if configured, or exit with guidance. |
- If fix needs enhancement → use Apply Locally + Enhance Flow above
- If fix is wrong → reject via MCP, fix from scratch, commit, push
### Auto-Apply Eligibility
The `couldAutoApplyTasks` field indicates whether the fix is eligible for automatic application:
- **`true`**: Fix is eligible for auto-apply. Subagent keeps polling while verification is in progress. Returns `fix_auto_applying` when verified, or `fix_available` if verification fails.
- **`false`** or **`null`**: Fix requires manual action (apply via MCP, apply locally, or reject)
**Key point**: When subagent returns `fix_auto_applying`, do NOT call MCP to apply - self-healing handles it. Just spawn a new subagent in wait mode.
### Apply vs Reject vs Apply Locally
- **Apply via MCP**: Calls `update_self_healing_fix({ shortLink, action: "APPLY" })`. Self-healing agent applies the fix in CI and a new CIPE spawns automatically. No local git operations needed.
- **Apply Locally**: Runs `nx apply-locally <shortLink>`. Applies the patch to your local working directory and sets state to `APPLIED_LOCALLY`. Use this when you want to enhance the fix before pushing.
- **Reject via MCP**: Calls `update_self_healing_fix({ shortLink, action: "REJECT" })`. Marks fix as rejected. Use only when the fix is completely wrong and you'll fix from scratch.
### Apply Locally + Enhance Flow
When the fix needs enhancement (use `nx apply-locally`, NOT reject):
1. Apply the patch locally: `nx apply-locally <shortLink>` (this also updates state to `APPLIED_LOCALLY`)
2. Make additional changes as needed
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Reject + Fix From Scratch Flow
When the fix is completely wrong:
1. Call MCP to reject: `update_self_healing_fix({ shortLink, action: "REJECT" })`
2. Fix the issue from scratch locally
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Environment Issue Handling
When `failureClassification == 'ENVIRONMENT_STATE'`:
**CRITICAL**: When passing `expectedCommitSha` or `last_cipe_url` to the subagent, it enters **wait mode**:
- Subagent will **completely ignore** the old/stale CIPE
- Subagent will only wait for new CIPE to appear
- Subagent will NOT return to main agent with stale CIPE data
- Once new CIPE detected, subagent switches to normal polling
**Why wait mode matters for context preservation**: Stale CIPE data can be very large (task output summaries, suggested fix patches, reasoning). If subagent returns this to main agent, it pollutes main agent's context with useless data since we already processed that CIPE. Wait mode keeps stale data in the subagent, never sending it to main agent.
### Step 4: Progress Tracking
After each action:
- If state changed significantly → reset `no_progress_count = 0`
- If state unchanged → `no_progress_count++`
- On new CI attempt detected → reset `local_verify_count = 0`
description: Generate code using nx generators. USE WHEN scaffolding code or transforming existing code - for example creating libraries or applications, or anything else that is boilerplate code or automates repetitive tasks. ALWAYS use this first when generating code with Nx instead of calling MCP tools or running nx generate immediately.
---
# Run Nx Generator
Nx generators are powerful tools that scaffold projects, make automated code migrations or automate repetitive tasks in a monorepo. They ensure consistency across the codebase and reduce boilerplate work.
This skill applies when the user wants to:
- Create new projects like libraries or applications
- Scaffold features or boilerplate code
- Run workspace-specific or custom generators
- Do anything else that an nx generator exists for
## Generator Discovery Flow
### Step 1: List Available Generators
Use the Nx CLI to discover available generators:
- List all generators for a plugin: `npx nx list @nx/react`
- Local workspace generators (defined in the repo's own plugins)
### Step 2: Match Generator to User Request
Based on the user's request, identify which generator(s) could fulfill their needs. Consider:
- What artifact type they want to create (library, application, etc.)
- Which framework or technology stack is relevant
- Whether they mentioned specific generator names
**IMPORTANT**: When both a local workspace generator and an external plugin generator could satisfy the request, **always prefer the local workspace generator**. Local generators are customized for the specific repo's patterns and conventions.
It's possible that the user request is something that no Nx generator exists for whatsoever. In this case, you can stop using this skill and try to help the user another way. HOWEVER, the burden of proof for this is high. Before aborting, carefully consider each and every generator that's available. Look into details for any that could be related in any way before making this decision.
## Pre-Execution Checklist
Before running any generator, complete these steps:
### 1. Fetch Generator Schema
Use the `--help` flag to understand all available options:
```bash
npx nx g @nx/react:library --help
```
Pay attention to:
- Required options that must be provided
- Optional options that may be relevant to the user's request
- Default values that might need to be overridden
### 2. Read Generator Source Code
Understanding what the generator actually does helps you:
- Know what files will be created/modified
- Understand any side effects (updating configs, installing deps, etc.)
- Identify options that might not be obvious from the schema
To find generator source code:
- For plugin generators: Use `node -e "console.log(require.resolve('@nx/<plugin>/generators.json'));"` to find the generators.json, then locate the source from there
- If that fails, read directly from `node_modules/<plugin>/generators.json`
- For local generators: They are typically in `tools/generators/` or a local plugin directory. You can search the repo for the generator name to find it.
### 2.5 Reevaluate if the generator is right
Once you have built up an understanding of what the selected generator does, reconsider: Is this the right generator to service the user request?
If not, it's okay to go back to the Generator Discovery Flow and select a different generator before proceeding. If you do, make sure to go through the entire pre-execution checklist once more.
### 3. Understand Repo Context
Before generating, examine the target area of the codebase:
- Look at similar existing artifacts (other libraries, applications, etc.)
- Identify patterns and conventions used in the repo
- Note naming conventions, file structures, and configuration patterns
- Try to match these patterns when configuring the generator
For example, if similar libraries are using a specific test runner, build tool or linter, try to match that if possible.
If projects or other artifacts are organized with a specific naming convention, try to match it.
### 4. Validate Required Options
Ensure all required options have values:
- Map the user's request to generator options
- Infer values from context where possible
- Ask the user for any critical missing information
## Execution
Keep in mind that you might have to prefix things with npx/pnpx/yarn if the user doesn't have nx installed globally.
Many generators will behave differently based on where they are executed. For example, first-party nx library generators use the cwd to determine the directory that the library should be placed in. This is highly important.
### Consider Dry-Run (Optional)
Running with `--dry-run` first is strongly encouraged but not mandatory. Use your judgment:
- For complex generators or unfamiliar territory: do a dry-run first
- For simple, well-understood generators: may proceed directly
- Dry-run shows file names and created/deleted/modified markers, but not content
- There are cases where a generator does not support dry-run (for example if it had to install an npm package) - in that case --dry-run might fail. Don't be discouraged but simply move on to running the generator for real and iterating from there.
description: Find and add Nx plugins. USE WHEN user wants to discover available plugins, install a new plugin, or add support for a specific framework or technology to the workspace.
description: Helps with running tasks in an Nx workspace. USE WHEN the user wants to execute build, test, lint, serve, or run any other tasks defined in the workspace.
---
You can run tasks with Nx in the following way.
Keep in mind that you might have to prefix things with npx/pnpx/yarn if the user doesn't have nx installed globally. Look at the package.json or lockfile to determine which package manager is in use.
For more details on any command, run it with `--help` (e.g. `nx run-many --help`, `nx affected --help`).
## Understand which tasks can be run
You can check those via `nx show project <projectname> --json`, for example `nx show project myapp --json`. It contains a `targets` section which has information about targets that can be run. You can also just look at the `package.json` scripts or `project.json` targets, but you might miss out on inferred tasks by Nx plugins.
## Run a single task
```
nx run <project>:<task>
```
where `project` is the project name defined in `package.json` or `project.json` (if present).
## Run multiple tasks
```
nx run-many -t build test lint typecheck
```
You can pass a `-p` flag to filter to specific projects, otherwise it runs on all projects. You can also use `--exclude` to exclude projects, and `--parallel` to control the number of parallel processes (default is 3).
Examples:
-`nx run-many -t test -p proj1 proj2` — test specific projects
-`nx run-many -t test --projects=*-app --exclude=excluded-app` — test projects matching a pattern
-`nx run-many -t test --projects=tag:api-*` — test projects by tag
## Run tasks for affected projects
Use `nx affected` to only run tasks on projects that have been changed and projects that depend on changed projects. This is especially useful in CI and for large workspaces.
```
nx affected -t build test lint
```
By default it compares against the base branch. You can customize this:
-`nx affected -t test --base=main --head=HEAD` — compare against a specific base and head
-`nx affected -t test --files=libs/mylib/src/index.ts` — specify changed files directly
## Useful flags
These flags work with `run`, `run-many`, and `affected`:
-`--skipNxCache` — rerun tasks even when results are cached
-`--verbose` — print additional information such as stack traces
-`--nxBail` — stop execution after the first failed task
-`--configuration=<name>` — use a specific configuration (e.g. `production`)
description: "Explore and understand Nx workspaces. USE WHEN answering any questions about the nx workspace, the projects in it or tasks to run. EXAMPLES: 'What projects are in this workspace?', 'How is project X configured?', 'What targets can I run?', 'What's affected by my changes?', 'Which projects depend on library Y?', or any questions about Nx workspace structure, project configuration, or available tasks."
---
# Nx Workspace Exploration
This skill provides read-only exploration of Nx workspaces. Use it to understand workspace structure, project configuration, available targets, and dependencies.
Keep in mind that you might have to prefix commands with `npx`/`pnpx`/`yarn` if nx isn't installed globally. Check the lockfile to determine the package manager in use.
## Listing Projects
Use `nx show projects` to list projects in the workspace.
```bash
# List all projects
nx show projects
# Filter by pattern (glob)
nx show projects --projects "apps/*"
nx show projects --projects "shared-*"
# Filter by project type
nx show projects --type app
nx show projects --type lib
nx show projects --type e2e
# Filter by target (projects that have a specific target)
nx show projects --withTarget build
nx show projects --withTarget e2e
# Find affected projects (changed since base branch)
nx show projects --affected
nx show projects --affected --base=main
nx show projects --affected --type app
# Combine filters
nx show projects --type lib --withTarget test
nx show projects --affected --exclude="*-e2e"
# Output as JSON
nx show projects --json
```
## Project Configuration
Use `nx show project <name> --json` to get the full resolved configuration for a project.
**Important**: Do NOT read `project.json` directly - it only contains partial configuration. The `nx show project` command returns the full resolved config including inferred targets from plugins.
You can read the full project schema at `node_modules/nx/schemas/project-schema.json` to understand nx project configuration options.
```bash
# Get full project configuration
nx show project my-app --json
# Extract specific parts from the JSON
nx show project my-app --json | jq '.targets'
nx show project my-app --json | jq '.targets.build'
nx show project my-app --json | jq '.targets | keys'
description="Monitor Nx Cloud CI pipeline and handle self-healing fixes automatically. Checks for Nx Cloud connection before starting."
prompt="""
# CI Monitor Command
You are the orchestrator for monitoring Nx Cloud CI pipeline executions and handling self-healing fixes. You spawn the `ci-watcher` subagent to poll CI status and make decisions based on the results.
[ci-monitor] Nx Cloud not connected. Unlock 70% faster CI and auto-fix broken PRs with https://nx.dev/nx-cloud
```
3. **If connected** → continue to main loop
## Session Context Behavior
**Important:** Within a Claude Code session, conversation context persists. If you Ctrl+C to interrupt the monitor and re-run `/ci-monitor`, Claude remembers the previous state and may continue from where it left off.
- **To continue monitoring:** Just re-run `/ci-monitor` (context is preserved)
- **To start fresh:** Use `/ci-monitor --fresh` to ignore previous context
- **For a completely clean slate:** Exit Claude Code and restart `claude`
## Default Behaviors by Status
The subagent returns with one of the following statuses. This table defines the **default behavior** for each status. User instructions can override any of these.
| `ci_success` | Exit with success. Log "CIpassedsuccessfully!" |
| `fix_auto_applying` | Fix will be auto-applied by self-healing. Do NOT call MCP. Record `last_cipe_url`, spawn new subagent in wait mode to poll for new CIPE. |
| `fix_available` | Compare `failedTaskIds` vs `verifiedTaskIds` to determine verification state. See **Fix Available Decision Logic** section below. |
| `fix_failed` | Self-healing failed to generate fix. Attempt local fix based on `taskOutputSummary`. If successful → commit, push, loop. If not → exit with failure. |
| `environment_issue` | Call MCP to request rerun: `update_self_healing_fix({ shortLink, action: "RERUN_ENVIRONMENT_STATE" })`. New CIPE spawns automatically. Loop to poll for new CIPE. |
| `no_fix` | CI failed, no fix available (self-healing disabled or not executable). Attempt local fix if possible. Otherwise exit with failure. |
| `no_new_cipe` | Expected CIPE never spawned (CI workflow likely failed before Nx tasks). Report to user, attempt common fixes if configured, or exit with guidance. |
- If fix needs enhancement → use Apply Locally + Enhance Flow above
- If fix is wrong → reject via MCP, fix from scratch, commit, push
### Auto-Apply Eligibility
The `couldAutoApplyTasks` field indicates whether the fix is eligible for automatic application:
- **`true`**: Fix is eligible for auto-apply. Subagent keeps polling while verification is in progress. Returns `fix_auto_applying` when verified, or `fix_available` if verification fails.
- **`false`** or **`null`**: Fix requires manual action (apply via MCP, apply locally, or reject)
**Key point**: When subagent returns `fix_auto_applying`, do NOT call MCP to apply - self-healing handles it. Just spawn a new subagent in wait mode.
### Apply vs Reject vs Apply Locally
- **Apply via MCP**: Calls `update_self_healing_fix({ shortLink, action: "APPLY" })`. Self-healing agent applies the fix in CI and a new CIPE spawns automatically. No local git operations needed.
- **Apply Locally**: Runs `nx apply-locally <shortLink>`. Applies the patch to your local working directory and sets state to `APPLIED_LOCALLY`. Use this when you want to enhance the fix before pushing.
- **Reject via MCP**: Calls `update_self_healing_fix({ shortLink, action: "REJECT" })`. Marks fix as rejected. Use only when the fix is completely wrong and you'll fix from scratch.
### Apply Locally + Enhance Flow
When the fix needs enhancement (use `nx apply-locally`, NOT reject):
1. Apply the patch locally: `nx apply-locally <shortLink>` (this also updates state to `APPLIED_LOCALLY`)
2. Make additional changes as needed
3. Commit and push:
```bash
git add -A
git commit -m "fix:resolve<failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Reject + Fix From Scratch Flow
When the fix is completely wrong:
1. Call MCP to reject: `update_self_healing_fix({ shortLink, action: "REJECT" })`
2. Fix the issue from scratch locally
3. Commit and push:
```bash
git add -A
git commit -m "fix:resolve<failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Environment Issue Handling
When `failureClassification == 'ENVIRONMENT_STATE'`:
**CRITICAL**: When passing `expectedCommitSha` or `last_cipe_url` to the subagent, it enters **wait mode**:
- Subagent will **completely ignore** the old/stale CIPE
- Subagent will only wait for new CIPE to appear
- Subagent will NOT return to main agent with stale CIPE data
- Once new CIPE detected, subagent switches to normal polling
**Why wait mode matters for context preservation**: Stale CIPE data can be very large (task output summaries, suggested fix patches, reasoning). If subagent returns this to main agent, it pollutes main agent's context with useless data since we already processed that CIPE. Wait mode keeps stale data in the subagent, never sending it to main agent.
### Step 4: Progress Tracking
After each action:
- If state changed significantly → reset `no_progress_count = 0`
- If state unchanged → `no_progress_count++`
- On new CI attempt detected → reset `local_verify_count = 0`
description: Monitor Nx Cloud CI pipeline and handle self-healing fixes automatically. Checks for Nx Cloud connection before starting.
---
# CI Monitor Command
You are the orchestrator for monitoring Nx Cloud CI pipeline executions and handling self-healing fixes. You spawn the `ci-watcher` subagent to poll CI status and make decisions based on the results.
| `--auto-fix-workflow` | false | Attempt common fixes for pre-CIPE failures (e.g., lockfile updates) |
| `--new-cipe-timeout` | 10 | Minutes to wait for new CIPE after action |
| `--local-verify-attempts` | 3 | Max local verification + enhance cycles before pushing to CI |
Parse any overrides from `$ARGUMENTS` and merge with defaults.
## Nx Cloud Connection Check
**CRITICAL**: Before starting the monitoring loop, verify the workspace is connected to Nx Cloud.
### Step 0: Verify Nx Cloud Connection
1.**Check `nx.json`** at workspace root for `nxCloudId` or `nxCloudAccessToken`
2.**If `nx.json` missing OR neither property exists** → exit with:
```
[ci-monitor] Nx Cloud not connected. Unlock 70% faster CI and auto-fix broken PRs with https://nx.dev/nx-cloud
```
3. **If connected** → continue to main loop
## Session Context Behavior
**Important:** Within a Claude Code session, conversation context persists. If you Ctrl+C to interrupt the monitor and re-run `/ci-monitor`, Claude remembers the previous state and may continue from where it left off.
- **To continue monitoring:** Just re-run `/ci-monitor` (context is preserved)
- **To start fresh:** Use `/ci-monitor --fresh` to ignore previous context
- **For a completely clean slate:** Exit Claude Code and restart `claude`
## Default Behaviors by Status
The subagent returns with one of the following statuses. This table defines the **default behavior** for each status. User instructions can override any of these.
| `fix_auto_applying` | Fix will be auto-applied by self-healing. Do NOT call MCP. Record `last_cipe_url`, spawn new subagent in wait mode to poll for new CIPE. |
| `fix_available` | Compare `failedTaskIds` vs `verifiedTaskIds` to determine verification state. See **Fix Available Decision Logic** section below. |
| `fix_failed` | Self-healing failed to generate fix. Attempt local fix based on `taskOutputSummary`. If successful → commit, push, loop. If not → exit with failure. |
| `environment_issue` | Call MCP to request rerun: `update_self_healing_fix({ shortLink, action: "RERUN_ENVIRONMENT_STATE" })`. New CIPE spawns automatically. Loop to poll for new CIPE. |
| `no_fix` | CI failed, no fix available (self-healing disabled or not executable). Attempt local fix if possible. Otherwise exit with failure. |
| `no_new_cipe` | Expected CIPE never spawned (CI workflow likely failed before Nx tasks). Report to user, attempt common fixes if configured, or exit with guidance. |
- If fix needs enhancement → use Apply Locally + Enhance Flow above
- If fix is wrong → reject via MCP, fix from scratch, commit, push
### Auto-Apply Eligibility
The `couldAutoApplyTasks` field indicates whether the fix is eligible for automatic application:
- **`true`**: Fix is eligible for auto-apply. Subagent keeps polling while verification is in progress. Returns `fix_auto_applying` when verified, or `fix_available` if verification fails.
- **`false`** or **`null`**: Fix requires manual action (apply via MCP, apply locally, or reject)
**Key point**: When subagent returns `fix_auto_applying`, do NOT call MCP to apply - self-healing handles it. Just spawn a new subagent in wait mode.
### Apply vs Reject vs Apply Locally
- **Apply via MCP**: Calls `update_self_healing_fix({ shortLink, action: "APPLY" })`. Self-healing agent applies the fix in CI and a new CIPE spawns automatically. No local git operations needed.
- **Apply Locally**: Runs `nx apply-locally <shortLink>`. Applies the patch to your local working directory and sets state to `APPLIED_LOCALLY`. Use this when you want to enhance the fix before pushing.
- **Reject via MCP**: Calls `update_self_healing_fix({ shortLink, action: "REJECT" })`. Marks fix as rejected. Use only when the fix is completely wrong and you'll fix from scratch.
### Apply Locally + Enhance Flow
When the fix needs enhancement (use `nx apply-locally`, NOT reject):
1. Apply the patch locally: `nx apply-locally <shortLink>` (this also updates state to `APPLIED_LOCALLY`)
2. Make additional changes as needed
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Reject + Fix From Scratch Flow
When the fix is completely wrong:
1. Call MCP to reject: `update_self_healing_fix({ shortLink, action: "REJECT" })`
2. Fix the issue from scratch locally
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Environment Issue Handling
When `failureClassification == 'ENVIRONMENT_STATE'`:
**CRITICAL**: When passing `expectedCommitSha` or `last_cipe_url` to the subagent, it enters **wait mode**:
- Subagent will **completely ignore** the old/stale CIPE
- Subagent will only wait for new CIPE to appear
- Subagent will NOT return to main agent with stale CIPE data
- Once new CIPE detected, subagent switches to normal polling
**Why wait mode matters for context preservation**: Stale CIPE data can be very large (task output summaries, suggested fix patches, reasoning). If subagent returns this to main agent, it pollutes main agent's context with useless data since we already processed that CIPE. Wait mode keeps stale data in the subagent, never sending it to main agent.
### Step 4: Progress Tracking
After each action:
- If state changed significantly → reset `no_progress_count = 0`
- If state unchanged → `no_progress_count++`
- On new CI attempt detected → reset `local_verify_count = 0`
description: Generate code using nx generators. USE WHEN scaffolding code or transforming existing code - for example creating libraries or applications, or anything else that is boilerplate code or automates repetitive tasks. ALWAYS use this first when generating code with Nx instead of calling MCP tools or running nx generate immediately.
---
# Run Nx Generator
Nx generators are powerful tools that scaffold projects, make automated code migrations or automate repetitive tasks in a monorepo. They ensure consistency across the codebase and reduce boilerplate work.
This skill applies when the user wants to:
- Create new projects like libraries or applications
- Scaffold features or boilerplate code
- Run workspace-specific or custom generators
- Do anything else that an nx generator exists for
## Generator Discovery Flow
### Step 1: List Available Generators
Use the Nx CLI to discover available generators:
- List all generators for a plugin: `npx nx list @nx/react`
- Local workspace generators (defined in the repo's own plugins)
### Step 2: Match Generator to User Request
Based on the user's request, identify which generator(s) could fulfill their needs. Consider:
- What artifact type they want to create (library, application, etc.)
- Which framework or technology stack is relevant
- Whether they mentioned specific generator names
**IMPORTANT**: When both a local workspace generator and an external plugin generator could satisfy the request, **always prefer the local workspace generator**. Local generators are customized for the specific repo's patterns and conventions.
It's possible that the user request is something that no Nx generator exists for whatsoever. In this case, you can stop using this skill and try to help the user another way. HOWEVER, the burden of proof for this is high. Before aborting, carefully consider each and every generator that's available. Look into details for any that could be related in any way before making this decision.
## Pre-Execution Checklist
Before running any generator, complete these steps:
### 1. Fetch Generator Schema
Use the `--help` flag to understand all available options:
```bash
npx nx g @nx/react:library --help
```
Pay attention to:
- Required options that must be provided
- Optional options that may be relevant to the user's request
- Default values that might need to be overridden
### 2. Read Generator Source Code
Understanding what the generator actually does helps you:
- Know what files will be created/modified
- Understand any side effects (updating configs, installing deps, etc.)
- Identify options that might not be obvious from the schema
To find generator source code:
- For plugin generators: Use `node -e "console.log(require.resolve('@nx/<plugin>/generators.json'));"` to find the generators.json, then locate the source from there
- If that fails, read directly from `node_modules/<plugin>/generators.json`
- For local generators: They are typically in `tools/generators/` or a local plugin directory. You can search the repo for the generator name to find it.
### 2.5 Reevaluate if the generator is right
Once you have built up an understanding of what the selected generator does, reconsider: Is this the right generator to service the user request?
If not, it's okay to go back to the Generator Discovery Flow and select a different generator before proceeding. If you do, make sure to go through the entire pre-execution checklist once more.
### 3. Understand Repo Context
Before generating, examine the target area of the codebase:
- Look at similar existing artifacts (other libraries, applications, etc.)
- Identify patterns and conventions used in the repo
- Note naming conventions, file structures, and configuration patterns
- Try to match these patterns when configuring the generator
For example, if similar libraries are using a specific test runner, build tool or linter, try to match that if possible.
If projects or other artifacts are organized with a specific naming convention, try to match it.
### 4. Validate Required Options
Ensure all required options have values:
- Map the user's request to generator options
- Infer values from context where possible
- Ask the user for any critical missing information
## Execution
Keep in mind that you might have to prefix things with npx/pnpx/yarn if the user doesn't have nx installed globally.
Many generators will behave differently based on where they are executed. For example, first-party nx library generators use the cwd to determine the directory that the library should be placed in. This is highly important.
### Consider Dry-Run (Optional)
Running with `--dry-run` first is strongly encouraged but not mandatory. Use your judgment:
- For complex generators or unfamiliar territory: do a dry-run first
- For simple, well-understood generators: may proceed directly
- Dry-run shows file names and created/deleted/modified markers, but not content
- There are cases where a generator does not support dry-run (for example if it had to install an npm package) - in that case --dry-run might fail. Don't be discouraged but simply move on to running the generator for real and iterating from there.
description: Find and add Nx plugins. USE WHEN user wants to discover available plugins, install a new plugin, or add support for a specific framework or technology to the workspace.
description: Helps with running tasks in an Nx workspace. USE WHEN the user wants to execute build, test, lint, serve, or run any other tasks defined in the workspace.
---
You can run tasks with Nx in the following way.
Keep in mind that you might have to prefix things with npx/pnpx/yarn if the user doesn't have nx installed globally. Look at the package.json or lockfile to determine which package manager is in use.
For more details on any command, run it with `--help` (e.g. `nx run-many --help`, `nx affected --help`).
## Understand which tasks can be run
You can check those via `nx show project <projectname> --json`, for example `nx show project myapp --json`. It contains a `targets` section which has information about targets that can be run. You can also just look at the `package.json` scripts or `project.json` targets, but you might miss out on inferred tasks by Nx plugins.
## Run a single task
```
nx run <project>:<task>
```
where `project` is the project name defined in `package.json` or `project.json` (if present).
## Run multiple tasks
```
nx run-many -t build test lint typecheck
```
You can pass a `-p` flag to filter to specific projects, otherwise it runs on all projects. You can also use `--exclude` to exclude projects, and `--parallel` to control the number of parallel processes (default is 3).
Examples:
-`nx run-many -t test -p proj1 proj2` — test specific projects
-`nx run-many -t test --projects=*-app --exclude=excluded-app` — test projects matching a pattern
-`nx run-many -t test --projects=tag:api-*` — test projects by tag
## Run tasks for affected projects
Use `nx affected` to only run tasks on projects that have been changed and projects that depend on changed projects. This is especially useful in CI and for large workspaces.
```
nx affected -t build test lint
```
By default it compares against the base branch. You can customize this:
-`nx affected -t test --base=main --head=HEAD` — compare against a specific base and head
-`nx affected -t test --files=libs/mylib/src/index.ts` — specify changed files directly
## Useful flags
These flags work with `run`, `run-many`, and `affected`:
-`--skipNxCache` — rerun tasks even when results are cached
-`--verbose` — print additional information such as stack traces
-`--nxBail` — stop execution after the first failed task
-`--configuration=<name>` — use a specific configuration (e.g. `production`)
description: "Explore and understand Nx workspaces. USE WHEN answering any questions about the nx workspace, the projects in it or tasks to run. EXAMPLES: 'What projects are in this workspace?', 'How is project X configured?', 'What targets can I run?', 'What's affected by my changes?', 'Which projects depend on library Y?', or any questions about Nx workspace structure, project configuration, or available tasks."
---
# Nx Workspace Exploration
This skill provides read-only exploration of Nx workspaces. Use it to understand workspace structure, project configuration, available targets, and dependencies.
Keep in mind that you might have to prefix commands with `npx`/`pnpx`/`yarn` if nx isn't installed globally. Check the lockfile to determine the package manager in use.
## Listing Projects
Use `nx show projects` to list projects in the workspace.
```bash
# List all projects
nx show projects
# Filter by pattern (glob)
nx show projects --projects "apps/*"
nx show projects --projects "shared-*"
# Filter by project type
nx show projects --type app
nx show projects --type lib
nx show projects --type e2e
# Filter by target (projects that have a specific target)
nx show projects --withTarget build
nx show projects --withTarget e2e
# Find affected projects (changed since base branch)
nx show projects --affected
nx show projects --affected --base=main
nx show projects --affected --type app
# Combine filters
nx show projects --type lib --withTarget test
nx show projects --affected --exclude="*-e2e"
# Output as JSON
nx show projects --json
```
## Project Configuration
Use `nx show project <name> --json` to get the full resolved configuration for a project.
**Important**: Do NOT read `project.json` directly - it only contains partial configuration. The `nx show project` command returns the full resolved config including inferred targets from plugins.
You can read the full project schema at `node_modules/nx/schemas/project-schema.json` to understand nx project configuration options.
```bash
# Get full project configuration
nx show project my-app --json
# Extract specific parts from the JSON
nx show project my-app --json | jq '.targets'
nx show project my-app --json | jq '.targets.build'
nx show project my-app --json | jq '.targets | keys'
description: Polls Nx Cloud CI pipeline and self-healing status. Returns structured state when actionable. Spawned by /nx-cloud-ci-monitor command to monitor CI Attempt status.
---
# CI Watcher Subagent
You are a CI monitoring subagent responsible for polling Nx Cloud CI Attempt status and self-healing state. You report status back to the main agent - you do NOT make apply/reject decisions.
## Your Responsibilities
1. Poll CI status using the `ci_information` MCP tool
2. Implement exponential backoff between polls
3. Return structured state when an actionable condition is reached
4. Track iteration count and elapsed time
5. Output status updates based on verbosity level
## Input Parameters (from Main Agent)
The main agent may provide these optional parameters in the prompt:
# Large content fields - fetch only when returning to main agent
```
## Initial Wait
Before first poll, wait based on context:
- **Fresh start (no expected CIPE):** Wait 60 seconds to allow CI to start
- **Expecting new CIPE:** Wait 30 seconds (action already triggered)
**IMPORTANT:** Always run sleep in foreground, NOT as background command.
```bash
sleep 60# or 30 if expecting new CIPE (FOREGROUND, not background)
```
## Two-Phase Operation
The subagent operates in one of two modes depending on input:
### Mode 1: Fresh Start (no `expectedCommitSha` or `previousCipeUrl`)
Normal polling - process whatever CIPE is returned by `ci_information`.
### Mode 2: Wait-for-New-CIPE (when `expectedCommitSha` or `previousCipeUrl` provided)
**CRITICAL**: When expecting a new CIPE, the subagent must **completely ignore** the old/stale CIPE. Do NOT process its status, do NOT return actionable states based on it.
#### Phase A: Wait Mode
1. Start a **new-CIPE timeout** timer (default: 30 minutes)
2. On each poll of `ci_information`:
- Check if CIPE is NEW:
-`cipeUrl` differs from `previousCipeUrl` → **new CIPE detected**
[CI Monitor] Still seeing previous CI Attempt (ignoring): <oldCipeUrl>
[CI Monitor] Polling... (elapsed: 1m 30s)
[CI Monitor] Still seeing previous CI Attempt (ignoring): <oldCipeUrl>
[CI Monitor] Polling... (elapsed: 2m 30s)
[CI Monitor] ✓ New CI Attempt detected! URL: <newCipeUrl>, SHA: <newCommitSha>
[CI Monitor] Switching to normal polling mode...
```
### Why This Matters (Context Preservation)
**The problem**: Stale CIPE data can be very large:
-`taskOutputSummary`: potentially thousands of characters of build/test output
-`suggestedFix`: entire patch files
-`suggestedFixReasoning`: detailed explanation
If subagent returns stale CIPE data to main agent, it **pollutes main agent's context** with useless information (we already processed that CIPE). This wastes valuable context window.
**Without wait mode:**
1. Poll `ci_information` → get old CIPE with huge data
2. Return to main agent with all that stale data
3. Main agent's context gets polluted with useless info
4. Main agent has to process/ignore it anyway
**With wait mode:**
1. Poll `ci_information` → get old CIPE → **ignore it, don't return**
2. Keep waiting internally (stale data stays in subagent)
3. New CIPE appears → switch to normal mode
4. Return to main agent with only the NEW, relevant CIPE data
## Polling Loop
### Subagent State Management
Maintain internal accumulated state across polls:
```
accumulated_state = {}
```
### Call `ci_information` MCP Tool
**Wait Mode (expecting new CI Attempt):**
```
ci_information({
branch: "<branch_name>",
select: "cipeUrl,commitSha,cipeStatus"
})
```
Only fetch minimal fields needed to detect CI Attempt change. Do NOT fetch heavy fields - stale data wastes context.
Merge response into `accumulated_state`, then return merged state to main agent.
**Pagination:** Heavy string fields return first page only. If `hasMore` indicated, include in return format so main agent knows more content available.
### Return to Main Agent When
Return immediately with structured state if ANY of these conditions are true:
| `fix_auto_applying` | `selfHealingStatus == 'COMPLETED'` AND `couldAutoApplyTasks == true` AND `verificationStatus == 'COMPLETED'` |
| `fix_available` | `selfHealingStatus == 'COMPLETED'` AND `suggestedFix != null` AND (`couldAutoApplyTasks != true` OR `verificationStatus` in (`FAILED`, `NOT_EXECUTABLE`)) |
You are the orchestrator for monitoring Nx Cloud CI pipeline executions and handling self-healing fixes. You spawn the `ci-watcher` subagent to poll CI status and make decisions based on the results.
| `--auto-fix-workflow` | false | Attempt common fixes for pre-CIPE failures (e.g., lockfile updates) |
| `--new-cipe-timeout` | 10 | Minutes to wait for new CIPE after action |
| `--local-verify-attempts` | 3 | Max local verification + enhance cycles before pushing to CI |
Parse any overrides from `${input:args}` and merge with defaults.
## Nx Cloud Connection Check
**CRITICAL**: Before starting the monitoring loop, verify the workspace is connected to Nx Cloud.
### Step 0: Verify Nx Cloud Connection
1.**Check `nx.json`** at workspace root for `nxCloudId` or `nxCloudAccessToken`
2.**If `nx.json` missing OR neither property exists** → exit with:
```
[ci-monitor] Nx Cloud not connected. Unlock 70% faster CI and auto-fix broken PRs with https://nx.dev/nx-cloud
```
3. **If connected** → continue to main loop
## Session Context Behavior
**Important:** Within a Claude Code session, conversation context persists. If you Ctrl+C to interrupt the monitor and re-run `/ci-monitor`, Claude remembers the previous state and may continue from where it left off.
- **To continue monitoring:** Just re-run `/ci-monitor` (context is preserved)
- **To start fresh:** Use `/ci-monitor --fresh` to ignore previous context
- **For a completely clean slate:** Exit Claude Code and restart `claude`
## Default Behaviors by Status
The subagent returns with one of the following statuses. This table defines the **default behavior** for each status. User instructions can override any of these.
| `fix_auto_applying` | Fix will be auto-applied by self-healing. Do NOT call MCP. Record `last_cipe_url`, spawn new subagent in wait mode to poll for new CIPE. |
| `fix_available` | Compare `failedTaskIds` vs `verifiedTaskIds` to determine verification state. See **Fix Available Decision Logic** section below. |
| `fix_failed` | Self-healing failed to generate fix. Attempt local fix based on `taskOutputSummary`. If successful → commit, push, loop. If not → exit with failure. |
| `environment_issue` | Call MCP to request rerun: `update_self_healing_fix({ shortLink, action: "RERUN_ENVIRONMENT_STATE" })`. New CIPE spawns automatically. Loop to poll for new CIPE. |
| `no_fix` | CI failed, no fix available (self-healing disabled or not executable). Attempt local fix if possible. Otherwise exit with failure. |
| `no_new_cipe` | Expected CIPE never spawned (CI workflow likely failed before Nx tasks). Report to user, attempt common fixes if configured, or exit with guidance. |
- If fix needs enhancement → use Apply Locally + Enhance Flow above
- If fix is wrong → reject via MCP, fix from scratch, commit, push
### Auto-Apply Eligibility
The `couldAutoApplyTasks` field indicates whether the fix is eligible for automatic application:
- **`true`**: Fix is eligible for auto-apply. Subagent keeps polling while verification is in progress. Returns `fix_auto_applying` when verified, or `fix_available` if verification fails.
- **`false`** or **`null`**: Fix requires manual action (apply via MCP, apply locally, or reject)
**Key point**: When subagent returns `fix_auto_applying`, do NOT call MCP to apply - self-healing handles it. Just spawn a new subagent in wait mode.
### Apply vs Reject vs Apply Locally
- **Apply via MCP**: Calls `update_self_healing_fix({ shortLink, action: "APPLY" })`. Self-healing agent applies the fix in CI and a new CIPE spawns automatically. No local git operations needed.
- **Apply Locally**: Runs `nx apply-locally <shortLink>`. Applies the patch to your local working directory and sets state to `APPLIED_LOCALLY`. Use this when you want to enhance the fix before pushing.
- **Reject via MCP**: Calls `update_self_healing_fix({ shortLink, action: "REJECT" })`. Marks fix as rejected. Use only when the fix is completely wrong and you'll fix from scratch.
### Apply Locally + Enhance Flow
When the fix needs enhancement (use `nx apply-locally`, NOT reject):
1. Apply the patch locally: `nx apply-locally <shortLink>` (this also updates state to `APPLIED_LOCALLY`)
2. Make additional changes as needed
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Reject + Fix From Scratch Flow
When the fix is completely wrong:
1. Call MCP to reject: `update_self_healing_fix({ shortLink, action: "REJECT" })`
2. Fix the issue from scratch locally
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Environment Issue Handling
When `failureClassification == 'ENVIRONMENT_STATE'`:
**CRITICAL**: When passing `expectedCommitSha` or `last_cipe_url` to the subagent, it enters **wait mode**:
- Subagent will **completely ignore** the old/stale CIPE
- Subagent will only wait for new CIPE to appear
- Subagent will NOT return to main agent with stale CIPE data
- Once new CIPE detected, subagent switches to normal polling
**Why wait mode matters for context preservation**: Stale CIPE data can be very large (task output summaries, suggested fix patches, reasoning). If subagent returns this to main agent, it pollutes main agent's context with useless data since we already processed that CIPE. Wait mode keeps stale data in the subagent, never sending it to main agent.
### Step 4: Progress Tracking
After each action:
- If state changed significantly → reset `no_progress_count = 0`
- If state unchanged → `no_progress_count++`
- On new CI attempt detected → reset `local_verify_count = 0`
description: Monitor Nx Cloud CI pipeline and handle self-healing fixes automatically. Checks for Nx Cloud connection before starting.
---
# CI Monitor Command
You are the orchestrator for monitoring Nx Cloud CI pipeline executions and handling self-healing fixes. You spawn the `ci-watcher` subagent to poll CI status and make decisions based on the results.
| `--auto-fix-workflow` | false | Attempt common fixes for pre-CIPE failures (e.g., lockfile updates) |
| `--new-cipe-timeout` | 10 | Minutes to wait for new CIPE after action |
| `--local-verify-attempts` | 3 | Max local verification + enhance cycles before pushing to CI |
Parse any overrides from `$ARGUMENTS` and merge with defaults.
## Nx Cloud Connection Check
**CRITICAL**: Before starting the monitoring loop, verify the workspace is connected to Nx Cloud.
### Step 0: Verify Nx Cloud Connection
1.**Check `nx.json`** at workspace root for `nxCloudId` or `nxCloudAccessToken`
2.**If `nx.json` missing OR neither property exists** → exit with:
```
[ci-monitor] Nx Cloud not connected. Unlock 70% faster CI and auto-fix broken PRs with https://nx.dev/nx-cloud
```
3. **If connected** → continue to main loop
## Session Context Behavior
**Important:** Within a Claude Code session, conversation context persists. If you Ctrl+C to interrupt the monitor and re-run `/ci-monitor`, Claude remembers the previous state and may continue from where it left off.
- **To continue monitoring:** Just re-run `/ci-monitor` (context is preserved)
- **To start fresh:** Use `/ci-monitor --fresh` to ignore previous context
- **For a completely clean slate:** Exit Claude Code and restart `claude`
## Default Behaviors by Status
The subagent returns with one of the following statuses. This table defines the **default behavior** for each status. User instructions can override any of these.
| `fix_auto_applying` | Fix will be auto-applied by self-healing. Do NOT call MCP. Record `last_cipe_url`, spawn new subagent in wait mode to poll for new CIPE. |
| `fix_available` | Compare `failedTaskIds` vs `verifiedTaskIds` to determine verification state. See **Fix Available Decision Logic** section below. |
| `fix_failed` | Self-healing failed to generate fix. Attempt local fix based on `taskOutputSummary`. If successful → commit, push, loop. If not → exit with failure. |
| `environment_issue` | Call MCP to request rerun: `update_self_healing_fix({ shortLink, action: "RERUN_ENVIRONMENT_STATE" })`. New CIPE spawns automatically. Loop to poll for new CIPE. |
| `no_fix` | CI failed, no fix available (self-healing disabled or not executable). Attempt local fix if possible. Otherwise exit with failure. |
| `no_new_cipe` | Expected CIPE never spawned (CI workflow likely failed before Nx tasks). Report to user, attempt common fixes if configured, or exit with guidance. |
- If fix needs enhancement → use Apply Locally + Enhance Flow above
- If fix is wrong → reject via MCP, fix from scratch, commit, push
### Auto-Apply Eligibility
The `couldAutoApplyTasks` field indicates whether the fix is eligible for automatic application:
- **`true`**: Fix is eligible for auto-apply. Subagent keeps polling while verification is in progress. Returns `fix_auto_applying` when verified, or `fix_available` if verification fails.
- **`false`** or **`null`**: Fix requires manual action (apply via MCP, apply locally, or reject)
**Key point**: When subagent returns `fix_auto_applying`, do NOT call MCP to apply - self-healing handles it. Just spawn a new subagent in wait mode.
### Apply vs Reject vs Apply Locally
- **Apply via MCP**: Calls `update_self_healing_fix({ shortLink, action: "APPLY" })`. Self-healing agent applies the fix in CI and a new CIPE spawns automatically. No local git operations needed.
- **Apply Locally**: Runs `nx apply-locally <shortLink>`. Applies the patch to your local working directory and sets state to `APPLIED_LOCALLY`. Use this when you want to enhance the fix before pushing.
- **Reject via MCP**: Calls `update_self_healing_fix({ shortLink, action: "REJECT" })`. Marks fix as rejected. Use only when the fix is completely wrong and you'll fix from scratch.
### Apply Locally + Enhance Flow
When the fix needs enhancement (use `nx apply-locally`, NOT reject):
1. Apply the patch locally: `nx apply-locally <shortLink>` (this also updates state to `APPLIED_LOCALLY`)
2. Make additional changes as needed
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Reject + Fix From Scratch Flow
When the fix is completely wrong:
1. Call MCP to reject: `update_self_healing_fix({ shortLink, action: "REJECT" })`
2. Fix the issue from scratch locally
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Environment Issue Handling
When `failureClassification == 'ENVIRONMENT_STATE'`:
**CRITICAL**: When passing `expectedCommitSha` or `last_cipe_url` to the subagent, it enters **wait mode**:
- Subagent will **completely ignore** the old/stale CIPE
- Subagent will only wait for new CIPE to appear
- Subagent will NOT return to main agent with stale CIPE data
- Once new CIPE detected, subagent switches to normal polling
**Why wait mode matters for context preservation**: Stale CIPE data can be very large (task output summaries, suggested fix patches, reasoning). If subagent returns this to main agent, it pollutes main agent's context with useless data since we already processed that CIPE. Wait mode keeps stale data in the subagent, never sending it to main agent.
### Step 4: Progress Tracking
After each action:
- If state changed significantly → reset `no_progress_count = 0`
- If state unchanged → `no_progress_count++`
- On new CI attempt detected → reset `local_verify_count = 0`
description: Generate code using nx generators. USE WHEN scaffolding code or transforming existing code - for example creating libraries or applications, or anything else that is boilerplate code or automates repetitive tasks. ALWAYS use this first when generating code with Nx instead of calling MCP tools or running nx generate immediately.
---
# Run Nx Generator
Nx generators are powerful tools that scaffold projects, make automated code migrations or automate repetitive tasks in a monorepo. They ensure consistency across the codebase and reduce boilerplate work.
This skill applies when the user wants to:
- Create new projects like libraries or applications
- Scaffold features or boilerplate code
- Run workspace-specific or custom generators
- Do anything else that an nx generator exists for
## Generator Discovery Flow
### Step 1: List Available Generators
Use the Nx CLI to discover available generators:
- List all generators for a plugin: `npx nx list @nx/react`
- Local workspace generators (defined in the repo's own plugins)
### Step 2: Match Generator to User Request
Based on the user's request, identify which generator(s) could fulfill their needs. Consider:
- What artifact type they want to create (library, application, etc.)
- Which framework or technology stack is relevant
- Whether they mentioned specific generator names
**IMPORTANT**: When both a local workspace generator and an external plugin generator could satisfy the request, **always prefer the local workspace generator**. Local generators are customized for the specific repo's patterns and conventions.
It's possible that the user request is something that no Nx generator exists for whatsoever. In this case, you can stop using this skill and try to help the user another way. HOWEVER, the burden of proof for this is high. Before aborting, carefully consider each and every generator that's available. Look into details for any that could be related in any way before making this decision.
## Pre-Execution Checklist
Before running any generator, complete these steps:
### 1. Fetch Generator Schema
Use the `--help` flag to understand all available options:
```bash
npx nx g @nx/react:library --help
```
Pay attention to:
- Required options that must be provided
- Optional options that may be relevant to the user's request
- Default values that might need to be overridden
### 2. Read Generator Source Code
Understanding what the generator actually does helps you:
- Know what files will be created/modified
- Understand any side effects (updating configs, installing deps, etc.)
- Identify options that might not be obvious from the schema
To find generator source code:
- For plugin generators: Use `node -e "console.log(require.resolve('@nx/<plugin>/generators.json'));"` to find the generators.json, then locate the source from there
- If that fails, read directly from `node_modules/<plugin>/generators.json`
- For local generators: They are typically in `tools/generators/` or a local plugin directory. You can search the repo for the generator name to find it.
### 2.5 Reevaluate if the generator is right
Once you have built up an understanding of what the selected generator does, reconsider: Is this the right generator to service the user request?
If not, it's okay to go back to the Generator Discovery Flow and select a different generator before proceeding. If you do, make sure to go through the entire pre-execution checklist once more.
### 3. Understand Repo Context
Before generating, examine the target area of the codebase:
- Look at similar existing artifacts (other libraries, applications, etc.)
- Identify patterns and conventions used in the repo
- Note naming conventions, file structures, and configuration patterns
- Try to match these patterns when configuring the generator
For example, if similar libraries are using a specific test runner, build tool or linter, try to match that if possible.
If projects or other artifacts are organized with a specific naming convention, try to match it.
### 4. Validate Required Options
Ensure all required options have values:
- Map the user's request to generator options
- Infer values from context where possible
- Ask the user for any critical missing information
## Execution
Keep in mind that you might have to prefix things with npx/pnpx/yarn if the user doesn't have nx installed globally.
Many generators will behave differently based on where they are executed. For example, first-party nx library generators use the cwd to determine the directory that the library should be placed in. This is highly important.
### Consider Dry-Run (Optional)
Running with `--dry-run` first is strongly encouraged but not mandatory. Use your judgment:
- For complex generators or unfamiliar territory: do a dry-run first
- For simple, well-understood generators: may proceed directly
- Dry-run shows file names and created/deleted/modified markers, but not content
- There are cases where a generator does not support dry-run (for example if it had to install an npm package) - in that case --dry-run might fail. Don't be discouraged but simply move on to running the generator for real and iterating from there.
description: Find and add Nx plugins. USE WHEN user wants to discover available plugins, install a new plugin, or add support for a specific framework or technology to the workspace.
description: Helps with running tasks in an Nx workspace. USE WHEN the user wants to execute build, test, lint, serve, or run any other tasks defined in the workspace.
---
You can run tasks with Nx in the following way.
Keep in mind that you might have to prefix things with npx/pnpx/yarn if the user doesn't have nx installed globally. Look at the package.json or lockfile to determine which package manager is in use.
For more details on any command, run it with `--help` (e.g. `nx run-many --help`, `nx affected --help`).
## Understand which tasks can be run
You can check those via `nx show project <projectname> --json`, for example `nx show project myapp --json`. It contains a `targets` section which has information about targets that can be run. You can also just look at the `package.json` scripts or `project.json` targets, but you might miss out on inferred tasks by Nx plugins.
## Run a single task
```
nx run <project>:<task>
```
where `project` is the project name defined in `package.json` or `project.json` (if present).
## Run multiple tasks
```
nx run-many -t build test lint typecheck
```
You can pass a `-p` flag to filter to specific projects, otherwise it runs on all projects. You can also use `--exclude` to exclude projects, and `--parallel` to control the number of parallel processes (default is 3).
Examples:
-`nx run-many -t test -p proj1 proj2` — test specific projects
-`nx run-many -t test --projects=*-app --exclude=excluded-app` — test projects matching a pattern
-`nx run-many -t test --projects=tag:api-*` — test projects by tag
## Run tasks for affected projects
Use `nx affected` to only run tasks on projects that have been changed and projects that depend on changed projects. This is especially useful in CI and for large workspaces.
```
nx affected -t build test lint
```
By default it compares against the base branch. You can customize this:
-`nx affected -t test --base=main --head=HEAD` — compare against a specific base and head
-`nx affected -t test --files=libs/mylib/src/index.ts` — specify changed files directly
## Useful flags
These flags work with `run`, `run-many`, and `affected`:
-`--skipNxCache` — rerun tasks even when results are cached
-`--verbose` — print additional information such as stack traces
-`--nxBail` — stop execution after the first failed task
-`--configuration=<name>` — use a specific configuration (e.g. `production`)
description: "Explore and understand Nx workspaces. USE WHEN answering any questions about the nx workspace, the projects in it or tasks to run. EXAMPLES: 'What projects are in this workspace?', 'How is project X configured?', 'What targets can I run?', 'What's affected by my changes?', 'Which projects depend on library Y?', or any questions about Nx workspace structure, project configuration, or available tasks."
---
# Nx Workspace Exploration
This skill provides read-only exploration of Nx workspaces. Use it to understand workspace structure, project configuration, available targets, and dependencies.
Keep in mind that you might have to prefix commands with `npx`/`pnpx`/`yarn` if nx isn't installed globally. Check the lockfile to determine the package manager in use.
## Listing Projects
Use `nx show projects` to list projects in the workspace.
```bash
# List all projects
nx show projects
# Filter by pattern (glob)
nx show projects --projects "apps/*"
nx show projects --projects "shared-*"
# Filter by project type
nx show projects --type app
nx show projects --type lib
nx show projects --type e2e
# Filter by target (projects that have a specific target)
nx show projects --withTarget build
nx show projects --withTarget e2e
# Find affected projects (changed since base branch)
nx show projects --affected
nx show projects --affected --base=main
nx show projects --affected --type app
# Combine filters
nx show projects --type lib --withTarget test
nx show projects --affected --exclude="*-e2e"
# Output as JSON
nx show projects --json
```
## Project Configuration
Use `nx show project <name> --json` to get the full resolved configuration for a project.
**Important**: Do NOT read `project.json` directly - it only contains partial configuration. The `nx show project` command returns the full resolved config including inferred targets from plugins.
You can read the full project schema at `node_modules/nx/schemas/project-schema.json` to understand nx project configuration options.
```bash
# Get full project configuration
nx show project my-app --json
# Extract specific parts from the JSON
nx show project my-app --json | jq '.targets'
nx show project my-app --json | jq '.targets.build'
nx show project my-app --json | jq '.targets | keys'
issue-inactive-days:"30"# Lock issues after 30 days of being closed
pr-inactive-days:"5"# Lock closed PRs after 5 days. This ensures that issues that stem from a PR are opened as issues, rather than comments on the recently merged PR.
// TODO (Jack): Fix Windows support as gradle fails when running nx build https://staging.nx.app/runs/LgD4vxGn8w?utm_source=pull-request&utm_medium=comment
description: Polls Nx Cloud CI pipeline and self-healing status. Returns structured state when actionable. Spawned by /nx-cloud-ci-monitor command to monitor CI Attempt status.
mode: subagent
---
# CI Watcher Subagent
You are a CI monitoring subagent responsible for polling Nx Cloud CI Attempt status and self-healing state. You report status back to the main agent - you do NOT make apply/reject decisions.
## Your Responsibilities
1. Poll CI status using the `ci_information` MCP tool
2. Implement exponential backoff between polls
3. Return structured state when an actionable condition is reached
4. Track iteration count and elapsed time
5. Output status updates based on verbosity level
## Input Parameters (from Main Agent)
The main agent may provide these optional parameters in the prompt:
# Large content fields - fetch only when returning to main agent
```
## Initial Wait
Before first poll, wait based on context:
- **Fresh start (no expected CIPE):** Wait 60 seconds to allow CI to start
- **Expecting new CIPE:** Wait 30 seconds (action already triggered)
**IMPORTANT:** Always run sleep in foreground, NOT as background command.
```bash
sleep 60# or 30 if expecting new CIPE (FOREGROUND, not background)
```
## Two-Phase Operation
The subagent operates in one of two modes depending on input:
### Mode 1: Fresh Start (no `expectedCommitSha` or `previousCipeUrl`)
Normal polling - process whatever CIPE is returned by `ci_information`.
### Mode 2: Wait-for-New-CIPE (when `expectedCommitSha` or `previousCipeUrl` provided)
**CRITICAL**: When expecting a new CIPE, the subagent must **completely ignore** the old/stale CIPE. Do NOT process its status, do NOT return actionable states based on it.
#### Phase A: Wait Mode
1. Start a **new-CIPE timeout** timer (default: 30 minutes)
2. On each poll of `ci_information`:
- Check if CIPE is NEW:
-`cipeUrl` differs from `previousCipeUrl` → **new CIPE detected**
[CI Monitor] Still seeing previous CI Attempt (ignoring): <oldCipeUrl>
[CI Monitor] Polling... (elapsed: 1m 30s)
[CI Monitor] Still seeing previous CI Attempt (ignoring): <oldCipeUrl>
[CI Monitor] Polling... (elapsed: 2m 30s)
[CI Monitor] ✓ New CI Attempt detected! URL: <newCipeUrl>, SHA: <newCommitSha>
[CI Monitor] Switching to normal polling mode...
```
### Why This Matters (Context Preservation)
**The problem**: Stale CIPE data can be very large:
-`taskOutputSummary`: potentially thousands of characters of build/test output
-`suggestedFix`: entire patch files
-`suggestedFixReasoning`: detailed explanation
If subagent returns stale CIPE data to main agent, it **pollutes main agent's context** with useless information (we already processed that CIPE). This wastes valuable context window.
**Without wait mode:**
1. Poll `ci_information` → get old CIPE with huge data
2. Return to main agent with all that stale data
3. Main agent's context gets polluted with useless info
4. Main agent has to process/ignore it anyway
**With wait mode:**
1. Poll `ci_information` → get old CIPE → **ignore it, don't return**
2. Keep waiting internally (stale data stays in subagent)
3. New CIPE appears → switch to normal mode
4. Return to main agent with only the NEW, relevant CIPE data
## Polling Loop
### Subagent State Management
Maintain internal accumulated state across polls:
```
accumulated_state = {}
```
### Call `ci_information` MCP Tool
**Wait Mode (expecting new CI Attempt):**
```
ci_information({
branch: "<branch_name>",
select: "cipeUrl,commitSha,cipeStatus"
})
```
Only fetch minimal fields needed to detect CI Attempt change. Do NOT fetch heavy fields - stale data wastes context.
Merge response into `accumulated_state`, then return merged state to main agent.
**Pagination:** Heavy string fields return first page only. If `hasMore` indicated, include in return format so main agent knows more content available.
### Return to Main Agent When
Return immediately with structured state if ANY of these conditions are true:
| `fix_auto_applying` | `selfHealingStatus == 'COMPLETED'` AND `couldAutoApplyTasks == true` AND `verificationStatus == 'COMPLETED'` |
| `fix_available` | `selfHealingStatus == 'COMPLETED'` AND `suggestedFix != null` AND (`couldAutoApplyTasks != true` OR `verificationStatus` in (`FAILED`, `NOT_EXECUTABLE`)) |
You are the orchestrator for monitoring Nx Cloud CI pipeline executions and handling self-healing fixes. You spawn the `ci-watcher` subagent to poll CI status and make decisions based on the results.
| `--auto-fix-workflow` | false | Attempt common fixes for pre-CIPE failures (e.g., lockfile updates) |
| `--new-cipe-timeout` | 10 | Minutes to wait for new CIPE after action |
| `--local-verify-attempts` | 3 | Max local verification + enhance cycles before pushing to CI |
Parse any overrides from `$ARGUMENTS` and merge with defaults.
## Nx Cloud Connection Check
**CRITICAL**: Before starting the monitoring loop, verify the workspace is connected to Nx Cloud.
### Step 0: Verify Nx Cloud Connection
1.**Check `nx.json`** at workspace root for `nxCloudId` or `nxCloudAccessToken`
2.**If `nx.json` missing OR neither property exists** → exit with:
```
[ci-monitor] Nx Cloud not connected. Unlock 70% faster CI and auto-fix broken PRs with https://nx.dev/nx-cloud
```
3. **If connected** → continue to main loop
## Session Context Behavior
**Important:** Within a Claude Code session, conversation context persists. If you Ctrl+C to interrupt the monitor and re-run `/ci-monitor`, Claude remembers the previous state and may continue from where it left off.
- **To continue monitoring:** Just re-run `/ci-monitor` (context is preserved)
- **To start fresh:** Use `/ci-monitor --fresh` to ignore previous context
- **For a completely clean slate:** Exit Claude Code and restart `claude`
## Default Behaviors by Status
The subagent returns with one of the following statuses. This table defines the **default behavior** for each status. User instructions can override any of these.
| `fix_auto_applying` | Fix will be auto-applied by self-healing. Do NOT call MCP. Record `last_cipe_url`, spawn new subagent in wait mode to poll for new CIPE. |
| `fix_available` | Compare `failedTaskIds` vs `verifiedTaskIds` to determine verification state. See **Fix Available Decision Logic** section below. |
| `fix_failed` | Self-healing failed to generate fix. Attempt local fix based on `taskOutputSummary`. If successful → commit, push, loop. If not → exit with failure. |
| `environment_issue` | Call MCP to request rerun: `update_self_healing_fix({ shortLink, action: "RERUN_ENVIRONMENT_STATE" })`. New CIPE spawns automatically. Loop to poll for new CIPE. |
| `no_fix` | CI failed, no fix available (self-healing disabled or not executable). Attempt local fix if possible. Otherwise exit with failure. |
| `no_new_cipe` | Expected CIPE never spawned (CI workflow likely failed before Nx tasks). Report to user, attempt common fixes if configured, or exit with guidance. |
- If fix needs enhancement → use Apply Locally + Enhance Flow above
- If fix is wrong → reject via MCP, fix from scratch, commit, push
### Auto-Apply Eligibility
The `couldAutoApplyTasks` field indicates whether the fix is eligible for automatic application:
- **`true`**: Fix is eligible for auto-apply. Subagent keeps polling while verification is in progress. Returns `fix_auto_applying` when verified, or `fix_available` if verification fails.
- **`false`** or **`null`**: Fix requires manual action (apply via MCP, apply locally, or reject)
**Key point**: When subagent returns `fix_auto_applying`, do NOT call MCP to apply - self-healing handles it. Just spawn a new subagent in wait mode.
### Apply vs Reject vs Apply Locally
- **Apply via MCP**: Calls `update_self_healing_fix({ shortLink, action: "APPLY" })`. Self-healing agent applies the fix in CI and a new CIPE spawns automatically. No local git operations needed.
- **Apply Locally**: Runs `nx apply-locally <shortLink>`. Applies the patch to your local working directory and sets state to `APPLIED_LOCALLY`. Use this when you want to enhance the fix before pushing.
- **Reject via MCP**: Calls `update_self_healing_fix({ shortLink, action: "REJECT" })`. Marks fix as rejected. Use only when the fix is completely wrong and you'll fix from scratch.
### Apply Locally + Enhance Flow
When the fix needs enhancement (use `nx apply-locally`, NOT reject):
1. Apply the patch locally: `nx apply-locally <shortLink>` (this also updates state to `APPLIED_LOCALLY`)
2. Make additional changes as needed
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Reject + Fix From Scratch Flow
When the fix is completely wrong:
1. Call MCP to reject: `update_self_healing_fix({ shortLink, action: "REJECT" })`
2. Fix the issue from scratch locally
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Environment Issue Handling
When `failureClassification == 'ENVIRONMENT_STATE'`:
**CRITICAL**: When passing `expectedCommitSha` or `last_cipe_url` to the subagent, it enters **wait mode**:
- Subagent will **completely ignore** the old/stale CIPE
- Subagent will only wait for new CIPE to appear
- Subagent will NOT return to main agent with stale CIPE data
- Once new CIPE detected, subagent switches to normal polling
**Why wait mode matters for context preservation**: Stale CIPE data can be very large (task output summaries, suggested fix patches, reasoning). If subagent returns this to main agent, it pollutes main agent's context with useless data since we already processed that CIPE. Wait mode keeps stale data in the subagent, never sending it to main agent.
### Step 4: Progress Tracking
After each action:
- If state changed significantly → reset `no_progress_count = 0`
- If state unchanged → `no_progress_count++`
- On new CI attempt detected → reset `local_verify_count = 0`
description: Monitor Nx Cloud CI pipeline and handle self-healing fixes automatically. Checks for Nx Cloud connection before starting.
---
# CI Monitor Command
You are the orchestrator for monitoring Nx Cloud CI pipeline executions and handling self-healing fixes. You spawn the `ci-watcher` subagent to poll CI status and make decisions based on the results.
| `--auto-fix-workflow` | false | Attempt common fixes for pre-CIPE failures (e.g., lockfile updates) |
| `--new-cipe-timeout` | 10 | Minutes to wait for new CIPE after action |
| `--local-verify-attempts` | 3 | Max local verification + enhance cycles before pushing to CI |
Parse any overrides from `$ARGUMENTS` and merge with defaults.
## Nx Cloud Connection Check
**CRITICAL**: Before starting the monitoring loop, verify the workspace is connected to Nx Cloud.
### Step 0: Verify Nx Cloud Connection
1.**Check `nx.json`** at workspace root for `nxCloudId` or `nxCloudAccessToken`
2.**If `nx.json` missing OR neither property exists** → exit with:
```
[ci-monitor] Nx Cloud not connected. Unlock 70% faster CI and auto-fix broken PRs with https://nx.dev/nx-cloud
```
3. **If connected** → continue to main loop
## Session Context Behavior
**Important:** Within a Claude Code session, conversation context persists. If you Ctrl+C to interrupt the monitor and re-run `/ci-monitor`, Claude remembers the previous state and may continue from where it left off.
- **To continue monitoring:** Just re-run `/ci-monitor` (context is preserved)
- **To start fresh:** Use `/ci-monitor --fresh` to ignore previous context
- **For a completely clean slate:** Exit Claude Code and restart `claude`
## Default Behaviors by Status
The subagent returns with one of the following statuses. This table defines the **default behavior** for each status. User instructions can override any of these.
| `fix_auto_applying` | Fix will be auto-applied by self-healing. Do NOT call MCP. Record `last_cipe_url`, spawn new subagent in wait mode to poll for new CIPE. |
| `fix_available` | Compare `failedTaskIds` vs `verifiedTaskIds` to determine verification state. See **Fix Available Decision Logic** section below. |
| `fix_failed` | Self-healing failed to generate fix. Attempt local fix based on `taskOutputSummary`. If successful → commit, push, loop. If not → exit with failure. |
| `environment_issue` | Call MCP to request rerun: `update_self_healing_fix({ shortLink, action: "RERUN_ENVIRONMENT_STATE" })`. New CIPE spawns automatically. Loop to poll for new CIPE. |
| `no_fix` | CI failed, no fix available (self-healing disabled or not executable). Attempt local fix if possible. Otherwise exit with failure. |
| `no_new_cipe` | Expected CIPE never spawned (CI workflow likely failed before Nx tasks). Report to user, attempt common fixes if configured, or exit with guidance. |
- If fix needs enhancement → use Apply Locally + Enhance Flow above
- If fix is wrong → reject via MCP, fix from scratch, commit, push
### Auto-Apply Eligibility
The `couldAutoApplyTasks` field indicates whether the fix is eligible for automatic application:
- **`true`**: Fix is eligible for auto-apply. Subagent keeps polling while verification is in progress. Returns `fix_auto_applying` when verified, or `fix_available` if verification fails.
- **`false`** or **`null`**: Fix requires manual action (apply via MCP, apply locally, or reject)
**Key point**: When subagent returns `fix_auto_applying`, do NOT call MCP to apply - self-healing handles it. Just spawn a new subagent in wait mode.
### Apply vs Reject vs Apply Locally
- **Apply via MCP**: Calls `update_self_healing_fix({ shortLink, action: "APPLY" })`. Self-healing agent applies the fix in CI and a new CIPE spawns automatically. No local git operations needed.
- **Apply Locally**: Runs `nx apply-locally <shortLink>`. Applies the patch to your local working directory and sets state to `APPLIED_LOCALLY`. Use this when you want to enhance the fix before pushing.
- **Reject via MCP**: Calls `update_self_healing_fix({ shortLink, action: "REJECT" })`. Marks fix as rejected. Use only when the fix is completely wrong and you'll fix from scratch.
### Apply Locally + Enhance Flow
When the fix needs enhancement (use `nx apply-locally`, NOT reject):
1. Apply the patch locally: `nx apply-locally <shortLink>` (this also updates state to `APPLIED_LOCALLY`)
2. Make additional changes as needed
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Reject + Fix From Scratch Flow
When the fix is completely wrong:
1. Call MCP to reject: `update_self_healing_fix({ shortLink, action: "REJECT" })`
2. Fix the issue from scratch locally
3. Commit and push:
```bash
git add -A
git commit -m "fix: resolve <failedTaskIds>"
git push origin $(git branch --show-current)
```
4. Loop to poll for new CIPE
### Environment Issue Handling
When `failureClassification == 'ENVIRONMENT_STATE'`:
**CRITICAL**: When passing `expectedCommitSha` or `last_cipe_url` to the subagent, it enters **wait mode**:
- Subagent will **completely ignore** the old/stale CIPE
- Subagent will only wait for new CIPE to appear
- Subagent will NOT return to main agent with stale CIPE data
- Once new CIPE detected, subagent switches to normal polling
**Why wait mode matters for context preservation**: Stale CIPE data can be very large (task output summaries, suggested fix patches, reasoning). If subagent returns this to main agent, it pollutes main agent's context with useless data since we already processed that CIPE. Wait mode keeps stale data in the subagent, never sending it to main agent.
### Step 4: Progress Tracking
After each action:
- If state changed significantly → reset `no_progress_count = 0`
- If state unchanged → `no_progress_count++`
- On new CI attempt detected → reset `local_verify_count = 0`
description: Generate code using nx generators. USE WHEN scaffolding code or transforming existing code - for example creating libraries or applications, or anything else that is boilerplate code or automates repetitive tasks. ALWAYS use this first when generating code with Nx instead of calling MCP tools or running nx generate immediately.
---
# Run Nx Generator
Nx generators are powerful tools that scaffold projects, make automated code migrations or automate repetitive tasks in a monorepo. They ensure consistency across the codebase and reduce boilerplate work.
This skill applies when the user wants to:
- Create new projects like libraries or applications
- Scaffold features or boilerplate code
- Run workspace-specific or custom generators
- Do anything else that an nx generator exists for
## Generator Discovery Flow
### Step 1: List Available Generators
Use the Nx CLI to discover available generators:
- List all generators for a plugin: `npx nx list @nx/react`
- Local workspace generators (defined in the repo's own plugins)
### Step 2: Match Generator to User Request
Based on the user's request, identify which generator(s) could fulfill their needs. Consider:
- What artifact type they want to create (library, application, etc.)
- Which framework or technology stack is relevant
- Whether they mentioned specific generator names
**IMPORTANT**: When both a local workspace generator and an external plugin generator could satisfy the request, **always prefer the local workspace generator**. Local generators are customized for the specific repo's patterns and conventions.
It's possible that the user request is something that no Nx generator exists for whatsoever. In this case, you can stop using this skill and try to help the user another way. HOWEVER, the burden of proof for this is high. Before aborting, carefully consider each and every generator that's available. Look into details for any that could be related in any way before making this decision.
## Pre-Execution Checklist
Before running any generator, complete these steps:
### 1. Fetch Generator Schema
Use the `--help` flag to understand all available options:
```bash
npx nx g @nx/react:library --help
```
Pay attention to:
- Required options that must be provided
- Optional options that may be relevant to the user's request
- Default values that might need to be overridden
### 2. Read Generator Source Code
Understanding what the generator actually does helps you:
- Know what files will be created/modified
- Understand any side effects (updating configs, installing deps, etc.)
- Identify options that might not be obvious from the schema
To find generator source code:
- For plugin generators: Use `node -e "console.log(require.resolve('@nx/<plugin>/generators.json'));"` to find the generators.json, then locate the source from there
- If that fails, read directly from `node_modules/<plugin>/generators.json`
- For local generators: They are typically in `tools/generators/` or a local plugin directory. You can search the repo for the generator name to find it.
### 2.5 Reevaluate if the generator is right
Once you have built up an understanding of what the selected generator does, reconsider: Is this the right generator to service the user request?
If not, it's okay to go back to the Generator Discovery Flow and select a different generator before proceeding. If you do, make sure to go through the entire pre-execution checklist once more.
### 3. Understand Repo Context
Before generating, examine the target area of the codebase:
- Look at similar existing artifacts (other libraries, applications, etc.)
- Identify patterns and conventions used in the repo
- Note naming conventions, file structures, and configuration patterns
- Try to match these patterns when configuring the generator
For example, if similar libraries are using a specific test runner, build tool or linter, try to match that if possible.
If projects or other artifacts are organized with a specific naming convention, try to match it.
### 4. Validate Required Options
Ensure all required options have values:
- Map the user's request to generator options
- Infer values from context where possible
- Ask the user for any critical missing information
## Execution
Keep in mind that you might have to prefix things with npx/pnpx/yarn if the user doesn't have nx installed globally.
Many generators will behave differently based on where they are executed. For example, first-party nx library generators use the cwd to determine the directory that the library should be placed in. This is highly important.
### Consider Dry-Run (Optional)
Running with `--dry-run` first is strongly encouraged but not mandatory. Use your judgment:
- For complex generators or unfamiliar territory: do a dry-run first
- For simple, well-understood generators: may proceed directly
- Dry-run shows file names and created/deleted/modified markers, but not content
- There are cases where a generator does not support dry-run (for example if it had to install an npm package) - in that case --dry-run might fail. Don't be discouraged but simply move on to running the generator for real and iterating from there.
description: Find and add Nx plugins. USE WHEN user wants to discover available plugins, install a new plugin, or add support for a specific framework or technology to the workspace.
description: Helps with running tasks in an Nx workspace. USE WHEN the user wants to execute build, test, lint, serve, or run any other tasks defined in the workspace.
---
You can run tasks with Nx in the following way.
Keep in mind that you might have to prefix things with npx/pnpx/yarn if the user doesn't have nx installed globally. Look at the package.json or lockfile to determine which package manager is in use.
For more details on any command, run it with `--help` (e.g. `nx run-many --help`, `nx affected --help`).
## Understand which tasks can be run
You can check those via `nx show project <projectname> --json`, for example `nx show project myapp --json`. It contains a `targets` section which has information about targets that can be run. You can also just look at the `package.json` scripts or `project.json` targets, but you might miss out on inferred tasks by Nx plugins.
## Run a single task
```
nx run <project>:<task>
```
where `project` is the project name defined in `package.json` or `project.json` (if present).
## Run multiple tasks
```
nx run-many -t build test lint typecheck
```
You can pass a `-p` flag to filter to specific projects, otherwise it runs on all projects. You can also use `--exclude` to exclude projects, and `--parallel` to control the number of parallel processes (default is 3).
Examples:
-`nx run-many -t test -p proj1 proj2` — test specific projects
-`nx run-many -t test --projects=*-app --exclude=excluded-app` — test projects matching a pattern
-`nx run-many -t test --projects=tag:api-*` — test projects by tag
## Run tasks for affected projects
Use `nx affected` to only run tasks on projects that have been changed and projects that depend on changed projects. This is especially useful in CI and for large workspaces.
```
nx affected -t build test lint
```
By default it compares against the base branch. You can customize this:
-`nx affected -t test --base=main --head=HEAD` — compare against a specific base and head
-`nx affected -t test --files=libs/mylib/src/index.ts` — specify changed files directly
## Useful flags
These flags work with `run`, `run-many`, and `affected`:
-`--skipNxCache` — rerun tasks even when results are cached
-`--verbose` — print additional information such as stack traces
-`--nxBail` — stop execution after the first failed task
-`--configuration=<name>` — use a specific configuration (e.g. `production`)
description: "Explore and understand Nx workspaces. USE WHEN answering any questions about the nx workspace, the projects in it or tasks to run. EXAMPLES: 'What projects are in this workspace?', 'How is project X configured?', 'What targets can I run?', 'What's affected by my changes?', 'Which projects depend on library Y?', or any questions about Nx workspace structure, project configuration, or available tasks."
---
# Nx Workspace Exploration
This skill provides read-only exploration of Nx workspaces. Use it to understand workspace structure, project configuration, available targets, and dependencies.
Keep in mind that you might have to prefix commands with `npx`/`pnpx`/`yarn` if nx isn't installed globally. Check the lockfile to determine the package manager in use.
## Listing Projects
Use `nx show projects` to list projects in the workspace.
```bash
# List all projects
nx show projects
# Filter by pattern (glob)
nx show projects --projects "apps/*"
nx show projects --projects "shared-*"
# Filter by project type
nx show projects --type app
nx show projects --type lib
nx show projects --type e2e
# Filter by target (projects that have a specific target)
nx show projects --withTarget build
nx show projects --withTarget e2e
# Find affected projects (changed since base branch)
nx show projects --affected
nx show projects --affected --base=main
nx show projects --affected --type app
# Combine filters
nx show projects --type lib --withTarget test
nx show projects --affected --exclude="*-e2e"
# Output as JSON
nx show projects --json
```
## Project Configuration
Use `nx show project <name> --json` to get the full resolved configuration for a project.
**Important**: Do NOT read `project.json` directly - it only contains partial configuration. The `nx show project` command returns the full resolved config including inferred targets from plugins.
You can read the full project schema at `node_modules/nx/schemas/project-schema.json` to understand nx project configuration options.
```bash
# Get full project configuration
nx show project my-app --json
# Extract specific parts from the JSON
nx show project my-app --json | jq '.targets'
nx show project my-app --json | jq '.targets.build'
nx show project my-app --json | jq '.targets | keys'
This builds the package and copies it into the target repo's `node_modules`. It works for all packages including native Rust code.
### Testing Changes
After code changes are made, first test the specific project where the changes were made:
@@ -202,22 +212,23 @@ Fixes #ISSUE_NUMBER
<!-- nx configuration start-->
<!-- Leave the start & end comments to automatically receive updates. -->
# General Guidelines for working with Nx
## General Guidelines for working with Nx
- For navigating/exploring the workspace, invoke the `nx-workspace` skill first - it has patterns for querying projects, targets, and dependencies
- When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through `nx` (i.e. `nx run`, `nx run-many`, `nx affected`) instead of using the underlying tooling directly
- Prefix nx commands with the workspace's package manager (e.g., `pnpm nx build`, `npm exec nx test`) - avoids using globally installed CLI
- You have access to the Nx MCP server and its tools, use them to help the user
-When answering questions about the repository, use the `nx_workspace` tool first to gain an understanding of the workspace architecture where applicable.
-When working in individual projects, use the `nx_project_details` mcp tool to analyze and understand the specific project structure and dependencies
- For questions around nx configuration, best practices or if you're unsure, use the `nx_docs` tool to get relevant, up-to-date docs. Always use this instead of assuming things about nx configuration
- If the user needs help with an Nx configuration or project graph error, use the `nx_workspace` tool to get any errors
-For Nx plugin best practices, check `node_modules/@nx/<plugin>/PLUGIN.md`. Not all plugins have this file - proceed without it if unavailable.
-NEVER guess CLI flags - always check nx_docs or `--help` first when unsure
# CI Error Guidelines
## Scaffolding & Generators
If the user wants help with fixing an error in their CI pipeline, use the following flow:
- For scaffolding tasks (creating apps, libs, project structure, setup), ALWAYS invoke the `nx-generate` skill FIRST before exploring or calling MCP tools
- Retrieve the list of current CI Pipeline Executions (CIPEs) using the `nx_cloud_cipe_details` tool
- If there are any errors, use the `nx_cloud_fix_cipe_failure` tool to retrieve the logs for a specific task
- Use the task logs to see what's wrong and help the user fix their problem. Use the appropriate tools if necessary
-Make sure that the problem is fixed by running the task that you passed into the `nx_cloud_fix_cipe_failure` tool
<!-- Leave the start & end comments to automatically receive updates. -->
# General Guidelines for working with Nx
## General Guidelines for working with Nx
- For navigating/exploring the workspace, invoke the `nx-workspace` skill first - it has patterns for querying projects, targets, and dependencies
- When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through `nx` (i.e. `nx run`, `nx run-many`, `nx affected`) instead of using the underlying tooling directly
- Prefix nx commands with the workspace's package manager (e.g., `pnpm nx build`, `npm exec nx test`) - avoids using globally installed CLI
- You have access to the Nx MCP server and its tools, use them to help the user
-When answering questions about the repository, use the `nx_workspace` tool first to gain an understanding of the workspace architecture where applicable.
-When working in individual projects, use the `nx_project_details` mcp tool to analyze and understand the specific project structure and dependencies
- For questions around nx configuration, best practices or if you're unsure, use the `nx_docs` tool to get relevant, up-to-date docs. Always use this instead of assuming things about nx configuration
- If the user needs help with an Nx configuration or project graph error, use the `nx_workspace` tool to get any errors
-For Nx plugin best practices, check `node_modules/@nx/<plugin>/PLUGIN.md`. Not all plugins have this file - proceed without it if unavailable.
-NEVER guess CLI flags - always check nx_docs or `--help` first when unsure
# CI Error Guidelines
## Scaffolding & Generators
If the user wants help with fixing an error in their CI pipeline, use the following flow:
- For scaffolding tasks (creating apps, libs, project structure, setup), ALWAYS invoke the `nx-generate` skill FIRST before exploring or calling MCP tools
- Retrieve the list of current CI Pipeline Executions (CIPEs) using the `nx_cloud_cipe_details` tool
- If there are any errors, use the `nx_cloud_fix_cipe_failure` tool to retrieve the logs for a specific task
- Use the task logs to see what's wrong and help the user fix their problem. Use the appropriate tools if necessary
-Make sure that the problem is fixed by running the task that you passed into the `nx_cloud_fix_cipe_failure` tool
[](https://gitter.im/nrwl-nx/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[](https://go.nx.dev/community)
<h1 align="center">Smart Monorepos · Fast Builds</h1>
Nx is a monorepo solution for TypeScript and polyglot codebases. Built with Rust for performance, extensible via TypeScript. Caches what didn't change, runs only what's affected, and comes with an integrated CI solution. Start simple, scale as you grow.
<hr>
## Quick Start
# Smart Repos · Fast Builds
Visit the [Nx quickstart docs](https://nx.dev/docs/quickstart) to get started.
Get to green PRs in half the time. Nx optimizes your builds, scales your CI, and fixes failed PRs. Built for developers and AI agents.
## Why Nx?
Create a new Nx workspace with
- **Incremental by design -** Run `npx nx init` in any npm/pnpm/yarn workspace. Nx picks up your existing `package.json` scripts, caches their outputs, and runs only what's
affected. No changes to your setup required.
- **AI-native tooling -** The Nx CLI is optimized for autonomous AI agents so they get the context they need and can operate just like a human. [Learn more »](https://github.com/nrwl/nx-ai-agents-config)
- **Polyglot plugin system -** Optional plugins auto-discover tasks, configure cache inputs/outputs, and scaffold code based on your actual tooling. Works with Vite, Webpack, Jest, Vitest, ESLint, Gradle, Maven, .NET, Go, and [more](https://nx.dev/technologies).
- **Integrated CI solution -** [Connect Nx to your CI provider](https://nx.dev/ci/intro/ci-with-nx) (GitHub Actions, GitLab, Azure, etc.) to enable remote caching, task distribution across machines, affected-only runs, and automatic e2e test splitting. [Learn more »](https://nx.dev/ci/intro/ci-with-nx)
- **Self-healing CI -** An AI agent on your CI pipeline that detects failures, analyzes root cause, proposes a fix, and verifies it automatically. Local agents connect to CI via MCP to autonomously detect and fix failures. [Learn more »](https://nx.dev/ci/features/self-healing)
```shell
npx create-nx-workspace
```
## Who uses Nx?
...or run
```
npx nx init
```
to add Nx to your existing workspace to get faster task scheduling, caching and more. More [in the docs](https://nx.dev/getting-started/intro).
## Learn about CI with Nx Cloud
[Nx Cloud](https://nx.dev/nx-cloud) connects directly to your existing CI setup, helping you scale your monorepos on CI by leveraging [remote caching](https://nx.dev/ci/features/remote-cache?utm_source=nxrepo&utm_medium=readme&utm_campaign=nxrepo), [task distribution across multiple machines](https://nx.dev/ci/features/distribute-task-execution?utm_source=nxrepo&utm_medium=readme&utm_campaign=nxrepo), [automated e2e test splitting](https://nx.dev/ci/features/split-e2e-tasks?utm_source=nxrepo&utm_medium=readme&utm_campaign=nxrepo) and [automated task flakiness detection](https://nx.dev/ci/features/flaky-tasks?utm_source=nxrepo&utm_medium=readme&utm_campaign=nxrepo)
Connect your existing Nx workspace with
```
npx nx connect
```
Learn more in the [Nx CI docs »](https://nx.dev/ci/getting-started/intro?utm_source=nxrepo&utm_medium=readme&utm_campaign=nxrepo)
## Useful links
- [Our docs](https://nx.dev/docs)
- [Our blog](https://nx.dev/blog)
- [Our community discord, live stream,...](https://nx.dev/community)
@@ -13,3 +13,15 @@ Instead, please report them to the Security Team at security@nrwl.io.
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message.
Nx follows the principle of Coordinated Vulnerability Disclosure.
## What Should Be Reported
The security email is for **demonstrable, verified vulnerabilities within the Nx codebase itself**.
**Please do not use the security email for:**
- Reports about outdated dependencies (e.g., "package X has a newer version available")
- Reports about dependencies with known CVEs that do not directly affect Nx functionality
- General vulnerability scanner output
If you have a concern about an outdated dependency that you believe impacts Nx users, please open a [GitHub issue](https://github.com/nrwl/nx/issues/new/choose) instead.
2.**Technologies** - Framework and tool-specific guides (React, Angular, Node, build tools, test tools)
3.**Knowledge Base** - Recipes, troubleshooting, and topic-specific guides
4.**Reference** - Exhaustive facts, no narrative (CLI commands, configuration, API docs)
## Project Structure
```
@@ -280,3 +322,47 @@ The Framer page should render JSON inside a `<pre>` tag:
- Users can dismiss the banner (stored in localStorage)
- If `enabled` is `false` or `activeUntil` has passed, the banner won't show
- If `BANNER_URL` is not set, an empty collection is generated
## Versioned Docs
When a new major Nx version is released (or about to be released), create a versioned snapshot of the docs site so the previous version remains accessible at `{major}.nx.dev` (e.g. `22.nx.dev`).
### Creating a Version Snapshot
```bash
node ./scripts/create-versioned-docs.mts 22
```
This will:
1. Fetch tags from origin, find the latest stable release for that major (e.g. `22.6.4`)
2. Checkout that tag, install deps, and build the docs site
3. Create an orphan git branch `22` containing only the pre-built static site plus minimal scaffolding (root `package.json`, `nx.json`, `pnpm-lock.yaml`, `netlify.toml`, and a no-op `nx-dev` project) so Netlify's configured build command succeeds instantly
4. Return to your original branch
If no stable tags exist for that major version, it builds from the current branch.
For Nx 21+, the script builds `astro-docs` (Astro/Starlight). For legacy Nx 18–20, it builds `nx-dev` (Next.js with static export) — this path will be removed once those versions are no longer maintained.
#### Flags
-`--force` — overwrite an existing local/remote `{major}` branch
-`--redirect-to-prod` — skip the build and produce a branch that 301s every path to `https://nx.dev/docs`. Used to retire an old versioned subdomain (e.g. `16.nx.dev`) without maintaining its docs
Versioned sites are served via Netlify branch deploys of the main `nx-dev` Netlify site, with custom domains managed in Squarespace.
- **Netlify** — each `{major}` branch is deployed as a [branch deploy](https://docs.netlify.com/site-deploys/overview/#branch-deploy-controls) of the `nx-dev` site. The branch's root `netlify.toml` overrides the UI build settings so Netlify serves the pre-built static files (no rebuild, no `@netlify/plugin-nextjs`). Add the branch to the site's branch deploy allowlist, then add `{major}.nx.dev` as a domain alias pointing at the branch deploy
- **Squarespace** — DNS for `nx.dev` is managed in Squarespace. Add a CNAME for `{major}` pointing at the Netlify branch deploy hostname
2.**Technologies** - Framework and tool-specific guides (React, Angular, Node, build tools, test tools)
3.**Knowledge Base** - Recipes, troubleshooting, and topic-specific guides
4.**Reference** - Exhaustive facts, no narrative (CLI commands, configuration, API docs)
## Structural anti-AI rules (longform)
Sentence-level edits don't fix AI voice in longform pieces. The structural tells matter more.
### One canonical home per point
Each substantive point lives in exactly one section. Other sections link or reference it in one phrase. They don't re-explain.
### No drama-beat echoes
A short sentence (under ~10 words) immediately after a long one, restating the long one for emphasis, is an AI tic.
Cover the short sentence with your thumb. If nothing is lost, cut it.
### No restatement closers
Read the last sentence of each paragraph alone.
If it summarizes what the paragraph said rather than adding a fact, judgment, or turn, cut it.
### Match claims to evidence
Every quantifier ("always", "never", "all", "completely", "fully", "rarely") and every counterfactual ("would have prevented", "would have caught") should be checked against the evidence actually in the doc.
Two failure modes:
- **Over-generalization**: one observed instance written as a broad pattern. If the doc has one data point, don't write "users frequently" or "this always happens."
- **Under-calibration**: softening a true absolute, or absolutizing a partial fix. "Would have prevented" is fine when the fix categorically closes the outcome. It's wrong when the fix closes one path of several.
Ask of each strong claim: "what in this doc supports the strength of this word?" If nothing, weaken or cite.
### Pre-publish pass order
Run passes in this order. Structural first, vocabulary last.
1. Canonical-home audit: where does each substantive point live?
2. Repetition count: grep your two or three core findings. If a finding appears more than twice in prose, the third is probably redundant.
3. Drama-beat sweep.
4. Closer pass.
5. Claim audit: for each absolute and each counterfactual, check what evidence in the doc supports that strength. Weaken or cite.
6. End-to-end consistency read.
7. Vocabulary grep (cheapest, lowest value).
## The Nx voice
Nx documentation is **direct, practical, and confident**. We write like a knowledgeable colleague pairing with you, not like a textbook, not like a marketing page, and not like a chatbot.
The voice should be:
- **Conversational but efficient.** Use contractions. Get to the point. Don't pad sentences.
- **Second person.** Write "you". Address the reader directly.
- **Action-oriented.** Lead with what the reader can _do_, not what Nx _is_.
- **Honest about tradeoffs.** Don't oversell. If something has limitations, say so.
| "You can speed up builds by enabling remote caching." | "Nx allows you to speed up builds." |
| "Run `nx build` to build your project." | "In order to build your project, you can run the `nx build` command." |
| "This works best with fewer than 50 projects." | "This feature can easily scale to any number of projects." |
| "Nx reads your `vite.config.ts` and infers build targets automatically." | "Nx provides a robust and comprehensive mechanism for inferring build targets." |
| "If the cache is stale, delete `.nx/cache` and retry." | "Should you encounter issues with caching, you may want to consider clearing your cache directory." |
### Anti-AI language
Edit AI-assisted drafts so they don't read like AI wrote them. Phrase-level passes alone won't do it. Apply the structural rules above first.
**Never use these phrases:**
- "It's important to note that..."
- "It's worth noting that..." / "It should be noted that..."
- "In this section, we will explore..."
- "Let's dive into..." / "Let's take a closer look at..."
- "Delve into..." / "Delving into..."
- "Embark on a journey..." / "Embark on..."
- "Navigate the realm of..." / "In the realm of..." / "In the world of..."
- "At its core..."
- "A testament to..."
- "Rich tapestry" / "Tapestry of..."
- "Plays a vital/crucial/pivotal/key role"
- "Whether you're a beginner or an experienced developer..."
- "In today's fast-paced development environment..."
- "Unlock the power of..." / "Harness the power of..."
- "Take your workspace to the next level"
- "Streamline your workflow" (as a generic claim without specifics)
- "This comprehensive guide will..."
- "Without further ado..."
- "In conclusion..." / "To summarize..." / "As we've seen..."
- Lists where every item starts with the same grammatical structure repeated 5+ times with slight variation. Vary your phrasing.
### Self-referential writing
Don't write about the document itself.
Do:
- "Nx uses a project graph to determine task dependencies."
Don't:
- "This page explains how Nx uses a project graph."
- "In this guide, we'll walk through..."
- "This document covers..."
Get right to the point. The reader already knows they're on a page.
### Building trust
Don't use filler words that undermine the reader's trust.
- Don't use "easily", "simply", "just", or "straightforward". If something were truly simple, you wouldn't need to document it. These words also make readers feel bad when they struggle.
- Don't use marketing language: "This feature will save you hours" or "Nx makes CI effortless."
- Be specific instead: "Remote caching can reduce CI times from 45 minutes to under 5 minutes for cache-hit builds."
### Customer perspective
Focus on what the reader can do, not what Nx does.
Do:
- "Use `nx affected` to run tasks only for projects impacted by your changes."
Don't:
- "Nx allows you to run affected tasks."
- "Nx provides the ability to run tasks selectively."
Words like "allow" and "enable" are signals you're writing from the product's perspective instead of the reader's.
## Language
Write in US English.
### Active voice
Use active voice in most cases.
Do: "Nx caches the build output."
Don't: "The build output is cached by Nx."
Exception: When "Nx" as the subject sounds awkward, passive voice is fine. "The output is stored in `.nx/cache`" is better than "Nx stores the output in `.nx/cache`" if Nx isn't the focus of the sentence.
### Contractions
Use contractions. They make the text feel natural.
- "You'll need to configure..." not "You will need to configure..."
- "It doesn't support..." not "It does not support..."
Don't contract for emphasis in warnings or error descriptions:
- "**Do not** delete the `nx.json` file."
- "Requests to localhost **are not** allowed."
Don't contract proper nouns: "the Vite plugin is..." not "Vite's a plugin..."
### Capitalization
Use sentence case for headings. Capitalize proper nouns only.
-`# Use remote caching to speed up CI`
-`## Configure the Vite plugin`
Feature names are lowercase unless they are a proper product name:
| Correct | Incorrect |
| -------------- | -------------- |
| remote caching | Remote Caching |
| project graph | Project Graph |
| Nx Cloud | nx cloud |
| Nx Console | nx console |
| Nx Agents | nx agents |
| Nx Replay | nx replay |
### Acronyms
Spell out acronyms on first use per page. Don't spell out widely-known ones: CI, CD, API, URL, CLI, PR, IDE.
Don't make acronyms plural with apostrophes. Use `APIs`, not `API's`.
### Numbers
Spell out zero through nine. Use numerals for 10 and above. Always use numerals with units: "5 minutes", "3 projects."
### Possessives
Don't use possessives on product names. "the Docker CLI", not "Docker's CLI." "the Nx configuration", not "Nx's configuration."
## Text
### Headings
- Don't skip heading levels (e.g., `##` to `####`).
- Don't use code in headings unless it's essential (like a CLI command).
- Don't use bold text in headings.
- Keep headings short and scannable. Lead with keywords.
### Line length
- Wrap lines at approximately 100 characters for readability in diffs.
- Start each new sentence on a new line.
- Exception: Don't break links across lines.
### Punctuation
- Use serial (Oxford) commas: "React, Angular, and Vue."
- Use one space between sentences.
- Don't use semicolons. Use two sentences instead.
- Don't use em dashes or en dashes. Use commas or separate sentences.
### Placeholder text
Use `<` and `>` for values the reader must replace:
```shell
nx run <project-name>:build
```
If the placeholder is inline, wrap it in a single backtick: `<your-project>`.
### Bold
Use bold for:
- UI elements: "Select **Add Connection**."
- Navigation paths: "Go to **Settings** > **Workspace**."
Don't use bold for emphasis or keywords. If you need emphasis, rewrite the sentence to be clearer.
### Inline code
Use inline code (single backticks) for:
- Commands and CLI arguments: `nx build`, `--parallel`
- Short outputs and values: `true`, `false`, `success`
### Code blocks
Use triple backticks with a language identifier:
````markdown
```json
{
"targetDefaults": {
"build": {
"cache": true
}
}
}
```
````
- Always specify a syntax language. Use `plaintext` if nothing else fits.
- Add a blank line before and after code blocks.
- For long config files, show only the relevant section and use comments to indicate omitted parts:
```json
{
// ... other config
"targetDefaults": {
"build": {
"cache": true
}
}
}
```
## Links
Links help readers find related information, but too many links make text hard to read.
### General rules
- Don't duplicate links. If you link to a page once, don't link to it again on the same page.
- Don't use links in headings.
- Avoid more than 15 links to other pages on any single page.
- Avoid multiple links in a single paragraph when possible.
### Link text
Use descriptive text, not "here" or "this page."
Do:
- "For more information, see [remote caching](/features/cache)."
- "To configure task pipelines, see [task pipeline configuration](/concepts/task-pipeline-configuration)."
Don't:
- "For more information, see [this page](/features/cache)."
- "Click [here](/features/cache) to learn more."
- "For more information, see the [Remote Caching](/features/cache) documentation."
Standard patterns:
- `For more information, see [link text](url).`
- `To <do this thing>, see [link text](url).`
### External links
Minimize external links. They break over time and are hard to maintain. When you must link externally, prefer official documentation (e.g., Vite docs, Webpack docs) over blog posts or third-party guides.
## Lists
- Use ordered lists for sequences of steps.
- Use unordered lists when order doesn't matter.
- Use dashes (`-`) for unordered lists.
- Start ordered list items with `1.` (Markdown auto-increments).
- Make list items parallel in structure.
- Add a colon after the introductory phrase.
- Don't use list items to complete an introductory sentence.
Do:
```markdown
You can clear the cache in the following ways:
- Delete the `.nx/cache` directory manually.
- Run `nx reset` to clear all cached results.
```
Don't:
```markdown
You can clear the cache by:
- Deleting the `.nx/cache` directory manually.
- Running `nx reset`.
```
## Tables
Use tables for structured data that benefits from a matrix layout. For simple lists of items with descriptions, use a regular list instead.
- Don't leave cells empty. Use "N/A" or "None."
- Use sentence case for headers.
- Keep the header and delimiter rows the same length.
## Nx-specific terminology
Use these terms consistently. When writing about Nx concepts, use the exact term from this list.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.