Skip to content

Commit ad032ad

Browse files
fix(core): project graph creation processes project dependencies correctly (#32784)
## Current Behavior The project graph build process incorrectly handles dependencies when workspace projects have different versions or are referenced via specific version ranges. This causes several issues: 1. NPM lockfile parser crashes when encountering symlinked nested dependencies in workspaces (which don't have versions) 2. Package.json dependencies that reference workspace projects with specific versions (e.g., "proj4": "1.0.0" when workspace has "version": "2.0.0") incorrectly resolve to the workspace project instead of the installed npm package 3. Version ranges and file references to workspace projects are not properly validated ## Expected Behavior The dependency resolution should: - Handle symlinked workspace packages in npm lockfiles without crashing - Correctly differentiate between workspace projects and npm packages when specific versions are referenced - Properly validate version ranges against workspace package versions using semver - Support file references (e.g., "file:../proj6") for workspace dependencies - Only resolve to workspace projects when the version constraint is satisfied or when using wildcards ## Notes This has inadvertently caused issues when calculating which manifest files need to be updated in the JSVersionActions / Nx Release for Npm Packages ## Related Issues Fixes #31454 --------- Co-authored-by: FrozenPandaz <[email protected]>
1 parent 5d953a4 commit ad032ad

14 files changed

+705
-409
lines changed

e2e/release/jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable */
22
export default {
3+
testTimeout: 120000,
34
transform: {
45
'^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
56
},

e2e/release/src/conventional-commits-config.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { NxJsonConfiguration } from '@nx/devkit';
22
import {
33
cleanupProject,
4+
detectPackageManager,
5+
getPackageManagerCommand,
46
newProject,
57
readFile,
68
runCLI,
79
runCommandAsync,
810
uniq,
911
updateJson,
1012
} from '@nx/e2e-utils';
13+
import { setupWorkspaces } from './utils';
1114

1215
expect.addSnapshotSerializer({
1316
serialize(str: string) {
@@ -86,6 +89,10 @@ describe('nx release conventional commits config', () => {
8689
return nxJson;
8790
});
8891

92+
setupWorkspaces(detectPackageManager(), pkg1, pkg2, pkg3, pkg4, pkg5, pkg6);
93+
const pmc = getPackageManagerCommand();
94+
await runCommandAsync(pmc.install);
95+
8996
await runCommandAsync(`git add .`);
9097
await runCommandAsync(`git commit -m "chore: initial commit"`);
9198
await runCommandAsync(`git tag -a ${pkg1}@0.0.1 -m "${pkg1}@0.0.1"`);

e2e/release/src/independent-projects.test.ts

Lines changed: 331 additions & 318 deletions
Large diffs are not rendered by default.

e2e/release/src/preserve-matching-dependency-ranges.test.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ import { NxJsonConfiguration } from '@nx/devkit';
22
import {
33
cleanupProject,
44
newProject,
5-
readJson,
5+
runCommandAsync,
66
runCLI,
77
tmpProjPath,
88
uniq,
99
updateJson,
10+
getPackageManagerCommand,
11+
detectPackageManager,
1012
} from '@nx/e2e-utils';
1113
import { execSync } from 'node:child_process';
1214
import { join } from 'node:path';
15+
import { setupWorkspaces } from './utils';
1316

1417
expect.addSnapshotSerializer({
1518
serialize(str: string) {
@@ -33,6 +36,7 @@ expect.addSnapshotSerializer({
3336
.replaceAll(/\d*B\s+README.md/g, 'XXB README.md')
3437
.replaceAll(/Test @[\w\d]+/g, 'Test @{COMMIT_AUTHOR}')
3538
.replaceAll(/(\w+) lock file/g, 'PM lock file')
39+
.replaceAll('NX Updating PM lock file\n', '')
3640
// Normalize the version title date.
3741
.replaceAll(/\(\d{4}-\d{2}-\d{2}\)/g, '(YYYY-MM-DD)')
3842
// We trim each line to reduce the chances of snapshot flakiness
@@ -62,7 +66,7 @@ describe('nx release preserve matching dependency ranges', () => {
6266
/**
6367
* Initialize each test with a fresh workspace
6468
*/
65-
const initializeProject = () => {
69+
const initializeProject = async () => {
6670
newProject({
6771
packages: ['@nx/js'],
6872
});
@@ -74,6 +78,8 @@ describe('nx release preserve matching dependency ranges', () => {
7478
const pkg3 = uniq('my-pkg-3');
7579
runCLI(`generate @nx/workspace:npm-package ${pkg3}`);
7680

81+
setupWorkspaces(detectPackageManager(), pkg1, pkg2, pkg3);
82+
7783
// Set up dependencies with various range types
7884
updateJson(join(pkg1, 'package.json'), (packageJson) => {
7985
packageJson.version = '1.0.0';
@@ -103,15 +109,18 @@ describe('nx release preserve matching dependency ranges', () => {
103109
return packageJson;
104110
});
105111

112+
const pmc = getPackageManagerCommand();
113+
await runCommandAsync(pmc.install);
114+
106115
// workaround for NXC-143
107116
runCLI('reset');
108117

109118
return { workspacePath: tmpProjPath(), pkg1, pkg2, pkg3 };
110119
};
111120

112121
describe('when preserveMatchingDependencyRanges is set to false', () => {
113-
it('should update all dependency ranges', () => {
114-
const { workspacePath } = initializeProject();
122+
it('should update all dependency ranges', async () => {
123+
const { workspacePath } = await initializeProject();
115124

116125
updateJson<NxJsonConfiguration>('nx.json', (nxJson) => {
117126
nxJson.release = {
@@ -184,8 +193,8 @@ describe('nx release preserve matching dependency ranges', () => {
184193
});
185194

186195
describe('when preserveMatchingDependencyRanges is set to true', () => {
187-
it('should preserve dependency ranges when new version satisfies them', () => {
188-
const { workspacePath, pkg1, pkg3 } = initializeProject();
196+
it('should preserve dependency ranges when new version satisfies them', async () => {
197+
const { workspacePath, pkg1, pkg3 } = await initializeProject();
189198
updateJson(join(pkg1, 'package.json'), (packageJson) => {
190199
packageJson.dependencies[`@proj/${pkg3}`] = '^1.0.0';
191200
return packageJson;
@@ -244,8 +253,8 @@ describe('nx release preserve matching dependency ranges', () => {
244253
});
245254

246255
describe('when preserveMatchingDependencyRanges is set to specific dependency types', () => {
247-
it('should only preserve ranges for specified dependency types', () => {
248-
const { workspacePath } = initializeProject();
256+
it('should only preserve ranges for specified dependency types', async () => {
257+
const { workspacePath } = await initializeProject();
249258

250259
updateJson<NxJsonConfiguration>('nx.json', (nxJson) => {
251260
nxJson.release = {
@@ -311,8 +320,8 @@ describe('nx release preserve matching dependency ranges', () => {
311320
`);
312321
});
313322

314-
it('should handle empty array (no preservation)', () => {
315-
const { workspacePath } = initializeProject();
323+
it('should handle empty array (no preservation)', async () => {
324+
const { workspacePath } = await initializeProject();
316325

317326
updateJson<NxJsonConfiguration>('nx.json', (nxJson) => {
318327
nxJson.release = {
@@ -386,8 +395,8 @@ describe('nx release preserve matching dependency ranges', () => {
386395
});
387396

388397
describe('with patch versions', () => {
389-
it('should preserve ranges when patch version satisfies them', () => {
390-
const { workspacePath } = initializeProject();
398+
it('should preserve ranges when patch version satisfies them', async () => {
399+
const { workspacePath } = await initializeProject();
391400

392401
updateJson<NxJsonConfiguration>('nx.json', (nxJson) => {
393402
nxJson.release = {
@@ -443,8 +452,8 @@ describe('nx release preserve matching dependency ranges', () => {
443452
});
444453

445454
describe('with exact version dependencies', () => {
446-
it('should always update exact version dependencies', () => {
447-
const { workspacePath, pkg1, pkg2 } = initializeProject();
455+
it('should always update exact version dependencies', async () => {
456+
const { workspacePath, pkg1, pkg2 } = await initializeProject();
448457

449458
// Add exact version dependency
450459
updateJson(join(pkg1, 'package.json'), (packageJson) => {
@@ -512,8 +521,8 @@ describe('nx release preserve matching dependency ranges', () => {
512521
});
513522

514523
describe('with wildcard ranges', () => {
515-
it('should preserve wildcard ranges', () => {
516-
const { workspacePath, pkg1, pkg2, pkg3 } = initializeProject();
524+
it('should preserve wildcard ranges', async () => {
525+
const { workspacePath, pkg1, pkg2, pkg3 } = await initializeProject();
517526

518527
// Add wildcard dependency
519528
updateJson(join(pkg1, 'package.json'), (packageJson) => {

e2e/release/src/utils.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
runCommandAsync,
3+
createFile,
4+
updateJson,
5+
removeFile,
6+
} from '@nx/e2e-utils';
7+
8+
export function setupWorkspaces(
9+
packageManager: 'npm' | 'yarn' | 'pnpm' | 'bun',
10+
...packages: string[]
11+
) {
12+
if (packageManager === 'npm' || packageManager === 'yarn') {
13+
updateJson('package.json', (packageJson) => {
14+
packageJson.workspaces = packages;
15+
return packageJson;
16+
});
17+
} else if (packageManager === 'pnpm') {
18+
createFile(
19+
`pnpm-workspace.yaml`,
20+
`packages:
21+
${packages.map((p) => `- ${p}`).join('\n ')}
22+
`
23+
);
24+
}
25+
}
26+
27+
export async function prepareAndInstallDependencies(
28+
packageManager: 'npm' | 'yarn' | 'pnpm' | 'bun',
29+
installCommand: string
30+
) {
31+
if (packageManager === 'npm') {
32+
removeFile('yarn.lock');
33+
removeFile('pnpm-lock.yaml');
34+
removeFile('pnpm-workspace.yaml');
35+
} else if (packageManager === 'yarn') {
36+
removeFile('package-lock.json');
37+
removeFile('pnpm-lock.yaml');
38+
removeFile('pnpm-workspace.yaml');
39+
updateJson('package.json', (pkgJson) => {
40+
delete pkgJson.packageManager;
41+
return pkgJson;
42+
});
43+
await runCommandAsync(`yarn config set enableImmutableInstalls false`);
44+
} else if (packageManager === 'pnpm') {
45+
removeFile('package-lock.json');
46+
removeFile('yarn.lock');
47+
}
48+
await runCommandAsync(installCommand);
49+
}

e2e/release/tsconfig.spec.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"types": ["jest", "node"]
66
},
77
"include": [
8+
"src/utils.ts",
89
"**/*.test.ts",
910
"**/*.spec.ts",
1011
"**/*.spec.tsx",

packages/nx/src/config/workspace-json-project-json.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export interface ProjectMetadata {
157157
};
158158
js?: {
159159
packageName: string;
160+
packageVersion?: string;
160161
packageExports?: PackageJson['exports'];
161162
packageMain?: string;
162163
isInPackageManagerWorkspaces?: boolean;

packages/nx/src/plugins/js/lock-file/npm-parser.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ function getNodes(
116116

117117
const packageName = path.split('node_modules/').pop();
118118
const version = findV3Version(snapshot, packageName);
119-
createNode(packageName, version, path, nodes, keyMap, snapshot);
119+
// symlinked packages in workspaces do not have versions
120+
if (version) {
121+
createNode(packageName, version, path, nodes, keyMap, snapshot);
122+
}
120123
});
121124
} else {
122125
Object.entries(data.dependencies).forEach(([packageName, snapshot]) => {

0 commit comments

Comments
 (0)