-
Notifications
You must be signed in to change notification settings - Fork 5.1k
/
Copy pathcircular-deps.ts
186 lines (162 loc) · 5.26 KB
/
circular-deps.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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#!/usr/bin/env tsx
import fs, { readFileSync } from 'fs';
import madge from 'madge';
import fg from 'fast-glob';
const TARGET_FILE = 'development/circular-deps.jsonc';
const FILE_HEADER = `// This is a machine-generated file that tracks circular dependencies in the codebase.
// To understand changes in this file:
// - Each array represents a cycle of imports where the last file imports the first
// - The cycles are sorted alphabetically for consistent diffs
// - To update this file, run: yarn circular-deps:update
// - To prevent new circular dependencies, ensure your changes don't add new cycles
// - For more information contact the Extension Platform team.
`;
/**
* Message displayed when circular dependency checks fail and need resolution.
*/
const RESOLUTION_STEPS =
'To resolve this issue, run `yarn circular-deps:update` locally and commit the changes.';
/**
* Patterns for files and directories to ignore when checking for circular dependencies:
* - test files and directories
* - storybook files and directories
* - any file with .test., .spec., or .stories. in its name
*/
const IGNORE_PATTERNS = [
// Test files and directories
'**/test/**',
'**/tests/**',
'**/*.test.*',
'**/*.spec.*',
// Storybook files and directories
'**/stories/**',
'**/storybook/**',
'**/*.stories.*',
];
/**
* Source code directories to check for circular dependencies.
* These are the main app directories containing production code.
*/
const ENTRYPOINT_PATTERNS = [
'app/**/*', // Main application code
'shared/**/*', // Shared utilities and components
'ui/**/*', // UI components and styles
];
/**
* Circular dependencies are represented as an array of arrays, where each
* inner array represents a cycle of dependencies.
*/
type CircularDeps = string[][];
/**
* Normalizes JSON output by sorting both the individual cycles and the array
* of cycles. This ensures consistent output regardless of cycle starting point.
*
* Example:
* Input cycle: B -> C -> A -> B
* Output cycle: A -> B -> C -> A
*
* The normalization allows for reliable diff comparisons by eliminating
* ordering variations.
*…
*
* @param cycles
*/
function normalizeJson(cycles: CircularDeps): CircularDeps {
return cycles.map((cycle) => [...cycle].sort()).sort();
}
// Common madge configuration
const MADGE_CONFIG = JSON.parse(readFileSync('.madgerc', 'utf-8'));
async function getMadgeCircularDeps(): Promise<CircularDeps> {
console.log('Running madge to detect circular dependencies...');
try {
const entrypoints = (
await Promise.all(
ENTRYPOINT_PATTERNS.map((pattern) =>
fg(pattern, { ignore: IGNORE_PATTERNS }),
),
)
).flat();
console.log(
`Analyzing ${entrypoints.length} entry points for circular dependencies...`,
);
const result = await madge(entrypoints, MADGE_CONFIG);
const circularDeps = result.circular();
console.log(`Found ${circularDeps.length} circular dependencies`);
return normalizeJson(circularDeps);
} catch (error) {
console.error('Error while running madge:', error);
throw error;
}
}
async function update(): Promise<void> {
try {
console.log('Generating circular dependencies...');
const circularDeps = await getMadgeCircularDeps();
fs.writeFileSync(
TARGET_FILE,
`${FILE_HEADER + JSON.stringify(circularDeps, null, 2)}\n`,
);
console.log(`Wrote circular dependencies to ${TARGET_FILE}`);
} catch (error) {
console.error('Error while updating circular dependencies:', error);
process.exit(1);
}
}
/**
* Simplified version of stripJsonComments that removes any line that
* starts with // (ignoring whitespace).
*
* @param jsonc
*/
function stripJsonComments(jsonc: string): string {
return jsonc
.split('\n')
.filter((line) => !line.trim().startsWith('//'))
.join('\n');
}
async function check(): Promise<void> {
try {
// Check if target file exists
if (!fs.existsSync(TARGET_FILE)) {
console.error(`Error: ${TARGET_FILE} does not exist.`);
console.log(RESOLUTION_STEPS);
process.exit(1);
}
// Determine actual circular dependencies in the codebase
const actualDeps = await getMadgeCircularDeps();
// Read existing file and strip comments
const fileContents = fs.readFileSync(TARGET_FILE, 'utf-8');
const baselineDeps = JSON.parse(stripJsonComments(fileContents));
// Compare dependencies
const actualStr = JSON.stringify(actualDeps);
const baselineStr = JSON.stringify(baselineDeps);
if (actualStr !== baselineStr) {
console.error(
`Error: Codebase circular dependencies are out of sync with ${TARGET_FILE}`,
);
console.log(RESOLUTION_STEPS);
process.exit(1);
}
console.log('Circular dependencies check passed.');
} catch (error) {
console.error('Error while checking circular dependencies:', error);
process.exit(1);
}
}
// Main execution
async function main(): Promise<void> {
const command = process.argv[2];
if (command !== 'check' && command !== 'update') {
console.error('Usage: circular-deps.ts [check|update]');
process.exit(1);
}
if (command === 'update') {
await update();
} else {
await check();
}
}
main().catch((error) => {
console.error('Unexpected error:', error);
process.exit(1);
});