Skip to content

Commit 74f9831

Browse files
committed
feat(core): update getTouchedProjectsFromLockFile to detect which projects were changed from pnpm lock file diff
Closes nrwl#29986
1 parent 3b3b320 commit 74f9831

File tree

2 files changed

+177
-14
lines changed

2 files changed

+177
-14
lines changed

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

+100-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
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';
9+
// const tempFs = new TempFs('lock-file-changes-test');
410

511
describe('getTouchedProjectsFromLockFile', () => {
612
let graph: ProjectGraph;
713
let allNodes = [];
14+
let tempFs: TempFs;
815

916
beforeEach(() => {
1017
graph = {
@@ -74,4 +81,96 @@ describe('getTouchedProjectsFromLockFile', () => {
7481
});
7582
});
7683
});
84+
85+
PNPM_LOCK_FILES.forEach((lockFile) => {
86+
describe(`"${lockFile} with projectsAffectedByDependencyUpdates set to auto"`, () => {
87+
beforeAll(async () => {
88+
tempFs = new TempFs('lock-file-changes-test');
89+
await tempFs.createFiles({
90+
'./nx.json': JSON.stringify({
91+
pluginsConfig: {
92+
'@nx/js': {
93+
projectsAffectedByDependencyUpdates: 'auto',
94+
},
95+
},
96+
}),
97+
});
98+
});
99+
100+
afterAll(() => {
101+
tempFs.cleanup();
102+
});
103+
104+
it(`should not return changes when "${lockFile}" is not touched`, () => {
105+
const result = getTouchedProjectsFromLockFile(
106+
[
107+
{
108+
file: 'source.ts',
109+
hash: 'some-hash',
110+
getChanges: () => [new WholeFileChange()],
111+
},
112+
],
113+
graph.nodes
114+
);
115+
expect(result).toEqual([]);
116+
});
117+
118+
it(`should not return changes when whole lock file "${lockFile}" is changed`, () => {
119+
const result = getTouchedProjectsFromLockFile(
120+
[
121+
{
122+
file: lockFile,
123+
hash: 'some-hash',
124+
getChanges: () => [new WholeFileChange()],
125+
},
126+
],
127+
graph.nodes
128+
);
129+
expect(result).toEqual([]);
130+
});
131+
132+
it(`should return only changed projects when "${lockFile}" is touched`, () => {
133+
const result = getTouchedProjectsFromLockFile(
134+
[
135+
{
136+
file: lockFile,
137+
hash: 'some-hash',
138+
getChanges: () => [
139+
{
140+
type: JsonDiffType.Modified,
141+
path: [
142+
'importers',
143+
'libs/proj1',
144+
'dependencies',
145+
'some-external-package',
146+
'version',
147+
],
148+
value: {
149+
lhs: '0.0.1',
150+
rhs: '0.0.2',
151+
},
152+
},
153+
{
154+
type: JsonDiffType.Added,
155+
path: [
156+
'importers',
157+
'apps/app1',
158+
'devDependencies',
159+
'some-other-external-package',
160+
'version',
161+
],
162+
value: {
163+
lhs: undefined,
164+
rhs: '4.0.1',
165+
},
166+
},
167+
],
168+
},
169+
],
170+
graph.nodes
171+
);
172+
expect(result).toEqual(['proj1', 'app1']);
173+
});
174+
});
175+
});
77176
});
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+
}

0 commit comments

Comments
 (0)