fix(core): omit peer dependencies when installing packages to a temp dir - #36295
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 8736030
☁️ Nx Cloud last updated this comment at |
7fab5b8 to
5edaa13
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 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.
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
5edaa13 to
3c1655f
Compare
…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.
3c1655f to
8736030
Compare
…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.
…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)
…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>
…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)
…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>
…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 -->
Current Behavior
installPackageToTmp(behind devkit'sensurePackage) 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'stypescript: >3.0.0— pulls the newest major, TypeScript 7, into the temp dir. tsquery readsts.SyntaxKindat module load, which TS 7 no longer exposes as a top-level CommonJS export, so it crashes:Expected Behavior
Peer dependencies are the host's responsibility, not something a throwaway fetch should decide.
ensurePackagealready loads the package from the temp dir with the workspace'snode_modulesonNODE_PATH, so its peers resolve from the workspace — the correct provider. This omits peers from the temp install so nothing incompatible gets pulled:--omit=peer--config.auto-install-peers=falseVerified locally: with
--omit=peerthe temp dir no longer contains TypeScript 7, and loading the package resolvestypescript@6.0.3from the workspace viaNODE_PATH. Unit tests cover the emitted install command for every package manager.Related Issue(s)
Hardening for the
ensurePackagepath, surfaced while investigating the TypeScript 7 / tsquery crash. Complements bounding tsquery'stypescriptpeer range at the source.View session information ↗