Skip to content

Commit a5ccd13

Browse files
laneysmithjaysoo
andauthored
feat(core): update getTouchedProjectsFromLockFile to detect which projects were changed from pnpm lock file diff (#31091)
…jects were changed from pnpm lock file diff Closes #29986 ## Current Behavior Nx projects that use pnpm catalogs cannot take advantage of the `projectsAffectedByDependencyUpdates` `“auto”` setting because updating catalog versions does not touch project files. ## Expected Behavior When `projectsAffectedByDependencyUpdates` is set to `“auto”`, updating a catalog dependency version should result in all projects that use it getting marked as affected. A catalog version update and the affected projects can be detected from a changed pnpm lock file. This PR updates the `getTouchedProjectsFromLockFile` logic to check the lock file for pnpm monorepos. Example pnpm lock file diff after catalog dependency update: ```diff # pnpm-lock.yaml # ... catalogs: default: '@aws-sdk/client-s3': - specifier: ^3.535.0 - version: 3.535.0 + specifier: ^3.797.0 + version: 3.797.0 importers: apps/app1: dependencies: '@aws-sdk/client-s3': specifier: 'catalog:' - version: 3.535.0 + version: 3.797.0 # ... ``` ## Related Issue(s) #29986 Fixes #29986 --------- Co-authored-by: Jack Hsu <[email protected]>
1 parent a45ec7e commit a5ccd13

File tree

3 files changed

+186
-14
lines changed

3 files changed

+186
-14
lines changed

packages/nx/src/plugins/js/project-graph/affected/lock-file-changes.spec.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { ProjectGraph } from '../../../../config/project-graph';
22
import { WholeFileChange } from '../../../../project-graph/file-utils';
3-
import { getTouchedProjectsFromLockFile } from './lock-file-changes';
3+
import {
4+
getTouchedProjectsFromLockFile,
5+
PNPM_LOCK_FILES,
6+
} from './lock-file-changes';
7+
import { TempFs } from '../../../../internal-testing-utils/temp-fs';
8+
import { JsonDiffType } from '../../../../utils/json-diff';
49

510
describe('getTouchedProjectsFromLockFile', () => {
611
let graph: ProjectGraph;
712
let allNodes = [];
13+
let tempFs: TempFs;
814

915
beforeEach(() => {
1016
graph = {
@@ -74,4 +80,96 @@ describe('getTouchedProjectsFromLockFile', () => {
7480
});
7581
});
7682
});
83+
84+
PNPM_LOCK_FILES.forEach((lockFile) => {
85+
describe(`"${lockFile} with projectsAffectedByDependencyUpdates set to auto"`, () => {
86+
beforeAll(async () => {
87+
tempFs = new TempFs('lock-file-changes-test');
88+
await tempFs.createFiles({
89+
'./nx.json': JSON.stringify({
90+
pluginsConfig: {
91+
'@nx/js': {
92+
projectsAffectedByDependencyUpdates: 'auto',
93+
},
94+
},
95+
}),
96+
});
97+
});
98+
99+
afterAll(() => {
100+
tempFs.cleanup();
101+
});
102+
103+
it(`should not return changes when "${lockFile}" is not touched`, () => {
104+
const result = getTouchedProjectsFromLockFile(
105+
[
106+
{
107+
file: 'source.ts',
108+
hash: 'some-hash',
109+
getChanges: () => [new WholeFileChange()],
110+
},
111+
],
112+
graph.nodes
113+
);
114+
expect(result).toEqual([]);
115+
});
116+
117+
it(`should not return changes when whole lock file "${lockFile}" is changed`, () => {
118+
const result = getTouchedProjectsFromLockFile(
119+
[
120+
{
121+
file: lockFile,
122+
hash: 'some-hash',
123+
getChanges: () => [new WholeFileChange()],
124+
},
125+
],
126+
graph.nodes
127+
);
128+
expect(result).toEqual([]);
129+
});
130+
131+
it(`should return only changed projects when "${lockFile}" is touched`, () => {
132+
const result = getTouchedProjectsFromLockFile(
133+
[
134+
{
135+
file: lockFile,
136+
hash: 'some-hash',
137+
getChanges: () => [
138+
{
139+
type: JsonDiffType.Modified,
140+
path: [
141+
'importers',
142+
'libs/proj1',
143+
'dependencies',
144+
'some-external-package',
145+
'version',
146+
],
147+
value: {
148+
lhs: '0.0.1',
149+
rhs: '0.0.2',
150+
},
151+
},
152+
{
153+
type: JsonDiffType.Added,
154+
path: [
155+
'importers',
156+
'apps/app1',
157+
'devDependencies',
158+
'some-other-external-package',
159+
'version',
160+
],
161+
value: {
162+
lhs: undefined,
163+
rhs: '4.0.1',
164+
},
165+
},
166+
],
167+
},
168+
],
169+
graph.nodes
170+
);
171+
expect(result).toEqual(['proj1', 'app1']);
172+
});
173+
});
174+
});
77175
});
Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,100 @@
11
import { readNxJson } from '../../../../config/configuration';
22
import { TouchedProjectLocator } from '../../../../project-graph/affected/affected-project-graph-models';
3-
import { WholeFileChange } from '../../../../project-graph/file-utils';
4-
import { JsonChange } from '../../../../utils/json-diff';
3+
import {
4+
FileChange,
5+
WholeFileChange,
6+
} from '../../../../project-graph/file-utils';
7+
import { isJsonChange, JsonChange } from '../../../../utils/json-diff';
58
import { jsPluginConfig as readJsPluginConfig } from '../../utils/config';
69
import { findMatchingProjects } from '../../../../utils/find-matching-projects';
10+
import { ProjectGraphProjectNode } from '../../../../config/project-graph';
11+
12+
export const PNPM_LOCK_FILES = ['pnpm-lock.yaml', 'pnpm-lock.yml'];
13+
14+
const ALL_LOCK_FILES = [
15+
...PNPM_LOCK_FILES,
16+
'package-lock.json',
17+
'yarn.lock',
18+
'bun.lockb',
19+
'bun.lock',
20+
];
721

822
export const getTouchedProjectsFromLockFile: TouchedProjectLocator<
923
WholeFileChange | JsonChange
1024
> = (fileChanges, projectGraphNodes): string[] => {
1125
const nxJson = readNxJson();
1226
const { projectsAffectedByDependencyUpdates } = readJsPluginConfig(nxJson);
1327

28+
const changedLockFile = fileChanges.find((f) =>
29+
ALL_LOCK_FILES.includes(f.file)
30+
);
31+
1432
if (projectsAffectedByDependencyUpdates === 'auto') {
15-
return [];
33+
const changedProjectPaths =
34+
getProjectPathsAffectedByDependencyUpdates(changedLockFile);
35+
const changedProjectNames = getProjectsNamesFromPaths(
36+
projectGraphNodes,
37+
changedProjectPaths
38+
);
39+
return changedProjectNames;
1640
} else if (Array.isArray(projectsAffectedByDependencyUpdates)) {
1741
return findMatchingProjects(
1842
projectsAffectedByDependencyUpdates,
1943
projectGraphNodes
2044
);
2145
}
2246

23-
const lockFiles = [
24-
'package-lock.json',
25-
'yarn.lock',
26-
'pnpm-lock.yaml',
27-
'pnpm-lock.yml',
28-
'bun.lockb',
29-
'bun.lock',
30-
];
31-
32-
if (fileChanges.some((f) => lockFiles.includes(f.file))) {
47+
if (changedLockFile) {
3348
return Object.values(projectGraphNodes).map((p) => p.name);
3449
}
3550
return [];
3651
};
52+
53+
/**
54+
* For pnpm projects, check lock file for changes to importers and return the project paths that have changes.
55+
*/
56+
const getProjectPathsAffectedByDependencyUpdates = (
57+
changedLockFile?: FileChange<WholeFileChange | JsonChange>
58+
): string[] => {
59+
if (!changedLockFile) {
60+
return [];
61+
}
62+
const changedProjectPaths = new Set<string>();
63+
if (PNPM_LOCK_FILES.includes(changedLockFile.file)) {
64+
for (const change of changedLockFile.getChanges()) {
65+
if (
66+
isJsonChange(change) &&
67+
change.path[0] === 'importers' &&
68+
change.path[1] !== undefined
69+
) {
70+
changedProjectPaths.add(change.path[1]);
71+
}
72+
}
73+
}
74+
return Array.from(changedProjectPaths);
75+
};
76+
77+
const getProjectsNamesFromPaths = (
78+
projectGraphNodes: Record<string, ProjectGraphProjectNode>,
79+
projectPaths: string[]
80+
): string[] => {
81+
const lookup = new RootPathLookup(projectGraphNodes);
82+
return projectPaths.map((path) => {
83+
return lookup.findNodeNameByRoot(path);
84+
});
85+
};
86+
87+
class RootPathLookup {
88+
private rootToNameMap: Map<string, string>;
89+
90+
constructor(nodes: Record<string, ProjectGraphProjectNode>) {
91+
this.rootToNameMap = new Map();
92+
Object.entries(nodes).forEach(([name, node]) => {
93+
this.rootToNameMap.set(node.data.root, name);
94+
});
95+
}
96+
97+
findNodeNameByRoot(root: string): string | undefined {
98+
return this.rootToNameMap.get(root);
99+
}
100+
}

packages/nx/src/project-graph/file-utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ export function calculateFileChanges(
7676
} catch (e) {
7777
return [new WholeFileChange()];
7878
}
79+
case '.yml':
80+
case '.yaml':
81+
const { load } = require('@zkochan/js-yaml');
82+
try {
83+
const atBase = readFileAtRevision(f, nxArgs.base);
84+
const atHead = readFileAtRevision(f, nxArgs.head);
85+
return jsonDiff(load(atBase), load(atHead));
86+
} catch (e) {
87+
return [new WholeFileChange()];
88+
}
7989
default:
8090
return [new WholeFileChange()];
8191
}

0 commit comments

Comments
 (0)