Skip to content

fix(core): omit peer dependencies when installing packages to a temp dir - #36295

Merged
FrozenPandaz merged 2 commits into
masterfrom
fix/ensure-package-skip-peer-deps
Jul 10, 2026
Merged

fix(core): omit peer dependencies when installing packages to a temp dir#36295
FrozenPandaz merged 2 commits into
masterfrom
fix/ensure-package-skip-peer-deps

Conversation

@FrozenPandaz

Copy link
Copy Markdown
Contributor

Current Behavior

installPackageToTmp (behind devkit's ensurePackage) fetches a package into an empty temp directory. npm and bun auto-install that package's peer dependencies there. So a loose peer range — e.g. @phenomnomnominal/tsquery's typescript: >3.0.0 — pulls the newest major, TypeScript 7, into the temp dir. tsquery reads ts.SyntaxKind at module load, which TS 7 no longer exposes as a top-level CommonJS export, so it crashes:

 NX   Cannot convert undefined or null to object
    at Object.keys (<anonymous>)
    at .../@phenomnomnominal/tsquery/dist/src/syntax-kind.js:8:27

Expected Behavior

Peer dependencies are the host's responsibility, not something a throwaway fetch should decide. ensurePackage already loads the package from the temp dir with the workspace's node_modules on NODE_PATH, so its peers resolve from the workspace — the correct provider. This omits peers from the temp install so nothing incompatible gets pulled:

  • npm / bun: --omit=peer
  • pnpm: --config.auto-install-peers=false
  • Yarn (classic & Berry): never auto-installs peers, so no flag needed

Verified locally: with --omit=peer the temp dir no longer contains TypeScript 7, and loading the package resolves typescript@6.0.3 from the workspace via NODE_PATH. Unit tests cover the emitted install command for every package manager.

Related Issue(s)

Hardening for the ensurePackage path, surfaced while investigating the TypeScript 7 / tsquery crash. Complements bounding tsquery's typescript peer range at the source.


View session information ↗

@netlify

netlify Bot commented Jul 10, 2026

Copy link
Copy Markdown

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit 8736030
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/6a510ecae1253b000795da6b
😎 Deploy Preview https://deploy-preview-36295--nx-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Jul 10, 2026

Copy link
Copy Markdown

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit 8736030
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/6a510ecae9ebb80008e48da8
😎 Deploy Preview https://deploy-preview-36295--nx-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@nx-cloud

nx-cloud Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 8736030

Command Status Duration Result
nx affected --targets=lint,test,build,e2e,e2e-c... ✅ Succeeded 6m 38s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 4s View ↗
nx-cloud record -- pnpm nx-cloud conformance:check ✅ Succeeded 1m 4s View ↗
nx build workspace-plugin ✅ Succeeded <1s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded 19s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 7s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-10 18:17:35 UTC

@FrozenPandaz
FrozenPandaz force-pushed the fix/ensure-package-skip-peer-deps branch from 7fab5b8 to 5edaa13 Compare July 10, 2026 14:37

@nx-cloud nx-cloud Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 is proposing a fix for your failed CI:

We add installMissingPeersToTmpDir to ensurePackage to fix the regression introduced by omitting all peer deps from the temp-dir install. Peers already present in the workspace continue to resolve via NODE_PATH (preserving the original fix that prevents TypeScript 7 from shadowing the workspace version), while peers absent from the workspace — such as webpack during a fresh create-nx-workspace --preset=express — are now installed directly into the temp dir so require('webpack') inside @nx/webpack succeeds at module load time.

Warning

We could not verify this fix.

Suggested Fix changes
diff --git a/packages/devkit/src/utils/package-json.ts b/packages/devkit/src/utils/package-json.ts
index a17ec79d..787a07dd 100644
--- a/packages/devkit/src/utils/package-json.ts
+++ b/packages/devkit/src/utils/package-json.ts
@@ -1,8 +1,10 @@
+import { execSync } from 'child_process';
 import { existsSync } from 'fs';
 import { Module } from 'module';
 import {
   detectPackageManager,
   type GeneratorCallback,
+  getPackageManagerCommand,
   output,
   readJson,
   readJsonFile,
@@ -915,6 +917,14 @@ export function ensurePackage<T extends any = any>(
     detectPackageManager(workspaceRoot)
   );
 
+  // Peers that ARE in the workspace will be served via NODE_PATH below, so the
+  // temp-dir install skips them (preventing an incompatible version from being
+  // pulled in). Peers that are NOT in the workspace (e.g. `webpack` in a fresh
+  // express workspace before its first install) must be installed into the temp
+  // dir now, otherwise `require('webpack')` inside @nx/webpack will fail at
+  // module load time.
+  installMissingPeersToTmpDir(pkg, tempDir, workspaceRoot);
+
   addToNodePath(join(workspaceRoot, 'node_modules'));
   addToNodePath(join(tempDir, 'node_modules'));
 
@@ -942,6 +952,74 @@ export function ensurePackage<T extends any = any>(
   }
 }
 
+/**
+ * After a temp-dir install that omits peer dependencies, check whether any of
+ * the package's peers are absent from the workspace's node_modules.  Those
+ * missing peers must be installed into the temp dir directly so that
+ * `require('<peer>')` inside the package succeeds at module load time.
+ *
+ * Peers that *are* present in the workspace are intentionally left out of the
+ * temp dir — they resolve via NODE_PATH so the workspace version is used,
+ * which prevents an incompatible version (e.g. TypeScript 7 when the workspace
+ * uses TypeScript 6) from being pulled in.
+ */
+function installMissingPeersToTmpDir(
+  pkg: string,
+  tempDir: string,
+  workspaceRoot: string
+): void {
+  let peerDependencies: Record<string, string> = {};
+  try {
+    ({ peerDependencies = {} } = require(
+      require.resolve(`${pkg}/package.json`, { paths: [tempDir] })
+    ));
+  } catch {
+    return; // Can't read package.json — skip silently.
+  }
+
+  const peers = Object.entries(peerDependencies);
+  if (peers.length === 0) return;
+
+  // Identify peers that are not resolvable from the workspace.
+  const missingPeers = peers.filter(([peerPkg]) => {
+    try {
+      require.resolve(peerPkg, { paths: [workspaceRoot] });
+      return false; // Already in workspace — NODE_PATH will serve it.
+    } catch {
+      return true; // Absent from workspace — must be installed in temp dir.
+    }
+  });
+
+  if (missingPeers.length === 0) return;
+
+  const packageManager = detectPackageManager(workspaceRoot);
+  const pmCommands = getPackageManagerCommand(packageManager);
+  const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true';
+  const execOptions = {
+    cwd: tempDir,
+    stdio: isVerbose ? ('inherit' as const) : ('ignore' as const),
+    windowsHide: true,
+    env: { ...process.env, YARN_ENABLE_SCRIPTS: 'false' },
+  };
+
+  for (const [peerPkg, peerRange] of missingPeers) {
+    try {
+      execSync(
+        [
+          pmCommands.addDev,
+          `${peerPkg}@"${peerRange}"`,
+          pmCommands.ignoreScriptsFlag,
+        ]
+          .filter(Boolean)
+          .join(' '),
+        execOptions
+      );
+    } catch {
+      // Best-effort: if a peer can't be installed, continue.
+    }
+  }
+}
+
 function addToNodePath(dir: string) {
   // NODE_PATH is a delimited list of paths.
   // The delimiter is different for windows.

Apply fix via Nx Cloud  Reject fix via Nx Cloud


Or Apply changes locally with:

npx nx-cloud apply-locally anyh-38EH

Apply fix locally with your editor ↗   View interactive diff ↗



🎓 Learn more about Self-Healing CI on nx.dev

@FrozenPandaz
FrozenPandaz force-pushed the fix/ensure-package-skip-peer-deps branch from 5edaa13 to 3c1655f Compare July 10, 2026 15:07
…re the bundler

@nx/webpack's entry eagerly imported webpack (and its bundler-only peers:
copy-webpack-plugin, license-webpack-plugin, terser-webpack-plugin,
webpack-node-externals, webpack-subresource-integrity, mini-css-extract-plugin).
So loading the package to run a generator transitively required webpack, which
crashes when it isn't installed (e.g. during workspace creation).

Defer those to build/serve time: type-only imports become 'import type', and
value uses are lazy-required inside the functions that use them (notably
applyBaseConfig/applyWebConfig behind NxAppWebpackPlugin.apply() and withNx/
withWeb). Generators can now load @nx/webpack without the bundler present.
installPackageToTmp fetches a package into an empty temp dir, where the package
manager auto-installs its peer dependencies. A loose peer range can pull an
incompatible major into the temp dir and break loading the package.

Omit peers from the temp install; ensurePackage puts the workspace's
node_modules on NODE_PATH, so a loaded package resolves its peers from the
workspace. npm and bun via '--omit=peer', pnpm via '--config.auto-install-peers=false';
Yarn never auto-installs peers.
@FrozenPandaz
FrozenPandaz force-pushed the fix/ensure-package-skip-peer-deps branch from 3c1655f to 8736030 Compare July 10, 2026 15:24
@FrozenPandaz
FrozenPandaz marked this pull request as ready for review July 10, 2026 16:51
@FrozenPandaz
FrozenPandaz requested a review from a team as a code owner July 10, 2026 16:51
@FrozenPandaz
FrozenPandaz requested a review from lourw July 10, 2026 16:51
@FrozenPandaz
FrozenPandaz merged commit edc9212 into master Jul 10, 2026
30 of 34 checks passed
@FrozenPandaz
FrozenPandaz deleted the fix/ensure-package-skip-peer-deps branch July 10, 2026 18:24
FrozenPandaz added a commit that referenced this pull request Jul 23, 2026
…dir (#36295)

Backport of edc9212 to 22.7.x.

Only the packages/nx portion is backported. The webpack lazy-load commit
squashed into the same PR depends on `@nx/js/internal` and
`webpack/src/utils/deprecation.ts`, neither of which exists on 22.7.x.

The pnpm install command keeps 22.7.x's `pnpm add -D` override, so the
upstream test asserting `-Dw` is preserved verbatim does not apply here.
FrozenPandaz added a commit that referenced this pull request Jul 24, 2026
…dir (#36295)

`installPackageToTmp` (behind devkit's `ensurePackage`) fetches a
package into an **empty** temp directory. npm and bun **auto-install
that package's peer dependencies** there. So a loose peer range — e.g.
`@phenomnomnominal/tsquery`'s `typescript: >3.0.0` — pulls the
**newest** major, TypeScript 7, into the temp dir. tsquery reads
`ts.SyntaxKind` at module load, which TS 7 no longer exposes as a
top-level CommonJS export, so it crashes:

```
 NX   Cannot convert undefined or null to object
    at Object.keys (<anonymous>)
    at .../@phenomnomnominal/tsquery/dist/src/syntax-kind.js:8:27
```

Peer dependencies are the **host's** responsibility, not something a
throwaway fetch should decide. `ensurePackage` already loads the package
from the temp dir with the workspace's `node_modules` on `NODE_PATH`, so
its peers resolve from the workspace — the correct provider. This omits
peers from the temp install so nothing incompatible gets pulled:

- **npm** / **bun**: `--omit=peer`
- **pnpm**: `--config.auto-install-peers=false`
- **Yarn** (classic & Berry): never auto-installs peers, so no flag
needed

Verified locally: with `--omit=peer` the temp dir no longer contains
TypeScript 7, and loading the package resolves `typescript@6.0.3` from
the workspace via `NODE_PATH`. Unit tests cover the emitted install
command for every package manager.

Hardening for the `ensurePackage` path, surfaced while investigating the
TypeScript 7 / tsquery crash. Complements bounding tsquery's
`typescript` peer range at the source.

<!-- polygraph-session-start -->
---
[View session information
↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/Migrate-nrwl-repos-to-nx-23.1.0-rc.0-b8c94700)
<!-- polygraph-session-end -->

(cherry picked from commit edc9212)
FrozenPandaz added a commit that referenced this pull request Jul 30, 2026
…nstalls (#36518)

## Current Behavior

`ensurePackage` installs on-demand plugins into a temp dir. Since #36295
that install passes `--omit=peer` for npm, so peers resolve from the
workspace instead of being duplicated into the temp dir.

npm flags a package as a peer if **anything** in the tree peer-depends
on it — a real `dependencies` edge does not clear the flag.
`--omit=peer` therefore also prunes packages that are genuine
dependencies of the package being installed.

`@nx/detox` hard-depends on `@nx/jest` and `@nx/eslint`; `@nx/web`
declares both as optional peers. So installing `@nx/detox` on npm
silently drops both:

```console
$ npm i -D @nx/detox@22.7.7 --omit=peer --ignore-scripts
$ ls node_modules/@nx
detox devkit js module-federation nx-darwin-arm64 react rollup vitest web workspace
# @nx/jest and @nx/eslint are missing
```

They are still written to `package-lock.json` with `"peer": true`, are
absent from `node_modules/.package-lock.json`, and the install exits 0
with no warning.

Generating a React Native app with Detox then fails. Observed on 22.7.x,
where `ensure-dependencies.ts` imports `@nx/jest/src/utils/versions`:

```
NX  Cannot find module '@nx/jest/src/utils/versions'

Require stack:
- <tmp>/node_modules/@nx/detox/src/generators/application/lib/ensure-dependencies.js
```

On master the same file imports `@nx/jest/internal` instead — a
different subpath of the same pruned package, so it fails the same way.

This is not Detox-specific: 14 first-party plugins hard-depend on
`@nx/jest` or `@nx/eslint`, and several deep-import `@nx/eslint/src/*`
at runtime. Any of them fetched on demand in an npm workspace can lose a
dependency it needs.

## Expected Behavior

npm uses `--legacy-peer-deps` instead. That ignores `peerDependencies` —
the intent of #36295 — without pruning real dependencies:

```console
$ npm i -D @nx/detox@22.7.7 --legacy-peer-deps --ignore-scripts
$ ls node_modules/@nx
detox devkit eslint jest js module-federation nx-darwin-arm64 react rollup vite vitest web workspace
```

bun does not over-prune (verified against the same tree), so bun keeps
`--omit=peer`. pnpm and yarn are unchanged.

## Related Issue(s)

N/A — regression from #36295, which has not been released yet.

## Notes for reviewers

**CI will not exercise this change.** Two independent reasons:

1. The macOS Detox e2e only runs when the diff touches `packages/detox`,
`packages/react-native`, `packages/expo`, or their e2e projects
(`scripts/check-react-native-changes.js`). #36295 touched only
`packages/nx`, so the gate skipped it — and it skips this PR too.
2. Even when that job does run, master's e2e uses a shared base
workspace that preinstalls the plugins
(`<e2e>/nx/proj-backup/npm/node_modules/@nx/` contains detox, jest,
eslint, react-native). So `ensurePackage` short-circuits on
`require('@nx/detox')` and the temp-install path never executes at all.

Verified locally instead. The end-to-end run was done on **22.7.x**,
which has no shared base workspace and so genuinely fetches `@nx/detox`
on demand — same branch, same e2e, only the flag differing:

| temp dir | `node_modules/@nx/` contents | result |
| --- | --- | --- |
| `--omit=peer` | detox devkit js module-federation nx-darwin-arm64
react rollup vitest web workspace | 4 tests failed |
| `--legacy-peer-deps` | detox devkit **eslint jest** js
module-federation nx-darwin-arm64 react rollup vite vitest web workspace
| 4 tests passed |

`ensurePackage` never calls `cleanup()`, so these temp dirs survive and
are the reliable signal — `Fetching ...` log lines are absent from
passing runs either way because `runCLI` swallows child stdout on
success.

Also run:

- `nx run e2e-detox:e2e-macos-local` on 22.7.x with this change — 2
suites / 4 tests pass
- `nx test nx --testPathPatterns=src/utils/package-json.spec.ts` —
passes
- `tsc -p packages/nx/tsconfig.lib.json --noEmit` — clean
- `nx prepush` — passes
- the two `npm i` runs above, against published 22.7.7

**Needs backporting to 22.7.x**, which carries the same flag via
`74311e713d` and is the active patch line. Neither line has released
`--omit=peer` yet (`nx@22.7.7` still ships the old flag-less install
command), so there is no user impact today.

<!-- polygraph-session-start -->
---
<p><picture><source media="(prefers-color-scheme: dark)"
srcset="https://static.ops.cloud.nx.app/polygraph/session-logo-v4-dark.svg"><img
src="https://static.ops.cloud.nx.app/polygraph/session-logo-v4-light.svg"
width="16" height="22" align="middle" alt="Polygraph"></picture> <a
href="https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/Fix-npm-temp-install-pruning-real-dependencies-via---omitpeer-f7aff304">View
session ↗</a></p>
<!-- polygraph-session-end -->

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
FrozenPandaz added a commit that referenced this pull request Jul 30, 2026
…nstalls (#36518)

## Current Behavior

`ensurePackage` installs on-demand plugins into a temp dir. Since #36295
that install passes `--omit=peer` for npm, so peers resolve from the
workspace instead of being duplicated into the temp dir.

npm flags a package as a peer if **anything** in the tree peer-depends
on it — a real `dependencies` edge does not clear the flag.
`--omit=peer` therefore also prunes packages that are genuine
dependencies of the package being installed.

`@nx/detox` hard-depends on `@nx/jest` and `@nx/eslint`; `@nx/web`
declares both as optional peers. So installing `@nx/detox` on npm
silently drops both:

```console
$ npm i -D @nx/detox@22.7.7 --omit=peer --ignore-scripts
$ ls node_modules/@nx
detox devkit js module-federation nx-darwin-arm64 react rollup vitest web workspace
# @nx/jest and @nx/eslint are missing
```

They are still written to `package-lock.json` with `"peer": true`, are
absent from `node_modules/.package-lock.json`, and the install exits 0
with no warning.

Generating a React Native app with Detox then fails. Observed on 22.7.x,
where `ensure-dependencies.ts` imports `@nx/jest/src/utils/versions`:

```
NX  Cannot find module '@nx/jest/src/utils/versions'

Require stack:
- <tmp>/node_modules/@nx/detox/src/generators/application/lib/ensure-dependencies.js
```

On master the same file imports `@nx/jest/internal` instead — a
different subpath of the same pruned package, so it fails the same way.

This is not Detox-specific: 14 first-party plugins hard-depend on
`@nx/jest` or `@nx/eslint`, and several deep-import `@nx/eslint/src/*`
at runtime. Any of them fetched on demand in an npm workspace can lose a
dependency it needs.

## Expected Behavior

npm uses `--legacy-peer-deps` instead. That ignores `peerDependencies` —
the intent of #36295 — without pruning real dependencies:

```console
$ npm i -D @nx/detox@22.7.7 --legacy-peer-deps --ignore-scripts
$ ls node_modules/@nx
detox devkit eslint jest js module-federation nx-darwin-arm64 react rollup vite vitest web workspace
```

bun does not over-prune (verified against the same tree), so bun keeps
`--omit=peer`. pnpm and yarn are unchanged.

## Related Issue(s)

N/A — regression from #36295, which has not been released yet.

## Notes for reviewers

**CI will not exercise this change.** Two independent reasons:

1. The macOS Detox e2e only runs when the diff touches `packages/detox`,
`packages/react-native`, `packages/expo`, or their e2e projects
(`scripts/check-react-native-changes.js`). #36295 touched only
`packages/nx`, so the gate skipped it — and it skips this PR too.
2. Even when that job does run, master's e2e uses a shared base
workspace that preinstalls the plugins
(`<e2e>/nx/proj-backup/npm/node_modules/@nx/` contains detox, jest,
eslint, react-native). So `ensurePackage` short-circuits on
`require('@nx/detox')` and the temp-install path never executes at all.

Verified locally instead. The end-to-end run was done on **22.7.x**,
which has no shared base workspace and so genuinely fetches `@nx/detox`
on demand — same branch, same e2e, only the flag differing:

| temp dir | `node_modules/@nx/` contents | result |
| --- | --- | --- |
| `--omit=peer` | detox devkit js module-federation nx-darwin-arm64
react rollup vitest web workspace | 4 tests failed |
| `--legacy-peer-deps` | detox devkit **eslint jest** js
module-federation nx-darwin-arm64 react rollup vite vitest web workspace
| 4 tests passed |

`ensurePackage` never calls `cleanup()`, so these temp dirs survive and
are the reliable signal — `Fetching ...` log lines are absent from
passing runs either way because `runCLI` swallows child stdout on
success.

Also run:

- `nx run e2e-detox:e2e-macos-local` on 22.7.x with this change — 2
suites / 4 tests pass
- `nx test nx --testPathPatterns=src/utils/package-json.spec.ts` —
passes
- `tsc -p packages/nx/tsconfig.lib.json --noEmit` — clean
- `nx prepush` — passes
- the two `npm i` runs above, against published 22.7.7

**Needs backporting to 22.7.x**, which carries the same flag via
`74311e713d` and is the active patch line. Neither line has released
`--omit=peer` yet (`nx@22.7.7` still ships the old flag-less install
command), so there is no user impact today.

<!-- polygraph-session-start -->
---
<p><picture><source media="(prefers-color-scheme: dark)"
srcset="https://static.ops.cloud.nx.app/polygraph/session-logo-v4-dark.svg"><img
src="https://static.ops.cloud.nx.app/polygraph/session-logo-v4-light.svg"
width="16" height="22" align="middle" alt="Polygraph"></picture> <a
href="https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/Fix-npm-temp-install-pruning-real-dependencies-via---omitpeer-f7aff304">View
session ↗</a></p>
<!-- polygraph-session-end -->

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
(cherry picked from commit 1a1fbe9)
polygraph-snapshot-app Bot pushed a commit that referenced this pull request Aug 11, 2026
…nstalls (#36518)

## Current Behavior

`ensurePackage` installs on-demand plugins into a temp dir. Since #36295
that install passes `--omit=peer` for npm, so peers resolve from the
workspace instead of being duplicated into the temp dir.

npm flags a package as a peer if **anything** in the tree peer-depends
on it — a real `dependencies` edge does not clear the flag.
`--omit=peer` therefore also prunes packages that are genuine
dependencies of the package being installed.

`@nx/detox` hard-depends on `@nx/jest` and `@nx/eslint`; `@nx/web`
declares both as optional peers. So installing `@nx/detox` on npm
silently drops both:

```console
$ npm i -D @nx/detox@22.7.7 --omit=peer --ignore-scripts
$ ls node_modules/@nx
detox devkit js module-federation nx-darwin-arm64 react rollup vitest web workspace
# @nx/jest and @nx/eslint are missing
```

They are still written to `package-lock.json` with `"peer": true`, are
absent from `node_modules/.package-lock.json`, and the install exits 0
with no warning.

Generating a React Native app with Detox then fails. Observed on 22.7.x,
where `ensure-dependencies.ts` imports `@nx/jest/src/utils/versions`:

```
NX  Cannot find module '@nx/jest/src/utils/versions'

Require stack:
- <tmp>/node_modules/@nx/detox/src/generators/application/lib/ensure-dependencies.js
```

On master the same file imports `@nx/jest/internal` instead — a
different subpath of the same pruned package, so it fails the same way.

This is not Detox-specific: 14 first-party plugins hard-depend on
`@nx/jest` or `@nx/eslint`, and several deep-import `@nx/eslint/src/*`
at runtime. Any of them fetched on demand in an npm workspace can lose a
dependency it needs.

## Expected Behavior

npm uses `--legacy-peer-deps` instead. That ignores `peerDependencies` —
the intent of #36295 — without pruning real dependencies:

```console
$ npm i -D @nx/detox@22.7.7 --legacy-peer-deps --ignore-scripts
$ ls node_modules/@nx
detox devkit eslint jest js module-federation nx-darwin-arm64 react rollup vite vitest web workspace
```

bun does not over-prune (verified against the same tree), so bun keeps
`--omit=peer`. pnpm and yarn are unchanged.

## Related Issue(s)

N/A — regression from #36295, which has not been released yet.

## Notes for reviewers

**CI will not exercise this change.** Two independent reasons:

1. The macOS Detox e2e only runs when the diff touches `packages/detox`,
`packages/react-native`, `packages/expo`, or their e2e projects
(`scripts/check-react-native-changes.js`). #36295 touched only
`packages/nx`, so the gate skipped it — and it skips this PR too.
2. Even when that job does run, master's e2e uses a shared base
workspace that preinstalls the plugins
(`<e2e>/nx/proj-backup/npm/node_modules/@nx/` contains detox, jest,
eslint, react-native). So `ensurePackage` short-circuits on
`require('@nx/detox')` and the temp-install path never executes at all.

Verified locally instead. The end-to-end run was done on **22.7.x**,
which has no shared base workspace and so genuinely fetches `@nx/detox`
on demand — same branch, same e2e, only the flag differing:

| temp dir | `node_modules/@nx/` contents | result |
| --- | --- | --- |
| `--omit=peer` | detox devkit js module-federation nx-darwin-arm64
react rollup vitest web workspace | 4 tests failed |
| `--legacy-peer-deps` | detox devkit **eslint jest** js
module-federation nx-darwin-arm64 react rollup vite vitest web workspace
| 4 tests passed |

`ensurePackage` never calls `cleanup()`, so these temp dirs survive and
are the reliable signal — `Fetching ...` log lines are absent from
passing runs either way because `runCLI` swallows child stdout on
success.

Also run:

- `nx run e2e-detox:e2e-macos-local` on 22.7.x with this change — 2
suites / 4 tests pass
- `nx test nx --testPathPatterns=src/utils/package-json.spec.ts` —
passes
- `tsc -p packages/nx/tsconfig.lib.json --noEmit` — clean
- `nx prepush` — passes
- the two `npm i` runs above, against published 22.7.7

**Needs backporting to 22.7.x**, which carries the same flag via
`74311e713d` and is the active patch line. Neither line has released
`--omit=peer` yet (`nx@22.7.7` still ships the old flag-less install
command), so there is no user impact today.

<!-- polygraph-session-start -->
---
<p><picture><source media="(prefers-color-scheme: dark)"
srcset="https://static.ops.cloud.nx.app/polygraph/session-logo-v4-dark.svg"><img
src="https://static.ops.cloud.nx.app/polygraph/session-logo-v4-light.svg"
width="16" height="22" align="middle" alt="Polygraph"></picture> <a
href="https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/Fix-npm-temp-install-pruning-real-dependencies-via---omitpeer-f7aff304">View
session ↗</a></p>
<!-- polygraph-session-end -->

---------

Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
FrozenPandaz added a commit that referenced this pull request Aug 11, 2026
…y off TypeScript 7 (#36478)

## Current Behavior

`@phenomnomnominal/tsquery@6.2.0` declares an unbounded peer:

```json
"peerDependencies": { "typescript": ">3.0.0" }
```

npm auto-installs missing peers and resolves that range to `latest`,
which has been **TypeScript 7 since 7.0.2 was published on 2026-07-08**.
TypeScript 7 dropped the top-level CommonJS `SyntaxKind` export — its
CJS entry now exports only `{ version, versionMajorMinor }` — and
tsquery reads `ts.SyntaxKind` at module load time:

```
 NX   Cannot convert undefined or null to object

TypeError: Cannot convert undefined or null to object
    at Object.keys (<anonymous>)
    at .../@phenomnomnominal/tsquery/dist/src/syntax-kind.js:8:27
    at .../@phenomnomnominal/tsquery/dist/src/traverse.js:5:23
```

npm hoists that copy into the single root `node_modules/typescript` slot
and demotes the compatible version into a nested `node_modules`, so
tsquery resolves TypeScript 7 and any generator that loads it fails.

This is what turns `e2e-vite` and `e2e-web` red on the npm legs of the
nightly matrix — e.g. `nx generate @nx/react:lib
--unitTestRunner=vitest`, which reaches tsquery through `@nx/vitest`'s
configuration generator and `@nx/vite`'s `vite-config-edit-utils`.

The timeline matches exactly: the 2026-07-08 nightly (06:00 UTC) was
green on `Linux/npm`; TypeScript 7.0.2 hit `latest` at 15:55 UTC that
day; every nightly since has been red on npm.

## Expected Behavior

After installing the requested `@nx/*` packages, the e2e harness
installs `typescript` directly. A direct dependency wins the hoisted
root slot, so tsquery resolves the pinned version and loads normally.

Installing it **after** the plugin install means it repairs whatever
tree npm produced rather than relying on a particular ordering. Verified
against a deliberately broken tree:

```
BEFORE: node_modules/typescript -> 7.0.2
        npm add -D typescript@~6.0.3
AFTER : node_modules/typescript -> 6.0.3 (only copy)
        tsquery resolves 6.0.3 -> loads OK
```

### Why npm only

Same scenario, per package manager:

| package manager | behavior |
| --- | --- |
| **npm** | installs a *second* typescript at `latest` (7.0.2) and
hoists it to the single root slot, demoting the compatible 6.0.3 |
| **pnpm** | reuses the version already in the graph; the peer is
encoded in the path (`@phenomnomnominal+tsquery@6.2.0_typescript@6.0.3`)
— TypeScript 7 is never downloaded |
| **yarn** | does not auto-install peers at all (warns only) |

That matches the nightly matrix, where the npm legs are red while yarn
stays green. The change is gated to npm accordingly.

### Notes

- `typescriptVersion` is imported from `@nx/js/src/utils/versions`,
which is already an exports-map subpath, so no published package
changes.
- TypeScript 7.1 does **not** restore the CJS export
(`7.1.0-dev.20260727.1` still exports only
`version`/`versionMajorMinor`), so this is not something a newer
TypeScript fixes. The `TODO` points at the real resolutions: tsquery
bounding its peer upstream, or Nx retiring its tsquery call sites (as
#36304 began).
- Complements #36295, which applied `--omit=peer` to the `ensurePackage`
temp-dir install path; this covers the workspace-root install that path
does not touch.

## Related Issue(s)

N/A — surfaced from the nightly golden-test matrix rather than a filed
issue.

<!-- polygraph-session-start -->
---
[View session information
↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/Fix-TypeScript-7-tsquery-crash-in-npm-e2e-workspaces-bbfe7561)
<!-- polygraph-session-end -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants