-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
Copy pathpackage-tree.ts
140 lines (117 loc) · 3.74 KB
/
package-tree.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import is from '@sindresorhus/is';
import { Graph, hasCycle } from 'graph-data-structure';
import upath from 'upath';
import { logger } from '../../../logger';
import { minimatchFilter } from '../../../util/minimatch';
import { scm } from '../../platform/scm';
import type { ProjectFile } from './types';
import { readFileAsXmlDocument } from './util';
export const NUGET_CENTRAL_FILE = 'Directory.Packages.props';
export const MSBUILD_CENTRAL_FILE = 'Packages.props';
/**
* Get all leaf package files of ancestry that depend on packageFileName.
*/
export async function getDependentPackageFiles(
packageFileName: string,
isCentralManagement = false,
): Promise<ProjectFile[]> {
const packageFiles = await getAllPackageFiles();
const graph = new Graph();
if (isCentralManagement) {
graph.addNode(packageFileName);
}
const parentDir =
packageFileName === NUGET_CENTRAL_FILE ||
packageFileName === MSBUILD_CENTRAL_FILE
? ''
: upath.dirname(packageFileName);
for (const f of packageFiles) {
graph.addNode(f);
if (isCentralManagement && upath.dirname(f).startsWith(parentDir)) {
graph.addEdge(packageFileName, f);
}
}
for (const f of packageFiles) {
const doc = await readFileAsXmlDocument(f);
if (!doc) {
continue;
}
const projectReferenceAttributes = doc
.childrenNamed('ItemGroup')
.map((ig) => ig.childrenNamed('ProjectReference'))
.flat()
.map((pf) => pf.attr['Include'])
.filter(is.nonEmptyString);
const projectReferences = projectReferenceAttributes.map((a) =>
upath.normalize(a),
);
const normalizedRelativeProjectReferences = projectReferences.map((r) =>
reframeRelativePathToRootOfRepo(f, r),
);
for (const ref of normalizedRelativeProjectReferences) {
graph.addEdge(ref, f);
}
if (hasCycle(graph)) {
throw new Error('Circular reference detected in NuGet package files');
}
}
const deps = new Map<string, boolean>();
recursivelyGetDependentPackageFiles(packageFileName, graph, deps);
if (isCentralManagement) {
// remove props file, as we don't need it
deps.delete(packageFileName);
}
// deduplicate
return Array.from(deps).map(([name, isLeaf]) => ({ name, isLeaf }));
}
/**
* Traverse graph and find dependent package files at any level of ancestry
*/
function recursivelyGetDependentPackageFiles(
packageFileName: string,
graph: Graph,
deps: Map<string, boolean>,
): void {
const dependents = graph.adjacent(packageFileName);
if (!dependents || dependents.size === 0) {
deps.set(packageFileName, true);
return;
}
deps.set(packageFileName, false);
for (const dep of dependents) {
recursivelyGetDependentPackageFiles(dep, graph, deps);
}
}
/**
* Take the path relative from a project file, and make it relative from the root of the repo
*/
function reframeRelativePathToRootOfRepo(
dependentProjectRelativePath: string,
projectReference: string,
): string {
const virtualRepoRoot = '/';
const absoluteDependentProjectPath = upath.resolve(
virtualRepoRoot,
dependentProjectRelativePath,
);
const absoluteProjectReferencePath = upath.resolve(
upath.dirname(absoluteDependentProjectPath),
projectReference,
);
const relativeProjectReferencePath = upath.relative(
virtualRepoRoot,
absoluteProjectReferencePath,
);
return relativeProjectReferencePath;
}
/**
* Get a list of package files in localDir
*/
async function getAllPackageFiles(): Promise<string[]> {
const allFiles = await scm.getFileList();
const filteredPackageFiles = allFiles.filter(
minimatchFilter('*.{cs,vb,fs}proj', { matchBase: true, nocase: true }),
);
logger.trace({ filteredPackageFiles }, 'Found package files');
return filteredPackageFiles;
}