forked from openshift/console
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwebpack.circular-deps.ts
143 lines (119 loc) · 4.51 KB
/
webpack.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
/* eslint-env node */
/* eslint-disable no-console */
import * as webpack from 'webpack';
import * as path from 'path';
import * as fs from 'fs';
import * as moment from 'moment';
import * as CircularDependencyPlugin from 'circular-dependency-plugin';
import chalk from 'chalk';
const HandleCyclesPluginName = 'HandleCyclesPlugin';
type PresetOptions = {
exclude: RegExp;
reportFile: string;
thresholds: Partial<{
// max # of total cycles
totalCycles: number;
// max # of min-length cycles (A -> B -> A)
minLengthCycles: number;
}>;
};
type DetectedCycle = {
// webpack module record that caused the cycle
causedBy: string;
// relative module paths that make up the cycle
modulePaths: string[];
};
const minLengthCycleCount = (cycles: DetectedCycle[]) =>
cycles.filter((c) => c.modulePaths.length === 3).length;
const getCycleStats = (cycles: DetectedCycle[]): string => {
type ItemCount = { [key: string]: number };
const lines: string[] = [];
const sortedEntries = (obj: ItemCount): [string, number][] =>
Object.entries(obj).sort((a, b) => b[1] - a[1]); // descending order
const minLengthCycles = minLengthCycleCount(cycles);
const cycleCountByDir = cycles
.map((c) => {
const startPath = c.modulePaths[0];
const elements = startPath.split('/');
return startPath.startsWith('packages') ? elements.slice(0, 2).join('/') : elements[0];
})
.reduce((acc, dir) => {
acc[dir] = (acc[dir] ?? 0) + 1;
return acc;
}, {} as ItemCount);
const topIndexFiles = cycles.reduce((acc, c) => {
c.modulePaths
.slice(1, -1) // exclude outer edges
.filter((p) => /\/index\.tsx?$/.test(p))
.forEach((p) => {
acc[p] = (acc[p] ?? 0) + 1;
});
return acc;
}, {} as ItemCount);
lines.push(`${cycles.length} total cycles, ${minLengthCycles} min-length cycles (A -> B -> A)\n`);
lines.push('\nCycle count per directory:\n');
lines.push(...sortedEntries(cycleCountByDir).map(([dir, count]) => ` ${dir} (${count})\n`));
lines.push('\nIndex files occurring within cycles:\n');
lines.push(...sortedEntries(topIndexFiles).map(([file, count]) => ` ${file} (${count})\n`));
return lines.join('');
};
const getCycleEntries = (cycles: DetectedCycle[]): string => {
return cycles.map((c) => `${c.causedBy}\n ${c.modulePaths.join('\n ')}\n`).join('\n');
};
const applyThresholds = (
cycles: DetectedCycle[],
thresholds: PresetOptions['thresholds'],
compilation: webpack.compilation.Compilation,
) => {
const totalCycles = cycles.length;
if (thresholds.totalCycles && totalCycles > thresholds.totalCycles) {
compilation.errors.push(
new Error(
`${HandleCyclesPluginName}: total cycles (${totalCycles}) exceeds threshold (${thresholds.totalCycles})`,
),
);
}
const minLengthCycles = minLengthCycleCount(cycles);
if (thresholds.minLengthCycles && minLengthCycles > thresholds.minLengthCycles) {
compilation.errors.push(
new Error(
`${HandleCyclesPluginName}: min-length cycles (${minLengthCycles}) exceeds threshold (${thresholds.minLengthCycles})`,
),
);
}
};
export class CircularDependencyPreset {
constructor(private readonly options: PresetOptions) {}
apply(plugins: webpack.Plugin[]): void {
const cycles: DetectedCycle[] = [];
plugins.push(
new CircularDependencyPlugin({
exclude: this.options.exclude,
onDetected: ({ module: { resource }, paths: modulePaths }) => {
cycles.push({ causedBy: resource, modulePaths });
},
}),
{
// Ad-hoc plugin to handle detected module cycle information
apply: (compiler) => {
compiler.hooks.emit.tap(HandleCyclesPluginName, (compilation) => {
if (cycles.length === 0) {
return;
}
const hash = compilation.getStats().hash;
const builtAt = moment(compilation.getStats().endTime).format('MM/DD/YYYY HH:mm:ss');
const header = `webpack compilation ${hash} built at ${builtAt}\n`;
const reportPath = path.resolve(__dirname, this.options.reportFile);
fs.writeFileSync(
reportPath,
[header, getCycleStats(cycles), getCycleEntries(cycles)].join('\n'),
);
console.log(chalk.bold.yellow(`Detected ${cycles.length} cycles`));
console.log(`Module cycle report written to ${chalk.bold(reportPath)}`);
applyThresholds(cycles, this.options.thresholds, compilation);
});
},
},
);
}
}