Loading workspace insights... Statistics interval
7 days30 daysLatest CI Pipeline Executions
19dbbb07 fix(angular): only add @oxc-project/runtime on the vitest-analog path (#35734)
The Angular vitest generators added `@oxc-project/runtime` to the user's
`devDependencies` on **both** the vitest-angular and vitest-analog
paths, with a comment claiming `@angular/build`'s rolldown usage emits
external `@oxc-project/runtime/helpers/*` imports. That claim doesn't
match `@angular/build`'s source: its vitest builder sets
`optimizeDeps.noDiscovery: true` plus an in-memory test provider, so no
rolldown pre-bundling against `@angular/*` runs on that path.
Separately, `addVitestAngular`/`addVitestAnalog` silently dropped the
`GeneratorCallback`s returned by `addDependenciesToPackageJson` and
`@nx/vitest`'s `configurationGenerator`. Install still ran in practice
because the parent application/library generators call
`installPackagesTask` separately, but the wiring was incorrect and any
caller that didn't double-call install would lose the post-add hooks.
- `addVitestAngular` no longer adds `@oxc-project/runtime` (it's not
needed on that path).
- `addVitestAnalog` continues to add `@oxc-project/runtime`, with a
corrected comment that accurately attributes the cause.
- Both helpers return proper `GeneratorCallback`s; the application and
library generators chain them into their task lists.
- The matching `update-23-0-0` migration AI-instructions section is
scoped to the vitest-analog path with the corrected explanation.
`@nx/vitest`'s `configurationGenerator` (for `uiFramework: 'angular'`)
registers `@analogjs/vite-plugin-angular`'s `angular()`. In test mode,
analog additionally registers an `angularVitestPlugin` whose `transform`
hook matches `@angular/*` `fesm2022` modules containing `async ` (plus
any `@angular/cdk` file) and calls:
```ts
vite.transformWithOxc(code, id, { target: 'es2016', … })
```
The downlevel is deliberate. The plugin source comments it as
*"downlevels any dependencies that use async/await to support zone.js
testing and tests w/fakeAsync"* — Zone.js relies on monkey-patching
promise scheduling for `fakeAsync` and friends, which it cannot do
against native `async`/`await`, so the plugin lowers them to a form
Zone.js can intercept.
With `target: 'es2016'`, oxc emits the helpers as external
`@oxc-project/runtime/helpers/*` imports (oxc's default `HelperMode =
'Runtime'`). Nothing in the upstream chain
(`@analogjs/vite-plugin-angular`, `@angular/core`, `vite`, `rolldown`)
declares `@oxc-project/runtime` in a way that's resolvable from the
consumer's workspace, so `vite:import-analysis` fails to resolve those
imports unless the dep is added explicitly. This behavior is unchanged
through analog `3.0.0-alpha.54` (latest at time of writing).
- `@angular/build:unit-test` (and `@nx/angular:unit-test` for libraries)
bypasses analog entirely.
- It sets `optimizeDeps.noDiscovery: true` and uses an in-memory test
provider, so no rolldown pre-bundling runs against `@angular/*`.
- No `angularVitestPlugin` is loaded → no `target: 'es2016'` downlevel →
no `@oxc-project/runtime/helpers/*` imports emitted.
- `addVitestAngular`/`addVitestAnalog` return
`Promise<GeneratorCallback>`; the application and library generators
chain them through `runTasksInSerial(...)` so the install-packages and
configuration callbacks actually run through the generator pipeline.
- `@oxc-project/runtime` is added only by `addVitestAnalog`, with the
comment now describing the actual mechanism (analog's
`angularVitestPlugin` + `transformWithOxc({ target: 'es2016' })` for
Zone.js compatibility).
- `update-23-0-0/ai-instructions-for-vite-8.md` section 3 rewritten with
the corrected mechanism, scoped to the vitest-analog path. Detection:
`rg '"@nx/vitest:test"' --type json` + `rg
'@analogjs/vite-plugin-angular' --type ts --type js`.
- e2e: new case in `projects-build-and-test.test.ts` opts into
vitest-angular explicitly (app w/ `--bundler=esbuild`, lib w/
`--buildable`) and runs `nx test` against both. The existing test
exercises vitest-analog implicitly through `app1` (webpack) →
`setGeneratorDefaults` writes `unitTestRunner: vitest-analog` to
`nx.json`, locking subsequent generations to that runner regardless of
per-project defaults.
- Reproduced the failure in an Nx e2e-generated workspace: with
`@oxc-project/runtime` absent, `nx run <lib>:test` fails at
`vite:import-analysis` trying to resolve
`@oxc-project/runtime/helpers/defineProperty` from
`@angular/core/fesm2022/testing.mjs`. The on-disk `testing.mjs` does
**not** contain those imports — they are injected in-memory by analog's
`angularVitestPlugin.transform`. Installing the dep makes the test pass.
- Confirmed the mechanism against analog plugin source in versions
`2.1.3`, `2.5.1`, and `3.0.0-alpha.54` — the `target: 'es2016'`
downlevel is unchanged.
- The new e2e covers the inverse: vitest-angular runs `nx test`
successfully without relying on `@oxc-project/runtime` for the path.
High confidence in the root cause and the path-scoped fix.
Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
(cherry picked from commit 526418996d1b33d4c51def42b1239a1f08fb0370) b58ccd83 fix(core): preserve input order in createNodes plugin results (#35595)
Two structural sources of non-determinism existed inside Nx's
`createNodes`/`createNodesV2` pipeline. Both are invisible to plugin
authors but produce different project graphs across runs on the same
workspace.
`createNodesFromFiles` (in
`packages/nx/src/project-graph/plugins/utils.ts`) — the helper that ~20
first-party plugins use to fan out per-config-file work — runs callbacks
in parallel via `Promise.all(configFiles.map(async (file, idx) => { ...
results.push([file, value]) }))`. Tuples are pushed into the shared
`results` array in the **resolution order of the callbacks**, not the
input order of `configFiles`.
The matched file list arriving at the plugin is sorted (the Rust
`glob_files` impl `par_sort`s, and Rayon's
`par_iter().filter().collect()` preserves order). The downstream merge
(`mergeCreateNodesResultsFromSinglePlugin` → `for (const result of
pluginResults)` → `for (const root in projectNodes)`) walks results in
array / insertion order. So if any plugin returns multiple contributions
for the same project root, the *only* place the order can scramble is
the helper's `Promise.all + .push` race.
A second instance of the same pattern lives in `@nx/eslint`'s
`internalCreateNodesV2`, which mutates `projects[projectRoot] = project`
from inside a `Promise.all`. The `projects` object's key-insertion order
then tracks `eslint.isPathIgnored` / `getProjectUsingESLintConfig`
resolution races and propagates the same non-determinism.
For atomizing plugins, the order of dynamically-generated target names
(`<ciTargetName>--<relativePath>`) leaks into the project graph through
`targets[name]` insertion order, `dependsOn[]`, and
`targetGroups[group][]`. The order is deterministic when the file list
comes from Nx's Rust glob (sorted), but **not** when it comes from a
non-Nx file-discovery layer:
- **`@nx/jest`** (runtime branch, `disableJestRuntime: false`) — uses
`jest.SearchSource.getTestPaths()` which walks via jest-haste-map's
parallel workers; ordering not guaranteed. The `disableJestRuntime:
true` branch was already fine (sorted glob).
- **`@nx/vitest`** and **`@nx/vite`** — both have a
`getTestPathsRelativeToProjectRoot` helper that returns
`vitest.getRelevantTestSpecifications()` directly. Vitest uses
tinyglobby internally, which doesn't sort.
`@nx/cypress`, `@nx/playwright`, `@nx/gradle` (v1 + v2), and
`@nx/eslint`'s atomizer paths all source from sorted Rust glob and
iterate synchronously — they're fine.
Project graph construction is deterministic across runs given a
deterministic input file list. Specifically:
- `createNodesFromFiles` returns `results` and `errors` arrays in
`configFiles` input order, regardless of which callback resolves first.
- `@nx/eslint`'s `projects` map keys are inserted in input order of
`projectRootsByEslintRoots.get(configDir)`.
- Atomized target names from `@nx/jest`, `@nx/vitest`, and `@nx/vite`
are inserted in lexicographic order of relative path.
- **`packages/nx/src/project-graph/plugins/utils.ts`** — settle each
callback into a discriminated tuple `{ kind: 'value' | 'empty' |
'error', ... }` from inside `Promise.all`. `await
Promise.all(arr.map(...))` returns an array indexed by input position,
so a synchronous post-pass over that array bins values and errors in
input order. No change to public API or error semantics.
- **`packages/eslint/src/plugins/plugin.ts`** — each parallel branch
*returns* its contribution (or `null`) instead of mutating the shared
`projects` object. A synchronous post-pass over `orderedProjectRoots`
`Object.assign`s contributions into `projects` in input order.
- **`packages/jest/src/plugins/plugin.ts`** — sort `specs.tests.map(({
path }) => path)` before constructing the `Set` of test paths.
- **`packages/vitest/src/plugins/plugin.ts`** +
**`packages/vite/src/plugins/plugin.ts`** — `.sort()` the relative paths
returned by `getTestPathsRelativeToProjectRoot` before they reach the
atomizer loop.
- `@nx/jest` (`disableJestRuntime: true`) — sources from
`globWithWorkspaceContext` (sorted by Rust glob).
- `@nx/cypress`, `@nx/playwright` — sources from
`globWithWorkspaceContext` / `getFilesInDirectoryUsingContext` (both
deterministic; `get_child_files` is a sequential
`into_iter().filter().collect()` over a sorted file list) and iterates
with `for (const ... of ...)`.
- `@nx/gradle` v1 — `splitConfigFiles` + `forEach` over already-sorted
glob output.
- `@nx/gradle` v2 — synchronous `for...of` over `Array.from(new
Set([...]))`; `Set` iterates in insertion order, source arrays
deterministic.
- `@nx/maven`, `@nx/nuxt`, `@nx/remix`, `@nx/rollup`, `@nx/detox`,
`@nx/dotnet`, `@nx/react/router-plugin`, and the nx-core `project-json`
/ `package-json` / `js` plugins — no parallel-write-to-shared-object
patterns.
Two new regression tests in
`packages/nx/src/project-graph/plugins/utils.spec.ts` force later inputs
to resolve faster (e.g. `file1` waits 30ms, `file2` resolves
immediately) and assert that `results` and `errors` both follow input
order. Existing snapshot tests continue to pass — they had been passing
only coincidentally because trivial sync paths happened to push in input
order; now the guarantee is structural.
For the atomizer sort fixes, existing snapshot tests pass (jest 54/54,
vitest 7/7, vite 22/22, cypress, playwright, eslint). The fixes are pure
ordering — no observable change when test discovery happens to already
be sorted.
<!-- No tracked issue — this came out of an audit of createNodes for
non-determinism. -->
---------
Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
(cherry picked from commit e736a6391c901ea3f733f98664ce19fe8917d34a)