@fuzdev/fuz_gitops 0.65.1 → 0.65.2
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/dependency_graph.d.ts +5 -4
- package/dist/dependency_graph.d.ts.map +1 -1
- package/dist/dependency_graph.js +40 -153
- package/dist/semver.d.ts.map +1 -1
- package/dist/semver.js +1 -6
- package/dist/version_utils.d.ts.map +1 -1
- package/dist/version_utils.js +1 -0
- package/package.json +5 -5
- package/src/lib/dependency_graph.ts +42 -171
- package/src/lib/semver.ts +2 -0
- package/src/lib/version_utils.ts +2 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dependency graph data structure and algorithms for multi-repo publishing.
|
|
3
3
|
*
|
|
4
|
-
* Provides `DependencyGraph` class with topological sort
|
|
4
|
+
* Provides `DependencyGraph` class with topological sort (via `@fuzdev/fuz_util/sort.js`)
|
|
5
|
+
* and cycle detection by dependency type.
|
|
5
6
|
* For validation workflow and publishing order computation, see `graph_validation.ts`.
|
|
6
7
|
*
|
|
7
8
|
* @module
|
|
@@ -43,6 +44,7 @@ export interface DependencyNode {
|
|
|
43
44
|
publishable: boolean;
|
|
44
45
|
}
|
|
45
46
|
export declare class DependencyGraph {
|
|
47
|
+
#private;
|
|
46
48
|
nodes: Map<string, DependencyNode>;
|
|
47
49
|
edges: Map<string, Set<string>>;
|
|
48
50
|
constructor();
|
|
@@ -53,8 +55,8 @@ export declare class DependencyGraph {
|
|
|
53
55
|
/**
|
|
54
56
|
* Computes topological sort order for dependency graph.
|
|
55
57
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
+
* Delegates to `@fuzdev/fuz_util/sort.js` for the sorting algorithm.
|
|
59
|
+
* Throws if cycles detected.
|
|
58
60
|
*
|
|
59
61
|
* @param exclude_dev if true, excludes dev dependencies to break cycles.
|
|
60
62
|
* Publishing uses exclude_dev=true to handle circular dev deps.
|
|
@@ -62,7 +64,6 @@ export declare class DependencyGraph {
|
|
|
62
64
|
* @throws {Error} if circular dependencies detected in included dependency types
|
|
63
65
|
*/
|
|
64
66
|
topological_sort(exclude_dev?: boolean): Array<string>;
|
|
65
|
-
detect_cycles(): Array<Array<string>>;
|
|
66
67
|
/**
|
|
67
68
|
* Detects circular dependencies, categorized by severity.
|
|
68
69
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dependency_graph.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/dependency_graph.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"dependency_graph.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/dependency_graph.ts"],"names":[],"mappings":"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"}
|
package/dist/dependency_graph.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dependency graph data structure and algorithms for multi-repo publishing.
|
|
3
3
|
*
|
|
4
|
-
* Provides `DependencyGraph` class with topological sort
|
|
4
|
+
* Provides `DependencyGraph` class with topological sort (via `@fuzdev/fuz_util/sort.js`)
|
|
5
|
+
* and cycle detection by dependency type.
|
|
5
6
|
* For validation workflow and publishing order computation, see `graph_validation.ts`.
|
|
6
7
|
*
|
|
7
8
|
* @module
|
|
8
9
|
*/
|
|
9
10
|
import { EMPTY_OBJECT } from '@fuzdev/fuz_util/object.js';
|
|
11
|
+
import { topological_sort as topological_sort_generic } from '@fuzdev/fuz_util/sort.js';
|
|
10
12
|
export const DEPENDENCY_TYPE = {
|
|
11
13
|
PROD: 'prod',
|
|
12
14
|
PEER: 'peer',
|
|
@@ -77,8 +79,8 @@ export class DependencyGraph {
|
|
|
77
79
|
/**
|
|
78
80
|
* Computes topological sort order for dependency graph.
|
|
79
81
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
+
* Delegates to `@fuzdev/fuz_util/sort.js` for the sorting algorithm.
|
|
83
|
+
* Throws if cycles detected.
|
|
82
84
|
*
|
|
83
85
|
* @param exclude_dev if true, excludes dev dependencies to break cycles.
|
|
84
86
|
* Publishing uses exclude_dev=true to handle circular dev deps.
|
|
@@ -86,97 +88,21 @@ export class DependencyGraph {
|
|
|
86
88
|
* @throws {Error} if circular dependencies detected in included dependency types
|
|
87
89
|
*/
|
|
88
90
|
topological_sort(exclude_dev = false) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
for (const name of this.nodes.keys()) {
|
|
94
|
-
in_degree.set(name, 0);
|
|
95
|
-
}
|
|
96
|
-
for (const node of this.nodes.values()) {
|
|
97
|
-
for (const [dep_name, spec] of node.dependencies) {
|
|
98
|
-
// Skip dev dependencies if requested
|
|
91
|
+
const items = Array.from(this.nodes.values()).map((node) => ({
|
|
92
|
+
id: node.name,
|
|
93
|
+
depends_on: Array.from(node.dependencies.entries())
|
|
94
|
+
.filter(([dep_name, spec]) => {
|
|
99
95
|
if (exclude_dev && spec.type === DEPENDENCY_TYPE.DEV)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
for (const [name, degree] of in_degree) {
|
|
109
|
-
if (degree === 0) {
|
|
110
|
-
queue.push(name);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// Sort initial queue alphabetically for deterministic ordering within tier
|
|
114
|
-
queue.sort();
|
|
115
|
-
// Process nodes
|
|
116
|
-
while (queue.length > 0) {
|
|
117
|
-
const name = queue.shift();
|
|
118
|
-
result.push(name);
|
|
119
|
-
visited.add(name);
|
|
120
|
-
// Reduce in-degree for dependents
|
|
121
|
-
const node = this.nodes.get(name);
|
|
122
|
-
if (node) {
|
|
123
|
-
// Find packages that depend on this one
|
|
124
|
-
// Sort nodes to ensure deterministic iteration order
|
|
125
|
-
const sorted_nodes = Array.from(this.nodes.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
126
|
-
for (const other_node of sorted_nodes) {
|
|
127
|
-
for (const [dep_name, spec] of other_node.dependencies) {
|
|
128
|
-
// Skip dev dependencies if requested
|
|
129
|
-
if (exclude_dev && spec.type === DEPENDENCY_TYPE.DEV)
|
|
130
|
-
continue;
|
|
131
|
-
if (dep_name === name) {
|
|
132
|
-
const new_degree = in_degree.get(other_node.name) - 1;
|
|
133
|
-
in_degree.set(other_node.name, new_degree);
|
|
134
|
-
if (new_degree === 0) {
|
|
135
|
-
queue.push(other_node.name);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// Check for cycles
|
|
143
|
-
if (result.length !== this.nodes.size) {
|
|
144
|
-
const unvisited = Array.from(this.nodes.keys()).filter((n) => !visited.has(n));
|
|
145
|
-
throw new Error(`Circular dependency detected involving: ${unvisited.join(', ')}`);
|
|
146
|
-
}
|
|
147
|
-
return result;
|
|
148
|
-
}
|
|
149
|
-
detect_cycles() {
|
|
150
|
-
const cycles = [];
|
|
151
|
-
const visited = new Set();
|
|
152
|
-
const rec_stack = new Set();
|
|
153
|
-
const dfs = (name, path) => {
|
|
154
|
-
visited.add(name);
|
|
155
|
-
rec_stack.add(name);
|
|
156
|
-
path.push(name);
|
|
157
|
-
const node = this.nodes.get(name);
|
|
158
|
-
if (node) {
|
|
159
|
-
for (const [dep_name] of node.dependencies) {
|
|
160
|
-
if (this.nodes.has(dep_name)) {
|
|
161
|
-
if (!visited.has(dep_name)) {
|
|
162
|
-
dfs(dep_name, [...path]);
|
|
163
|
-
}
|
|
164
|
-
else if (rec_stack.has(dep_name)) {
|
|
165
|
-
// Found a cycle
|
|
166
|
-
const cycle_start = path.indexOf(dep_name);
|
|
167
|
-
cycles.push(path.slice(cycle_start).concat(dep_name));
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
rec_stack.delete(name);
|
|
173
|
-
};
|
|
174
|
-
for (const name of this.nodes.keys()) {
|
|
175
|
-
if (!visited.has(name)) {
|
|
176
|
-
dfs(name, []);
|
|
177
|
-
}
|
|
96
|
+
return false;
|
|
97
|
+
return this.nodes.has(dep_name);
|
|
98
|
+
})
|
|
99
|
+
.map(([dep_name]) => dep_name),
|
|
100
|
+
}));
|
|
101
|
+
const result = topological_sort_generic(items, 'package');
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
throw new Error(result.error);
|
|
178
104
|
}
|
|
179
|
-
return
|
|
105
|
+
return result.sorted.map((item) => item.id);
|
|
180
106
|
}
|
|
181
107
|
/**
|
|
182
108
|
* Detects circular dependencies, categorized by severity.
|
|
@@ -190,87 +116,48 @@ export class DependencyGraph {
|
|
|
190
116
|
* @returns object with production_cycles (errors) and dev_cycles (info)
|
|
191
117
|
*/
|
|
192
118
|
detect_cycles_by_type() {
|
|
193
|
-
const production_cycles =
|
|
194
|
-
const dev_cycles =
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (node) {
|
|
206
|
-
for (const [dep_name, spec] of node.dependencies) {
|
|
207
|
-
// Skip dev dependencies
|
|
208
|
-
if (spec.type === DEPENDENCY_TYPE.DEV)
|
|
209
|
-
continue;
|
|
210
|
-
if (this.nodes.has(dep_name)) {
|
|
211
|
-
if (!visited_prod.has(dep_name)) {
|
|
212
|
-
dfs_prod(dep_name, [...path]);
|
|
213
|
-
}
|
|
214
|
-
else if (rec_stack_prod.has(dep_name)) {
|
|
215
|
-
// Found a production cycle
|
|
216
|
-
const cycle_start = path.indexOf(dep_name);
|
|
217
|
-
const cycle = path.slice(cycle_start).concat(dep_name);
|
|
218
|
-
// Check if this cycle is unique
|
|
219
|
-
const cycle_key = [...cycle].sort().join(',');
|
|
220
|
-
const exists = production_cycles.some((c) => [...c].sort().join(',') === cycle_key);
|
|
221
|
-
if (!exists) {
|
|
222
|
-
production_cycles.push(cycle);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
rec_stack_prod.delete(name);
|
|
229
|
-
};
|
|
230
|
-
// DFS for dev dependencies only
|
|
231
|
-
const dfs_dev = (name, path) => {
|
|
232
|
-
visited_dev.add(name);
|
|
233
|
-
rec_stack_dev.add(name);
|
|
119
|
+
const production_cycles = this.#find_cycles((spec) => spec.type !== DEPENDENCY_TYPE.DEV);
|
|
120
|
+
const dev_cycles = this.#find_cycles((spec) => spec.type === DEPENDENCY_TYPE.DEV);
|
|
121
|
+
return { production_cycles, dev_cycles };
|
|
122
|
+
}
|
|
123
|
+
/** DFS cycle detection following only edges that match the filter. */
|
|
124
|
+
#find_cycles(include) {
|
|
125
|
+
const cycles = [];
|
|
126
|
+
const visited = new Set();
|
|
127
|
+
const rec_stack = new Set();
|
|
128
|
+
const dfs = (name, path) => {
|
|
129
|
+
visited.add(name);
|
|
130
|
+
rec_stack.add(name);
|
|
234
131
|
path.push(name);
|
|
235
132
|
const node = this.nodes.get(name);
|
|
236
133
|
if (node) {
|
|
237
134
|
for (const [dep_name, spec] of node.dependencies) {
|
|
238
|
-
|
|
239
|
-
if (spec.type !== DEPENDENCY_TYPE.DEV)
|
|
135
|
+
if (!include(spec))
|
|
240
136
|
continue;
|
|
241
137
|
if (this.nodes.has(dep_name)) {
|
|
242
|
-
if (!
|
|
243
|
-
|
|
138
|
+
if (!visited.has(dep_name)) {
|
|
139
|
+
dfs(dep_name, [...path]);
|
|
244
140
|
}
|
|
245
|
-
else if (
|
|
246
|
-
// Found a dev cycle
|
|
141
|
+
else if (rec_stack.has(dep_name)) {
|
|
247
142
|
const cycle_start = path.indexOf(dep_name);
|
|
248
143
|
const cycle = path.slice(cycle_start).concat(dep_name);
|
|
249
|
-
// Check if this cycle is unique
|
|
250
144
|
const cycle_key = [...cycle].sort().join(',');
|
|
251
|
-
const exists =
|
|
145
|
+
const exists = cycles.some((c) => [...c].sort().join(',') === cycle_key);
|
|
252
146
|
if (!exists) {
|
|
253
|
-
|
|
147
|
+
cycles.push(cycle);
|
|
254
148
|
}
|
|
255
149
|
}
|
|
256
150
|
}
|
|
257
151
|
}
|
|
258
152
|
}
|
|
259
|
-
|
|
153
|
+
rec_stack.delete(name);
|
|
260
154
|
};
|
|
261
|
-
// Check for production/peer cycles
|
|
262
|
-
for (const name of this.nodes.keys()) {
|
|
263
|
-
if (!visited_prod.has(name)) {
|
|
264
|
-
dfs_prod(name, []);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
// Check for dev cycles
|
|
268
155
|
for (const name of this.nodes.keys()) {
|
|
269
|
-
if (!
|
|
270
|
-
|
|
156
|
+
if (!visited.has(name)) {
|
|
157
|
+
dfs(name, []);
|
|
271
158
|
}
|
|
272
159
|
}
|
|
273
|
-
return
|
|
160
|
+
return cycles;
|
|
274
161
|
}
|
|
275
162
|
toJSON() {
|
|
276
163
|
const nodes = Array.from(this.nodes.values()).map((node) => ({
|
package/dist/semver.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semver.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/semver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"semver.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/semver.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAEnD,MAAM,WAAW,MAAM;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAiGD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,MAqB9D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,MAAM,EAAE,MAAM,QAAQ,KAAG,MAuBrE,CAAC"}
|
package/dist/semver.js
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Semantic Versioning 2.0.0 utilities
|
|
3
|
-
* @see https://semver.org/
|
|
4
|
-
*
|
|
5
|
-
* @module
|
|
6
|
-
*/
|
|
1
|
+
// TODO: candidate for extraction to `@fuzdev/fuz_util`
|
|
7
2
|
/**
|
|
8
3
|
* SemVer 2.0.0 validation regex.
|
|
9
4
|
* @see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version_utils.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/version_utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"version_utils.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/version_utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,aAAa,CAAC;AAE1C,eAAO,MAAM,WAAW,GAAI,SAAS,MAAM,KAAG,OAE7C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,KAAG,MAEtD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MAGpD,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,MAAM,KAAG,MAWlE,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,EAAE,aAAa,MAAM,KAAG,OASnE,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,GAC7B,iBAAiB,MAAM,EACvB,mBAAkB,GAAG,GAAG,GAAG,GAAG,EAAE,GAAG,IAAU,KAC3C,MAcF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC9B,aAAa,MAAM,EACnB,WAAW,OAAO,GAAG,OAAO,GAAG,OAAO,KACpC,OAWF,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC5B,aAAa,MAAM,EACnB,aAAa,MAAM,KACjB,OAAO,GAAG,OAAO,GAAG,OAOtB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,GAAG,QAAQ,EAAE,GAAG,QAAQ,KAAG,MAO7D,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAI,iBAAiB,MAAM,EAAE,WAAW,QAAQ,KAAG,MAkBrF,CAAC"}
|
package/dist/version_utils.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_gitops",
|
|
3
|
-
"version": "0.65.
|
|
3
|
+
"version": "0.65.2",
|
|
4
4
|
"description": "a tool for managing many repos",
|
|
5
5
|
"glyph": "🪄",
|
|
6
6
|
"logo": "logo.svg",
|
|
@@ -32,18 +32,18 @@
|
|
|
32
32
|
"@fuzdev/fuz_css": ">=0.44.1",
|
|
33
33
|
"@fuzdev/fuz_ui": ">=0.179.0",
|
|
34
34
|
"@fuzdev/fuz_util": ">=0.45.3",
|
|
35
|
-
"@fuzdev/gro": "^0.
|
|
35
|
+
"@fuzdev/gro": "^0.194.0",
|
|
36
36
|
"@sveltejs/kit": "^2",
|
|
37
37
|
"svelte": "^5",
|
|
38
38
|
"zod": "^4.1.13"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@changesets/changelog-git": "^0.2.1",
|
|
42
|
-
"@fuzdev/fuz_code": "^0.45.
|
|
42
|
+
"@fuzdev/fuz_code": "^0.45.1",
|
|
43
43
|
"@fuzdev/fuz_css": "^0.50.0",
|
|
44
|
-
"@fuzdev/fuz_ui": "^0.183.
|
|
44
|
+
"@fuzdev/fuz_ui": "^0.183.2",
|
|
45
45
|
"@fuzdev/fuz_util": "^0.50.1",
|
|
46
|
-
"@fuzdev/gro": "^0.
|
|
46
|
+
"@fuzdev/gro": "^0.194.0",
|
|
47
47
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
48
48
|
"@ryanatkn/eslint-config": "^0.9.0",
|
|
49
49
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dependency graph data structure and algorithms for multi-repo publishing.
|
|
3
3
|
*
|
|
4
|
-
* Provides `DependencyGraph` class with topological sort
|
|
4
|
+
* Provides `DependencyGraph` class with topological sort (via `@fuzdev/fuz_util/sort.js`)
|
|
5
|
+
* and cycle detection by dependency type.
|
|
5
6
|
* For validation workflow and publishing order computation, see `graph_validation.ts`.
|
|
6
7
|
*
|
|
7
8
|
* @module
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
import type {LocalRepo} from './local_repo.js';
|
|
11
11
|
import {EMPTY_OBJECT} from '@fuzdev/fuz_util/object.js';
|
|
12
|
+
import {topological_sort as topological_sort_generic} from '@fuzdev/fuz_util/sort.js';
|
|
13
|
+
|
|
14
|
+
import type {LocalRepo} from './local_repo.js';
|
|
12
15
|
|
|
13
16
|
export const DEPENDENCY_TYPE = {
|
|
14
17
|
PROD: 'prod',
|
|
@@ -121,8 +124,8 @@ export class DependencyGraph {
|
|
|
121
124
|
/**
|
|
122
125
|
* Computes topological sort order for dependency graph.
|
|
123
126
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
127
|
+
* Delegates to `@fuzdev/fuz_util/sort.js` for the sorting algorithm.
|
|
128
|
+
* Throws if cycles detected.
|
|
126
129
|
*
|
|
127
130
|
* @param exclude_dev if true, excludes dev dependencies to break cycles.
|
|
128
131
|
* Publishing uses exclude_dev=true to handle circular dev deps.
|
|
@@ -130,111 +133,20 @@ export class DependencyGraph {
|
|
|
130
133
|
* @throws {Error} if circular dependencies detected in included dependency types
|
|
131
134
|
*/
|
|
132
135
|
topological_sort(exclude_dev = false): Array<string> {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (this.nodes.has(dep_name)) {
|
|
147
|
-
in_degree.set(node.name, in_degree.get(node.name)! + 1);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Start with nodes that have no dependencies
|
|
153
|
-
const queue: Array<string> = [];
|
|
154
|
-
for (const [name, degree] of in_degree) {
|
|
155
|
-
if (degree === 0) {
|
|
156
|
-
queue.push(name);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Sort initial queue alphabetically for deterministic ordering within tier
|
|
161
|
-
queue.sort();
|
|
162
|
-
|
|
163
|
-
// Process nodes
|
|
164
|
-
while (queue.length > 0) {
|
|
165
|
-
const name = queue.shift()!;
|
|
166
|
-
result.push(name);
|
|
167
|
-
visited.add(name);
|
|
168
|
-
|
|
169
|
-
// Reduce in-degree for dependents
|
|
170
|
-
const node = this.nodes.get(name);
|
|
171
|
-
if (node) {
|
|
172
|
-
// Find packages that depend on this one
|
|
173
|
-
// Sort nodes to ensure deterministic iteration order
|
|
174
|
-
const sorted_nodes = Array.from(this.nodes.values()).sort((a, b) =>
|
|
175
|
-
a.name.localeCompare(b.name),
|
|
176
|
-
);
|
|
177
|
-
for (const other_node of sorted_nodes) {
|
|
178
|
-
for (const [dep_name, spec] of other_node.dependencies) {
|
|
179
|
-
// Skip dev dependencies if requested
|
|
180
|
-
if (exclude_dev && spec.type === DEPENDENCY_TYPE.DEV) continue;
|
|
181
|
-
|
|
182
|
-
if (dep_name === name) {
|
|
183
|
-
const new_degree = in_degree.get(other_node.name)! - 1;
|
|
184
|
-
in_degree.set(other_node.name, new_degree);
|
|
185
|
-
if (new_degree === 0) {
|
|
186
|
-
queue.push(other_node.name);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Check for cycles
|
|
195
|
-
if (result.length !== this.nodes.size) {
|
|
196
|
-
const unvisited = Array.from(this.nodes.keys()).filter((n) => !visited.has(n));
|
|
197
|
-
throw new Error(`Circular dependency detected involving: ${unvisited.join(', ')}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return result;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
detect_cycles(): Array<Array<string>> {
|
|
204
|
-
const cycles: Array<Array<string>> = [];
|
|
205
|
-
const visited: Set<string> = new Set();
|
|
206
|
-
const rec_stack: Set<string> = new Set();
|
|
207
|
-
|
|
208
|
-
const dfs = (name: string, path: Array<string>): void => {
|
|
209
|
-
visited.add(name);
|
|
210
|
-
rec_stack.add(name);
|
|
211
|
-
path.push(name);
|
|
212
|
-
|
|
213
|
-
const node = this.nodes.get(name);
|
|
214
|
-
if (node) {
|
|
215
|
-
for (const [dep_name] of node.dependencies) {
|
|
216
|
-
if (this.nodes.has(dep_name)) {
|
|
217
|
-
if (!visited.has(dep_name)) {
|
|
218
|
-
dfs(dep_name, [...path]);
|
|
219
|
-
} else if (rec_stack.has(dep_name)) {
|
|
220
|
-
// Found a cycle
|
|
221
|
-
const cycle_start = path.indexOf(dep_name);
|
|
222
|
-
cycles.push(path.slice(cycle_start).concat(dep_name));
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
rec_stack.delete(name);
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
for (const name of this.nodes.keys()) {
|
|
232
|
-
if (!visited.has(name)) {
|
|
233
|
-
dfs(name, []);
|
|
234
|
-
}
|
|
136
|
+
const items = Array.from(this.nodes.values()).map((node) => ({
|
|
137
|
+
id: node.name,
|
|
138
|
+
depends_on: Array.from(node.dependencies.entries())
|
|
139
|
+
.filter(([dep_name, spec]) => {
|
|
140
|
+
if (exclude_dev && spec.type === DEPENDENCY_TYPE.DEV) return false;
|
|
141
|
+
return this.nodes.has(dep_name);
|
|
142
|
+
})
|
|
143
|
+
.map(([dep_name]) => dep_name),
|
|
144
|
+
}));
|
|
145
|
+
const result = topological_sort_generic(items, 'package');
|
|
146
|
+
if (!result.ok) {
|
|
147
|
+
throw new Error(result.error);
|
|
235
148
|
}
|
|
236
|
-
|
|
237
|
-
return cycles;
|
|
149
|
+
return result.sorted.map((item) => item.id);
|
|
238
150
|
}
|
|
239
151
|
|
|
240
152
|
/**
|
|
@@ -252,94 +164,53 @@ export class DependencyGraph {
|
|
|
252
164
|
production_cycles: Array<Array<string>>;
|
|
253
165
|
dev_cycles: Array<Array<string>>;
|
|
254
166
|
} {
|
|
255
|
-
const production_cycles
|
|
256
|
-
const dev_cycles
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const rec_stack_prod: Set<string> = new Set();
|
|
260
|
-
const rec_stack_dev: Set<string> = new Set();
|
|
261
|
-
|
|
262
|
-
// DFS for production/peer dependencies only
|
|
263
|
-
const dfs_prod = (name: string, path: Array<string>): void => {
|
|
264
|
-
visited_prod.add(name);
|
|
265
|
-
rec_stack_prod.add(name);
|
|
266
|
-
path.push(name);
|
|
267
|
-
|
|
268
|
-
const node = this.nodes.get(name);
|
|
269
|
-
if (node) {
|
|
270
|
-
for (const [dep_name, spec] of node.dependencies) {
|
|
271
|
-
// Skip dev dependencies
|
|
272
|
-
if (spec.type === DEPENDENCY_TYPE.DEV) continue;
|
|
273
|
-
|
|
274
|
-
if (this.nodes.has(dep_name)) {
|
|
275
|
-
if (!visited_prod.has(dep_name)) {
|
|
276
|
-
dfs_prod(dep_name, [...path]);
|
|
277
|
-
} else if (rec_stack_prod.has(dep_name)) {
|
|
278
|
-
// Found a production cycle
|
|
279
|
-
const cycle_start = path.indexOf(dep_name);
|
|
280
|
-
const cycle = path.slice(cycle_start).concat(dep_name);
|
|
281
|
-
// Check if this cycle is unique
|
|
282
|
-
const cycle_key = [...cycle].sort().join(',');
|
|
283
|
-
const exists = production_cycles.some((c) => [...c].sort().join(',') === cycle_key);
|
|
284
|
-
if (!exists) {
|
|
285
|
-
production_cycles.push(cycle);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
167
|
+
const production_cycles = this.#find_cycles((spec) => spec.type !== DEPENDENCY_TYPE.DEV);
|
|
168
|
+
const dev_cycles = this.#find_cycles((spec) => spec.type === DEPENDENCY_TYPE.DEV);
|
|
169
|
+
return {production_cycles, dev_cycles};
|
|
170
|
+
}
|
|
291
171
|
|
|
292
|
-
|
|
293
|
-
|
|
172
|
+
/** DFS cycle detection following only edges that match the filter. */
|
|
173
|
+
#find_cycles(include: (spec: DependencySpec) => boolean): Array<Array<string>> {
|
|
174
|
+
const cycles: Array<Array<string>> = [];
|
|
175
|
+
const visited: Set<string> = new Set();
|
|
176
|
+
const rec_stack: Set<string> = new Set();
|
|
294
177
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
rec_stack_dev.add(name);
|
|
178
|
+
const dfs = (name: string, path: Array<string>): void => {
|
|
179
|
+
visited.add(name);
|
|
180
|
+
rec_stack.add(name);
|
|
299
181
|
path.push(name);
|
|
300
182
|
|
|
301
183
|
const node = this.nodes.get(name);
|
|
302
184
|
if (node) {
|
|
303
185
|
for (const [dep_name, spec] of node.dependencies) {
|
|
304
|
-
|
|
305
|
-
if (spec.type !== DEPENDENCY_TYPE.DEV) continue;
|
|
186
|
+
if (!include(spec)) continue;
|
|
306
187
|
|
|
307
188
|
if (this.nodes.has(dep_name)) {
|
|
308
|
-
if (!
|
|
309
|
-
|
|
310
|
-
} else if (
|
|
311
|
-
// Found a dev cycle
|
|
189
|
+
if (!visited.has(dep_name)) {
|
|
190
|
+
dfs(dep_name, [...path]);
|
|
191
|
+
} else if (rec_stack.has(dep_name)) {
|
|
312
192
|
const cycle_start = path.indexOf(dep_name);
|
|
313
193
|
const cycle = path.slice(cycle_start).concat(dep_name);
|
|
314
|
-
// Check if this cycle is unique
|
|
315
194
|
const cycle_key = [...cycle].sort().join(',');
|
|
316
|
-
const exists =
|
|
195
|
+
const exists = cycles.some((c) => [...c].sort().join(',') === cycle_key);
|
|
317
196
|
if (!exists) {
|
|
318
|
-
|
|
197
|
+
cycles.push(cycle);
|
|
319
198
|
}
|
|
320
199
|
}
|
|
321
200
|
}
|
|
322
201
|
}
|
|
323
202
|
}
|
|
324
203
|
|
|
325
|
-
|
|
204
|
+
rec_stack.delete(name);
|
|
326
205
|
};
|
|
327
206
|
|
|
328
|
-
// Check for production/peer cycles
|
|
329
|
-
for (const name of this.nodes.keys()) {
|
|
330
|
-
if (!visited_prod.has(name)) {
|
|
331
|
-
dfs_prod(name, []);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Check for dev cycles
|
|
336
207
|
for (const name of this.nodes.keys()) {
|
|
337
|
-
if (!
|
|
338
|
-
|
|
208
|
+
if (!visited.has(name)) {
|
|
209
|
+
dfs(name, []);
|
|
339
210
|
}
|
|
340
211
|
}
|
|
341
212
|
|
|
342
|
-
return
|
|
213
|
+
return cycles;
|
|
343
214
|
}
|
|
344
215
|
|
|
345
216
|
toJSON(): DependencyGraphJson {
|
package/src/lib/semver.ts
CHANGED