@crossplatformai/dependency-graph 0.9.4 → 0.11.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/pr-preview.d.ts +5 -0
- package/dist/cli/pr-preview.d.ts.map +1 -0
- package/dist/cli/validate-workflows.d.ts +3 -0
- package/dist/cli/validate-workflows.d.ts.map +1 -0
- package/dist/graph/analysis.d.ts +6 -0
- package/dist/graph/analysis.d.ts.map +1 -0
- package/dist/graph/builder.d.ts +3 -0
- package/dist/graph/builder.d.ts.map +1 -0
- package/dist/graph/traversal.d.ts +5 -0
- package/dist/graph/traversal.d.ts.map +1 -0
- package/dist/graph/types.d.ts +47 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/index-cli.js +1172 -0
- package/dist/index-cli.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +791 -0
- package/dist/index.js.map +1 -0
- package/dist/types/clients.d.ts +15 -0
- package/dist/types/clients.d.ts.map +1 -0
- package/dist/workflow/discovery.d.ts +4 -0
- package/dist/workflow/discovery.d.ts.map +1 -0
- package/dist/workflow/expected-paths.d.ts +13 -0
- package/dist/workflow/expected-paths.d.ts.map +1 -0
- package/dist/workflow/impact.d.ts +5 -0
- package/dist/workflow/impact.d.ts.map +1 -0
- package/dist/workflow/parser.d.ts +3 -0
- package/dist/workflow/parser.d.ts.map +1 -0
- package/dist/workflow/policy.d.ts +3 -0
- package/dist/workflow/policy.d.ts.map +1 -0
- package/dist/workflow/types.d.ts +41 -0
- package/dist/workflow/types.d.ts.map +1 -0
- package/dist/workflow/validator.d.ts +3 -0
- package/dist/workflow/validator.d.ts.map +1 -0
- package/dist/workspace/discovery.d.ts +12 -0
- package/dist/workspace/discovery.d.ts.map +1 -0
- package/dist/workspace/file-mapping.d.ts +4 -0
- package/dist/workspace/file-mapping.d.ts.map +1 -0
- package/dist/workspace/package-map.d.ts +12 -0
- package/dist/workspace/package-map.d.ts.map +1 -0
- package/package.json +33 -28
- package/src/cli/pr-preview.ts +0 -388
- package/src/cli/validate-workflows.ts +0 -287
- package/src/graph/analysis.ts +0 -147
- package/src/graph/builder.ts +0 -52
- package/src/graph/traversal.ts +0 -132
- package/src/graph/types.ts +0 -50
- package/src/index.test.ts +0 -94
- package/src/index.ts +0 -51
- package/src/types/clients.ts +0 -19
- package/src/workflow/discovery.ts +0 -58
- package/src/workflow/expected-paths.ts +0 -112
- package/src/workflow/parser.test.ts +0 -54
- package/src/workflow/parser.ts +0 -48
- package/src/workflow/policy.ts +0 -13
- package/src/workflow/types.ts +0 -42
- package/src/workflow/validator.test.ts +0 -214
- package/src/workflow/validator.ts +0 -230
- package/src/workspace/discovery.ts +0 -94
- package/src/workspace/file-mapping.ts +0 -35
- package/src/workspace/package-map.test.ts +0 -95
- package/src/workspace/package-map.ts +0 -74
package/dist/index.js
ADDED
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
// src/graph/builder.ts
|
|
2
|
+
function buildDependencyGraph(packages) {
|
|
3
|
+
const graph = {
|
|
4
|
+
packages: /* @__PURE__ */ new Map(),
|
|
5
|
+
dependsOn: /* @__PURE__ */ new Map(),
|
|
6
|
+
dependedBy: /* @__PURE__ */ new Map()
|
|
7
|
+
};
|
|
8
|
+
for (const pkg of packages) {
|
|
9
|
+
graph.packages.set(pkg.name, pkg);
|
|
10
|
+
graph.dependsOn.set(pkg.name, /* @__PURE__ */ new Set());
|
|
11
|
+
graph.dependedBy.set(pkg.name, /* @__PURE__ */ new Set());
|
|
12
|
+
}
|
|
13
|
+
for (const pkg of packages) {
|
|
14
|
+
const allDeps = {
|
|
15
|
+
...pkg.dependencies,
|
|
16
|
+
...pkg.devDependencies
|
|
17
|
+
};
|
|
18
|
+
for (const depName of Object.keys(allDeps)) {
|
|
19
|
+
const matchedPkg = findMatchingPackage(depName, packages);
|
|
20
|
+
if (matchedPkg) {
|
|
21
|
+
graph.dependsOn.get(pkg.name).add(matchedPkg.name);
|
|
22
|
+
graph.dependedBy.get(matchedPkg.name).add(pkg.name);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return graph;
|
|
27
|
+
}
|
|
28
|
+
function findMatchingPackage(depName, packages) {
|
|
29
|
+
let match = packages.find((p) => p.name === depName);
|
|
30
|
+
if (match) return match;
|
|
31
|
+
match = packages.find((p) => p.name === `@repo/${depName}`);
|
|
32
|
+
if (match) return match;
|
|
33
|
+
const nameWithoutRepo = depName.replace(/^@repo\//, "");
|
|
34
|
+
match = packages.find((p) => p.name === nameWithoutRepo);
|
|
35
|
+
if (match) return match;
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/graph/traversal.ts
|
|
40
|
+
function findAffectedPackages(startingPackages, graph, options = {}) {
|
|
41
|
+
const {
|
|
42
|
+
direction = "upstream",
|
|
43
|
+
maxDepth = Infinity,
|
|
44
|
+
filter,
|
|
45
|
+
respectAffectsUpstream = false
|
|
46
|
+
} = options;
|
|
47
|
+
const affected = new Set(startingPackages);
|
|
48
|
+
const queue = Array.from(
|
|
49
|
+
startingPackages
|
|
50
|
+
).map((name) => ({ name, depth: 0 }));
|
|
51
|
+
const visited = /* @__PURE__ */ new Set();
|
|
52
|
+
while (queue.length > 0) {
|
|
53
|
+
const current = queue.shift();
|
|
54
|
+
if (visited.has(current.name)) continue;
|
|
55
|
+
visited.add(current.name);
|
|
56
|
+
if (current.depth >= maxDepth) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const pkg = graph.packages.get(current.name);
|
|
60
|
+
if (!pkg) continue;
|
|
61
|
+
if (respectAffectsUpstream && direction === "upstream") {
|
|
62
|
+
const release = pkg.packageJson.release;
|
|
63
|
+
if (release && release.affectsUpstream === false) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const nextPackages = /* @__PURE__ */ new Set();
|
|
68
|
+
if (direction === "upstream" || direction === "both") {
|
|
69
|
+
const upstream = graph.dependedBy.get(current.name) || /* @__PURE__ */ new Set();
|
|
70
|
+
upstream.forEach((p) => nextPackages.add(p));
|
|
71
|
+
}
|
|
72
|
+
if (direction === "downstream" || direction === "both") {
|
|
73
|
+
const downstream = graph.dependsOn.get(current.name) || /* @__PURE__ */ new Set();
|
|
74
|
+
downstream.forEach((p) => nextPackages.add(p));
|
|
75
|
+
}
|
|
76
|
+
for (const pkgName of nextPackages) {
|
|
77
|
+
const nextPkg = graph.packages.get(pkgName);
|
|
78
|
+
if (filter && nextPkg && !filter(nextPkg)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!affected.has(pkgName)) {
|
|
82
|
+
affected.add(pkgName);
|
|
83
|
+
queue.push({ name: pkgName, depth: current.depth + 1 });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return affected;
|
|
88
|
+
}
|
|
89
|
+
function findDependencyPath(from, to, graph) {
|
|
90
|
+
const queue = [[from]];
|
|
91
|
+
const visited = /* @__PURE__ */ new Set([from]);
|
|
92
|
+
while (queue.length > 0) {
|
|
93
|
+
const path = queue.shift();
|
|
94
|
+
const current = path[path.length - 1];
|
|
95
|
+
if (!current) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (current === to) {
|
|
99
|
+
return path;
|
|
100
|
+
}
|
|
101
|
+
const dependents = graph.dependedBy.get(current) || /* @__PURE__ */ new Set();
|
|
102
|
+
for (const dependent of dependents) {
|
|
103
|
+
if (!visited.has(dependent)) {
|
|
104
|
+
visited.add(dependent);
|
|
105
|
+
queue.push([...path, dependent]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function findAllPaths(from, to, graph, maxPaths = 10) {
|
|
112
|
+
const paths = [];
|
|
113
|
+
const visited = /* @__PURE__ */ new Set();
|
|
114
|
+
function dfs(current, path) {
|
|
115
|
+
if (paths.length >= maxPaths) return;
|
|
116
|
+
if (current === to) {
|
|
117
|
+
paths.push([...path]);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (visited.has(current)) return;
|
|
121
|
+
visited.add(current);
|
|
122
|
+
const dependents = graph.dependedBy.get(current) || /* @__PURE__ */ new Set();
|
|
123
|
+
for (const dependent of dependents) {
|
|
124
|
+
dfs(dependent, [...path, dependent]);
|
|
125
|
+
}
|
|
126
|
+
visited.delete(current);
|
|
127
|
+
}
|
|
128
|
+
dfs(from, [from]);
|
|
129
|
+
return paths;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/graph/analysis.ts
|
|
133
|
+
function analyzeGraph(graph) {
|
|
134
|
+
const leafNodes = [];
|
|
135
|
+
const rootNodes = [];
|
|
136
|
+
let totalEdges = 0;
|
|
137
|
+
for (const [pkgName, dependents] of graph.dependedBy.entries()) {
|
|
138
|
+
if (dependents.size === 0) {
|
|
139
|
+
leafNodes.push(pkgName);
|
|
140
|
+
}
|
|
141
|
+
totalEdges += dependents.size;
|
|
142
|
+
}
|
|
143
|
+
for (const [pkgName, dependencies] of graph.dependsOn.entries()) {
|
|
144
|
+
if (dependencies.size === 0) {
|
|
145
|
+
rootNodes.push(pkgName);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const maxDepth = calculateMaxDepth(graph);
|
|
149
|
+
const cycles = detectCycles(graph);
|
|
150
|
+
return {
|
|
151
|
+
totalPackages: graph.packages.size,
|
|
152
|
+
totalEdges,
|
|
153
|
+
maxDepth,
|
|
154
|
+
leafNodes,
|
|
155
|
+
rootNodes,
|
|
156
|
+
cycles
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function calculateMaxDepth(graph) {
|
|
160
|
+
let maxDepth = 0;
|
|
161
|
+
for (const pkgName of graph.packages.keys()) {
|
|
162
|
+
const depth = getPackageDepth(pkgName, graph);
|
|
163
|
+
if (depth > maxDepth) {
|
|
164
|
+
maxDepth = depth;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return maxDepth;
|
|
168
|
+
}
|
|
169
|
+
function getPackageDepth(pkgName, graph, visited = /* @__PURE__ */ new Set()) {
|
|
170
|
+
if (visited.has(pkgName)) return 0;
|
|
171
|
+
visited.add(pkgName);
|
|
172
|
+
const dependencies = graph.dependsOn.get(pkgName) || /* @__PURE__ */ new Set();
|
|
173
|
+
if (dependencies.size === 0) return 0;
|
|
174
|
+
let maxDepth = 0;
|
|
175
|
+
for (const dep of dependencies) {
|
|
176
|
+
const depth = getPackageDepth(dep, graph, new Set(visited));
|
|
177
|
+
if (depth > maxDepth) {
|
|
178
|
+
maxDepth = depth;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return maxDepth + 1;
|
|
182
|
+
}
|
|
183
|
+
function detectCycles(graph) {
|
|
184
|
+
const cycles = [];
|
|
185
|
+
const visited = /* @__PURE__ */ new Set();
|
|
186
|
+
const recursionStack = /* @__PURE__ */ new Set();
|
|
187
|
+
function dfs(pkgName, path) {
|
|
188
|
+
visited.add(pkgName);
|
|
189
|
+
recursionStack.add(pkgName);
|
|
190
|
+
path.push(pkgName);
|
|
191
|
+
const dependencies = graph.dependsOn.get(pkgName) || /* @__PURE__ */ new Set();
|
|
192
|
+
for (const dep of dependencies) {
|
|
193
|
+
if (!visited.has(dep)) {
|
|
194
|
+
dfs(dep, [...path]);
|
|
195
|
+
} else if (recursionStack.has(dep)) {
|
|
196
|
+
const cycleStart = path.indexOf(dep);
|
|
197
|
+
if (cycleStart !== -1) {
|
|
198
|
+
const cycle = path.slice(cycleStart);
|
|
199
|
+
cycle.push(dep);
|
|
200
|
+
cycles.push(cycle);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
recursionStack.delete(pkgName);
|
|
205
|
+
}
|
|
206
|
+
for (const pkgName of graph.packages.keys()) {
|
|
207
|
+
if (!visited.has(pkgName)) {
|
|
208
|
+
dfs(pkgName, []);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return cycles;
|
|
212
|
+
}
|
|
213
|
+
function getTransitiveDependencies(pkgName, graph) {
|
|
214
|
+
const transitive = /* @__PURE__ */ new Set();
|
|
215
|
+
const visited = /* @__PURE__ */ new Set();
|
|
216
|
+
function collect(current) {
|
|
217
|
+
if (visited.has(current)) return;
|
|
218
|
+
visited.add(current);
|
|
219
|
+
const deps = graph.dependsOn.get(current) || /* @__PURE__ */ new Set();
|
|
220
|
+
for (const dep of deps) {
|
|
221
|
+
transitive.add(dep);
|
|
222
|
+
collect(dep);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
collect(pkgName);
|
|
226
|
+
return transitive;
|
|
227
|
+
}
|
|
228
|
+
function getTransitiveDependents(pkgName, graph) {
|
|
229
|
+
const transitive = /* @__PURE__ */ new Set();
|
|
230
|
+
const visited = /* @__PURE__ */ new Set();
|
|
231
|
+
function collect(current) {
|
|
232
|
+
if (visited.has(current)) return;
|
|
233
|
+
visited.add(current);
|
|
234
|
+
const dependents = graph.dependedBy.get(current) || /* @__PURE__ */ new Set();
|
|
235
|
+
for (const dependent of dependents) {
|
|
236
|
+
transitive.add(dependent);
|
|
237
|
+
collect(dependent);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
collect(pkgName);
|
|
241
|
+
return transitive;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/workspace/discovery.ts
|
|
245
|
+
import { join, resolve } from "path";
|
|
246
|
+
async function discoverWorkspaces(rootDir, config) {
|
|
247
|
+
const workspaceConfig = await loadWorkspaceConfig(rootDir, config);
|
|
248
|
+
const packages = [];
|
|
249
|
+
for (const pattern of workspaceConfig.packages) {
|
|
250
|
+
if (pattern.startsWith("!")) continue;
|
|
251
|
+
const pkgDirs = await findPackageDirectories(rootDir, pattern, config);
|
|
252
|
+
for (const pkgDir of pkgDirs) {
|
|
253
|
+
const pkgJsonPath = join(pkgDir, "package.json");
|
|
254
|
+
try {
|
|
255
|
+
const pkgJsonContent = await config.fs.readFile(
|
|
256
|
+
pkgJsonPath,
|
|
257
|
+
"utf-8"
|
|
258
|
+
);
|
|
259
|
+
const pkgJson = JSON.parse(pkgJsonContent);
|
|
260
|
+
packages.push({
|
|
261
|
+
name: pkgJson.name,
|
|
262
|
+
version: pkgJson.version || "0.0.0",
|
|
263
|
+
path: pkgDir,
|
|
264
|
+
packageJson: pkgJson,
|
|
265
|
+
dependencies: pkgJson.dependencies || {},
|
|
266
|
+
devDependencies: pkgJson.devDependencies || {}
|
|
267
|
+
});
|
|
268
|
+
} catch {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return packages;
|
|
274
|
+
}
|
|
275
|
+
async function loadWorkspaceConfig(rootDir, config) {
|
|
276
|
+
const workspaceFilePath = join(rootDir, "pnpm-workspace.yaml");
|
|
277
|
+
try {
|
|
278
|
+
const content = await config.fs.readFile(
|
|
279
|
+
workspaceFilePath,
|
|
280
|
+
"utf-8"
|
|
281
|
+
);
|
|
282
|
+
const parsed = config.yaml.parse(content);
|
|
283
|
+
return parsed;
|
|
284
|
+
} catch {
|
|
285
|
+
return { packages: [] };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function findPackageDirectories(rootDir, pattern, config) {
|
|
289
|
+
const matches = await config.glob.glob(pattern, {
|
|
290
|
+
cwd: rootDir,
|
|
291
|
+
absolute: false,
|
|
292
|
+
ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**"]
|
|
293
|
+
});
|
|
294
|
+
return matches.map((match) => resolve(rootDir, match));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/workspace/package-map.ts
|
|
298
|
+
import { existsSync } from "fs";
|
|
299
|
+
import { join as join2, relative, sep } from "path";
|
|
300
|
+
function normalizeRelativePath(path) {
|
|
301
|
+
return path.split(sep).join("/");
|
|
302
|
+
}
|
|
303
|
+
function resolveWorkflowPath(relativePath, rootDir) {
|
|
304
|
+
if (!relativePath.startsWith("../")) {
|
|
305
|
+
return relativePath;
|
|
306
|
+
}
|
|
307
|
+
const segments = relativePath.split("/");
|
|
308
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
309
|
+
const candidateSegments = segments.slice(index);
|
|
310
|
+
if (candidateSegments.length === 0 || candidateSegments[0] === "..") {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const candidatePath = candidateSegments.join("/");
|
|
314
|
+
if (existsSync(join2(rootDir, candidatePath))) {
|
|
315
|
+
return candidatePath;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
function buildPackageMap(packages, rootDir) {
|
|
321
|
+
const packageMap = /* @__PURE__ */ new Map();
|
|
322
|
+
const workspaceRoots = /* @__PURE__ */ new Set();
|
|
323
|
+
for (const pkg of packages) {
|
|
324
|
+
const relativePath = normalizeRelativePath(relative(rootDir, pkg.path));
|
|
325
|
+
const workflowPath = resolveWorkflowPath(relativePath, rootDir);
|
|
326
|
+
packageMap.set(pkg.name, {
|
|
327
|
+
filesystemPath: relativePath,
|
|
328
|
+
workflowPath
|
|
329
|
+
});
|
|
330
|
+
if (!workflowPath) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const [workspaceRoot] = workflowPath.split("/");
|
|
334
|
+
if (workspaceRoot) {
|
|
335
|
+
workspaceRoots.add(workspaceRoot);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
packageMap,
|
|
340
|
+
workspaceRoots
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/workspace/file-mapping.ts
|
|
345
|
+
function findPackageForFile(filePath, packages) {
|
|
346
|
+
const sorted = packages.sort((a, b) => b.path.length - a.path.length);
|
|
347
|
+
for (const pkg of sorted) {
|
|
348
|
+
if (filePath.startsWith(pkg.path + "/") || filePath === pkg.path) {
|
|
349
|
+
return pkg;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return void 0;
|
|
353
|
+
}
|
|
354
|
+
function mapFilesToPackages(files, packages) {
|
|
355
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
356
|
+
for (const file of files) {
|
|
357
|
+
const pkg = findPackageForFile(file, packages);
|
|
358
|
+
if (pkg) {
|
|
359
|
+
if (!fileMap.has(pkg.name)) {
|
|
360
|
+
fileMap.set(pkg.name, []);
|
|
361
|
+
}
|
|
362
|
+
fileMap.get(pkg.name).push(file);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return fileMap;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/workflow/policy.ts
|
|
369
|
+
var defaultWorkflowValidationPolicy = {
|
|
370
|
+
workflowFilePatterns: ["deploy-*.yml", "release-*.yml"],
|
|
371
|
+
allowedRootPaths: [
|
|
372
|
+
"package.json",
|
|
373
|
+
"pnpm-lock.yaml",
|
|
374
|
+
"pnpm-workspace.yaml",
|
|
375
|
+
"turbo.json"
|
|
376
|
+
],
|
|
377
|
+
includeDevDependenciesForRootPackage: true,
|
|
378
|
+
includeDevDependenciesTransitively: false
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/workflow/discovery.ts
|
|
382
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
383
|
+
import { join as join3 } from "path";
|
|
384
|
+
function patternToRegExp(pattern) {
|
|
385
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
386
|
+
return new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
|
|
387
|
+
}
|
|
388
|
+
function discoverWorkflowTargets(rootDir, packages, policy) {
|
|
389
|
+
const workflowsDir = join3(rootDir, ".github/workflows");
|
|
390
|
+
if (!existsSync2(workflowsDir)) {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
const { packageMap } = buildPackageMap(packages, rootDir);
|
|
394
|
+
const appPackages = packages.filter(
|
|
395
|
+
(pkg) => packageMap.get(pkg.name)?.workflowPath?.startsWith("apps/")
|
|
396
|
+
);
|
|
397
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
398
|
+
for (const pkg of appPackages) {
|
|
399
|
+
const relativePath = packageMap.get(pkg.name)?.workflowPath;
|
|
400
|
+
if (!relativePath) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const slug = relativePath.split("/").at(-1);
|
|
404
|
+
if (slug) {
|
|
405
|
+
bySlug.set(slug, pkg.name);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const allowedPatterns = policy.workflowFilePatterns.map(patternToRegExp);
|
|
409
|
+
const files = readdirSync(workflowsDir).filter((file) => file.endsWith(".yml")).filter((file) => allowedPatterns.some((pattern) => pattern.test(file)));
|
|
410
|
+
return files.map((workflowFile) => {
|
|
411
|
+
const match = /^(?:deploy|release)-(.+)\.yml$/.exec(workflowFile);
|
|
412
|
+
const targetSlug = match?.[1] ?? workflowFile.replace(/\.yml$/, "");
|
|
413
|
+
return {
|
|
414
|
+
workflowFile,
|
|
415
|
+
workflowPath: `.github/workflows/${workflowFile}`,
|
|
416
|
+
targetSlug,
|
|
417
|
+
targetPackage: bySlug.get(targetSlug) ?? null
|
|
418
|
+
};
|
|
419
|
+
}).sort((a, b) => a.workflowFile.localeCompare(b.workflowFile));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/workflow/expected-paths.ts
|
|
423
|
+
function uniqueSorted(values) {
|
|
424
|
+
return Array.from(new Set(values)).sort();
|
|
425
|
+
}
|
|
426
|
+
function resolveWorkspaceDependency(dependencyName, packages) {
|
|
427
|
+
let match = packages.find((pkg) => pkg.name === dependencyName);
|
|
428
|
+
if (match) {
|
|
429
|
+
return match;
|
|
430
|
+
}
|
|
431
|
+
match = packages.find((pkg) => pkg.name === `@repo/${dependencyName}`);
|
|
432
|
+
if (match) {
|
|
433
|
+
return match;
|
|
434
|
+
}
|
|
435
|
+
const nameWithoutRepo = dependencyName.replace(/^@repo\//, "");
|
|
436
|
+
return packages.find((pkg) => pkg.name === nameWithoutRepo);
|
|
437
|
+
}
|
|
438
|
+
function collectWorkspaceDependencyNames(pkg, packages, visited, includeDevDependencies, includeDevDependenciesTransitively) {
|
|
439
|
+
const collected = /* @__PURE__ */ new Set();
|
|
440
|
+
const dependencyEntries = Object.entries(pkg.dependencies);
|
|
441
|
+
const devDependencyEntries = includeDevDependencies ? Object.entries(pkg.devDependencies) : [];
|
|
442
|
+
for (const [dependencyName] of [
|
|
443
|
+
...dependencyEntries,
|
|
444
|
+
...devDependencyEntries
|
|
445
|
+
]) {
|
|
446
|
+
const dependency = resolveWorkspaceDependency(dependencyName, packages);
|
|
447
|
+
if (!dependency || visited.has(dependency.name)) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
visited.add(dependency.name);
|
|
451
|
+
collected.add(dependency.name);
|
|
452
|
+
const nestedDependencies = collectWorkspaceDependencyNames(
|
|
453
|
+
dependency,
|
|
454
|
+
packages,
|
|
455
|
+
visited,
|
|
456
|
+
includeDevDependenciesTransitively,
|
|
457
|
+
includeDevDependenciesTransitively
|
|
458
|
+
);
|
|
459
|
+
for (const nestedDependency of nestedDependencies) {
|
|
460
|
+
collected.add(nestedDependency);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return collected;
|
|
464
|
+
}
|
|
465
|
+
function getExpectedWorkflowPaths({
|
|
466
|
+
workflowTarget,
|
|
467
|
+
packages,
|
|
468
|
+
packageMap,
|
|
469
|
+
policy
|
|
470
|
+
}) {
|
|
471
|
+
const targetPackageName = workflowTarget.targetPackage;
|
|
472
|
+
if (!targetPackageName) {
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
const targetPackage = packages.find((pkg) => pkg.name === targetPackageName);
|
|
476
|
+
if (!targetPackage) {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
const dependencyNames = collectWorkspaceDependencyNames(
|
|
480
|
+
targetPackage,
|
|
481
|
+
packages,
|
|
482
|
+
/* @__PURE__ */ new Set(),
|
|
483
|
+
policy.includeDevDependenciesForRootPackage,
|
|
484
|
+
policy.includeDevDependenciesTransitively
|
|
485
|
+
);
|
|
486
|
+
const expectedPaths = [];
|
|
487
|
+
const targetPackagePath = packageMap.get(targetPackageName)?.workflowPath;
|
|
488
|
+
if (targetPackagePath) {
|
|
489
|
+
expectedPaths.push(`${targetPackagePath}/**`);
|
|
490
|
+
}
|
|
491
|
+
for (const dependencyName of dependencyNames) {
|
|
492
|
+
const dependencyPath = packageMap.get(dependencyName)?.workflowPath;
|
|
493
|
+
if (dependencyPath) {
|
|
494
|
+
expectedPaths.push(`${dependencyPath}/**`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return uniqueSorted(expectedPaths);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/workflow/impact.ts
|
|
501
|
+
function uniqueSorted2(values) {
|
|
502
|
+
return Array.from(new Set(values)).sort();
|
|
503
|
+
}
|
|
504
|
+
function mergeWorkflowPolicy(policyOverrides) {
|
|
505
|
+
return {
|
|
506
|
+
...defaultWorkflowValidationPolicy,
|
|
507
|
+
...policyOverrides,
|
|
508
|
+
allowedRootPaths: uniqueSorted2([
|
|
509
|
+
...defaultWorkflowValidationPolicy.allowedRootPaths,
|
|
510
|
+
...policyOverrides?.allowedRootPaths ?? []
|
|
511
|
+
])
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function matchesWorkflowPathFilter(changedPath, workflowPathFilter) {
|
|
515
|
+
if (workflowPathFilter === changedPath) {
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
if (!workflowPathFilter.endsWith("/**")) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
const workflowPrefix = workflowPathFilter.slice(0, -3);
|
|
522
|
+
return changedPath.startsWith(`${workflowPrefix}/`);
|
|
523
|
+
}
|
|
524
|
+
function getWorkflowImpacts(rootDir, packages, changedPaths, policyOverrides) {
|
|
525
|
+
const policy = mergeWorkflowPolicy(policyOverrides);
|
|
526
|
+
const workflowTargets = discoverWorkflowTargets(rootDir, packages, policy);
|
|
527
|
+
const { packageMap } = buildPackageMap(packages, rootDir);
|
|
528
|
+
return workflowTargets.map((workflowTarget) => {
|
|
529
|
+
const calculatedPaths = uniqueSorted2([
|
|
530
|
+
...getExpectedWorkflowPaths({
|
|
531
|
+
workflowTarget,
|
|
532
|
+
packages,
|
|
533
|
+
packageMap,
|
|
534
|
+
policy
|
|
535
|
+
}),
|
|
536
|
+
...policy.allowedRootPaths,
|
|
537
|
+
workflowTarget.workflowPath
|
|
538
|
+
]);
|
|
539
|
+
const matchedPaths = calculatedPaths.filter(
|
|
540
|
+
(calculatedPath) => changedPaths.some(
|
|
541
|
+
(changedPath) => matchesWorkflowPathFilter(changedPath, calculatedPath)
|
|
542
|
+
)
|
|
543
|
+
);
|
|
544
|
+
return {
|
|
545
|
+
...workflowTarget,
|
|
546
|
+
matchedPaths: uniqueSorted2(matchedPaths)
|
|
547
|
+
};
|
|
548
|
+
}).filter((workflowImpact) => workflowImpact.matchedPaths.length > 0);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/workflow/validator.ts
|
|
552
|
+
import { existsSync as existsSync4 } from "fs";
|
|
553
|
+
import { readFile } from "fs/promises";
|
|
554
|
+
import { glob } from "glob";
|
|
555
|
+
import { parse as parseYaml2 } from "yaml";
|
|
556
|
+
|
|
557
|
+
// src/workflow/parser.ts
|
|
558
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
559
|
+
import { join as join4 } from "path";
|
|
560
|
+
import { parse as parseYaml } from "yaml";
|
|
561
|
+
function uniqueSorted3(values) {
|
|
562
|
+
return Array.from(new Set(values)).sort();
|
|
563
|
+
}
|
|
564
|
+
function parseWorkflowFile(workflowFile, rootDir) {
|
|
565
|
+
const workflowPath = join4(rootDir, ".github/workflows", workflowFile);
|
|
566
|
+
if (!existsSync3(workflowPath)) {
|
|
567
|
+
throw new Error(`Workflow file not found: ${workflowFile}`);
|
|
568
|
+
}
|
|
569
|
+
const content = readFileSync(workflowPath, "utf-8");
|
|
570
|
+
const workflow = parseYaml(content);
|
|
571
|
+
const pushPaths = workflow.on?.push?.paths ?? [];
|
|
572
|
+
const pullRequestPaths = workflow.on?.pull_request?.paths ?? [];
|
|
573
|
+
const parsedWorkflow = {
|
|
574
|
+
pushPaths: uniqueSorted3(pushPaths),
|
|
575
|
+
pullRequestPaths: uniqueSorted3(pullRequestPaths),
|
|
576
|
+
paths: uniqueSorted3([...pushPaths, ...pullRequestPaths])
|
|
577
|
+
};
|
|
578
|
+
if (workflow.name) {
|
|
579
|
+
parsedWorkflow.name = workflow.name;
|
|
580
|
+
}
|
|
581
|
+
return parsedWorkflow;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/workflow/validator.ts
|
|
585
|
+
function uniqueSorted4(values) {
|
|
586
|
+
return Array.from(new Set(values)).sort();
|
|
587
|
+
}
|
|
588
|
+
function buildBroadWildcards(workspaceRoots) {
|
|
589
|
+
const wildcards = /* @__PURE__ */ new Set();
|
|
590
|
+
for (const root of workspaceRoots) {
|
|
591
|
+
wildcards.add(`${root}/*`);
|
|
592
|
+
wildcards.add(`${root}/**`);
|
|
593
|
+
}
|
|
594
|
+
return wildcards;
|
|
595
|
+
}
|
|
596
|
+
function splitActualPaths(paths, workspaceRoots, allowedRootPaths, workflowPath) {
|
|
597
|
+
const workspacePaths = [];
|
|
598
|
+
const ignoredPaths = [];
|
|
599
|
+
for (const path of paths) {
|
|
600
|
+
if (path === workflowPath || allowedRootPaths.includes(path)) {
|
|
601
|
+
ignoredPaths.push(path);
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const [rootSegment] = path.split("/");
|
|
605
|
+
if (rootSegment && workspaceRoots.has(rootSegment)) {
|
|
606
|
+
workspacePaths.push(path);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
ignoredPaths.push(path);
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
workspacePaths: uniqueSorted4(workspacePaths),
|
|
613
|
+
ignoredPaths: uniqueSorted4(ignoredPaths)
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function isCoveredByExpectedPath(actualPath, expectedPaths) {
|
|
617
|
+
return expectedPaths.some((expectedPath) => {
|
|
618
|
+
if (expectedPath === actualPath) {
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
if (!expectedPath.endsWith("/**")) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
const expectedPrefix = expectedPath.slice(0, -3);
|
|
625
|
+
return actualPath.startsWith(`${expectedPrefix}/`);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
function validateWorkflowResult(workflow, targetPackage, expectedPaths, actualPaths, broadWildcards) {
|
|
629
|
+
const issues = [];
|
|
630
|
+
const missing = expectedPaths.filter((path) => !actualPaths.includes(path));
|
|
631
|
+
const unnecessary = actualPaths.filter(
|
|
632
|
+
(path) => !isCoveredByExpectedPath(path, expectedPaths)
|
|
633
|
+
);
|
|
634
|
+
for (const path of missing) {
|
|
635
|
+
issues.push({
|
|
636
|
+
kind: "missing",
|
|
637
|
+
path,
|
|
638
|
+
message: `Missing path '${path}'`
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
for (const path of unnecessary) {
|
|
642
|
+
issues.push({
|
|
643
|
+
kind: "unnecessary",
|
|
644
|
+
path,
|
|
645
|
+
message: `Unnecessary path '${path}'`
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
for (const path of actualPaths) {
|
|
649
|
+
if (broadWildcards.has(path)) {
|
|
650
|
+
issues.push({
|
|
651
|
+
kind: "broad-wildcard",
|
|
652
|
+
path,
|
|
653
|
+
message: `Uses broad wildcard '${path}' which triggers on all workspace changes under that root`
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
workflow,
|
|
659
|
+
targetPackage,
|
|
660
|
+
valid: issues.length === 0,
|
|
661
|
+
expectedPaths,
|
|
662
|
+
actualPaths,
|
|
663
|
+
missing,
|
|
664
|
+
unnecessary,
|
|
665
|
+
issues
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
async function validateWorkflows(rootDir, policyOverrides) {
|
|
669
|
+
const discoveredPackages = await discoverWorkspaces(rootDir, {
|
|
670
|
+
fs: {
|
|
671
|
+
readFile: (path, encoding) => readFile(path, encoding),
|
|
672
|
+
exists: (path) => Promise.resolve(existsSync4(path))
|
|
673
|
+
},
|
|
674
|
+
glob: {
|
|
675
|
+
glob: (pattern, options) => glob(pattern, options)
|
|
676
|
+
},
|
|
677
|
+
yaml: {
|
|
678
|
+
parse: (content) => parseYaml2(content)
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
const policy = {
|
|
682
|
+
...defaultWorkflowValidationPolicy,
|
|
683
|
+
...policyOverrides,
|
|
684
|
+
allowedRootPaths: uniqueSorted4([
|
|
685
|
+
...defaultWorkflowValidationPolicy.allowedRootPaths,
|
|
686
|
+
...policyOverrides?.allowedRootPaths ?? []
|
|
687
|
+
])
|
|
688
|
+
};
|
|
689
|
+
const { packageMap, workspaceRoots } = buildPackageMap(
|
|
690
|
+
discoveredPackages,
|
|
691
|
+
rootDir
|
|
692
|
+
);
|
|
693
|
+
const workflowTargets = discoverWorkflowTargets(
|
|
694
|
+
rootDir,
|
|
695
|
+
discoveredPackages,
|
|
696
|
+
policy
|
|
697
|
+
);
|
|
698
|
+
const broadWildcards = buildBroadWildcards(workspaceRoots);
|
|
699
|
+
return workflowTargets.map((workflowTarget) => {
|
|
700
|
+
if (!workflowTarget.targetPackage) {
|
|
701
|
+
return {
|
|
702
|
+
workflow: workflowTarget.workflowFile,
|
|
703
|
+
targetPackage: workflowTarget.targetSlug,
|
|
704
|
+
valid: false,
|
|
705
|
+
expectedPaths: [],
|
|
706
|
+
actualPaths: [],
|
|
707
|
+
missing: [],
|
|
708
|
+
unnecessary: [],
|
|
709
|
+
issues: [
|
|
710
|
+
{
|
|
711
|
+
kind: "config-error",
|
|
712
|
+
message: `Could not resolve workflow target package for '${workflowTarget.workflowFile}'`
|
|
713
|
+
}
|
|
714
|
+
]
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
const parsedWorkflow = parseWorkflowFile(
|
|
719
|
+
workflowTarget.workflowFile,
|
|
720
|
+
rootDir
|
|
721
|
+
);
|
|
722
|
+
if (parsedWorkflow.paths.length === 0) {
|
|
723
|
+
return {
|
|
724
|
+
workflow: workflowTarget.workflowFile,
|
|
725
|
+
targetPackage: workflowTarget.targetPackage,
|
|
726
|
+
valid: true,
|
|
727
|
+
expectedPaths: [],
|
|
728
|
+
actualPaths: [],
|
|
729
|
+
missing: [],
|
|
730
|
+
unnecessary: [],
|
|
731
|
+
issues: []
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
const { workspacePaths: actualPaths } = splitActualPaths(
|
|
735
|
+
parsedWorkflow.paths,
|
|
736
|
+
workspaceRoots,
|
|
737
|
+
policy.allowedRootPaths,
|
|
738
|
+
workflowTarget.workflowPath
|
|
739
|
+
);
|
|
740
|
+
const expectedPaths = getExpectedWorkflowPaths({
|
|
741
|
+
workflowTarget,
|
|
742
|
+
packages: discoveredPackages,
|
|
743
|
+
packageMap,
|
|
744
|
+
policy
|
|
745
|
+
});
|
|
746
|
+
return validateWorkflowResult(
|
|
747
|
+
workflowTarget.workflowFile,
|
|
748
|
+
workflowTarget.targetPackage,
|
|
749
|
+
expectedPaths,
|
|
750
|
+
actualPaths,
|
|
751
|
+
broadWildcards
|
|
752
|
+
);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
return {
|
|
755
|
+
workflow: workflowTarget.workflowFile,
|
|
756
|
+
targetPackage: workflowTarget.targetPackage,
|
|
757
|
+
valid: false,
|
|
758
|
+
expectedPaths: [],
|
|
759
|
+
actualPaths: [],
|
|
760
|
+
missing: [],
|
|
761
|
+
unnecessary: [],
|
|
762
|
+
issues: [
|
|
763
|
+
{
|
|
764
|
+
kind: "parse-error",
|
|
765
|
+
message: error instanceof Error ? error.message : String(error)
|
|
766
|
+
}
|
|
767
|
+
]
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
export {
|
|
773
|
+
analyzeGraph,
|
|
774
|
+
buildDependencyGraph,
|
|
775
|
+
buildPackageMap,
|
|
776
|
+
defaultWorkflowValidationPolicy,
|
|
777
|
+
detectCycles,
|
|
778
|
+
discoverWorkspaces,
|
|
779
|
+
findAffectedPackages,
|
|
780
|
+
findAllPaths,
|
|
781
|
+
findDependencyPath,
|
|
782
|
+
findPackageForFile,
|
|
783
|
+
getTransitiveDependencies,
|
|
784
|
+
getTransitiveDependents,
|
|
785
|
+
getWorkflowImpacts,
|
|
786
|
+
mapFilesToPackages,
|
|
787
|
+
matchesWorkflowPathFilter,
|
|
788
|
+
normalizeRelativePath,
|
|
789
|
+
validateWorkflows
|
|
790
|
+
};
|
|
791
|
+
//# sourceMappingURL=index.js.map
|