chore(repo): declare lazy-loaded packages as optional peers#35392
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. |
|
View your CI Pipeline Execution ↗ for commit 5e620c1
☁️ Nx Cloud last updated this comment at |
9f6d9ea to
1228d82
Compare
ade30f4 to
05992b4
Compare
… storybook, web, vitest, and vue
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`
Declares each as an optional `peerDependency` with
`peerDependenciesMeta.*.optional: true`. `explicit-package-json-dependencies`
walks `peerDependencies` alongside `dependencies`, materializing the
edges in the graph. Optional keeps them off the user's install surface.
`@nx/vitest` is already declared as a devDependency of `@nx/web`, so no
change needed there.
Follows the pattern established in #35377 for `@nx/eslint → @nx/jest`.
Narrower subset of #35392 (storybook, web, vitest, and vue only),
avoiding the `workspace`/`js` circular-dep negations that PR needs.
Verified no cycles introduced in the project graph.
f41f9df to
b6bc5a4
Compare
… storybook, web, vitest, and vue (#35393) ## 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 -->
b6bc5a4 to
d7a0cbe
Compare
f1fca9f to
1b4a1d9
Compare
0906610 to
90eddf9
Compare
|
NOTE: Blocked by #35401, currently no way to skip the sync generator adding the refs |
7a11084 to
060a922
Compare
ad3e185 to
b4898b5
Compare
8d78691 to
c2c384e
Compare
c2c384e to
2f4bfbf
Compare
There was a problem hiding this comment.
Important
At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.
Nx Cloud has identified a possible root cause for your failed CI:
We classified this failure as environment_state rather than code_change because none of the PR's changes (optional peer declarations, implicitDependencies additions, and a nx.json cache-bust) touch the Gradle plugin or its e2e test infrastructure. The failures are generic "Command failed" errors for basic Nx operations inside isolated temporary sandboxes, with no captured output linking them to any code modified in this PR.
No code changes were suggested for this issue.
🔂 A CI rerun has been triggered by adding an empty commit to this branch.
🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.
🎓 Learn more about Self-Healing CI on nx.dev
@nx/workspace and @nx/js both fetch other plugins at runtime via
ensurePackage() or require('@nx' + '/...'). Declare those as
peerDependencies with peerDependenciesMeta.optional so publish and
install semantics are honest about what may or may not be present.
Because several of these peers transitively reverse-depend on
@nx/workspace and @nx/js, the raw graph edges would produce TS6202
cycles once @nx/js:typescript-sync walks them. Use implicitDependencies
with "!name" entries in project.json to remove those edges from the
project graph, which keeps tsconfig sync and tsc --build cycle-free
without modifying the sync generator.
- workspace: 14 in-repo peers + typescript
- js: 5 in-repo peers + prettier
Each of these plugins fetches other plugins at runtime via ensurePackage()
or require('@nx' + '/...'). Declare those dependencies as peerDependencies
with peerDependenciesMeta.optional so publish/install semantics correctly
reflect that they may or may not be present.
None of the newly-declared peers reverse-depend on their source package,
so no project-graph cycles form and no implicitDependencies negations are
needed here. @nx/js:typescript-sync populates the corresponding
tsconfig.lib.json project references; the updated tsconfigs are included.
Packages updated: angular, expo, next, nuxt, react-native, storybook,
vite, vue, web. @nx/eslint had a redundant optionalDependencies entry
for @nx/jest (already declared as an optional peer) that was removed.
The @nx/dependency-checks lint rule flags peerDependencies that aren't
statically imported. The peers declared in the previous commits are
loaded dynamically via ensurePackage() / require('@nx' + '/...'), which
the rule can't detect. Add them to each package's ignoredDependencies
so lint passes without relaxing the rule elsewhere.
Three more packages whose tests reach into other plugins' source via ensurePackage() paths were missing peer declarations. Observed by the fact that each package's unit tests were loading source from plugins that weren't declared anywhere. - @nx/node: adds @nx/angular, @nx/module-federation, @nx/nest, @nx/playwright, @nx/webpack as optional peers. @nx/nest and @nx/angular reverse-depend on @nx/node (angular via the angular -> rspack -> nest -> node chain), so implicitDependencies drops both edges to keep the task graph acyclic. - @nx/plugin: adds @nx/vitest as an optional peer. No cycle. - @nx/react: adds @nx/next and @nx/rspack as optional peers. @nx/next reverse-depends on @nx/react, so implicitDependencies drops that edge. eslint.config.mjs ignoredDependencies updated in each so @nx/dependency-checks doesn't flag the new peers. tsconfig.lib.json refs populated by nx sync.
Sweep of every package with a test target, using `nx show target <project>:test inputs` to find other packages whose source is pulled into tests but isn't declared in package.json. Each missing dep is added as peerDependencies + peerDependenciesMeta.optional, matching the convention from the previous commits. Where the addition would create a project-graph cycle (the peer transitively depends back on the source), implicitDependencies gets a `!<name>` entry to strip the edge. eslint.config.mjs ignoredDependencies updated so @nx/dependency-checks doesn't flag the new peers. Packages modified (peer additions): - @nx/nest: @nx/cypress, @nx/docker, @nx/module-federation, @nx/webpack - @nx/express: @nx/cypress, @nx/docker, @nx/eslint, @nx/module-federation, @nx/webpack - @nx/detox: @nx/cypress, @nx/docker, @nx/module-federation, @nx/nest, @nx/node - @nx/expo: +@nx/docker, +@nx/module-federation, +@nx/nest - @nx/next: +@nx/docker, +@nx/module-federation, +@nx/nest, +@nx/node - @nx/react-native: +@nx/cypress, +@nx/docker, +@nx/module-federation, +@nx/nest, +@nx/node - @nx/remix: @nx/cypress, @nx/docker, @nx/eslint, @nx/module-federation, @nx/nest, @nx/node - @nx/node: +@nx/cypress - @nx/react: +@nx/docker, +@nx/nest, +@nx/node - @nx/module-federation: @nx/cypress, @nx/eslint - @nx/rspack: @nx/cypress, @nx/docker, @nx/eslint, @nx/node (with implicitDependencies: ["!node"] to break angular -> rspack cycle) - @nx/create-nx-plugin: @nx/angular - @nx/create-nx-workspace: @nx/angular Also adds `"nx": "workspace:*"` to devDependencies where the standard convention was missing: @nx/angular-rspack, @nx/create-nx-plugin, @nx/create-nx-workspace, @nx/express, @nx/nest, @nx/node. tsconfig.lib.json project references populated by `nx sync`.
## Current Behavior
The plugin-package peer-dep commits in this branch relied on
`implicitDependencies: ["!name", ...]` negations in each `project.json`
to drop the cyclic edges from the project graph (on `@nx/workspace`,
`@nx/js`, `@nx/node`, `@nx/react`, and `@nx/rspack`). The negations
were load-bearing for four things:
1. Keeping `@nx/js:typescript-sync` from generating circular TypeScript
project references (TS6202).
2. Keeping the task graph acyclic for `build`/`build-base` (so
`^build`/`^build-base` don't loop through the cyclic peers).
3. Keeping the task graph acyclic for `nx-release-publish`.
4. Silencing `@nx/enforce-module-boundaries` circular-dep errors.
Stripping the edges from the project graph also broke
`@nx/js:release-publish` substitution of `workspace:*` peers: `nx
release version` only substitutes dependencies it sees in the project
graph, so peer deps dropped by a `!name` negation kept the raw
`workspace:*` protocol in the published `dist/packages/*/package.json`
and made `pnpm publish` fail with
`ERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL`.
## Expected Behavior
The project graph now reflects the real `package.json` peer edges and
is allowed to be cyclic. The concerns above are addressed independently:
1. Each affected `tsconfig.lib.json` uses `nx.sync.ignoredDependencies`
(new in `@nx/js@22.7.0-rc.1`) to opt the cyclic peers out of
project-reference generation — `workspace` opts out of
`angular/eslint/expo/.../web`; `js` opts out of
`eslint/jest/rollup/vite/vitest`; `node` opts out of `angular/nest`;
`react` opts out of `next`; `rspack` opts out of `node`.
2. Each affected `project.json` sets explicit `build.dependsOn:
["build-base"]` and an explicit `build-base.dependsOn` listing only
the non-cyclic deps, so `^build`/`^build-base` don't expand through
the cyclic peer edges. The plugin-inferred `build-base` merges with
this override, so `tsc --build tsconfig.lib.json` still runs.
3. Each affected `project.json` sets `nx-release-publish: {dependsOn: []}`
so the publish task graph doesn't cascade through the cyclic peers.
`nx release publish` already passes `nxIgnoreCycles: true`, but the
explicit override keeps other code paths (e.g. `nx run X:nx-release-publish`)
from cycling either. The project-graph edges remain, so `nx release
version` now sees and substitutes every `workspace:*` peer in
`dist/packages/*/package.json` before publish.
4. The root `eslint.config.mjs` registers the intentional cycles in
`@nx/enforce-module-boundaries.ignoredCircularDependencies`.
The `implicitDependencies: ["!..."]` negations on `@nx/workspace`,
`@nx/js`, `@nx/node`, `@nx/react`, and `@nx/rspack` are removed.
The "declare missing lazy-loaded peers across plugin packages" sweep added `@nx/angular`, `@nx/cypress`, `@nx/module-federation`, and `@nx/playwright` to `@nx/node`'s `peerDependencies`. None of those are actually loaded at runtime by `@nx/node`'s published source (only `@nx/nest` and `@nx/webpack` are, via `ensurePackage`); the imports are all in spec files that pull in those packages' generators to set up test fixtures. Declaring them as peers makes npm eagerly resolve them through the peer chain when a consumer installs anything that depends on `@nx/node`. For `@nx/remix` (which lists `@nx/node` as an optional peer), that pulled `@nx/angular` in, which pulled `@angular/build`, which pulled `vitest@4`, which requires `vite >= 6` — conflicting with `@remix-run/dev`'s `vite ^5.1.0 || ^6.0.0` and failing install with `ERESOLVE`. Moving them to `devDependencies` keeps the workspace-level tests working (devDeps are still installed in the monorepo) while stripping them from the published `package.json`, so consumer installs don't cascade through them. Project-graph edges are preserved (devDeps still contribute), so `nx release version` continues to substitute `workspace:*` and `nx.sync.ignoredDependencies` on the tsconfig still filters them out of the TS reference graph.
Same pattern as the previous @nx/node fix: the "declare missing lazy-loaded peers" sweep added @nx/docker, @nx/module-federation, @nx/nest, and @nx/node to @nx/next's optional peerDependencies, but none of them are actually loaded by the plugin at runtime — there are zero imports of any of these packages in packages/next/src/** outside of test fixture directories. Generated pnpm workspaces have auto-install-peers: true, and pnpm >= 8.7 auto-installs optional peers. With these peers in the published package.json, any downstream workspace installing @nx/next eagerly pulls all four plus their transitive optional peers — roughly 600 extra packages — which is what was consistently blowing the 300s install budget on e2e-next and e2e-react-native on main-linux. Removed from peerDependencies and peerDependenciesMeta, removed from the corresponding @nx/dependency-checks ignore list in packages/next/eslint.config.mjs, and let nx sync drop the now-stale project references in packages/next/tsconfig.lib.json.
…plugin packages
Generalises the @nx/next fix to every plugin package the
"declare missing lazy-loaded peers" sweep over-broadly added
optional peer deps to. Generated pnpm workspaces have
auto-install-peers: true, and pnpm >= 8.7 auto-resolves optional
peers — so each test-only @nx/* peer fans out and pulls a few
hundred extra packages on every consumer install. Stacked across
packages, that's what was running e2e workspace setups past the
300s install budget on main-linux (most visibly e2e-next /
e2e-react-native, but the same chain applied through every plugin
that listed @nx/node, @nx/nest, @nx/module-federation, etc. as
optional peers without actually loading them).
For each package, the criteria for keeping a peer:
- the package is loaded at runtime via ensurePackage in source, OR
- it's a non-@nx/* peer that was already there on master.
Packages updated (peers removed):
- @nx/nest: @nx/cypress, @nx/docker, @nx/module-federation,
@nx/webpack
- @nx/express: @nx/cypress, @nx/docker, @nx/eslint,
@nx/module-federation, @nx/webpack
(@nx/eslint moved to dependencies — used as a
type-only import in schema.d.ts)
- @nx/detox: @nx/cypress, @nx/docker, @nx/module-federation,
@nx/nest, @nx/node
- @nx/expo: @nx/docker, @nx/module-federation, @nx/nest,
@nx/rollup
- @nx/react-native: @nx/cypress, @nx/docker, @nx/module-federation,
@nx/nest, @nx/node, @nx/rollup
- @nx/remix: @nx/docker, @nx/module-federation, @nx/nest,
@nx/node
- @nx/react: @nx/docker, @nx/nest, @nx/next, @nx/node
@nx/dependency-checks ignoredDependencies entries pruned to match,
nx sync dropped the now-stale tsconfig project references, and the
ignoredDependencies entry for "next" in packages/react/tsconfig.lib.json
is removed since @nx/next is no longer in @nx/react's project graph.
The previous sweep kept @nx/* peers that were loaded at runtime via ensurePackage (@nx/js -> @nx/vitest, etc.). Under npm 7+'s auto-install behavior — which Nx stopped forcing --legacy-peer-deps against in #33014 — even "optional" peers get auto-resolved. That pulls @nx/vitest into every consumer install; @nx/vitest's wide `vite` peer range combined with @nx/remix -> @remix-run/dev -> @vanilla-extract/integration (hard dep on vite@5.4) then pins vite to ^5 at the workspace root. When the @nx/react:app generator later adds vite@^8 + @vitejs/plugin-react@^6, npm ERESOLVEs. Moving the @nx/* entries to devDependencies keeps the monorepo project graph intact (Nx reads devDependencies) but stops npm from auto- installing them for downstream users. ensurePackage already handles runtime install of these at generator time, so the peer declaration wasn't doing useful work. External peers (typescript, @remix-run/dev, metro-config, next, @nuxt/schema, etc.) are unchanged.
Previous commit overzealously moved @nx/web's pre-existing optional peers (@nx/cypress, @nx/eslint, @nx/jest, @nx/playwright, @nx/vite, @nx/webpack) to devDependencies. Those are unrelated to the vite ERESOLVE cascade that fix targeted (which traced through @nx/js -> @nx/vitest) and should match master.
…ncies Revert the package.json/eslint/tsconfig peer-dep additions that fed the project graph for ensurePackage'd plugins. Express the same edges via `implicitDependencies` in each project.json (and inline nx config for angular-rspack). Keep build-base `dependsOn` overrides for task order. @nx/js implicits include esbuild.
…ncies [Self-Healing CI Rerun]
Drop implicit edges that don't change resolved test inputs (verified via `nx show target inputs <p>:test` — byte-identical before/after across the 8 touched projects). The same source files for cypress, docker, eslint, node, module-federation, playwright still arrive via paths through `workspace`'s implicit list and `node`'s kept edges, so sandbox coverage is unchanged. Drops: - angular-rspack: nx (covered via devkit -> nx) - create-nx-plugin: nx, angular (no source / spec evidence) - create-nx-workspace: nx (covered transitively) - express: nx (covered via devkit -> nx) - module-federation: cypress, eslint - node: cypress, module-federation, playwright - rspack: cypress, docker, eslint, node Restored: - plugin: vitest (contract coupling — generator emits @nx/vitest:test executor and the spec asserts against that string) Bump bust to invalidate caches for fresh sandbox-report run.
1a8c31e to
05b2c34
Compare
| "command": "node ./scripts/copy-readme.js rspack", | ||
| "inputs": ["copyReadme"] | ||
| }, | ||
| "build-base": { |
There was a problem hiding this comment.
this stuff changed even though no implicits were added?
The hardcoded `build-base.dependsOn` and `nx-release-publish: {dependsOn: []}`
override was added when this branch had @nx/rspack declaring node/cypress/docker/eslint
as real package.json deps, which formed cycles in the task graph. After
reverting rspack/package.json to master, rspack's only graph edges are
devkit/js/module-federation/nest/web — none cycle back to rspack — so
`^build-base` from the targetDefault is sufficient and the override
forces unnecessary task ordering.
Verified via `nx run rspack:build-base --graph`: 38-task plan, acyclic,
still pulls cypress/docker/eslint/node transitively through nest -> node.
|
This pull request has already been merged/closed. If you experience issues related to these changes, please open a new issue referencing this pull request. |
Current Behavior
Many Nx plugin packages lazy-load other plugins at runtime via
ensurePackage()or therequire('@nx' + '/...')pattern. These dependencies weren't declared inpackage.jsonat all, which meant:pnpm installin the monorepo happened to find them only via hoisting from unrelateddevDependencies.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/workspaceand@nx/js— several of their newly-declared peers transitively reverse-depend on them. Raw package.json edges would cause@nx/js:typescript-syncto produce circular TypeScript project references (TS6202). Those two packages useimplicitDependencies: ["!name", …]inproject.jsonto drop the cyclic graph edges, keeping the task graph and tsc builds cycle-free without modifying the sync generator itself.Commits:
@nx/workspace, 5 on@nx/js, plusimplicitDependenciesnegations in eachproject.json.@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 noimplicitDependenciesnegations were needed.@nx/js:typescript-syncpopulated the correspondingtsconfig.lib.jsonproject references, which are committed alongside thepackage.jsonchanges.Related Issue(s)
Fixes #
Test plan
see: https://staging.nx.app/runs/m3Otv2Xl7m?sandboxViolations=true&query=%3Atest