Repository navigation
chore(core): run nx package unit tests with vitest - #36754
Merged
Merged
Conversation
✅ Deploy Preview for nx-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for nx-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
FrozenPandaz
force-pushed
the
worktree-vitest-nx-explore
branch
from
August 21, 2026 19:38
03bd6c7 to
898b15c
Compare
Contributor
|
View your CI Pipeline Execution ↗ for commit c40de2b
☁️ Nx Cloud last updated this comment at |
FrozenPandaz
force-pushed
the
worktree-vitest-nx-explore
branch
2 times, most recently
from
August 24, 2026 14:32
c9b0bee to
d96d9b6
Compare
FrozenPandaz
marked this pull request as ready for review
August 24, 2026 19:35
FrozenPandaz
force-pushed
the
worktree-vitest-nx-explore
branch
from
August 25, 2026 20:02
775decd to
762b6f6
Compare
leosvelperez
approved these changes
Aug 26, 2026
FrozenPandaz
enabled auto-merge (squash)
August 26, 2026 14:11
…ow-test timeout for vitest
…un-information spec
AgentEnder
added a commit
that referenced
this pull request
Sep 2, 2026
packages/nx moved to vitest in #36754, which has no `jest` global.
AgentEnder
added a commit
that referenced
this pull request
Sep 2, 2026
packages/nx moved to vitest in #36754, which has no `jest` global.
AgentEnder
added a commit
that referenced
this pull request
Sep 2, 2026
packages/nx moved to vitest in #36754, which has no `jest` global.
AgentEnder
added a commit
that referenced
this pull request
Sep 2, 2026
packages/nx moved to vitest in #36754, which has no `jest` global.
AgentEnder
added a commit
that referenced
this pull request
Sep 3, 2026
The nx package moved to vitest in #36754; this spec was written against jest and died at module evaluation with `ReferenceError: jest is not defined`. `NxCache` is mocked with a plain function rather than an arrow, because vitest calls the implementation with `new` and arrows are not constructable. Claude-Session: https://claude.ai/code/session_01Sr9WDU54dksQvnDxB6csPZ
AgentEnder
added a commit
that referenced
this pull request
Sep 8, 2026
The nx package moved to vitest in #36754; both specs were written against jest and died at module evaluation with `ReferenceError: jest is not defined`. Claude-Session: https://claude.ai/code/session_01Sr9WDU54dksQvnDxB6csPZ
AgentEnder
added a commit
that referenced
this pull request
Sep 9, 2026
packages/nx moved to vitest in #36754, which has no `jest` global. The AI-agent output-style tests looked up `isAiAgent` with `require()`, which reached the real module rather than the mock: vitest intercepts imports, not CJS requires. Bound to the hoisted import instead.
AgentEnder
added a commit
that referenced
this pull request
Sep 9, 2026
The nx package moved to vitest in #36754; this spec was written against jest and died at module evaluation with `ReferenceError: jest is not defined`. `NxCache` is mocked with a plain function rather than an arrow, because vitest calls the implementation with `new` and arrows are not constructable. Claude-Session: https://claude.ai/code/session_01Sr9WDU54dksQvnDxB6csPZ
AgentEnder
added a commit
that referenced
this pull request
Sep 9, 2026
packages/nx moved to vitest in #36754, which has no `jest` global.
AgentEnder
added a commit
that referenced
this pull request
Sep 9, 2026
The nx package moved to vitest in #36754; both specs were written against jest and died at module evaluation with `ReferenceError: jest is not defined`. Claude-Session: https://claude.ai/code/session_01Sr9WDU54dksQvnDxB6csPZ
FrozenPandaz
added a commit
that referenced
this pull request
Sep 9, 2026
Written from PR #36754 and corrected against the packages/workspace migration.
FrozenPandaz
added a commit
that referenced
this pull request
Sep 9, 2026
Written from PR #36754 and corrected against the packages/workspace migration.
AgentEnder
added a commit
that referenced
this pull request
Sep 10, 2026
…prefix the wire protocol (#36838) ## Current Behavior The daemon mirrors the client's wire format when serializing a response, with no fallback if that format cannot carry the payload. A large `HASH_TASKS` response kills the daemon: ``` Serializing response for HASH_TASKS message in json mode RangeError: Invalid string length at serializeUnserializedResult (daemon/server/server.js:514:21) at handleResult ``` The client side already had a JSON/v8 fallback, but it only covers messages sent to the daemon. Nothing covered responses. The throw escapes an un-awaited async callback, so it becomes an unhandled rejection and terminates the process rather than failing one request. Messages were also framed with a trailing `NX_MSG_END` delimiter and carried as latin1 strings, so every message was capped at Node's max string length of 536,870,888 characters in three places: `.toString('binary')` on a v8 buffer, the response concatenation, and the client's accumulating string. ## Expected Behavior Daemon responses fall back to the other format instead of throwing, and messages are no longer bounded by the max string length. Two commits: 1. **Framing.** Messages carry an `NX_MSG_<byteLength>:` header and travel as buffers end to end. Buffers sit outside the V8 heap, so a large response stops counting against `--max-old-space-size`. Framing is O(1) per message instead of a scan, so a payload containing the framing marker can no longer desynchronize the stream, and a complete message sharing a chunk with an incomplete one is delivered immediately rather than held until a chunk ends on a boundary. An incomplete message waits for its remaining bytes indefinitely — there is deliberately no idle timer, since Node runs timer callbacks before the poll phase, so a timer could discard bytes that had already arrived — while a runaway declared length is bounded by `NX_MAX_MESSAGE_SIZE`, and a header that does not parse fails the stream instead of hanging. 2. **Fallback.** `serializeWithFallback(data, preferred)` replaces `serializeUnserializedResult` and backs both directions. This also fixes a precedence bug it surfaced: `serialize()` branched on `force === 'v8' || isV8SerializerEnabled()`, so `force: 'json'` was ignored whenever `NX_USE_V8_SERIALIZER` was set. `processInBackground` forces JSON precisely because its payloads cannot be v8-cloned, so it paid a failed serialization and a spurious warning on every call under that env var. The format is detected per message after framing rather than per connection, because a single chunk can carry a JSON streaming progress message and a v8 response together. Responding in a format the client did not send is already safe: streaming progress messages have always been serialized from the daemon's own preference rather than the requesting client's. ### Why both changes ship together Measured on a `HASH_TASKS`-shaped payload, v8 output is 2.4% smaller than JSON. With a string transport the fallback only widens the working range from about 537MB to about 550MB, which stops the crash without giving the reporting workspace headroom. Opting into `NX_USE_V8_SERIALIZER` was not enough for them either. ### Compatibility The wire format changes in both directions across the four channels that share the framing: daemon client and server, plugin worker IPC, and pseudo-IPC. Version skew is already covered by the `nxVersion` handshake in `daemon/cache.ts`, which throws before any bytes are exchanged, so a new client never talks to an old daemon. ### Testing `consume-messages-from-socket.spec.ts` covers framing, header splits, desync, the size ceiling, the absence of idle timers, a payload whose bytes contain the framing marker, and three round-trips over a real unix socket where the OS picks the chunk boundaries. The fallback tests avoid allocating 512MB by using inputs each format rejects: `{value: 1n}` fails JSON, `{fn(){}}` fails v8. 3178 tests pass across `daemon`, `utils`, `project-graph/plugins`, and `tasks-runner`, with no new failures. ### Not affected `serializeResult()` builds the `REQUEST_PROJECT_GRAPH` response by string concatenation rather than going through this path. Its only other caller passes `(error, null, null)`. The project graph does not carry per-file data, so neither payload is anywhere near the limit that the task hash details hit. ### Stacked on #36850 This PR is based on #36850, which raises the `packages/nx` vitest timeout. `project-graph-incremental-recomputation.spec.ts` needs 42.4s on CI against the previous 35s limit and fails on any branch that executes it; this branch touches `packages/nx`, so it runs the spec every push. Merge #36850 first, then retarget this PR to `master`. The commit range here is only the nine daemon commits; the timeout change is not part of this diff. ### CI note Intermittent `Command timed out after 300s` failures on rollup builds in `e2e-react` / `e2e-remix` / `e2e-rollup` are the pre-existing flake tracked in #36794, not this change. That flake predates this branch, hits about 4% of master runs, and the dump there showed the surviving process is rollup's own CLI while the daemon round-trip finished in milliseconds. This branch touches `packages/nx`, so every e2e task runs rather than reading from cache, which is why it shows up more often here. ## Related Issue(s) Fixes NXC-4901 <!-- polygraph-session-start --> --- <p><a href="https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/daemon-serializer-920ef087">View Polygraph session ↗</a></p> <!-- polygraph-session-end --> --------- Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com> Backport notes: the NX_MAX_MESSAGE_SIZE daemon-env handling is dropped. 22.7.x has no client-env reflection (daemon-environment.ts is 134 lines here against 411 on master), so the exclusion-list entry, the getDaemonSpawnEnv pin and daemon-environment.spec.ts have nothing to attach to. The knob itself still works: consume-messages-from-socket.ts reads process.env directly. Also dropped: the WatcherFailedError class and its dispatch, another post-22.7 feature, keeping only the framing-failure counter the new code needs; the astro-docs row, since the site publishes from master; and applyDaemonEnvFromClient in the plugin worker's setWorkerEnv, which keeps 22.7.x direct assignment loop. New specs converted from vitest to jest, since #36754 is not on this branch.
AgentEnder
added a commit
that referenced
this pull request
Sep 10, 2026
…prefix the wire protocol (#36838) ## Current Behavior The daemon mirrors the client's wire format when serializing a response, with no fallback if that format cannot carry the payload. A large `HASH_TASKS` response kills the daemon: ``` Serializing response for HASH_TASKS message in json mode RangeError: Invalid string length at serializeUnserializedResult (daemon/server/server.js:514:21) at handleResult ``` The client side already had a JSON/v8 fallback, but it only covers messages sent to the daemon. Nothing covered responses. The throw escapes an un-awaited async callback, so it becomes an unhandled rejection and terminates the process rather than failing one request. Messages were also framed with a trailing `NX_MSG_END` delimiter and carried as latin1 strings, so every message was capped at Node's max string length of 536,870,888 characters in three places: `.toString('binary')` on a v8 buffer, the response concatenation, and the client's accumulating string. ## Expected Behavior Daemon responses fall back to the other format instead of throwing, and messages are no longer bounded by the max string length. Two commits: 1. **Framing.** Messages carry an `NX_MSG_<byteLength>:` header and travel as buffers end to end. Buffers sit outside the V8 heap, so a large response stops counting against `--max-old-space-size`. Framing is O(1) per message instead of a scan, so a payload containing the framing marker can no longer desynchronize the stream, and a complete message sharing a chunk with an incomplete one is delivered immediately rather than held until a chunk ends on a boundary. An incomplete message waits for its remaining bytes indefinitely — there is deliberately no idle timer, since Node runs timer callbacks before the poll phase, so a timer could discard bytes that had already arrived — while a runaway declared length is bounded by `NX_MAX_MESSAGE_SIZE`, and a header that does not parse fails the stream instead of hanging. 2. **Fallback.** `serializeWithFallback(data, preferred)` replaces `serializeUnserializedResult` and backs both directions. This also fixes a precedence bug it surfaced: `serialize()` branched on `force === 'v8' || isV8SerializerEnabled()`, so `force: 'json'` was ignored whenever `NX_USE_V8_SERIALIZER` was set. `processInBackground` forces JSON precisely because its payloads cannot be v8-cloned, so it paid a failed serialization and a spurious warning on every call under that env var. The format is detected per message after framing rather than per connection, because a single chunk can carry a JSON streaming progress message and a v8 response together. Responding in a format the client did not send is already safe: streaming progress messages have always been serialized from the daemon's own preference rather than the requesting client's. ### Why both changes ship together Measured on a `HASH_TASKS`-shaped payload, v8 output is 2.4% smaller than JSON. With a string transport the fallback only widens the working range from about 537MB to about 550MB, which stops the crash without giving the reporting workspace headroom. Opting into `NX_USE_V8_SERIALIZER` was not enough for them either. ### Compatibility The wire format changes in both directions across the four channels that share the framing: daemon client and server, plugin worker IPC, and pseudo-IPC. Version skew is already covered by the `nxVersion` handshake in `daemon/cache.ts`, which throws before any bytes are exchanged, so a new client never talks to an old daemon. ### Testing `consume-messages-from-socket.spec.ts` covers framing, header splits, desync, the size ceiling, the absence of idle timers, a payload whose bytes contain the framing marker, and three round-trips over a real unix socket where the OS picks the chunk boundaries. The fallback tests avoid allocating 512MB by using inputs each format rejects: `{value: 1n}` fails JSON, `{fn(){}}` fails v8. 3178 tests pass across `daemon`, `utils`, `project-graph/plugins`, and `tasks-runner`, with no new failures. ### Not affected `serializeResult()` builds the `REQUEST_PROJECT_GRAPH` response by string concatenation rather than going through this path. Its only other caller passes `(error, null, null)`. The project graph does not carry per-file data, so neither payload is anywhere near the limit that the task hash details hit. ### Stacked on #36850 This PR is based on #36850, which raises the `packages/nx` vitest timeout. `project-graph-incremental-recomputation.spec.ts` needs 42.4s on CI against the previous 35s limit and fails on any branch that executes it; this branch touches `packages/nx`, so it runs the spec every push. Merge #36850 first, then retarget this PR to `master`. The commit range here is only the nine daemon commits; the timeout change is not part of this diff. ### CI note Intermittent `Command timed out after 300s` failures on rollup builds in `e2e-react` / `e2e-remix` / `e2e-rollup` are the pre-existing flake tracked in #36794, not this change. That flake predates this branch, hits about 4% of master runs, and the dump there showed the surviving process is rollup's own CLI while the daemon round-trip finished in milliseconds. This branch touches `packages/nx`, so every e2e task runs rather than reading from cache, which is why it shows up more often here. ## Related Issue(s) Fixes NXC-4901 <!-- polygraph-session-start --> --- <p><a href="https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/daemon-serializer-920ef087">View Polygraph session ↗</a></p> <!-- polygraph-session-end --> --------- Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com> Backport notes: the NX_MAX_MESSAGE_SIZE daemon-env handling is dropped. 22.7.x has no client-env reflection (daemon-environment.ts is 134 lines here against 411 on master), so the exclusion-list entry, the getDaemonSpawnEnv pin and daemon-environment.spec.ts have nothing to attach to. The knob itself still works: consume-messages-from-socket.ts reads process.env directly. Also dropped: the WatcherFailedError class and its dispatch, another post-22.7 feature, keeping only the framing-failure counter the new code needs; the astro-docs row, since the site publishes from master; and applyDaemonEnvFromClient in the plugin worker's setWorkerEnv, which keeps 22.7.x direct assignment loop. New specs converted from vitest to jest, since #36754 is not on this branch.
FrozenPandaz
pushed a commit
that referenced
this pull request
Sep 10, 2026
…prefix the wire protocol (#36838) ## Current Behavior The daemon mirrors the client's wire format when serializing a response, with no fallback if that format cannot carry the payload. A large `HASH_TASKS` response kills the daemon: ``` Serializing response for HASH_TASKS message in json mode RangeError: Invalid string length at serializeUnserializedResult (daemon/server/server.js:514:21) at handleResult ``` The client side already had a JSON/v8 fallback, but it only covers messages sent to the daemon. Nothing covered responses. The throw escapes an un-awaited async callback, so it becomes an unhandled rejection and terminates the process rather than failing one request. Messages were also framed with a trailing `NX_MSG_END` delimiter and carried as latin1 strings, so every message was capped at Node's max string length of 536,870,888 characters in three places: `.toString('binary')` on a v8 buffer, the response concatenation, and the client's accumulating string. ## Expected Behavior Daemon responses fall back to the other format instead of throwing, and messages are no longer bounded by the max string length. Two commits: 1. **Framing.** Messages carry an `NX_MSG_<byteLength>:` header and travel as buffers end to end. Buffers sit outside the V8 heap, so a large response stops counting against `--max-old-space-size`. Framing is O(1) per message instead of a scan, so a payload containing the framing marker can no longer desynchronize the stream, and a complete message sharing a chunk with an incomplete one is delivered immediately rather than held until a chunk ends on a boundary. An incomplete message waits for its remaining bytes indefinitely — there is deliberately no idle timer, since Node runs timer callbacks before the poll phase, so a timer could discard bytes that had already arrived — while a runaway declared length is bounded by `NX_MAX_MESSAGE_SIZE`, and a header that does not parse fails the stream instead of hanging. 2. **Fallback.** `serializeWithFallback(data, preferred)` replaces `serializeUnserializedResult` and backs both directions. This also fixes a precedence bug it surfaced: `serialize()` branched on `force === 'v8' || isV8SerializerEnabled()`, so `force: 'json'` was ignored whenever `NX_USE_V8_SERIALIZER` was set. `processInBackground` forces JSON precisely because its payloads cannot be v8-cloned, so it paid a failed serialization and a spurious warning on every call under that env var. The format is detected per message after framing rather than per connection, because a single chunk can carry a JSON streaming progress message and a v8 response together. Responding in a format the client did not send is already safe: streaming progress messages have always been serialized from the daemon's own preference rather than the requesting client's. ### Why both changes ship together Measured on a `HASH_TASKS`-shaped payload, v8 output is 2.4% smaller than JSON. With a string transport the fallback only widens the working range from about 537MB to about 550MB, which stops the crash without giving the reporting workspace headroom. Opting into `NX_USE_V8_SERIALIZER` was not enough for them either. ### Compatibility The wire format changes in both directions across the four channels that share the framing: daemon client and server, plugin worker IPC, and pseudo-IPC. Version skew is already covered by the `nxVersion` handshake in `daemon/cache.ts`, which throws before any bytes are exchanged, so a new client never talks to an old daemon. ### Testing `consume-messages-from-socket.spec.ts` covers framing, header splits, desync, the size ceiling, the absence of idle timers, a payload whose bytes contain the framing marker, and three round-trips over a real unix socket where the OS picks the chunk boundaries. The fallback tests avoid allocating 512MB by using inputs each format rejects: `{value: 1n}` fails JSON, `{fn(){}}` fails v8. 3178 tests pass across `daemon`, `utils`, `project-graph/plugins`, and `tasks-runner`, with no new failures. ### Not affected `serializeResult()` builds the `REQUEST_PROJECT_GRAPH` response by string concatenation rather than going through this path. Its only other caller passes `(error, null, null)`. The project graph does not carry per-file data, so neither payload is anywhere near the limit that the task hash details hit. ### Stacked on #36850 This PR is based on #36850, which raises the `packages/nx` vitest timeout. `project-graph-incremental-recomputation.spec.ts` needs 42.4s on CI against the previous 35s limit and fails on any branch that executes it; this branch touches `packages/nx`, so it runs the spec every push. Merge #36850 first, then retarget this PR to `master`. The commit range here is only the nine daemon commits; the timeout change is not part of this diff. ### CI note Intermittent `Command timed out after 300s` failures on rollup builds in `e2e-react` / `e2e-remix` / `e2e-rollup` are the pre-existing flake tracked in #36794, not this change. That flake predates this branch, hits about 4% of master runs, and the dump there showed the surviving process is rollup's own CLI while the daemon round-trip finished in milliseconds. This branch touches `packages/nx`, so every e2e task runs rather than reading from cache, which is why it shows up more often here. ## Related Issue(s) Fixes NXC-4901 <!-- polygraph-session-start --> --- <p><a href="https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/daemon-serializer-920ef087">View Polygraph session ↗</a></p> <!-- polygraph-session-end --> --------- Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com> Backport notes: the NX_MAX_MESSAGE_SIZE daemon-env handling is dropped. 22.7.x has no client-env reflection (daemon-environment.ts is 134 lines here against 411 on master), so the exclusion-list entry, the getDaemonSpawnEnv pin and daemon-environment.spec.ts have nothing to attach to. The knob itself still works: consume-messages-from-socket.ts reads process.env directly. Also dropped: the WatcherFailedError class and its dispatch, another post-22.7 feature, keeping only the framing-failure counter the new code needs; the astro-docs row, since the site publishes from master; and applyDaemonEnvFromClient in the plugin worker's setWorkerEnv, which keeps 22.7.x direct assignment loop. New specs converted from vitest to jest, since #36754 is not on this branch.
AgentEnder
added a commit
that referenced
this pull request
Sep 11, 2026
packages/nx moved to vitest in #36754, which has no `jest` global. The AI-agent output-style tests looked up `isAiAgent` with `require()`, which reached the real module rather than the mock: vitest intercepts imports, not CJS requires. Bound to the hoisted import instead.
AgentEnder
added a commit
that referenced
this pull request
Sep 11, 2026
packages/nx moved to vitest in #36754, which has no `jest` global.
FrozenPandaz
added a commit
that referenced
this pull request
Sep 14, 2026
…37039) ## Current Behavior `nx test nx` intermittently loses a vitest fork on macOS (and, less often, on the Linux CI agents) with `[vitest-pool]: Worker forks emitted error ... Worker exited unexpectedly` while no test fails. macOS crash reports show SIGSEGV at address 0 inside `nx.darwin-arm64.node`, in jemalloc's `tcache_bin_flush_small` or `arena_ralloc_no_move`, under `drop_in_place<TaskHasher>`, `Arc::drop_slow`, a `DashMap` drop, or a `format!` inside `InstructionPool::intern`. The rate swings between builds (2/10 to 5/6 runs of `native-task-hasher-impl.spec.ts`) and the crashing tests never touch the code under review, which made it look like a latent heap corruption in the Rust binding. It is not. Every crash report's `usedImages` lists the binding **twice** in the same worker process, same uuid, two paths: - `packages/nx/src/native/nx.darwin-arm64.node`: loaded through `native-bindings.js`, because the `nx-native-shim` vite plugin routed every *import* of `src/native/index.js` there. - `/tmp/.nx/<uid>/native-cache/0.0.1/0.0.1-<hash>-nx.darwin-arm64.node`: loaded through `index.js`, because the lazy `require('../native')` calls in nx source (`workspace-context.ts`, `file-hasher.ts`, `cache.ts`, `analytics.ts`, ...) run through node's loader, never vite, and `index.js`'s file cache copies the binary and loads the copy. Two dylib images are two copies of the Rust statics and two independent jemalloc heaps. `TaskHasher::new` (first image, from the vite import) clones `Arc`s out of the `projectFileMap` / `allWorkspaceFiles` externals that `WorkspaceContext.getFileMap()` created in the second image (from the runtime require). Whichever image drops the last `Arc` frees memory the other heap owns. jemalloc's sized free (`sdallocx`) pushes the pointer into its thread cache without an ownership check, so nothing fails at the free itself. The crash comes when that cache bin flushes (`emap_edata_lookup` finds no extent, NULL `edata`, read of address 0), or when `malloc` hands the recycled foreign block to a `String` that is then grown with `rallocx`. That deferral is why the crashing frame varies and why the rate depends on build layout. This started with the vitest migration (#36754): under jest both paths went through `index.js`. Separately, `src/native/index.js` ends with `module.exports = indexModule`. Node's CommonJS export lexer cannot follow an assignment from a variable, so an ES module doing `import { hashArray } from 'nx/src/native/index.js'` fails with `Named export 'hashArray' not found`. Only the default import works. That is also what stopped the test setup from simply externalizing `index.js`: the setup's `vi.doMock` of the native module spreads `vi.importActual(...)`, which came back with no named exports. ## Expected Behavior Two commits: 1. `index.js` ends with `module.exports = require('./native-bindings.js')`. Same require, same order, same object, but a form the lexer recognizes and follows to the named exports in `native-bindings.js`. Verified with plain node against the published package layout: the named import fails before and works after; default import and CommonJS `require` are unchanged. 2. The vitest config externalizes `index.js` and drops the `nx-native-shim` plugin. Every path to the binding, vite imports and lazy requires alike, now runs the one loader under node, so a worker holds a single image. A new case in `native-bindings.spec.ts` pins it: the `TaskHasher` constructor from a vite import and from a runtime `require('..')` must be the same function. It fails deterministically when the binding loads twice, so the regression cannot come back as a flake. Verification on master's binary (uuid 4D093B75), loops of `native-task-hasher-impl.spec.ts`: | setup | crashes | |---|---| | master as is | 2/10 | | `NX_SKIP_NATIVE_FILE_CACHE=true` (control: one image) | 0/20 | | this branch | 0/20 | A `Module._load` trace confirms the fixed worker loads the binding once and all 15 runtime requires hit the cached `index.js`. Five runs of the five native-context specs together: 0/5 crashes (114 tests each). Full `packages/nx` suite: 338 files, 7670 passed, 0 worker crashes; the one failure is the "blind window" case in `watch-before-scan.spec.ts`, which fails the same way on untouched master on macOS (2/3 runs) and is unrelated. Considered and not taken: a node-side `Module._load` shim mirroring the vite plugin (an earlier revision of this PR) also works, but leaves two loaders and two shims in the test setup; `NX_SKIP_NATIVE_FILE_CACHE=true` in `test.env` works only by coincidence of node's require cache. ## Related Issue(s) N/A. This is the pre-existing worker crash noted while reviewing #37017 and #37025. <!-- polygraph-session-start --> --- <p><a href="https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/Stop-vitest-workers-loading-the-nx-native-binding-twice-jemalloc-SIGSEG-ecfbc350">View Polygraph session ↗</a></p> <!-- polygraph-session-end --> --------- Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
AgentEnder
added a commit
that referenced
this pull request
Sep 16, 2026
The nx package moved to vitest in #36754; both specs were written against jest and died at module evaluation with `ReferenceError: jest is not defined`. The `require` calls become `await import`. Under jest a require after `resetModules` resolved to the mocked module; under vitest it bypasses the module mocker, so the specs got the real `installPackageToTmpAsync` and a second copy of `latest-nx` with its own cached install path. Claude-Session: https://claude.ai/code/session_01Sr9WDU54dksQvnDxB6csPZ
AgentEnder
added a commit
that referenced
this pull request
Sep 16, 2026
packages/nx moved to vitest in #36754, which has no `jest` global. The AI-agent output-style tests looked up `isAiAgent` with `require()`, which reached the real module rather than the mock: vitest intercepts imports, not CJS requires. Bound to the hoisted import instead.
AgentEnder
added a commit
that referenced
this pull request
Sep 16, 2026
packages/nx moved to vitest in #36754, which has no `jest` global.
FrozenPandaz
added a commit
that referenced
this pull request
Sep 23, 2026
Written from PR #36754 and corrected against the packages/workspace migration.
FrozenPandaz
added a commit
that referenced
this pull request
Sep 24, 2026
## Current Behavior The `@nx/workspace` package's 420 unit tests run with Jest through the shared `jest.preset.js`: pinned to `maxWorkers: 1`, on top of a custom resolver (`scripts/patched-jest-resolver.js`), the workspace-wide guards in `scripts/unit-test-setup.js`, and CJS shims for ESM-only dependencies. A full `nx test workspace` takes 7m53s. ## Expected Behavior `nx test workspace` runs the same 420 tests with Vitest 4, inferred through the `@nx/vitest` plugin, in ~17.5s (~27x faster). Test count, suite count, and every recorded snapshot value are unchanged; the suite is stable over repeated runs. Other packages are unaffected and still infer Jest. Follows #36754, which moved `packages/nx`. Two pieces of that migration turned out to be specific to `nx` — the one package that barely imports its siblings — so this PR adds the shared machinery every other package will need: - `scripts/vitest-nx-source-resolver.mts` replaces `scripts/patched-jest-resolver.js`. `resolve.conditions: ['@nx/nx-source']` is **not** sufficient on its own: `node_modules/nx` and `node_modules/@nx/*` are the published tarballs (dist only), and their exports maps advertise `@nx/nx-source` entries pointing at source files the tarball does not ship. The plugin maps `nx` / `@nx/*` through the local `packages/<pkg>/package.json` instead, with a file fallback for deep imports no exports entry covers. - `scripts/vitest-setup.mts` ports `scripts/unit-test-setup.js` (jest-only, `jest.doMock`) and adds what vitest's execution model requires: - the same source mapping on `Module._resolveFilename`, so node's `require` and vite's module graph agree — without it a lazy `require('@nx/js')` fails outright under `--conditions=@nx/nx-source`; - **the graph mocks repeated on the CJS channel** via `Module._load`. Generators reach graph builders through lazy `require()`, which `vi.mock` cannot see; unmocked, `createProjectGraphAsync` takes `project-graph.lock` and deadlocks the worker with no output and no test timeout; - `NX_ISOLATE_PLUGINS=false`, so plugin isolation does not spawn worker subprocesses that are never torn down (two `packages/nx` specs already carry this note); - `NX_WORKSPACE_ROOT_PATH` under `tmp/unit/<pid>`, per worker. The jest resolver set a single `tmp/unit` as a side effect; with parallel workers one shared root makes every worker queue on the same lock. - The `@clack/prompts` shim is kept. It is not only ESM interop: the real library drives a *synchronous* prompt, so a generator that asks a question blocks the worker forever. Spec changes are the usual codemod (`jest.*` → `vi.*`, async mock factories with `vi.importActual`, `xdescribe` → `describe.skip`, vitest type imports) plus `vi.mock('child_process', { spy: true })` where a frozen ESM namespace was previously spied on directly. Snapshots are rekeyed, not rewritten: vitest joins describe and test names with `' > '` where jest used a space, so every key reads as new. All 176 recorded values were diffed against the jest originals after normalizing the separator and match exactly. The one shape change is `toThrowErrorMatchingInlineSnapshot`, which vitest records as `[Error: msg]` rather than `"msg"`. ### Verification - `nx test workspace` — 420 tests (417 passed, 3 skipped), matching jest exactly; stable across four consecutive runs. - `nx run-many -t build,lint -p workspace` — green. - `devkit` run as a canary — 563 tests, unchanged. A full `nx affected` is not meaningful here: a new file under `scripts/` marks all 94 projects affected, and nothing jest reads was modified (`jest.preset.js`, `scripts/unit-test-setup.js`, and `scripts/patched-jest-resolver.js` are untouched). ### Follow-ups, not in this PR - `packages/nx/src/internal-testing-utils/mock-project-graph.ts` logs a vitest deprecation warning ("vi.mock call is not at the top level ... will become an error in a future version") because of its dual-runner `if (typeof vi !== 'undefined')` branch. Pre-existing; the `nx` suite hits it too. - `scripts/jest-mocks/` now serves vitest as well and wants renaming to `scripts/test-mocks/` once a second package migrates. - Each worker leaves a `tmp/unit/<pid>` directory behind (gitignored). `packages/devkit/jest-setup-nx-workspace-data-dir.js` has the `mkdtemp` + `rmSync`-on-exit pattern if we want them cleaned up. ## Related Issue(s) N/A <!-- polygraph-session-start --> --- <p><a href="https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/Migrate-packages-workspace-unit-tests-to-vitest-a01a636a">View Polygraph session ↗</a></p> <!-- polygraph-session-end -->
AgentEnder
added a commit
that referenced
this pull request
Oct 2, 2026
The nx package moved to vitest in #36754; this spec was written against jest and died at module evaluation with `ReferenceError: jest is not defined`. `NxCache` is mocked with a plain function rather than an arrow, because vitest calls the implementation with `new` and arrows are not constructable. Claude-Session: https://claude.ai/code/session_01Sr9WDU54dksQvnDxB6csPZ
AgentEnder
added a commit
that referenced
this pull request
Oct 5, 2026
The nx package moved to vitest in #36754; both specs were written against jest and died at module evaluation with `ReferenceError: jest is not defined`. The `require` calls become `await import`. Under jest a require after `resetModules` resolved to the mocked module; under vitest it bypasses the module mocker, so the specs got the real `installPackageToTmpAsync` and a second copy of `latest-nx` with its own cached install path. Claude-Session: https://claude.ai/code/session_01Sr9WDU54dksQvnDxB6csPZ
AgentEnder
added a commit
that referenced
this pull request
Oct 5, 2026
The nx package moved to vitest in #36754; this spec was written against jest and died at module evaluation with `ReferenceError: jest is not defined`. `NxCache` is mocked with a plain function rather than an arrow, because vitest calls the implementation with `new` and arrows are not constructable. Claude-Session: https://claude.ai/code/session_01Sr9WDU54dksQvnDxB6csPZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Current Behavior
The
nxpackage's ~6400 unit tests run with Jest, pinned tomaxWorkers: 1(a fullnx test nxtakes ~4 minutes), on top of a custom resolver, an SWCmut-cjs-exportsplugin, and half a dozen CJS mock shims for ESM-only dependencies.Expected Behavior
nx test nxruns the same 6423 tests with Vitest 4, inferred through the@nx/vitestplugin. The suite is parallel-safe (verified over repeated parallel and serial runs), which brings the wall time to ~50s (~4.5x faster). Other projects are unaffected and still infer Jest.Highlights:
vitest.config.mtsreproduces the Jest setup's special behavior: the@nx/nx-sourceresolve condition (replacingjest-resolver.js), a plugin routing the napi loader to the self-containednative-bindings.js, deepnx/src/*import aliases, and the CJSyargsentry.vitest.setup.mtsports theunit-test-setup.jsguards tovi.doMock, registers@swc-node/registerso the codebase's lazyrequire()calls can load TS source (restoringError.prepareStackTraceafterwards — the hook's source-map-support otherwise mis-maps vite-transformed frames, breaking error locations and inline-snapshot updates), and pins color detection off so snapshots are stable regardless of how the suite is invoked.internal-testing-utils/cjs-mock.tshelper patchesModule._loadjest-style so specs can mock modules that the source loads with barerequire()— the channelvi.mockcannot reach.jest.*→vi.*(with real hoisting),jest.requireActual→await vi.importActual,jest.isolateModules→vi.resetModules()+ dynamicimport(),done()callbacks → promises, plus per-file fixes for Vitest semantics (constructible class mocks, spy-mode module mocks for frozen ESM namespaces, hooks that must not return mocks,vi.resetAllMocksrestoring real spy implementations).@clack/prompts/ora/chalk/etc. are no longer needed for this suite.The migration also surfaced and fixed two latent test bugs where assertions passed while the mock never applied — one of which let a spec overwrite the repository's real
nx.jsonduring test runs.jest.config.ctsandjest-resolver.jsare removed; thenx-scoped branches inscripts/unit-test-setup.jsare now dead code and can be cleaned up separately.Related Issue(s)
N/A
View Polygraph session ↗