Skip to content

Commit b9a7703

Browse files
committed
feat(core): add multiple Nx version detection to nx report (#33599)
## Current Behavior When multiple versions of the `nx` package are installed in a workspace (e.g., due to a third-party package incorrectly depending on nx), users have no visibility into this issue through `nx report`. ## Expected Behavior The `nx report` command now detects when other packages depend on a different version of nx than the workspace version and reports this clearly: ⚠️ Multiple Nx versions detected Your workspace uses [email protected], but other packages depend on a different version: - some-package → @scope/tool → [email protected] These packages should not have nx as a dependency. Please report this issue to the package maintainers. Run pnpm why [email protected] for more details. This helps users identify and report problematic packages that bundle their own version of nx. ## Related Issue(s) N/A - This is a proactive improvement to help users diagnose workspace issues.
1 parent 3f2d373 commit b9a7703

2 files changed

Lines changed: 107 additions & 0 deletions

File tree

packages/nx/src/command-line/report/report.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { output } from '../../utils/output';
33
import { join } from 'path';
44
import {
55
detectPackageManager,
6+
getPackageManagerCommand,
67
getPackageManagerVersion,
78
PackageManager,
89
} from '../../utils/package-manager';
@@ -23,6 +24,8 @@ import { getNxRequirePaths } from '../../utils/installation-directory';
2324
import { NxJsonConfiguration, readNxJson } from '../../config/nx-json';
2425
import { ProjectGraph } from '../../config/project-graph';
2526
import { ProjectGraphError } from '../../project-graph/error-types';
27+
import { reverse } from '../../project-graph/operators';
28+
import { nxVersion } from '../../utils/versions';
2629
import {
2730
getNxKeyInformation,
2831
NxKeyNotInstalledError,
@@ -79,6 +82,7 @@ export async function reportHandler() {
7982
registeredPlugins,
8083
packageVersionsWeCareAbout,
8184
outOfSyncPackageGroup,
85+
mismatchedNxVersions,
8286
projectGraphError,
8387
nativeTarget,
8488
cache,
@@ -221,6 +225,32 @@ export async function reportHandler() {
221225
);
222226
}
223227

228+
if (mismatchedNxVersions && mismatchedNxVersions.length > 0) {
229+
bodyLines.push(LINE_SEPARATOR);
230+
bodyLines.push(chalk.yellow('⚠️ Multiple Nx versions detected'));
231+
bodyLines.push('');
232+
bodyLines.push(
233+
`Your workspace uses nx@${nxVersion}, but other packages depend on a different version:`
234+
);
235+
for (const { version, chain } of mismatchedNxVersions) {
236+
if (chain.length === 0) {
237+
bodyLines.push(` - ${chalk.bold(`nx@${version}`)}`);
238+
} else {
239+
bodyLines.push(
240+
` - ${chain.reverse().join(' → ')}${chalk.bold(`nx@${version}`)}`
241+
);
242+
}
243+
}
244+
bodyLines.push('');
245+
bodyLines.push(
246+
'These packages should not have nx as a dependency. Please report this issue to the package maintainers.'
247+
);
248+
const whyCommand = getPackageManagerCommand(pm).why;
249+
for (const { version } of mismatchedNxVersions) {
250+
bodyLines.push(`Run \`${whyCommand} nx@${version}\` for more details.`);
251+
}
252+
}
253+
224254
if (projectGraphError) {
225255
bodyLines.push(LINE_SEPARATOR);
226256
bodyLines.push('⚠️ Unable to construct project graph.');
@@ -255,6 +285,10 @@ export interface ReportData {
255285
}[];
256286
migrateTarget: string;
257287
};
288+
mismatchedNxVersions?: Array<{
289+
version: string;
290+
chain: string[];
291+
}>;
258292
projectGraphError?: Error | null;
259293
nativeTarget: string | null;
260294
cache: {
@@ -263,6 +297,71 @@ export interface ReportData {
263297
} | null;
264298
}
265299

300+
function findDependencyChain(
301+
graph: ProjectGraph,
302+
targetNode: string
303+
): string[] {
304+
const reversedGraph = reverse(graph);
305+
306+
// BFS to find shortest path to root dependency
307+
const queue: { node: string; path: string[] }[] = [
308+
{ node: targetNode, path: [] },
309+
];
310+
const visited = new Set<string>();
311+
312+
while (queue.length > 0) {
313+
const { node, path } = queue.shift()!;
314+
315+
if (visited.has(node)) continue;
316+
visited.add(node);
317+
318+
const deps = reversedGraph.dependencies[node] || [];
319+
320+
// Check for unvisited dependents
321+
const unvisitedDeps = deps.filter((dep) => !visited.has(dep.target));
322+
323+
// No unvisited dependents - this is our shortest path
324+
if (unvisitedDeps.length === 0) {
325+
return path;
326+
}
327+
328+
for (const dep of unvisitedDeps) {
329+
const depName =
330+
graph.externalNodes?.[dep.target]?.data?.packageName ?? dep.target;
331+
queue.push({
332+
node: dep.target,
333+
path: [...path, depName],
334+
});
335+
}
336+
}
337+
338+
return [];
339+
}
340+
341+
function findMismatchedNxVersions(
342+
graph: ProjectGraph
343+
): Array<{ version: string; chain: string[] }> {
344+
if (!graph || !graph.externalNodes) {
345+
return [];
346+
}
347+
348+
const result: Array<{ version: string; chain: string[] }> = [];
349+
350+
// Find all nx package versions that don't match the workspace version
351+
for (const nodeName of Object.keys(graph.externalNodes)) {
352+
const node = graph.externalNodes[nodeName];
353+
if (node.data?.packageName === 'nx') {
354+
const version = node.data.version || 'unknown';
355+
if (version !== nxVersion) {
356+
const chain = findDependencyChain(graph, nodeName);
357+
result.push({ version, chain });
358+
}
359+
}
360+
}
361+
362+
return result;
363+
}
364+
266365
export async function getReportData(): Promise<ReportData> {
267366
const pm = detectPackageManager();
268367
const pmVersion = getPackageManagerVersion(pm);
@@ -289,6 +388,8 @@ export async function getReportData(): Promise<ReportData> {
289388

290389
const outOfSyncPackageGroup = findMisalignedPackagesForPackage(nxPackageJson);
291390

391+
const mismatchedNxVersions = findMismatchedNxVersions(graph);
392+
292393
const native = isNativeAvailable();
293394

294395
let nxKey = null;
@@ -319,6 +420,7 @@ export async function getReportData(): Promise<ReportData> {
319420
registeredPlugins,
320421
packageVersionsWeCareAbout,
321422
outOfSyncPackageGroup,
423+
mismatchedNxVersions,
322424
projectGraphError,
323425
nativeTarget: native ? native.getBinaryTarget() : null,
324426
cache,

packages/nx/src/utils/package-manager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export interface PackageManagerCommands {
4242
exec: string;
4343
dlx: string;
4444
list: string;
45+
why: string;
4546
run: (script: string, args?: string) => string;
4647
// Make this required once bun adds programatically support for reading config https://github.com/oven-sh/bun/issues/7140
4748
getRegistryUrl?: string;
@@ -148,6 +149,7 @@ export function getPackageManagerCommand(
148149
run: (script: string, args?: string) =>
149150
`yarn ${script}${args ? ` ${args}` : ''}`,
150151
list: useBerry ? 'yarn info --name-only' : 'yarn list',
152+
why: 'yarn why',
151153
getRegistryUrl: useBerry
152154
? 'yarn config get npmRegistryServer'
153155
: 'yarn config get registry',
@@ -195,6 +197,7 @@ export function getPackageManagerCommand(
195197
: ''
196198
}`,
197199
list: 'pnpm ls --depth 100',
200+
why: 'pnpm why',
198201
getRegistryUrl: 'pnpm config get registry',
199202
publish: (packageRoot, registry, registryConfigKey, tag) =>
200203
`pnpm publish "${packageRoot}" --json --"${
@@ -216,6 +219,7 @@ export function getPackageManagerCommand(
216219
run: (script: string, args?: string) =>
217220
`npm run ${script}${args ? ' -- ' + args : ''}`,
218221
list: 'npm ls',
222+
why: 'npm explain',
219223
getRegistryUrl: 'npm config get registry',
220224
publish: (packageRoot, registry, registryConfigKey, tag) =>
221225
`npm publish "${packageRoot}" --json --"${registryConfigKey}=${registry}" --tag=${tag}`,
@@ -235,6 +239,7 @@ export function getPackageManagerCommand(
235239
dlx: 'bunx',
236240
run: (script: string, args: string) => `bun run ${script} -- ${args}`,
237241
list: 'bun pm ls',
242+
why: 'bun why',
238243
// Unlike npm, bun publish does not support a custom registryConfigKey option
239244
publish: (packageRoot, registry, registryConfigKey, tag) =>
240245
`bun publish --cwd="${packageRoot}" --json --registry="${registry}" --tag=${tag}`,

0 commit comments

Comments
 (0)