hermex 1.3.0-beta.3 ā 2.0.0-beta.1
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/README.md +7 -0
- package/dist/cli.mjs +1391 -1257
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -2,540 +2,1038 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import ora from "ora";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import
|
|
6
|
-
import fs, { existsSync } from "node:fs";
|
|
5
|
+
import Table from "cli-table3";
|
|
6
|
+
import fs, { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
8
|
import path, { join, resolve } from "node:path";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { parseSync } from "@swc/core";
|
|
8
11
|
import micromatch from "micromatch";
|
|
9
12
|
import { glob, globSync } from "glob";
|
|
10
13
|
import fs$1 from "fs";
|
|
11
14
|
import path$1 from "path";
|
|
12
15
|
import semver from "semver";
|
|
13
|
-
import Table from "cli-table3";
|
|
14
16
|
import { load } from "js-yaml";
|
|
15
17
|
import lockfile from "@yarnpkg/lockfile";
|
|
16
|
-
|
|
17
|
-
import { z } from "zod";
|
|
18
|
-
//#region src/swc-parser/core/state.ts
|
|
19
|
-
function createState() {
|
|
20
|
-
return {
|
|
21
|
-
usagePatterns: {
|
|
22
|
-
namedImports: /* @__PURE__ */ new Set(),
|
|
23
|
-
namespaceImports: /* @__PURE__ */ new Set(),
|
|
24
|
-
defaultImports: /* @__PURE__ */ new Set(),
|
|
25
|
-
aliasedImports: /* @__PURE__ */ new Map(),
|
|
26
|
-
variableAssignments: /* @__PURE__ */ new Map(),
|
|
27
|
-
lazyImports: /* @__PURE__ */ new Set(),
|
|
28
|
-
dynamicImports: /* @__PURE__ */ new Set(),
|
|
29
|
-
conditionalUsage: /* @__PURE__ */ new Set(),
|
|
30
|
-
arrayMappings: /* @__PURE__ */ new Set(),
|
|
31
|
-
objectMappings: /* @__PURE__ */ new Set(),
|
|
32
|
-
hocUsage: /* @__PURE__ */ new Set(),
|
|
33
|
-
forwardedRefs: /* @__PURE__ */ new Set(),
|
|
34
|
-
memoizedComponents: /* @__PURE__ */ new Set(),
|
|
35
|
-
portalUsage: /* @__PURE__ */ new Set(),
|
|
36
|
-
jsxUsage: /* @__PURE__ */ new Map(),
|
|
37
|
-
destructuredUsage: /* @__PURE__ */ new Set(),
|
|
38
|
-
propsAnalysis: /* @__PURE__ */ new Map()
|
|
39
|
-
},
|
|
40
|
-
componentNames: /* @__PURE__ */ new Set(),
|
|
41
|
-
allIdentifiers: /* @__PURE__ */ new Set()
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
//#endregion
|
|
45
|
-
//#region src/swc-parser/patterns/imports.ts
|
|
18
|
+
//#region src/utils/format-utils.ts
|
|
46
19
|
/**
|
|
47
|
-
*
|
|
48
|
-
* -
|
|
49
|
-
*
|
|
50
|
-
* - Namespace imports
|
|
51
|
-
* - Aliased imports
|
|
20
|
+
* Format a number with thousand separators
|
|
21
|
+
* @param num - Number to format
|
|
22
|
+
* @returns Formatted string (e.g., 1,234,567)
|
|
52
23
|
*/
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
for (const spec of node.specifiers) switch (spec.type) {
|
|
56
|
-
case "ImportDefaultSpecifier":
|
|
57
|
-
analyzeDefaultImport(spec, source, node, state);
|
|
58
|
-
break;
|
|
59
|
-
case "ImportNamespaceSpecifier":
|
|
60
|
-
analyzeNamespaceImport(spec, source, node, state);
|
|
61
|
-
break;
|
|
62
|
-
case "ImportSpecifier":
|
|
63
|
-
analyzeNamedImport(spec, source, node, state);
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
24
|
+
function formatCount(num) {
|
|
25
|
+
return num.toLocaleString();
|
|
66
26
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
source,
|
|
72
|
-
line: node.span?.start || 0
|
|
73
|
-
});
|
|
74
|
-
state.componentNames.add(name);
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/utils/print-summary.ts
|
|
29
|
+
function printHeader$4() {
|
|
30
|
+
console.log(chalk.green.bold("\nš Summary\n"));
|
|
75
31
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
32
|
+
function printSummary(aggregated) {
|
|
33
|
+
printHeader$4();
|
|
34
|
+
const table = new Table({
|
|
35
|
+
head: ["Metric", "Count"],
|
|
36
|
+
style: {
|
|
37
|
+
head: ["cyan"],
|
|
38
|
+
border: ["gray"]
|
|
39
|
+
}
|
|
82
40
|
});
|
|
83
|
-
|
|
41
|
+
const externalComponents = aggregated.topComponents.filter((comp) => comp.source !== "unknown" && comp.source !== "local").length;
|
|
42
|
+
const totalExternalUsage = aggregated.packageDistribution.reduce((sum, pkg) => sum + pkg.usageCount, 0);
|
|
43
|
+
table.push(["Files Analyzed", formatCount(aggregated.filesAnalyzed)], ["External Packages", formatCount(aggregated.packageDistribution.length)], ["External Components", formatCount(externalComponents)], ["Total Usages", formatCount(totalExternalUsage)]);
|
|
44
|
+
console.log(table.toString());
|
|
84
45
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
});
|
|
93
|
-
if (
|
|
94
|
-
imported: importedName,
|
|
95
|
-
local: localName,
|
|
96
|
-
source,
|
|
97
|
-
line: node.span?.start || 0
|
|
98
|
-
});
|
|
99
|
-
state.componentNames.add(localName);
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/utils/print-details.ts
|
|
48
|
+
function printHeader$3() {
|
|
49
|
+
console.log(chalk.cyan.bold("\nš Details\n"));
|
|
50
|
+
}
|
|
51
|
+
function printDetails(aggregated) {
|
|
52
|
+
printHeader$3();
|
|
53
|
+
console.log(chalk.cyan(` Total usage patterns: ${formatCount(aggregated.totalUsagePatterns)}`));
|
|
54
|
+
for (const pattern of aggregated.patternCounts) if (pattern.count > 0) console.log(chalk.cyan(` ${pattern.displayName}: ${formatCount(pattern.count)}`));
|
|
100
55
|
}
|
|
101
56
|
//#endregion
|
|
102
|
-
//#region src/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
switch (nameNode.type) {
|
|
109
|
-
case "Identifier": return nameNode.value;
|
|
110
|
-
case "JSXMemberExpression": return `${getJSXElementName(nameNode.object)}.${nameNode.property.value}`;
|
|
111
|
-
default: return "";
|
|
57
|
+
//#region src/utils/chart-renderer.ts
|
|
58
|
+
function renderBarChart(data, options = {}) {
|
|
59
|
+
const { maxWidth = 50, showValues = true, barChar = "ā", emptyChar = "ā" } = options;
|
|
60
|
+
if (data.length === 0) {
|
|
61
|
+
console.log(chalk.gray(" No data to display"));
|
|
62
|
+
return;
|
|
112
63
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
64
|
+
const maxValue = Math.max(...data.map((d) => d.value));
|
|
65
|
+
if (maxValue === 0) {
|
|
66
|
+
console.log(chalk.gray(" All values are zero"));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const maxLabelLength = Math.max(...data.map((d) => d.label.length));
|
|
70
|
+
for (const item of data) {
|
|
71
|
+
const percentage = item.value / maxValue;
|
|
72
|
+
const barLength = Math.round(percentage * maxWidth);
|
|
73
|
+
const emptyLength = maxWidth - barLength;
|
|
74
|
+
const paddedLabel = item.label.padEnd(maxLabelLength, " ");
|
|
75
|
+
const bar = chalk.green(barChar.repeat(barLength)) + chalk.gray(emptyChar.repeat(emptyLength));
|
|
76
|
+
const valueStr = showValues ? ` ${formatCount(item.value)}` : "";
|
|
77
|
+
console.log(`${paddedLabel} ${bar}${valueStr}\n`);
|
|
121
78
|
}
|
|
122
|
-
return false;
|
|
123
79
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (!attributes) return [];
|
|
129
|
-
return attributes.map((attr) => {
|
|
130
|
-
if (attr.type === "JSXAttribute") return {
|
|
131
|
-
name: attr.name?.value || attr.name?.name?.value,
|
|
132
|
-
value: extractJSXAttributeValue(attr.value)
|
|
133
|
-
};
|
|
134
|
-
if (attr.type === "SpreadElement") return {
|
|
135
|
-
name: "...",
|
|
136
|
-
value: "[spread]",
|
|
137
|
-
isSpread: true
|
|
138
|
-
};
|
|
139
|
-
return null;
|
|
140
|
-
}).filter(Boolean);
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/utils/print-components.ts
|
|
82
|
+
function printHeader$2() {
|
|
83
|
+
console.log(chalk.magenta.bold("\nāļø Components\n"));
|
|
141
84
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (!value) return true;
|
|
147
|
-
switch (value.type) {
|
|
148
|
-
case "StringLiteral": return value.value;
|
|
149
|
-
case "JSXExpressionContainer": return extractExpressionValue(value.expression);
|
|
150
|
-
default: return "[complex]";
|
|
151
|
-
}
|
|
85
|
+
function printComponents(aggregated, mode) {
|
|
86
|
+
const components = aggregated.topComponents;
|
|
87
|
+
if (mode === "table") printComponentsTable(components);
|
|
88
|
+
else if (mode === "chart") printComponentsChart(components);
|
|
152
89
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
case "StringLiteral":
|
|
160
|
-
case "NumericLiteral":
|
|
161
|
-
case "BooleanLiteral": return expr.value;
|
|
162
|
-
case "Identifier": return `{${expr.value}}`;
|
|
163
|
-
case "ArrowFunctionExpression":
|
|
164
|
-
case "FunctionExpression": return "[function]";
|
|
165
|
-
case "ObjectExpression": return "[object]";
|
|
166
|
-
case "ArrayExpression": return "[array]";
|
|
167
|
-
default: return "[expression]";
|
|
90
|
+
function printComponentsTable(components) {
|
|
91
|
+
printHeader$2();
|
|
92
|
+
const externalComponents = components.filter((comp) => comp.source !== "unknown" && comp.source !== "local");
|
|
93
|
+
if (externalComponents.length === 0) {
|
|
94
|
+
console.log(chalk.gray(" No external components found"));
|
|
95
|
+
return;
|
|
168
96
|
}
|
|
97
|
+
const table = new Table({
|
|
98
|
+
head: [
|
|
99
|
+
"Component",
|
|
100
|
+
"Package",
|
|
101
|
+
"Count"
|
|
102
|
+
],
|
|
103
|
+
style: {
|
|
104
|
+
head: ["cyan"],
|
|
105
|
+
border: ["gray"]
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
externalComponents.forEach((comp) => {
|
|
109
|
+
table.push([
|
|
110
|
+
comp.name,
|
|
111
|
+
comp.source,
|
|
112
|
+
comp.count.toString()
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
console.log(table.toString());
|
|
169
116
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
case "ConditionalExpression": return "conditional";
|
|
177
|
-
case "ArrayExpression": return "array";
|
|
178
|
-
case "ObjectExpression": return "object";
|
|
179
|
-
case "CallExpression": return "hoc";
|
|
180
|
-
case "VariableDeclarator": return "variable";
|
|
181
|
-
default: return "jsx";
|
|
117
|
+
function printComponentsChart(components) {
|
|
118
|
+
printHeader$2();
|
|
119
|
+
const externalComponents = components.filter((comp) => comp.source !== "unknown" && comp.source !== "local");
|
|
120
|
+
if (externalComponents.length === 0) {
|
|
121
|
+
console.log(chalk.gray(" No external components found"));
|
|
122
|
+
return;
|
|
182
123
|
}
|
|
124
|
+
renderBarChart(externalComponents.map((comp) => ({
|
|
125
|
+
label: comp.name,
|
|
126
|
+
value: comp.count
|
|
127
|
+
})), { maxWidth: 50 });
|
|
183
128
|
}
|
|
184
129
|
//#endregion
|
|
185
|
-
//#region src/
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
*/
|
|
189
|
-
function analyzePropsInDetail(attributes, componentName, state) {
|
|
190
|
-
const analysis = {
|
|
191
|
-
namedProps: [],
|
|
192
|
-
hasSpread: false,
|
|
193
|
-
hasComplexProps: false,
|
|
194
|
-
hasEventHandlers: false,
|
|
195
|
-
propDetails: []
|
|
196
|
-
};
|
|
197
|
-
if (!attributes) return analysis;
|
|
198
|
-
for (const attr of attributes) if (attr.type === "JSXAttribute") {
|
|
199
|
-
const propName = attr.name?.value || attr.name?.name?.value;
|
|
200
|
-
if (propName) {
|
|
201
|
-
analysis.namedProps.push(propName);
|
|
202
|
-
const propDetail = {
|
|
203
|
-
name: propName,
|
|
204
|
-
type: getPropType(attr.value),
|
|
205
|
-
isEventHandler: propName.startsWith("on"),
|
|
206
|
-
isComplex: isComplexProp(attr.value)
|
|
207
|
-
};
|
|
208
|
-
if (propDetail.isEventHandler) analysis.hasEventHandlers = true;
|
|
209
|
-
if (propDetail.isComplex) analysis.hasComplexProps = true;
|
|
210
|
-
analysis.propDetails.push(propDetail);
|
|
211
|
-
}
|
|
212
|
-
} else if (attr.type === "SpreadElement") {
|
|
213
|
-
analysis.hasSpread = true;
|
|
214
|
-
analysis.propDetails.push({
|
|
215
|
-
name: "...",
|
|
216
|
-
type: "spread",
|
|
217
|
-
isSpread: true,
|
|
218
|
-
isComplex: true,
|
|
219
|
-
isEventHandler: false,
|
|
220
|
-
warning: "Spread props cannot be statically analyzed"
|
|
221
|
-
});
|
|
222
|
-
analysis.hasComplexProps = true;
|
|
223
|
-
}
|
|
224
|
-
state.usagePatterns.propsAnalysis.set(componentName, analysis);
|
|
225
|
-
return analysis;
|
|
130
|
+
//#region src/utils/print-patterns.ts
|
|
131
|
+
function printHeader$1() {
|
|
132
|
+
console.log(chalk.blue.bold("\nš Code Patterns\n"));
|
|
226
133
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
switch (expr.type) {
|
|
238
|
-
case "NumericLiteral": return "number";
|
|
239
|
-
case "BooleanLiteral": return "boolean";
|
|
240
|
-
case "StringLiteral": return "string";
|
|
241
|
-
case "ArrowFunctionExpression":
|
|
242
|
-
case "FunctionExpression": return "function";
|
|
243
|
-
case "ObjectExpression": return "object";
|
|
244
|
-
case "ArrayExpression": return "array";
|
|
245
|
-
case "Identifier": return "variable";
|
|
246
|
-
default: return "expression";
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
default: return "unknown";
|
|
134
|
+
function printPatterns(aggregated, mode) {
|
|
135
|
+
const patterns = aggregated.patternCounts.filter((p) => p.count > 0);
|
|
136
|
+
if (mode === "table") printPatternsTable(patterns);
|
|
137
|
+
else if (mode === "chart") printPatternsChart(patterns);
|
|
138
|
+
}
|
|
139
|
+
function printPatternsTable(patterns) {
|
|
140
|
+
printHeader$1();
|
|
141
|
+
if (patterns.length === 0) {
|
|
142
|
+
console.log(chalk.gray(" No patterns found"));
|
|
143
|
+
return;
|
|
250
144
|
}
|
|
145
|
+
const table = new Table({
|
|
146
|
+
head: ["Pattern", "Count"],
|
|
147
|
+
style: {
|
|
148
|
+
head: ["cyan"],
|
|
149
|
+
border: ["gray"]
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
patterns.forEach((pattern) => {
|
|
153
|
+
table.push([pattern.displayName, pattern.count.toString()]);
|
|
154
|
+
});
|
|
155
|
+
console.log(table.toString());
|
|
156
|
+
const totalPatterns = patterns.reduce((sum, p) => sum + p.count, 0);
|
|
157
|
+
console.log(chalk.gray(`\nTotal: ${totalPatterns} patterns detected`));
|
|
251
158
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (value.type === "JSXExpressionContainer") {
|
|
258
|
-
const expr = value.expression;
|
|
259
|
-
if (!expr) return false;
|
|
260
|
-
return expr.type === "ObjectExpression" || expr.type === "ArrayExpression" || expr.type === "CallExpression" || expr.type === "ConditionalExpression";
|
|
159
|
+
function printPatternsChart(patterns) {
|
|
160
|
+
printHeader$1();
|
|
161
|
+
if (patterns.length === 0) {
|
|
162
|
+
console.log(chalk.gray(" No patterns found"));
|
|
163
|
+
return;
|
|
261
164
|
}
|
|
262
|
-
|
|
165
|
+
renderBarChart(patterns.map((pattern) => ({
|
|
166
|
+
label: pattern.displayName,
|
|
167
|
+
value: pattern.count
|
|
168
|
+
})), { maxWidth: 50 });
|
|
263
169
|
}
|
|
264
170
|
//#endregion
|
|
265
|
-
//#region src/
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
*/
|
|
269
|
-
function analyzeJSXElement(node, state) {
|
|
270
|
-
if (node.opening) analyzeJSXOpeningElement(node.opening, state, node);
|
|
171
|
+
//#region src/utils/print-packages.ts
|
|
172
|
+
function printHeader() {
|
|
173
|
+
console.log(chalk.blueBright.bold("\nš¦ Packages\n"));
|
|
271
174
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const propsAnalysis = analyzePropsInDetail(node.attributes, elementName, state);
|
|
279
|
-
const usage = {
|
|
280
|
-
component: elementName,
|
|
281
|
-
props: extractJSXProps(node.attributes).map((p) => p.name),
|
|
282
|
-
propsAnalysis,
|
|
283
|
-
line: node.span?.start || 0,
|
|
284
|
-
context: getUsageContext(parent)
|
|
285
|
-
};
|
|
286
|
-
if (!state.usagePatterns.jsxUsage.has(elementName)) state.usagePatterns.jsxUsage.set(elementName, usage);
|
|
175
|
+
function formatPackageName(pkg, banned) {
|
|
176
|
+
let prefix = "";
|
|
177
|
+
if (pkg.releaseAge?.deprecated) prefix += chalk.red("[DEPRECATED] ");
|
|
178
|
+
if (banned) prefix += banned.severity === "error" ? chalk.red("[BANNED] ") : chalk.yellow("[RESTRICTED] ");
|
|
179
|
+
else if (pkg.internal) prefix += chalk.yellow("[int] ");
|
|
180
|
+
return prefix + pkg.packageName;
|
|
287
181
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
182
|
+
function formatUpgradeCell(releaseAge) {
|
|
183
|
+
if (!releaseAge) return "";
|
|
184
|
+
const { worstLevel, upgrades, severity } = releaseAge;
|
|
185
|
+
if (!worstLevel) return chalk.green("ā");
|
|
186
|
+
const top = upgrades[0];
|
|
187
|
+
if (!top) return chalk.green("ā");
|
|
188
|
+
const suffix = severity === "warn" ? chalk.gray(" [not enforced]") : "";
|
|
189
|
+
if (worstLevel === "mandatory_upgrade") return (severity === "warn" ? chalk.yellow : chalk.red)(`ā ${top.semverBump} ${top.version} (${top.releasedDaysAgo}d)`) + suffix;
|
|
190
|
+
return chalk.yellow(`ā ${top.semverBump} ${top.version} (${top.releasedDaysAgo}d)`) + suffix;
|
|
295
191
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Analyzes variable declarations for component assignments
|
|
300
|
-
*/
|
|
301
|
-
function analyzeVariableDeclaration(node, state) {
|
|
302
|
-
if (!node.declarations) return;
|
|
303
|
-
for (const decl of node.declarations) {
|
|
304
|
-
if (decl.id?.type === "Identifier") {
|
|
305
|
-
const varName = decl.id.value;
|
|
306
|
-
if (decl.init) {
|
|
307
|
-
const assignment = extractAssignmentInfo(decl.init);
|
|
308
|
-
if (assignment && isKnownComponent(assignment, state)) {
|
|
309
|
-
state.usagePatterns.variableAssignments.set(varName, {
|
|
310
|
-
assignment,
|
|
311
|
-
line: node.span?.start || 0
|
|
312
|
-
});
|
|
313
|
-
state.componentNames.add(varName);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
if (decl.id?.type === "ObjectPattern") analyzeDestructuringPattern(decl.id, decl.init, state);
|
|
318
|
-
}
|
|
192
|
+
function getBannedViolation(pkg, violations) {
|
|
193
|
+
return violations.find((v) => v.packageName === pkg.packageName);
|
|
319
194
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
line: pattern.span?.start || 0
|
|
332
|
-
});
|
|
333
|
-
state.componentNames.add(propName);
|
|
334
|
-
}
|
|
195
|
+
function printPackages(aggregated, mode) {
|
|
196
|
+
const packages = aggregated.packageDistribution;
|
|
197
|
+
const violations = aggregated.bannedPackageViolations;
|
|
198
|
+
if (mode === "table") printPackagesTable(packages, violations);
|
|
199
|
+
else if (mode === "chart") printPackagesChart(packages, violations);
|
|
200
|
+
}
|
|
201
|
+
function printPackagesTable(packages, violations) {
|
|
202
|
+
printHeader();
|
|
203
|
+
if (packages.length === 0) {
|
|
204
|
+
console.log(chalk.gray(" No packages found"));
|
|
205
|
+
return;
|
|
335
206
|
}
|
|
207
|
+
const hasReleaseAge = packages.some((p) => p.releaseAge !== void 0);
|
|
208
|
+
const head = [
|
|
209
|
+
"Package",
|
|
210
|
+
"Version",
|
|
211
|
+
"Components",
|
|
212
|
+
"Usage",
|
|
213
|
+
"Percentage"
|
|
214
|
+
];
|
|
215
|
+
if (hasReleaseAge) head.push("Upgrades");
|
|
216
|
+
const table = new Table({
|
|
217
|
+
head,
|
|
218
|
+
style: {
|
|
219
|
+
head: ["cyan"],
|
|
220
|
+
border: ["gray"]
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
packages.forEach((pkg) => {
|
|
224
|
+
const versionCell = pkg.hasVersionConflict ? chalk.yellow(`ā ${pkg.allVersions.join(", ")} (multiple ā bundle impact)`) : pkg.version || "N/A";
|
|
225
|
+
const row = [
|
|
226
|
+
formatPackageName(pkg, getBannedViolation(pkg, violations)),
|
|
227
|
+
versionCell,
|
|
228
|
+
formatCount(pkg.componentCount),
|
|
229
|
+
formatCount(pkg.usageCount),
|
|
230
|
+
`${pkg.percentage.toFixed(1)}%`
|
|
231
|
+
];
|
|
232
|
+
if (hasReleaseAge) row.push(formatUpgradeCell(pkg.releaseAge));
|
|
233
|
+
table.push(row);
|
|
234
|
+
});
|
|
235
|
+
console.log(table.toString());
|
|
236
|
+
const totalComponents = packages.reduce((sum, p) => sum + p.componentCount, 0);
|
|
237
|
+
const totalExternalUsage = packages.reduce((sum, p) => sum + p.usageCount, 0);
|
|
238
|
+
console.log(chalk.gray(`\nTotal: ${formatCount(packages.length)} packages | ${formatCount(totalComponents)} unique components | ${formatCount(totalExternalUsage)} total usages`));
|
|
336
239
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
case "Identifier": return node.value;
|
|
343
|
-
case "MemberExpression": return `${extractAssignmentInfo(node.object)}.${node.property.value}`;
|
|
344
|
-
case "ConditionalExpression": return `${extractAssignmentInfo(node.consequent)} | ${extractAssignmentInfo(node.alternate)}`;
|
|
345
|
-
default: return null;
|
|
240
|
+
function printPackagesChart(packages, violations) {
|
|
241
|
+
printHeader();
|
|
242
|
+
if (packages.length === 0) {
|
|
243
|
+
console.log(chalk.gray(" No packages found"));
|
|
244
|
+
return;
|
|
346
245
|
}
|
|
246
|
+
const maxBarWidth = 40;
|
|
247
|
+
const maxPercentage = Math.max(...packages.map((p) => p.percentage));
|
|
248
|
+
const maxLabelLength = Math.max(...packages.map((p) => p.packageName.length + (p.internal ? 6 : 0)));
|
|
249
|
+
packages.forEach((pkg) => {
|
|
250
|
+
const barLength = Math.round(pkg.percentage / maxPercentage * maxBarWidth);
|
|
251
|
+
const emptyLength = maxBarWidth - barLength;
|
|
252
|
+
const label = formatPackageName(pkg, getBannedViolation(pkg, violations)).padEnd(maxLabelLength, " ");
|
|
253
|
+
const bar = chalk.green("ā".repeat(barLength)) + chalk.gray("ā".repeat(emptyLength));
|
|
254
|
+
console.log(`${label} ${bar} ${chalk.bold(pkg.percentage.toFixed(1) + "%")} (${pkg.usageCount})`);
|
|
255
|
+
});
|
|
347
256
|
}
|
|
348
257
|
//#endregion
|
|
349
|
-
//#region src/
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const alternate = node.alternate?.type === "Identifier" ? node.alternate.value : null;
|
|
356
|
-
if (consequent && state.componentNames.has(consequent) || alternate && state.componentNames.has(alternate)) state.usagePatterns.conditionalUsage.add({
|
|
357
|
-
consequent: consequent || "",
|
|
358
|
-
alternate: alternate || "",
|
|
359
|
-
line: node.span?.start || 0
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
//#endregion
|
|
363
|
-
//#region src/swc-parser/patterns/collections.ts
|
|
364
|
-
/**
|
|
365
|
-
* Analyzes array expressions containing components
|
|
366
|
-
*/
|
|
367
|
-
function analyzeArrayExpression(node, state) {
|
|
368
|
-
if (node.elements?.some((elem) => {
|
|
369
|
-
if (elem?.type === "Identifier") return state.componentNames.has(elem.value);
|
|
370
|
-
return false;
|
|
371
|
-
})) state.usagePatterns.arrayMappings.add({
|
|
372
|
-
components: node.elements?.map((elem) => elem?.value).filter(Boolean),
|
|
373
|
-
line: node.span?.start || 0
|
|
374
|
-
});
|
|
258
|
+
//#region src/utils/print-versus.ts
|
|
259
|
+
const BAR_WIDTH = 30;
|
|
260
|
+
function renderBar(percentage) {
|
|
261
|
+
const filled = Math.round(percentage / 100 * BAR_WIDTH);
|
|
262
|
+
const empty = BAR_WIDTH - filled;
|
|
263
|
+
return chalk.cyan("ā".repeat(filled)) + chalk.gray("ā".repeat(empty));
|
|
375
264
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
return false;
|
|
383
|
-
});
|
|
384
|
-
if (componentProps?.length > 0) state.usagePatterns.objectMappings.add({
|
|
385
|
-
mappings: componentProps.map((prop) => ({
|
|
386
|
-
key: prop.key?.value || "[computed]",
|
|
387
|
-
component: prop.value?.value
|
|
388
|
-
})),
|
|
389
|
-
line: node.span?.start || 0
|
|
390
|
-
});
|
|
265
|
+
function formatComponents(components, max = 3) {
|
|
266
|
+
if (components.length === 0) return "";
|
|
267
|
+
const shown = components.slice(0, max);
|
|
268
|
+
const rest = components.length - max;
|
|
269
|
+
const list = shown.join(", ");
|
|
270
|
+
return rest > 0 ? `${list} (+${rest} more)` : list;
|
|
391
271
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (source) state.usagePatterns.lazyImports.add({
|
|
404
|
-
source,
|
|
405
|
-
line: node.span?.start || 0
|
|
406
|
-
});
|
|
407
|
-
}
|
|
272
|
+
function printVersusResult(result) {
|
|
273
|
+
console.log(chalk.bold(` ${result.name}`));
|
|
274
|
+
console.log(chalk.gray(` ${"ā".repeat(50)}`));
|
|
275
|
+
const maxNameLen = Math.max(...result.entries.map((e) => e.packageName.length));
|
|
276
|
+
for (const entry of result.entries) {
|
|
277
|
+
const name = entry.packageName.padEnd(maxNameLen);
|
|
278
|
+
const bar = renderBar(entry.percentage);
|
|
279
|
+
const pct = chalk.bold(`${entry.percentage.toFixed(1)}%`);
|
|
280
|
+
const usage = chalk.gray(`(${entry.count} usages)`);
|
|
281
|
+
const components = entry.components.length > 0 ? chalk.gray(` ${formatComponents(entry.components)}`) : "";
|
|
282
|
+
console.log(` ${name} ${bar} ${pct} ${usage}${components}`);
|
|
408
283
|
}
|
|
284
|
+
if (result.totalCount === 0) console.log(chalk.gray(" No usage detected for any package in this group."));
|
|
285
|
+
console.log();
|
|
409
286
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const source = node.arguments?.[0]?.value;
|
|
415
|
-
if (source) state.usagePatterns.dynamicImports.add({
|
|
416
|
-
source,
|
|
417
|
-
line: node.span?.start || 0
|
|
418
|
-
});
|
|
287
|
+
function printVersus(aggregated) {
|
|
288
|
+
if (aggregated.versusResults.length === 0) return;
|
|
289
|
+
console.log(chalk.magentaBright.bold("\nāļø Versus\n"));
|
|
290
|
+
for (const result of aggregated.versusResults) printVersusResult(result);
|
|
419
291
|
}
|
|
420
292
|
//#endregion
|
|
421
|
-
//#region src/
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Analyzes React.memo() usage
|
|
434
|
-
*/
|
|
435
|
-
function analyzeMemoUsage(node, state) {
|
|
436
|
-
const component = node.arguments?.[0];
|
|
437
|
-
if (component?.type === "Identifier" && state.componentNames.has(component.value)) state.usagePatterns.memoizedComponents.add({
|
|
438
|
-
component: component.value,
|
|
439
|
-
line: node.span?.start || 0
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
/**
|
|
443
|
-
* Analyzes React.forwardRef() usage
|
|
444
|
-
*/
|
|
445
|
-
function analyzeForwardRefUsage(node, state) {
|
|
446
|
-
state.usagePatterns.forwardedRefs.add({ line: node.span?.start || 0 });
|
|
293
|
+
//#region src/utils/print-rules.ts
|
|
294
|
+
function formatRuleType(type) {
|
|
295
|
+
switch (type) {
|
|
296
|
+
case "detect_files": return "detect_files";
|
|
297
|
+
case "require_files": return "require_files";
|
|
298
|
+
case "forbid_packages": return "forbid_packages";
|
|
299
|
+
case "require_packages": return "require_packages";
|
|
300
|
+
case "require_scripts": return "require_scripts";
|
|
301
|
+
case "require_package_fields": return "pkg_fields";
|
|
302
|
+
case "engine_version": return "engine_version";
|
|
303
|
+
}
|
|
447
304
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
state.usagePatterns.portalUsage.add({ line: node.span?.start || 0 });
|
|
305
|
+
function ruleIcon(violation) {
|
|
306
|
+
if (violation.severity === "error") return chalk.red("ā");
|
|
307
|
+
if (violation.severity === "info") return chalk.blue("ā¹");
|
|
308
|
+
return chalk.yellow("ā ");
|
|
453
309
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
310
|
+
function describeViolation(v) {
|
|
311
|
+
const patterns = v.patterns.join(", ");
|
|
312
|
+
const suffix = v.message ? chalk.gray(` ā ${v.message}`) : "";
|
|
313
|
+
if (v.type === "detect_files") return `${patterns} detected (${v.matchedFiles.map((f) => {
|
|
314
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
315
|
+
return parts[parts.length - 1];
|
|
316
|
+
}).join(", ")})${suffix}`;
|
|
317
|
+
if (v.type === "require_files") return `${patterns} not found${suffix}`;
|
|
318
|
+
if (v.type === "require_packages") return `${patterns} not installed${suffix}`;
|
|
319
|
+
if (v.type === "forbid_packages") return `${patterns} is forbidden${suffix}`;
|
|
320
|
+
if (v.type === "require_scripts") return `script ${patterns} missing in package.json${suffix}`;
|
|
321
|
+
if (v.type === "require_package_fields") return `field ${patterns} missing in package.json${suffix}`;
|
|
322
|
+
if (v.type === "engine_version") {
|
|
323
|
+
if (!v.installedRange) return `engines.node not specified (required ${v.requiredRange})${suffix}`;
|
|
324
|
+
return `engines.node is ${chalk.yellow(v.installedRange)}, required ${chalk.cyan(v.requiredRange)}${suffix}`;
|
|
461
325
|
}
|
|
326
|
+
return `${patterns} not present${suffix}`;
|
|
462
327
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
328
|
+
function printRules(aggregated) {
|
|
329
|
+
const { ruleViolations, bannedPackageViolations } = aggregated;
|
|
330
|
+
const hasRuleViolations = ruleViolations.length > 0;
|
|
331
|
+
const hasBannedViolations = bannedPackageViolations.length > 0;
|
|
332
|
+
if (!hasRuleViolations && !hasBannedViolations) {
|
|
333
|
+
console.log(chalk.greenBright.bold("\nā Compliance\n"));
|
|
334
|
+
console.log(chalk.gray(" All compliance checks passed"));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
console.log(chalk.blueBright.bold("\nš Compliance\n"));
|
|
338
|
+
if (hasRuleViolations) for (const v of ruleViolations) {
|
|
339
|
+
const icon = ruleIcon(v);
|
|
340
|
+
const type = chalk.gray(formatRuleType(v.type).padEnd(14));
|
|
341
|
+
const severityTag = v.severity === "error" ? chalk.red("[ERROR]") : v.severity === "info" ? chalk.blue("[INFO]") : chalk.yellow("[WARN]");
|
|
342
|
+
console.log(` ${icon} ${type} ${describeViolation(v)} ${severityTag}`);
|
|
343
|
+
}
|
|
344
|
+
if (hasBannedViolations) {
|
|
345
|
+
if (hasRuleViolations) console.log();
|
|
346
|
+
for (const v of bannedPackageViolations) {
|
|
347
|
+
const icon = v.severity === "error" ? chalk.red("ā") : chalk.yellow("ā ");
|
|
348
|
+
const tag = v.severity === "error" ? chalk.red("[BANNED]") : chalk.yellow("[RESTRICTED]");
|
|
349
|
+
const msg = v.message ? chalk.gray(` ā ${v.message}`) : "";
|
|
350
|
+
console.log(` ${icon} ${tag} ${v.packageName}${msg}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const errorCount = [...ruleViolations.filter((v) => v.severity === "error"), ...bannedPackageViolations.filter((v) => v.severity === "error")].length;
|
|
354
|
+
const warnCount = [...ruleViolations.filter((v) => v.severity === "warn"), ...bannedPackageViolations.filter((v) => v.severity === "warn")].length;
|
|
355
|
+
const parts = [];
|
|
356
|
+
if (errorCount > 0) parts.push(chalk.red(`${errorCount} error${errorCount > 1 ? "s" : ""}`));
|
|
357
|
+
if (warnCount > 0) parts.push(chalk.yellow(`${warnCount} warning${warnCount > 1 ? "s" : ""}`));
|
|
358
|
+
console.log(chalk.gray(`\n ${parts.join(", ")}`));
|
|
468
359
|
}
|
|
469
360
|
//#endregion
|
|
470
|
-
//#region src/
|
|
361
|
+
//#region src/utils/version.ts
|
|
362
|
+
let cachedVersion;
|
|
471
363
|
/**
|
|
472
|
-
*
|
|
364
|
+
* Walks up from `dir` to find the nearest package.json.
|
|
365
|
+
* Needed because this module's own path differs between the tsdown-bundled
|
|
366
|
+
* single-file `dist/cli.mjs` and the unbundled source used by tests.
|
|
473
367
|
*/
|
|
474
|
-
function
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
parent: node
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
break;
|
|
486
|
-
case "ImportDeclaration":
|
|
487
|
-
analyzeImportDeclaration(node, state);
|
|
488
|
-
break;
|
|
489
|
-
case "CallExpression":
|
|
490
|
-
analyzeCallExpression(node, state, context);
|
|
491
|
-
break;
|
|
492
|
-
case "VariableDeclaration":
|
|
493
|
-
analyzeVariableDeclaration(node, state);
|
|
494
|
-
visitChildren(node, state, context);
|
|
495
|
-
break;
|
|
496
|
-
case "JSXElement":
|
|
497
|
-
case "JSXFragment":
|
|
498
|
-
analyzeJSXElement(node, state);
|
|
499
|
-
visitChildren(node, state, context);
|
|
500
|
-
break;
|
|
501
|
-
case "JSXOpeningElement":
|
|
502
|
-
analyzeJSXOpeningElement(node, state, context.parent);
|
|
503
|
-
break;
|
|
504
|
-
case "ArrayExpression":
|
|
505
|
-
analyzeArrayExpression(node, state);
|
|
506
|
-
visitChildren(node, state, context);
|
|
507
|
-
break;
|
|
508
|
-
case "ObjectExpression":
|
|
509
|
-
analyzeObjectExpression(node, state);
|
|
510
|
-
visitChildren(node, state, context);
|
|
511
|
-
break;
|
|
512
|
-
case "MemberExpression":
|
|
513
|
-
analyzeMemberExpression(node, state);
|
|
514
|
-
visitChildren(node, state, context);
|
|
515
|
-
break;
|
|
516
|
-
case "ConditionalExpression":
|
|
517
|
-
analyzeConditionalExpression(node, state);
|
|
518
|
-
visitChildren(node, state, context);
|
|
519
|
-
break;
|
|
520
|
-
case "FunctionDeclaration":
|
|
521
|
-
case "ClassDeclaration":
|
|
522
|
-
case "ExpressionStatement":
|
|
523
|
-
case "ReturnStatement":
|
|
524
|
-
case "VariableDeclarator":
|
|
525
|
-
case "ArrowFunctionExpression":
|
|
526
|
-
case "FunctionExpression":
|
|
527
|
-
visitChildren(node, state, {
|
|
528
|
-
...context,
|
|
529
|
-
parent: node
|
|
530
|
-
});
|
|
531
|
-
break;
|
|
532
|
-
default:
|
|
533
|
-
visitChildren(node, state, context);
|
|
534
|
-
break;
|
|
368
|
+
function findPackageJson(dir) {
|
|
369
|
+
let current = dir;
|
|
370
|
+
while (true) {
|
|
371
|
+
const candidate = path.join(current, "package.json");
|
|
372
|
+
if (existsSync(candidate)) return candidate;
|
|
373
|
+
const parent = path.dirname(current);
|
|
374
|
+
if (parent === current) throw new Error(`Could not locate package.json above ${dir}`);
|
|
375
|
+
current = parent;
|
|
535
376
|
}
|
|
536
377
|
}
|
|
537
|
-
|
|
538
|
-
|
|
378
|
+
function getVersion() {
|
|
379
|
+
if (cachedVersion) return cachedVersion;
|
|
380
|
+
const pkgPath = findPackageJson(path.dirname(fileURLToPath(import.meta.url)));
|
|
381
|
+
cachedVersion = JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
382
|
+
return cachedVersion;
|
|
383
|
+
}
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region src/utils/print-json.ts
|
|
386
|
+
function printJson(aggregated) {
|
|
387
|
+
const result = {
|
|
388
|
+
version: getVersion(),
|
|
389
|
+
summary: {
|
|
390
|
+
filesAnalyzed: aggregated.filesAnalyzed,
|
|
391
|
+
totalImports: aggregated.totalImports,
|
|
392
|
+
totalComponents: aggregated.totalComponents,
|
|
393
|
+
totalUsagePatterns: aggregated.totalUsagePatterns
|
|
394
|
+
},
|
|
395
|
+
packages: aggregated.packageDistribution,
|
|
396
|
+
components: aggregated.topComponents.map((c) => ({
|
|
397
|
+
...c,
|
|
398
|
+
files: [...c.files]
|
|
399
|
+
})),
|
|
400
|
+
patterns: aggregated.patternCounts,
|
|
401
|
+
versus: aggregated.versusResults,
|
|
402
|
+
ruleViolations: aggregated.ruleViolations,
|
|
403
|
+
bannedPackageViolations: aggregated.bannedPackageViolations
|
|
404
|
+
};
|
|
405
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
406
|
+
}
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/config/schema.ts
|
|
409
|
+
const RuleSeveritySchema = z.enum([
|
|
410
|
+
"error",
|
|
411
|
+
"warn",
|
|
412
|
+
"info"
|
|
413
|
+
]);
|
|
414
|
+
const RuleConfigSchema = z.object({
|
|
415
|
+
severity: RuleSeveritySchema,
|
|
416
|
+
patterns: z.array(z.string()),
|
|
417
|
+
message: z.string().optional()
|
|
418
|
+
});
|
|
419
|
+
const RuleConfigOrArraySchema = z.union([RuleConfigSchema, z.array(RuleConfigSchema)]);
|
|
420
|
+
const EngineVersionRuleSchema = z.object({
|
|
421
|
+
severity: RuleSeveritySchema,
|
|
422
|
+
range: z.string(),
|
|
423
|
+
message: z.string().optional()
|
|
424
|
+
});
|
|
425
|
+
const ThresholdSchema = z.union([z.number(), z.literal(false)]);
|
|
426
|
+
const HermexConfigSchema = z.object({
|
|
427
|
+
includes: z.array(z.string()).default(["**/*.{tsx,jsx,ts,js}"]),
|
|
428
|
+
excludes: z.array(z.string()).default([
|
|
429
|
+
"**/node_modules/**",
|
|
430
|
+
"**/dist/**",
|
|
431
|
+
"**/build/**"
|
|
432
|
+
]),
|
|
433
|
+
packages: z.object({
|
|
434
|
+
internal: z.array(z.string()).default([]),
|
|
435
|
+
ignore: z.array(z.string()).default([])
|
|
436
|
+
}).default(() => ({
|
|
437
|
+
internal: [],
|
|
438
|
+
ignore: []
|
|
439
|
+
})),
|
|
440
|
+
versus: z.array(z.object({
|
|
441
|
+
name: z.string(),
|
|
442
|
+
packages: z.array(z.string()).min(2)
|
|
443
|
+
})).default([]),
|
|
444
|
+
rules: z.object({
|
|
445
|
+
detect_files: RuleConfigOrArraySchema.default([]),
|
|
446
|
+
require_files: RuleConfigOrArraySchema.default([]),
|
|
447
|
+
forbid_packages: RuleConfigOrArraySchema.default([]),
|
|
448
|
+
require_packages: RuleConfigOrArraySchema.default([]),
|
|
449
|
+
require_scripts: RuleConfigOrArraySchema.default([]),
|
|
450
|
+
require_package_fields: RuleConfigOrArraySchema.default([]),
|
|
451
|
+
engine_version: z.union([EngineVersionRuleSchema, z.array(EngineVersionRuleSchema)]).optional()
|
|
452
|
+
}).default(() => ({
|
|
453
|
+
detect_files: [],
|
|
454
|
+
require_files: [],
|
|
455
|
+
forbid_packages: [],
|
|
456
|
+
require_packages: [],
|
|
457
|
+
require_scripts: [],
|
|
458
|
+
require_package_fields: []
|
|
459
|
+
})),
|
|
460
|
+
output: z.object({
|
|
461
|
+
summary: z.union([z.literal("log"), z.literal(false)]).default("log"),
|
|
462
|
+
components: z.union([z.enum(["table", "chart"]), z.literal(false)]).default("table"),
|
|
463
|
+
packages: z.union([z.enum(["table", "chart"]), z.literal(false)]).default("table"),
|
|
464
|
+
patterns: z.union([z.enum(["table", "chart"]), z.literal(false)]).default("table"),
|
|
465
|
+
details: z.boolean().default(false),
|
|
466
|
+
versus: z.boolean().default(true),
|
|
467
|
+
rules: z.boolean().default(true),
|
|
468
|
+
format: z.enum(["human", "json"]).default("human")
|
|
469
|
+
}).default(() => ({
|
|
470
|
+
summary: "log",
|
|
471
|
+
components: "table",
|
|
472
|
+
packages: "table",
|
|
473
|
+
patterns: "table",
|
|
474
|
+
details: false,
|
|
475
|
+
versus: true,
|
|
476
|
+
rules: true,
|
|
477
|
+
format: "human"
|
|
478
|
+
})),
|
|
479
|
+
releaseAge: z.object({
|
|
480
|
+
enabled: z.boolean().default(false),
|
|
481
|
+
registry: z.string().default("https://registry.npmjs.org"),
|
|
482
|
+
authToken: z.string().optional(),
|
|
483
|
+
thresholds: z.object({
|
|
484
|
+
patch: ThresholdSchema.default(30),
|
|
485
|
+
minor: ThresholdSchema.default(45),
|
|
486
|
+
major: ThresholdSchema.default(60)
|
|
487
|
+
}).default(() => ({
|
|
488
|
+
patch: 30,
|
|
489
|
+
minor: 45,
|
|
490
|
+
major: 60
|
|
491
|
+
})),
|
|
492
|
+
enforceOn: z.array(z.string()).default([])
|
|
493
|
+
}).default(() => ({
|
|
494
|
+
enabled: false,
|
|
495
|
+
registry: "https://registry.npmjs.org",
|
|
496
|
+
thresholds: {
|
|
497
|
+
patch: 30,
|
|
498
|
+
minor: 45,
|
|
499
|
+
major: 60
|
|
500
|
+
},
|
|
501
|
+
enforceOn: []
|
|
502
|
+
}))
|
|
503
|
+
});
|
|
504
|
+
//#endregion
|
|
505
|
+
//#region src/config/loader.ts
|
|
506
|
+
async function loadConfig(cwd, explicitPath) {
|
|
507
|
+
const configPath = explicitPath ? resolve(explicitPath) : join(cwd, "hermex.config.ts");
|
|
508
|
+
if (explicitPath && !existsSync(configPath)) throw new Error(`Config file not found: ${configPath}`);
|
|
509
|
+
if (existsSync(configPath)) {
|
|
510
|
+
const mod = await import(pathToFileURL(configPath).href);
|
|
511
|
+
return HermexConfigSchema.parse(mod.default ?? mod);
|
|
512
|
+
}
|
|
513
|
+
return HermexConfigSchema.parse({});
|
|
514
|
+
}
|
|
515
|
+
//#endregion
|
|
516
|
+
//#region src/swc-parser/core/state.ts
|
|
517
|
+
function createState() {
|
|
518
|
+
return {
|
|
519
|
+
usagePatterns: {
|
|
520
|
+
namedImports: /* @__PURE__ */ new Set(),
|
|
521
|
+
namespaceImports: /* @__PURE__ */ new Set(),
|
|
522
|
+
defaultImports: /* @__PURE__ */ new Set(),
|
|
523
|
+
aliasedImports: /* @__PURE__ */ new Map(),
|
|
524
|
+
variableAssignments: /* @__PURE__ */ new Map(),
|
|
525
|
+
lazyImports: /* @__PURE__ */ new Set(),
|
|
526
|
+
dynamicImports: /* @__PURE__ */ new Set(),
|
|
527
|
+
conditionalUsage: /* @__PURE__ */ new Set(),
|
|
528
|
+
arrayMappings: /* @__PURE__ */ new Set(),
|
|
529
|
+
objectMappings: /* @__PURE__ */ new Set(),
|
|
530
|
+
hocUsage: /* @__PURE__ */ new Set(),
|
|
531
|
+
forwardedRefs: /* @__PURE__ */ new Set(),
|
|
532
|
+
memoizedComponents: /* @__PURE__ */ new Set(),
|
|
533
|
+
portalUsage: /* @__PURE__ */ new Set(),
|
|
534
|
+
jsxUsage: /* @__PURE__ */ new Map(),
|
|
535
|
+
destructuredUsage: /* @__PURE__ */ new Set(),
|
|
536
|
+
propsAnalysis: /* @__PURE__ */ new Map()
|
|
537
|
+
},
|
|
538
|
+
componentNames: /* @__PURE__ */ new Set(),
|
|
539
|
+
allIdentifiers: /* @__PURE__ */ new Set()
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/swc-parser/patterns/imports.ts
|
|
544
|
+
/**
|
|
545
|
+
* Analyzes import declarations and tracks all types:
|
|
546
|
+
* - Default imports
|
|
547
|
+
* - Named imports
|
|
548
|
+
* - Namespace imports
|
|
549
|
+
* - Aliased imports
|
|
550
|
+
*/
|
|
551
|
+
function analyzeImportDeclaration(node, state) {
|
|
552
|
+
const source = node.source.value;
|
|
553
|
+
for (const spec of node.specifiers) switch (spec.type) {
|
|
554
|
+
case "ImportDefaultSpecifier":
|
|
555
|
+
analyzeDefaultImport(spec, source, node, state);
|
|
556
|
+
break;
|
|
557
|
+
case "ImportNamespaceSpecifier":
|
|
558
|
+
analyzeNamespaceImport(spec, source, node, state);
|
|
559
|
+
break;
|
|
560
|
+
case "ImportSpecifier":
|
|
561
|
+
analyzeNamedImport(spec, source, node, state);
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function analyzeDefaultImport(spec, source, node, state) {
|
|
566
|
+
const name = spec.local.value;
|
|
567
|
+
state.usagePatterns.defaultImports.add({
|
|
568
|
+
name,
|
|
569
|
+
source,
|
|
570
|
+
line: node.span?.start || 0
|
|
571
|
+
});
|
|
572
|
+
state.componentNames.add(name);
|
|
573
|
+
}
|
|
574
|
+
function analyzeNamespaceImport(spec, source, node, state) {
|
|
575
|
+
const name = spec.local.value;
|
|
576
|
+
state.usagePatterns.namespaceImports.add({
|
|
577
|
+
name,
|
|
578
|
+
source,
|
|
579
|
+
line: node.span?.start || 0
|
|
580
|
+
});
|
|
581
|
+
state.allIdentifiers.add(name);
|
|
582
|
+
}
|
|
583
|
+
function analyzeNamedImport(spec, source, node, state) {
|
|
584
|
+
const importedName = spec.imported ? spec.imported.value : spec.local.value;
|
|
585
|
+
const localName = spec.local.value;
|
|
586
|
+
state.usagePatterns.namedImports.add({
|
|
587
|
+
name: importedName,
|
|
588
|
+
source,
|
|
589
|
+
line: node.span?.start || 0
|
|
590
|
+
});
|
|
591
|
+
if (importedName !== localName) state.usagePatterns.aliasedImports.set(localName, {
|
|
592
|
+
imported: importedName,
|
|
593
|
+
local: localName,
|
|
594
|
+
source,
|
|
595
|
+
line: node.span?.start || 0
|
|
596
|
+
});
|
|
597
|
+
state.componentNames.add(localName);
|
|
598
|
+
}
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/swc-parser/utils/jsx-helpers.ts
|
|
601
|
+
/**
|
|
602
|
+
* Extracts the name from a JSX element (handles identifiers and member expressions)
|
|
603
|
+
*/
|
|
604
|
+
function getJSXElementName(nameNode) {
|
|
605
|
+
if (!nameNode) return "";
|
|
606
|
+
switch (nameNode.type) {
|
|
607
|
+
case "Identifier": return nameNode.value;
|
|
608
|
+
case "JSXMemberExpression": return `${getJSXElementName(nameNode.object)}.${nameNode.property.value}`;
|
|
609
|
+
default: return "";
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Checks if a JSX member expression is a known component
|
|
614
|
+
*/
|
|
615
|
+
function isMemberExpressionComponent(nameNode, state) {
|
|
616
|
+
if (nameNode?.type === "JSXMemberExpression") {
|
|
617
|
+
const objectName = getJSXElementName(nameNode.object);
|
|
618
|
+
return state.allIdentifiers.has(objectName);
|
|
619
|
+
}
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Extracts props from JSX attributes
|
|
624
|
+
*/
|
|
625
|
+
function extractJSXProps(attributes) {
|
|
626
|
+
if (!attributes) return [];
|
|
627
|
+
return attributes.map((attr) => {
|
|
628
|
+
if (attr.type === "JSXAttribute") return {
|
|
629
|
+
name: attr.name?.value || attr.name?.name?.value,
|
|
630
|
+
value: extractJSXAttributeValue(attr.value)
|
|
631
|
+
};
|
|
632
|
+
if (attr.type === "SpreadElement") return {
|
|
633
|
+
name: "...",
|
|
634
|
+
value: "[spread]",
|
|
635
|
+
isSpread: true
|
|
636
|
+
};
|
|
637
|
+
return null;
|
|
638
|
+
}).filter(Boolean);
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Extracts value from JSX attribute
|
|
642
|
+
*/
|
|
643
|
+
function extractJSXAttributeValue(value) {
|
|
644
|
+
if (!value) return true;
|
|
645
|
+
switch (value.type) {
|
|
646
|
+
case "StringLiteral": return value.value;
|
|
647
|
+
case "JSXExpressionContainer": return extractExpressionValue(value.expression);
|
|
648
|
+
default: return "[complex]";
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Extracts a readable value from an expression
|
|
653
|
+
*/
|
|
654
|
+
function extractExpressionValue(expr) {
|
|
655
|
+
if (!expr) return "[unknown]";
|
|
656
|
+
switch (expr.type) {
|
|
657
|
+
case "StringLiteral":
|
|
658
|
+
case "NumericLiteral":
|
|
659
|
+
case "BooleanLiteral": return expr.value;
|
|
660
|
+
case "Identifier": return `{${expr.value}}`;
|
|
661
|
+
case "ArrowFunctionExpression":
|
|
662
|
+
case "FunctionExpression": return "[function]";
|
|
663
|
+
case "ObjectExpression": return "[object]";
|
|
664
|
+
case "ArrayExpression": return "[array]";
|
|
665
|
+
default: return "[expression]";
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Determines the context where a component is being used
|
|
670
|
+
*/
|
|
671
|
+
function getUsageContext(parent) {
|
|
672
|
+
if (!parent) return "direct";
|
|
673
|
+
switch (parent.type) {
|
|
674
|
+
case "ConditionalExpression": return "conditional";
|
|
675
|
+
case "ArrayExpression": return "array";
|
|
676
|
+
case "ObjectExpression": return "object";
|
|
677
|
+
case "CallExpression": return "hoc";
|
|
678
|
+
case "VariableDeclarator": return "variable";
|
|
679
|
+
default: return "jsx";
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
//#endregion
|
|
683
|
+
//#region src/swc-parser/patterns/props.ts
|
|
684
|
+
/**
|
|
685
|
+
* Analyzes props in detail for a component
|
|
686
|
+
*/
|
|
687
|
+
function analyzePropsInDetail(attributes, componentName, state) {
|
|
688
|
+
const analysis = {
|
|
689
|
+
namedProps: [],
|
|
690
|
+
hasSpread: false,
|
|
691
|
+
hasComplexProps: false,
|
|
692
|
+
hasEventHandlers: false,
|
|
693
|
+
propDetails: []
|
|
694
|
+
};
|
|
695
|
+
if (!attributes) return analysis;
|
|
696
|
+
for (const attr of attributes) if (attr.type === "JSXAttribute") {
|
|
697
|
+
const propName = attr.name?.value || attr.name?.name?.value;
|
|
698
|
+
if (propName) {
|
|
699
|
+
analysis.namedProps.push(propName);
|
|
700
|
+
const propDetail = {
|
|
701
|
+
name: propName,
|
|
702
|
+
type: getPropType(attr.value),
|
|
703
|
+
isEventHandler: propName.startsWith("on"),
|
|
704
|
+
isComplex: isComplexProp(attr.value)
|
|
705
|
+
};
|
|
706
|
+
if (propDetail.isEventHandler) analysis.hasEventHandlers = true;
|
|
707
|
+
if (propDetail.isComplex) analysis.hasComplexProps = true;
|
|
708
|
+
analysis.propDetails.push(propDetail);
|
|
709
|
+
}
|
|
710
|
+
} else if (attr.type === "SpreadElement") {
|
|
711
|
+
analysis.hasSpread = true;
|
|
712
|
+
analysis.propDetails.push({
|
|
713
|
+
name: "...",
|
|
714
|
+
type: "spread",
|
|
715
|
+
isSpread: true,
|
|
716
|
+
isComplex: true,
|
|
717
|
+
isEventHandler: false,
|
|
718
|
+
warning: "Spread props cannot be statically analyzed"
|
|
719
|
+
});
|
|
720
|
+
analysis.hasComplexProps = true;
|
|
721
|
+
}
|
|
722
|
+
state.usagePatterns.propsAnalysis.set(componentName, analysis);
|
|
723
|
+
return analysis;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Determines the type of a prop value
|
|
727
|
+
*/
|
|
728
|
+
function getPropType(value) {
|
|
729
|
+
if (!value) return "boolean";
|
|
730
|
+
switch (value.type) {
|
|
731
|
+
case "StringLiteral": return "string";
|
|
732
|
+
case "JSXExpressionContainer": {
|
|
733
|
+
const expr = value.expression;
|
|
734
|
+
if (!expr) return "unknown";
|
|
735
|
+
switch (expr.type) {
|
|
736
|
+
case "NumericLiteral": return "number";
|
|
737
|
+
case "BooleanLiteral": return "boolean";
|
|
738
|
+
case "StringLiteral": return "string";
|
|
739
|
+
case "ArrowFunctionExpression":
|
|
740
|
+
case "FunctionExpression": return "function";
|
|
741
|
+
case "ObjectExpression": return "object";
|
|
742
|
+
case "ArrayExpression": return "array";
|
|
743
|
+
case "Identifier": return "variable";
|
|
744
|
+
default: return "expression";
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
default: return "unknown";
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Checks if a prop value is complex (object, array, call, conditional)
|
|
752
|
+
*/
|
|
753
|
+
function isComplexProp(value) {
|
|
754
|
+
if (!value) return false;
|
|
755
|
+
if (value.type === "JSXExpressionContainer") {
|
|
756
|
+
const expr = value.expression;
|
|
757
|
+
if (!expr) return false;
|
|
758
|
+
return expr.type === "ObjectExpression" || expr.type === "ArrayExpression" || expr.type === "CallExpression" || expr.type === "ConditionalExpression";
|
|
759
|
+
}
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
//#endregion
|
|
763
|
+
//#region src/swc-parser/patterns/jsx.ts
|
|
764
|
+
/**
|
|
765
|
+
* Analyzes JSX element usage
|
|
766
|
+
*/
|
|
767
|
+
function analyzeJSXElement(node, state) {
|
|
768
|
+
if (node.opening) analyzeJSXOpeningElement(node.opening, state, node);
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Analyzes JSX opening element and tracks component usage
|
|
772
|
+
*/
|
|
773
|
+
function analyzeJSXOpeningElement(node, state, parent) {
|
|
774
|
+
const elementName = getJSXElementName(node.name);
|
|
775
|
+
if (!state.componentNames.has(elementName) && !isMemberExpressionComponent(node.name, state)) return;
|
|
776
|
+
const propsAnalysis = analyzePropsInDetail(node.attributes, elementName, state);
|
|
777
|
+
const usage = {
|
|
778
|
+
component: elementName,
|
|
779
|
+
props: extractJSXProps(node.attributes).map((p) => p.name),
|
|
780
|
+
propsAnalysis,
|
|
781
|
+
line: node.span?.start || 0,
|
|
782
|
+
context: getUsageContext(parent)
|
|
783
|
+
};
|
|
784
|
+
if (!state.usagePatterns.jsxUsage.has(elementName)) state.usagePatterns.jsxUsage.set(elementName, usage);
|
|
785
|
+
}
|
|
786
|
+
//#endregion
|
|
787
|
+
//#region src/swc-parser/utils/matchers.ts
|
|
788
|
+
/**
|
|
789
|
+
* Checks if a name is a known component from imports
|
|
790
|
+
*/
|
|
791
|
+
function isKnownComponent(name, state) {
|
|
792
|
+
return state.componentNames.has(name) || state.allIdentifiers.has(name);
|
|
793
|
+
}
|
|
794
|
+
//#endregion
|
|
795
|
+
//#region src/swc-parser/patterns/variables.ts
|
|
796
|
+
/**
|
|
797
|
+
* Analyzes variable declarations for component assignments
|
|
798
|
+
*/
|
|
799
|
+
function analyzeVariableDeclaration(node, state) {
|
|
800
|
+
if (!node.declarations) return;
|
|
801
|
+
for (const decl of node.declarations) {
|
|
802
|
+
if (decl.id?.type === "Identifier") {
|
|
803
|
+
const varName = decl.id.value;
|
|
804
|
+
if (decl.init) {
|
|
805
|
+
const assignment = extractAssignmentInfo(decl.init);
|
|
806
|
+
if (assignment && isKnownComponent(assignment, state)) {
|
|
807
|
+
state.usagePatterns.variableAssignments.set(varName, {
|
|
808
|
+
assignment,
|
|
809
|
+
line: node.span?.start || 0
|
|
810
|
+
});
|
|
811
|
+
state.componentNames.add(varName);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (decl.id?.type === "ObjectPattern") analyzeDestructuringPattern(decl.id, decl.init, state);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Analyzes destructuring patterns
|
|
820
|
+
*/
|
|
821
|
+
function analyzeDestructuringPattern(pattern, init, state) {
|
|
822
|
+
if (!pattern.properties) return;
|
|
823
|
+
for (const prop of pattern.properties) if (prop.type === "AssignmentPatternProperty" && prop.key?.type === "Identifier") {
|
|
824
|
+
const propName = prop.key.value;
|
|
825
|
+
if (init?.type === "Identifier" && state.allIdentifiers.has(init.value)) {
|
|
826
|
+
state.usagePatterns.destructuredUsage.add({
|
|
827
|
+
property: propName,
|
|
828
|
+
source: init.value,
|
|
829
|
+
line: pattern.span?.start || 0
|
|
830
|
+
});
|
|
831
|
+
state.componentNames.add(propName);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Extracts assignment information from various node types
|
|
837
|
+
*/
|
|
838
|
+
function extractAssignmentInfo(node) {
|
|
839
|
+
switch (node.type) {
|
|
840
|
+
case "Identifier": return node.value;
|
|
841
|
+
case "MemberExpression": return `${extractAssignmentInfo(node.object)}.${node.property.value}`;
|
|
842
|
+
case "ConditionalExpression": return `${extractAssignmentInfo(node.consequent)} | ${extractAssignmentInfo(node.alternate)}`;
|
|
843
|
+
default: return null;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/swc-parser/patterns/conditionals.ts
|
|
848
|
+
/**
|
|
849
|
+
* Analyzes conditional expressions (ternary operators) with components
|
|
850
|
+
*/
|
|
851
|
+
function analyzeConditionalExpression(node, state) {
|
|
852
|
+
const consequent = node.consequent?.type === "Identifier" ? node.consequent.value : null;
|
|
853
|
+
const alternate = node.alternate?.type === "Identifier" ? node.alternate.value : null;
|
|
854
|
+
if (consequent && state.componentNames.has(consequent) || alternate && state.componentNames.has(alternate)) state.usagePatterns.conditionalUsage.add({
|
|
855
|
+
consequent: consequent || "",
|
|
856
|
+
alternate: alternate || "",
|
|
857
|
+
line: node.span?.start || 0
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/swc-parser/patterns/collections.ts
|
|
862
|
+
/**
|
|
863
|
+
* Analyzes array expressions containing components
|
|
864
|
+
*/
|
|
865
|
+
function analyzeArrayExpression(node, state) {
|
|
866
|
+
if (node.elements?.some((elem) => {
|
|
867
|
+
if (elem?.type === "Identifier") return state.componentNames.has(elem.value);
|
|
868
|
+
return false;
|
|
869
|
+
})) state.usagePatterns.arrayMappings.add({
|
|
870
|
+
components: node.elements?.map((elem) => elem?.value).filter(Boolean),
|
|
871
|
+
line: node.span?.start || 0
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Analyzes object expressions with component mappings
|
|
876
|
+
*/
|
|
877
|
+
function analyzeObjectExpression(node, state) {
|
|
878
|
+
const componentProps = node.properties?.filter((prop) => {
|
|
879
|
+
if (prop.type === "KeyValueProperty" && prop.value?.type === "Identifier") return state.componentNames.has(prop.value.value);
|
|
880
|
+
return false;
|
|
881
|
+
});
|
|
882
|
+
if (componentProps?.length > 0) state.usagePatterns.objectMappings.add({
|
|
883
|
+
mappings: componentProps.map((prop) => ({
|
|
884
|
+
key: prop.key?.value || "[computed]",
|
|
885
|
+
component: prop.value?.value
|
|
886
|
+
})),
|
|
887
|
+
line: node.span?.start || 0
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
//#endregion
|
|
891
|
+
//#region src/swc-parser/patterns/lazy-dynamic.ts
|
|
892
|
+
/**
|
|
893
|
+
* Analyzes React.lazy() imports
|
|
894
|
+
*/
|
|
895
|
+
function analyzeLazyImport(node, state) {
|
|
896
|
+
const arg = node.arguments?.[0];
|
|
897
|
+
if (arg?.type === "ArrowFunctionExpression" && arg.body?.type === "CallExpression") {
|
|
898
|
+
const importCall = arg.body;
|
|
899
|
+
if (importCall.callee?.type === "Import") {
|
|
900
|
+
const source = importCall.arguments?.[0]?.value;
|
|
901
|
+
if (source) state.usagePatterns.lazyImports.add({
|
|
902
|
+
source,
|
|
903
|
+
line: node.span?.start || 0
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Analyzes dynamic import() calls
|
|
910
|
+
*/
|
|
911
|
+
function analyzeDynamicImport(node, state) {
|
|
912
|
+
const source = node.arguments?.[0]?.value;
|
|
913
|
+
if (source) state.usagePatterns.dynamicImports.add({
|
|
914
|
+
source,
|
|
915
|
+
line: node.span?.start || 0
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/swc-parser/patterns/advanced.ts
|
|
920
|
+
/**
|
|
921
|
+
* Analyzes Higher-Order Component (HOC) usage
|
|
922
|
+
*/
|
|
923
|
+
function analyzeHOCUsage(node, state) {
|
|
924
|
+
state.usagePatterns.hocUsage.add({
|
|
925
|
+
function: node.callee?.value || "[unknown]",
|
|
926
|
+
component: node.arguments?.[0]?.value || "[unknown]",
|
|
927
|
+
line: node.span?.start || 0
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Analyzes React.memo() usage
|
|
932
|
+
*/
|
|
933
|
+
function analyzeMemoUsage(node, state) {
|
|
934
|
+
const component = node.arguments?.[0];
|
|
935
|
+
if (component?.type === "Identifier" && state.componentNames.has(component.value)) state.usagePatterns.memoizedComponents.add({
|
|
936
|
+
component: component.value,
|
|
937
|
+
line: node.span?.start || 0
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Analyzes React.forwardRef() usage
|
|
942
|
+
*/
|
|
943
|
+
function analyzeForwardRefUsage(node, state) {
|
|
944
|
+
state.usagePatterns.forwardedRefs.add({ line: node.span?.start || 0 });
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Analyzes ReactDOM.createPortal() usage
|
|
948
|
+
*/
|
|
949
|
+
function analyzePortalUsage(node, state) {
|
|
950
|
+
state.usagePatterns.portalUsage.add({ line: node.span?.start || 0 });
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Analyzes member expression access (e.g., Foundation.Button)
|
|
954
|
+
*/
|
|
955
|
+
function analyzeMemberExpression(node, state) {
|
|
956
|
+
if (node.object?.type === "Identifier" && state.allIdentifiers.has(node.object.value)) {
|
|
957
|
+
const propertyName = node.property?.value;
|
|
958
|
+
if (propertyName) state.componentNames.add(propertyName);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Checks if a node represents HOC pattern
|
|
963
|
+
*/
|
|
964
|
+
function isHOCPattern(node, state) {
|
|
965
|
+
return node.callee?.type === "Identifier" && node.arguments?.some((arg) => arg.type === "Identifier" && state.componentNames.has(arg.value));
|
|
966
|
+
}
|
|
967
|
+
//#endregion
|
|
968
|
+
//#region src/swc-parser/core/visitor.ts
|
|
969
|
+
/**
|
|
970
|
+
* Main AST visitor that routes nodes to appropriate pattern analyzers
|
|
971
|
+
*/
|
|
972
|
+
function visitNode(node, state, context = {}) {
|
|
973
|
+
if (!node) return;
|
|
974
|
+
switch (node.type) {
|
|
975
|
+
case "Module":
|
|
976
|
+
if (node.body) {
|
|
977
|
+
for (const item of node.body) if (item.type === "ImportDeclaration") visitNode(item, state, context);
|
|
978
|
+
for (const item of node.body) if (item.type !== "ImportDeclaration") visitNode(item, state, {
|
|
979
|
+
...context,
|
|
980
|
+
parent: node
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
break;
|
|
984
|
+
case "ImportDeclaration":
|
|
985
|
+
analyzeImportDeclaration(node, state);
|
|
986
|
+
break;
|
|
987
|
+
case "CallExpression":
|
|
988
|
+
analyzeCallExpression(node, state, context);
|
|
989
|
+
break;
|
|
990
|
+
case "VariableDeclaration":
|
|
991
|
+
analyzeVariableDeclaration(node, state);
|
|
992
|
+
visitChildren(node, state, context);
|
|
993
|
+
break;
|
|
994
|
+
case "JSXElement":
|
|
995
|
+
case "JSXFragment":
|
|
996
|
+
analyzeJSXElement(node, state);
|
|
997
|
+
visitChildren(node, state, context);
|
|
998
|
+
break;
|
|
999
|
+
case "JSXOpeningElement":
|
|
1000
|
+
analyzeJSXOpeningElement(node, state, context.parent);
|
|
1001
|
+
break;
|
|
1002
|
+
case "ArrayExpression":
|
|
1003
|
+
analyzeArrayExpression(node, state);
|
|
1004
|
+
visitChildren(node, state, context);
|
|
1005
|
+
break;
|
|
1006
|
+
case "ObjectExpression":
|
|
1007
|
+
analyzeObjectExpression(node, state);
|
|
1008
|
+
visitChildren(node, state, context);
|
|
1009
|
+
break;
|
|
1010
|
+
case "MemberExpression":
|
|
1011
|
+
analyzeMemberExpression(node, state);
|
|
1012
|
+
visitChildren(node, state, context);
|
|
1013
|
+
break;
|
|
1014
|
+
case "ConditionalExpression":
|
|
1015
|
+
analyzeConditionalExpression(node, state);
|
|
1016
|
+
visitChildren(node, state, context);
|
|
1017
|
+
break;
|
|
1018
|
+
case "FunctionDeclaration":
|
|
1019
|
+
case "ClassDeclaration":
|
|
1020
|
+
case "ExpressionStatement":
|
|
1021
|
+
case "ReturnStatement":
|
|
1022
|
+
case "VariableDeclarator":
|
|
1023
|
+
case "ArrowFunctionExpression":
|
|
1024
|
+
case "FunctionExpression":
|
|
1025
|
+
visitChildren(node, state, {
|
|
1026
|
+
...context,
|
|
1027
|
+
parent: node
|
|
1028
|
+
});
|
|
1029
|
+
break;
|
|
1030
|
+
default:
|
|
1031
|
+
visitChildren(node, state, context);
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Analyzes call expressions and routes to specific analyzers
|
|
539
1037
|
*/
|
|
540
1038
|
function analyzeCallExpression(node, state, context) {
|
|
541
1039
|
if (node.callee?.value === "lazy" || node.callee?.object?.value === "React" && node.callee?.property?.value === "lazy") analyzeLazyImport(node, state);
|
|
@@ -645,251 +1143,21 @@ function swcOptionsForFile(filePath) {
|
|
|
645
1143
|
};
|
|
646
1144
|
return {
|
|
647
1145
|
syntax: "ecmascript",
|
|
648
|
-
jsx:
|
|
1146
|
+
jsx: true,
|
|
649
1147
|
decorators: true,
|
|
650
1148
|
importAssertions: true
|
|
651
1149
|
};
|
|
652
1150
|
}
|
|
653
1151
|
function parseCode(code, filePath = "file.tsx") {
|
|
654
|
-
const state = createState();
|
|
655
|
-
visitNode(parseSync(code, swcOptionsForFile(filePath)), state);
|
|
656
|
-
return generateReport(state);
|
|
657
|
-
}
|
|
658
|
-
function parseFile(filePath) {
|
|
659
|
-
return parseCode(fs.readFileSync(filePath, "utf8"), filePath);
|
|
660
|
-
}
|
|
661
|
-
//#endregion
|
|
662
|
-
//#region src/rules/shared.ts
|
|
663
|
-
function toArray(val) {
|
|
664
|
-
if (!val) return [];
|
|
665
|
-
return Array.isArray(val) ? val : [val];
|
|
666
|
-
}
|
|
667
|
-
function findMatches(patterns, repoPath, ignore) {
|
|
668
|
-
const matches = [];
|
|
669
|
-
for (const pattern of patterns) {
|
|
670
|
-
const found = globSync(pattern, {
|
|
671
|
-
cwd: repoPath,
|
|
672
|
-
nodir: true,
|
|
673
|
-
ignore
|
|
674
|
-
});
|
|
675
|
-
matches.push(...found.map((f) => path$1.join(repoPath, f)));
|
|
676
|
-
}
|
|
677
|
-
return [...new Set(matches)];
|
|
678
|
-
}
|
|
679
|
-
function readPackageJson(repoPath) {
|
|
680
|
-
try {
|
|
681
|
-
const content = fs$1.readFileSync(path$1.join(repoPath, "package.json"), "utf-8");
|
|
682
|
-
return JSON.parse(content);
|
|
683
|
-
} catch {
|
|
684
|
-
return null;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
//#endregion
|
|
688
|
-
//#region src/rules/file-rules.ts
|
|
689
|
-
function evaluateFileRules(repoPath, rulesConfig, excludes) {
|
|
690
|
-
const violations = [];
|
|
691
|
-
for (const rule of toArray(rulesConfig.forbid_files)) {
|
|
692
|
-
const matches = findMatches(rule.patterns, repoPath, excludes);
|
|
693
|
-
if (matches.length > 0) violations.push({
|
|
694
|
-
type: "forbid_files",
|
|
695
|
-
severity: rule.severity,
|
|
696
|
-
patterns: rule.patterns,
|
|
697
|
-
message: rule.message,
|
|
698
|
-
matchedFiles: matches
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
for (const rule of toArray(rulesConfig.require_files)) if (findMatches(rule.patterns, repoPath, excludes).length === 0) violations.push({
|
|
702
|
-
type: "require_files",
|
|
703
|
-
severity: rule.severity,
|
|
704
|
-
patterns: rule.patterns,
|
|
705
|
-
message: rule.message,
|
|
706
|
-
matchedFiles: []
|
|
707
|
-
});
|
|
708
|
-
for (const rule of toArray(rulesConfig.allow_files)) if (findMatches(rule.patterns, repoPath, excludes).length === 0) violations.push({
|
|
709
|
-
type: "allow_files",
|
|
710
|
-
severity: rule.severity,
|
|
711
|
-
patterns: rule.patterns,
|
|
712
|
-
message: rule.message,
|
|
713
|
-
matchedFiles: []
|
|
714
|
-
});
|
|
715
|
-
return violations;
|
|
716
|
-
}
|
|
717
|
-
//#endregion
|
|
718
|
-
//#region src/rules/script-rules.ts
|
|
719
|
-
function evaluateScriptRules(repoPath, rulesConfig) {
|
|
720
|
-
const rules = toArray(rulesConfig.require_scripts);
|
|
721
|
-
if (rules.length === 0) return [];
|
|
722
|
-
const pkg = readPackageJson(repoPath);
|
|
723
|
-
const scriptKeys = Object.keys(pkg?.scripts ?? {});
|
|
724
|
-
return rules.filter((rule) => !rule.patterns.some((p) => scriptKeys.some((k) => micromatch.isMatch(k, p)))).map((rule) => ({
|
|
725
|
-
type: "require_scripts",
|
|
726
|
-
severity: rule.severity,
|
|
727
|
-
patterns: rule.patterns,
|
|
728
|
-
message: rule.message,
|
|
729
|
-
matchedFiles: []
|
|
730
|
-
}));
|
|
731
|
-
}
|
|
732
|
-
//#endregion
|
|
733
|
-
//#region src/rules/package-field-rules.ts
|
|
734
|
-
function evaluatePackageFieldRules(repoPath, rulesConfig) {
|
|
735
|
-
const rules = toArray(rulesConfig.require_package_fields);
|
|
736
|
-
if (rules.length === 0) return [];
|
|
737
|
-
const pkg = readPackageJson(repoPath);
|
|
738
|
-
const fieldKeys = pkg ? Object.keys(pkg) : [];
|
|
739
|
-
return rules.filter((rule) => !rule.patterns.some((p) => fieldKeys.includes(p))).map((rule) => ({
|
|
740
|
-
type: "require_package_fields",
|
|
741
|
-
severity: rule.severity,
|
|
742
|
-
patterns: rule.patterns,
|
|
743
|
-
message: rule.message,
|
|
744
|
-
matchedFiles: []
|
|
745
|
-
}));
|
|
746
|
-
}
|
|
747
|
-
//#endregion
|
|
748
|
-
//#region src/rules/engine-version.ts
|
|
749
|
-
function evaluateEngineVersion(repoPath, rulesConfig) {
|
|
750
|
-
const rules = toArray(rulesConfig.engine_version);
|
|
751
|
-
if (rules.length === 0) return [];
|
|
752
|
-
const nodeRange = (readPackageJson(repoPath)?.engines)?.node;
|
|
753
|
-
return rules.flatMap((rule) => {
|
|
754
|
-
if (!nodeRange) return [{
|
|
755
|
-
type: "engine_version",
|
|
756
|
-
severity: rule.severity,
|
|
757
|
-
patterns: [],
|
|
758
|
-
message: rule.message ?? "engines.node not specified in package.json",
|
|
759
|
-
matchedFiles: [],
|
|
760
|
-
requiredRange: rule.range
|
|
761
|
-
}];
|
|
762
|
-
const minVer = semver.minVersion(nodeRange);
|
|
763
|
-
if (!minVer || !semver.satisfies(minVer, rule.range)) return [{
|
|
764
|
-
type: "engine_version",
|
|
765
|
-
severity: rule.severity,
|
|
766
|
-
patterns: [],
|
|
767
|
-
message: rule.message,
|
|
768
|
-
matchedFiles: [],
|
|
769
|
-
installedRange: nodeRange,
|
|
770
|
-
requiredRange: rule.range
|
|
771
|
-
}];
|
|
772
|
-
return [];
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
//#endregion
|
|
776
|
-
//#region src/rules/evaluator.ts
|
|
777
|
-
function evaluateRules(repoPath, rulesConfig, excludes) {
|
|
778
|
-
return [
|
|
779
|
-
...evaluateFileRules(repoPath, rulesConfig, excludes),
|
|
780
|
-
...evaluateScriptRules(repoPath, rulesConfig),
|
|
781
|
-
...evaluatePackageFieldRules(repoPath, rulesConfig),
|
|
782
|
-
...evaluateEngineVersion(repoPath, rulesConfig)
|
|
783
|
-
];
|
|
784
|
-
}
|
|
785
|
-
//#endregion
|
|
786
|
-
//#region src/utils/aggregator.ts
|
|
787
|
-
function toPercentage(count, total) {
|
|
788
|
-
return total > 0 ? count / total * 100 : 0;
|
|
789
|
-
}
|
|
790
|
-
function aggregateReports(reports, versions = {}, config, multiVersions = {}) {
|
|
791
|
-
const componentUsageMap = /* @__PURE__ */ new Map();
|
|
792
|
-
let totalImports = 0;
|
|
793
|
-
let totalUsagePatterns = 0;
|
|
794
|
-
const patternCountMap = /* @__PURE__ */ new Map();
|
|
795
|
-
const availablePackages = Object.keys(versions);
|
|
796
|
-
for (const report of reports) {
|
|
797
|
-
totalImports += report.summary.totalImports;
|
|
798
|
-
totalUsagePatterns += report.summary.totalUsagePatterns;
|
|
799
|
-
for (const jsx of report.patterns.usage.jsx) {
|
|
800
|
-
const key = jsx.component;
|
|
801
|
-
const existing = componentUsageMap.get(key);
|
|
802
|
-
if (existing) existing.count++;
|
|
803
|
-
else {
|
|
804
|
-
const source = findComponentSource(jsx.component, report, availablePackages);
|
|
805
|
-
componentUsageMap.set(key, {
|
|
806
|
-
name: jsx.component,
|
|
807
|
-
source,
|
|
808
|
-
count: 1,
|
|
809
|
-
files: /* @__PURE__ */ new Set()
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
countPatterns(report, patternCountMap);
|
|
814
|
-
}
|
|
815
|
-
const topComponents = Array.from(componentUsageMap.values()).sort((a, b) => b.count - a.count);
|
|
816
|
-
const allComponents = Array.from(componentUsageMap.keys()).sort();
|
|
817
|
-
const patternCounts = Array.from(patternCountMap.entries()).map(([type, count]) => ({
|
|
818
|
-
patternType: type,
|
|
819
|
-
displayName: getPatternDisplayName(type),
|
|
820
|
-
count
|
|
821
|
-
})).sort((a, b) => b.count - a.count);
|
|
822
|
-
const packageDistribution = calculatePackageDistribution(componentUsageMap, versions, config, multiVersions);
|
|
823
|
-
const versusResults = calculateVersusResults(packageDistribution, config?.versus ?? []);
|
|
824
|
-
const bannedPackageViolations = detectBannedPackages(packageDistribution, config);
|
|
825
|
-
const requiredPackageViolations = detectRequiredPackages(packageDistribution, versions, config);
|
|
826
|
-
return {
|
|
827
|
-
filesAnalyzed: reports.length,
|
|
828
|
-
totalImports,
|
|
829
|
-
totalComponents: componentUsageMap.size,
|
|
830
|
-
totalUsagePatterns,
|
|
831
|
-
patternCounts,
|
|
832
|
-
componentUsage: componentUsageMap,
|
|
833
|
-
topComponents,
|
|
834
|
-
allComponents,
|
|
835
|
-
packageDistribution,
|
|
836
|
-
versusResults,
|
|
837
|
-
ruleViolations: requiredPackageViolations,
|
|
838
|
-
bannedPackageViolations,
|
|
839
|
-
reports
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
function calculateVersusResults(distribution, versusConfigs) {
|
|
843
|
-
const distMap = new Map(distribution.map((p) => [p.packageName, p]));
|
|
844
|
-
return versusConfigs.map((vc) => {
|
|
845
|
-
const entries = vc.packages.map((pkgName) => {
|
|
846
|
-
const pkg = distMap.get(pkgName);
|
|
847
|
-
return {
|
|
848
|
-
packageName: pkgName,
|
|
849
|
-
count: pkg?.usageCount ?? 0,
|
|
850
|
-
percentage: 0,
|
|
851
|
-
components: pkg?.components ?? []
|
|
852
|
-
};
|
|
853
|
-
});
|
|
854
|
-
const totalCount = entries.reduce((sum, e) => sum + e.count, 0);
|
|
855
|
-
for (const entry of entries) entry.percentage = toPercentage(entry.count, totalCount);
|
|
856
|
-
entries.sort((a, b) => b.count - a.count);
|
|
857
|
-
return {
|
|
858
|
-
name: vc.name,
|
|
859
|
-
packages: vc.packages,
|
|
860
|
-
entries,
|
|
861
|
-
totalCount
|
|
862
|
-
};
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
function detectBannedPackages(distribution, config) {
|
|
866
|
-
const forbidRules = toArray(config?.rules.forbid_packages);
|
|
867
|
-
if (forbidRules.length === 0) return [];
|
|
868
|
-
const violations = [];
|
|
869
|
-
for (const pkg of distribution) for (const rule of forbidRules) if (micromatch.isMatch(pkg.packageName, rule.patterns)) {
|
|
870
|
-
violations.push({
|
|
871
|
-
packageName: pkg.packageName,
|
|
872
|
-
severity: rule.severity,
|
|
873
|
-
message: rule.message
|
|
874
|
-
});
|
|
875
|
-
break;
|
|
876
|
-
}
|
|
877
|
-
return violations;
|
|
878
|
-
}
|
|
879
|
-
function detectRequiredPackages(distribution, versions, config) {
|
|
880
|
-
const requireRules = toArray(config?.rules.require_packages);
|
|
881
|
-
if (requireRules.length === 0) return [];
|
|
882
|
-
const installedNames = /* @__PURE__ */ new Set([...Object.keys(versions), ...distribution.map((p) => p.packageName)]);
|
|
883
|
-
const violations = [];
|
|
884
|
-
for (const rule of requireRules) if (!rule.patterns.some((p) => [...installedNames].some((name) => micromatch.isMatch(name, p)))) violations.push({
|
|
885
|
-
type: "require_packages",
|
|
886
|
-
severity: rule.severity,
|
|
887
|
-
patterns: rule.patterns,
|
|
888
|
-
message: rule.message,
|
|
889
|
-
matchedFiles: []
|
|
890
|
-
});
|
|
891
|
-
return violations;
|
|
1152
|
+
const state = createState();
|
|
1153
|
+
visitNode(parseSync(code, swcOptionsForFile(filePath)), state);
|
|
1154
|
+
return generateReport(state);
|
|
1155
|
+
}
|
|
1156
|
+
function parseFile(filePath) {
|
|
1157
|
+
return parseCode(fs.readFileSync(filePath, "utf8"), filePath);
|
|
892
1158
|
}
|
|
1159
|
+
//#endregion
|
|
1160
|
+
//#region src/utils/package-distribution.ts
|
|
893
1161
|
function resolvePackageFromImportPath(importPath, availablePackages) {
|
|
894
1162
|
if (importPath.startsWith(".") || importPath.startsWith("/")) return "local";
|
|
895
1163
|
const sortedPackages = [...availablePackages].sort((a, b) => b.length - a.length);
|
|
@@ -908,47 +1176,6 @@ function findComponentSource(componentName, report, availablePackages) {
|
|
|
908
1176
|
if (aliasedImport) return resolvePackageFromImportPath(aliasedImport.source, availablePackages);
|
|
909
1177
|
return "unknown";
|
|
910
1178
|
}
|
|
911
|
-
function countPatterns(report, patternMap) {
|
|
912
|
-
increment(patternMap, "imports.default", report.patterns.imports.default.length);
|
|
913
|
-
increment(patternMap, "imports.named", report.patterns.imports.named.length);
|
|
914
|
-
increment(patternMap, "imports.namespace", report.patterns.imports.namespace.length);
|
|
915
|
-
increment(patternMap, "imports.aliased", report.patterns.imports.aliased.length);
|
|
916
|
-
increment(patternMap, "usage.jsx", report.patterns.usage.jsx.length);
|
|
917
|
-
increment(patternMap, "usage.variables", report.patterns.usage.variables.length);
|
|
918
|
-
increment(patternMap, "usage.destructuring", report.patterns.usage.destructuring.length);
|
|
919
|
-
increment(patternMap, "usage.conditional", report.patterns.usage.conditional.length);
|
|
920
|
-
increment(patternMap, "usage.arrays", report.patterns.usage.arrays.length);
|
|
921
|
-
increment(patternMap, "usage.objects", report.patterns.usage.objects.length);
|
|
922
|
-
increment(patternMap, "advanced.lazy", report.patterns.advanced.lazy.length);
|
|
923
|
-
increment(patternMap, "advanced.dynamic", report.patterns.advanced.dynamic.length);
|
|
924
|
-
increment(patternMap, "advanced.hoc", report.patterns.advanced.hoc.length);
|
|
925
|
-
increment(patternMap, "advanced.memo", report.patterns.advanced.memo.length);
|
|
926
|
-
increment(patternMap, "advanced.forwardRef", report.patterns.advanced.forwardRef.length);
|
|
927
|
-
increment(patternMap, "advanced.portal", report.patterns.advanced.portal.length);
|
|
928
|
-
}
|
|
929
|
-
function increment(map, key, value) {
|
|
930
|
-
map.set(key, (map.get(key) || 0) + value);
|
|
931
|
-
}
|
|
932
|
-
function getPatternDisplayName(patternType) {
|
|
933
|
-
return {
|
|
934
|
-
"imports.default": "Default Imports",
|
|
935
|
-
"imports.named": "Named Imports",
|
|
936
|
-
"imports.namespace": "Namespace Imports",
|
|
937
|
-
"imports.aliased": "Aliased Imports",
|
|
938
|
-
"usage.jsx": "JSX Usage",
|
|
939
|
-
"usage.variables": "Variable Assignments",
|
|
940
|
-
"usage.destructuring": "Destructuring",
|
|
941
|
-
"usage.conditional": "Conditional Usage",
|
|
942
|
-
"usage.arrays": "Array Mappings",
|
|
943
|
-
"usage.objects": "Object Mappings",
|
|
944
|
-
"advanced.lazy": "Lazy Loading",
|
|
945
|
-
"advanced.dynamic": "Dynamic Imports",
|
|
946
|
-
"advanced.hoc": "Higher-Order Components",
|
|
947
|
-
"advanced.memo": "Memoized Components",
|
|
948
|
-
"advanced.forwardRef": "Forward Refs",
|
|
949
|
-
"advanced.portal": "Portal Usage"
|
|
950
|
-
}[patternType] || patternType;
|
|
951
|
-
}
|
|
952
1179
|
function getPackageVersion(packageName, versions) {
|
|
953
1180
|
if (versions[packageName]) return versions[packageName];
|
|
954
1181
|
if (packageName.includes("/")) {
|
|
@@ -998,347 +1225,276 @@ function calculatePackageDistribution(componentUsageMap, versions, config, multi
|
|
|
998
1225
|
return distribution.sort((a, b) => b.usageCount - a.usageCount);
|
|
999
1226
|
}
|
|
1000
1227
|
//#endregion
|
|
1001
|
-
//#region src/
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
* @returns Formatted string (e.g., 1,234,567)
|
|
1006
|
-
*/
|
|
1007
|
-
function formatCount(num) {
|
|
1008
|
-
return num.toLocaleString();
|
|
1009
|
-
}
|
|
1010
|
-
//#endregion
|
|
1011
|
-
//#region src/utils/print-summary.ts
|
|
1012
|
-
function printHeader$4() {
|
|
1013
|
-
console.log(chalk.green.bold("\nš Summary\n"));
|
|
1014
|
-
}
|
|
1015
|
-
function printSummary(aggregated) {
|
|
1016
|
-
printHeader$4();
|
|
1017
|
-
const table = new Table({
|
|
1018
|
-
head: ["Metric", "Count"],
|
|
1019
|
-
style: {
|
|
1020
|
-
head: ["cyan"],
|
|
1021
|
-
border: ["gray"]
|
|
1022
|
-
}
|
|
1023
|
-
});
|
|
1024
|
-
const externalComponents = aggregated.topComponents.filter((comp) => comp.source !== "unknown" && comp.source !== "local").length;
|
|
1025
|
-
const totalExternalUsage = aggregated.packageDistribution.reduce((sum, pkg) => sum + pkg.usageCount, 0);
|
|
1026
|
-
table.push(["Files Analyzed", formatCount(aggregated.filesAnalyzed)], ["External Packages", formatCount(aggregated.packageDistribution.length)], ["External Components", formatCount(externalComponents)], ["Total Usages", formatCount(totalExternalUsage)]);
|
|
1027
|
-
console.log(table.toString());
|
|
1028
|
-
}
|
|
1029
|
-
//#endregion
|
|
1030
|
-
//#region src/utils/print-details.ts
|
|
1031
|
-
function printHeader$3() {
|
|
1032
|
-
console.log(chalk.cyan.bold("\nš Details\n"));
|
|
1033
|
-
}
|
|
1034
|
-
function printDetails(aggregated) {
|
|
1035
|
-
printHeader$3();
|
|
1036
|
-
console.log(chalk.cyan(` Total usage patterns: ${formatCount(aggregated.totalUsagePatterns)}`));
|
|
1037
|
-
for (const pattern of aggregated.patternCounts) if (pattern.count > 0) console.log(chalk.cyan(` ${pattern.displayName}: ${formatCount(pattern.count)}`));
|
|
1038
|
-
}
|
|
1039
|
-
//#endregion
|
|
1040
|
-
//#region src/utils/chart-renderer.ts
|
|
1041
|
-
function renderBarChart(data, options = {}) {
|
|
1042
|
-
const { maxWidth = 50, showValues = true, barChar = "ā", emptyChar = "ā" } = options;
|
|
1043
|
-
if (data.length === 0) {
|
|
1044
|
-
console.log(chalk.gray(" No data to display"));
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
const maxValue = Math.max(...data.map((d) => d.value));
|
|
1048
|
-
if (maxValue === 0) {
|
|
1049
|
-
console.log(chalk.gray(" All values are zero"));
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
const maxLabelLength = Math.max(...data.map((d) => d.label.length));
|
|
1053
|
-
for (const item of data) {
|
|
1054
|
-
const percentage = item.value / maxValue;
|
|
1055
|
-
const barLength = Math.round(percentage * maxWidth);
|
|
1056
|
-
const emptyLength = maxWidth - barLength;
|
|
1057
|
-
const paddedLabel = item.label.padEnd(maxLabelLength, " ");
|
|
1058
|
-
const bar = chalk.green(barChar.repeat(barLength)) + chalk.gray(emptyChar.repeat(emptyLength));
|
|
1059
|
-
const valueStr = showValues ? ` ${formatCount(item.value)}` : "";
|
|
1060
|
-
console.log(`${paddedLabel} ${bar}${valueStr}\n`);
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
//#endregion
|
|
1064
|
-
//#region src/utils/print-components.ts
|
|
1065
|
-
function printHeader$2() {
|
|
1066
|
-
console.log(chalk.magenta.bold("\nāļø Components\n"));
|
|
1067
|
-
}
|
|
1068
|
-
function printComponents(aggregated, mode) {
|
|
1069
|
-
const components = aggregated.topComponents;
|
|
1070
|
-
if (mode === "table") printComponentsTable(components);
|
|
1071
|
-
else if (mode === "chart") printComponentsChart(components);
|
|
1228
|
+
//#region src/rules/shared.ts
|
|
1229
|
+
function toArray(val) {
|
|
1230
|
+
if (!val) return [];
|
|
1231
|
+
return Array.isArray(val) ? val : [val];
|
|
1072
1232
|
}
|
|
1073
|
-
function
|
|
1074
|
-
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1233
|
+
function findMatches(patterns, repoPath, ignore) {
|
|
1234
|
+
const matches = [];
|
|
1235
|
+
for (const pattern of patterns) {
|
|
1236
|
+
const found = globSync(pattern, {
|
|
1237
|
+
cwd: repoPath,
|
|
1238
|
+
nodir: true,
|
|
1239
|
+
ignore
|
|
1240
|
+
});
|
|
1241
|
+
matches.push(...found.map((f) => f.replace(/\\/g, "/")));
|
|
1079
1242
|
}
|
|
1080
|
-
|
|
1081
|
-
head: [
|
|
1082
|
-
"Component",
|
|
1083
|
-
"Package",
|
|
1084
|
-
"Count"
|
|
1085
|
-
],
|
|
1086
|
-
style: {
|
|
1087
|
-
head: ["cyan"],
|
|
1088
|
-
border: ["gray"]
|
|
1089
|
-
}
|
|
1090
|
-
});
|
|
1091
|
-
externalComponents.forEach((comp) => {
|
|
1092
|
-
table.push([
|
|
1093
|
-
comp.name,
|
|
1094
|
-
comp.source,
|
|
1095
|
-
comp.count.toString()
|
|
1096
|
-
]);
|
|
1097
|
-
});
|
|
1098
|
-
console.log(table.toString());
|
|
1243
|
+
return [...new Set(matches)];
|
|
1099
1244
|
}
|
|
1100
|
-
function
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
return;
|
|
1245
|
+
function readPackageJson(repoPath) {
|
|
1246
|
+
try {
|
|
1247
|
+
const content = fs$1.readFileSync(path$1.join(repoPath, "package.json"), "utf-8");
|
|
1248
|
+
return JSON.parse(content);
|
|
1249
|
+
} catch {
|
|
1250
|
+
return null;
|
|
1106
1251
|
}
|
|
1107
|
-
renderBarChart(externalComponents.map((comp) => ({
|
|
1108
|
-
label: comp.name,
|
|
1109
|
-
value: comp.count
|
|
1110
|
-
})), { maxWidth: 50 });
|
|
1111
1252
|
}
|
|
1112
1253
|
//#endregion
|
|
1113
|
-
//#region src/
|
|
1114
|
-
function
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
console.log(chalk.gray(" No patterns found"));
|
|
1126
|
-
return;
|
|
1254
|
+
//#region src/rules/file-rules.ts
|
|
1255
|
+
function evaluateFileRules(repoPath, rulesConfig, excludes) {
|
|
1256
|
+
const violations = [];
|
|
1257
|
+
for (const rule of toArray(rulesConfig.detect_files)) {
|
|
1258
|
+
const matches = findMatches(rule.patterns, repoPath, excludes);
|
|
1259
|
+
if (matches.length > 0) violations.push({
|
|
1260
|
+
type: "detect_files",
|
|
1261
|
+
severity: rule.severity,
|
|
1262
|
+
patterns: rule.patterns,
|
|
1263
|
+
message: rule.message,
|
|
1264
|
+
matchedFiles: matches
|
|
1265
|
+
});
|
|
1127
1266
|
}
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
});
|
|
1135
|
-
patterns.forEach((pattern) => {
|
|
1136
|
-
table.push([pattern.displayName, pattern.count.toString()]);
|
|
1267
|
+
for (const rule of toArray(rulesConfig.require_files)) if (findMatches(rule.patterns, repoPath, excludes).length === 0) violations.push({
|
|
1268
|
+
type: "require_files",
|
|
1269
|
+
severity: rule.severity,
|
|
1270
|
+
patterns: rule.patterns,
|
|
1271
|
+
message: rule.message,
|
|
1272
|
+
matchedFiles: []
|
|
1137
1273
|
});
|
|
1138
|
-
|
|
1139
|
-
const totalPatterns = patterns.reduce((sum, p) => sum + p.count, 0);
|
|
1140
|
-
console.log(chalk.gray(`\nTotal: ${totalPatterns} patterns detected`));
|
|
1141
|
-
}
|
|
1142
|
-
function printPatternsChart(patterns) {
|
|
1143
|
-
printHeader$1();
|
|
1144
|
-
if (patterns.length === 0) {
|
|
1145
|
-
console.log(chalk.gray(" No patterns found"));
|
|
1146
|
-
return;
|
|
1147
|
-
}
|
|
1148
|
-
renderBarChart(patterns.map((pattern) => ({
|
|
1149
|
-
label: pattern.displayName,
|
|
1150
|
-
value: pattern.count
|
|
1151
|
-
})), { maxWidth: 50 });
|
|
1274
|
+
return violations;
|
|
1152
1275
|
}
|
|
1153
1276
|
//#endregion
|
|
1154
|
-
//#region src/
|
|
1155
|
-
function
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
const { worstLevel, upgrades } = releaseAge;
|
|
1168
|
-
if (!worstLevel) return chalk.green("ā");
|
|
1169
|
-
const top = upgrades[0];
|
|
1170
|
-
if (!top) return chalk.green("ā");
|
|
1171
|
-
if (worstLevel === "mandatory_upgrade") return chalk.red(`ā ${top.semverBump} ${top.version} (${top.releasedDaysAgo}d)`);
|
|
1172
|
-
return chalk.yellow(`ā ${top.semverBump} ${top.version} (${top.releasedDaysAgo}d)`);
|
|
1277
|
+
//#region src/rules/script-rules.ts
|
|
1278
|
+
function evaluateScriptRules(repoPath, rulesConfig) {
|
|
1279
|
+
const rules = toArray(rulesConfig.require_scripts);
|
|
1280
|
+
if (rules.length === 0) return [];
|
|
1281
|
+
const pkg = readPackageJson(repoPath);
|
|
1282
|
+
const scriptKeys = Object.keys(pkg?.scripts ?? {});
|
|
1283
|
+
return rules.filter((rule) => !rule.patterns.some((p) => scriptKeys.some((k) => micromatch.isMatch(k, p)))).map((rule) => ({
|
|
1284
|
+
type: "require_scripts",
|
|
1285
|
+
severity: rule.severity,
|
|
1286
|
+
patterns: rule.patterns,
|
|
1287
|
+
message: rule.message,
|
|
1288
|
+
matchedFiles: []
|
|
1289
|
+
}));
|
|
1173
1290
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1291
|
+
//#endregion
|
|
1292
|
+
//#region src/rules/package-field-rules.ts
|
|
1293
|
+
function evaluatePackageFieldRules(repoPath, rulesConfig) {
|
|
1294
|
+
const rules = toArray(rulesConfig.require_package_fields);
|
|
1295
|
+
if (rules.length === 0) return [];
|
|
1296
|
+
const pkg = readPackageJson(repoPath);
|
|
1297
|
+
const fieldKeys = pkg ? Object.keys(pkg) : [];
|
|
1298
|
+
return rules.filter((rule) => !rule.patterns.some((p) => fieldKeys.includes(p))).map((rule) => ({
|
|
1299
|
+
type: "require_package_fields",
|
|
1300
|
+
severity: rule.severity,
|
|
1301
|
+
patterns: rule.patterns,
|
|
1302
|
+
message: rule.message,
|
|
1303
|
+
matchedFiles: []
|
|
1304
|
+
}));
|
|
1176
1305
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1306
|
+
//#endregion
|
|
1307
|
+
//#region src/rules/engine-version.ts
|
|
1308
|
+
function evaluateEngineVersion(repoPath, rulesConfig) {
|
|
1309
|
+
const rules = toArray(rulesConfig.engine_version);
|
|
1310
|
+
if (rules.length === 0) return [];
|
|
1311
|
+
const nodeRange = (readPackageJson(repoPath)?.engines)?.node;
|
|
1312
|
+
return rules.flatMap((rule) => {
|
|
1313
|
+
if (!nodeRange) return [{
|
|
1314
|
+
type: "engine_version",
|
|
1315
|
+
severity: rule.severity,
|
|
1316
|
+
patterns: [],
|
|
1317
|
+
message: rule.message ?? "engines.node not specified in package.json",
|
|
1318
|
+
matchedFiles: [],
|
|
1319
|
+
requiredRange: rule.range
|
|
1320
|
+
}];
|
|
1321
|
+
const minVer = semver.minVersion(nodeRange);
|
|
1322
|
+
if (!minVer || !semver.satisfies(minVer, rule.range)) return [{
|
|
1323
|
+
type: "engine_version",
|
|
1324
|
+
severity: rule.severity,
|
|
1325
|
+
patterns: [],
|
|
1326
|
+
message: rule.message,
|
|
1327
|
+
matchedFiles: [],
|
|
1328
|
+
installedRange: nodeRange,
|
|
1329
|
+
requiredRange: rule.range
|
|
1330
|
+
}];
|
|
1331
|
+
return [];
|
|
1332
|
+
});
|
|
1182
1333
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
"Package",
|
|
1192
|
-
"Version",
|
|
1193
|
-
"Components",
|
|
1194
|
-
"Usage",
|
|
1195
|
-
"Percentage"
|
|
1334
|
+
//#endregion
|
|
1335
|
+
//#region src/rules/evaluator.ts
|
|
1336
|
+
function evaluateRules(repoPath, rulesConfig, excludes) {
|
|
1337
|
+
return [
|
|
1338
|
+
...evaluateFileRules(repoPath, rulesConfig, excludes),
|
|
1339
|
+
...evaluateScriptRules(repoPath, rulesConfig),
|
|
1340
|
+
...evaluatePackageFieldRules(repoPath, rulesConfig),
|
|
1341
|
+
...evaluateEngineVersion(repoPath, rulesConfig)
|
|
1196
1342
|
];
|
|
1197
|
-
if (hasReleaseAge) head.push("Upgrades");
|
|
1198
|
-
const table = new Table({
|
|
1199
|
-
head,
|
|
1200
|
-
style: {
|
|
1201
|
-
head: ["cyan"],
|
|
1202
|
-
border: ["gray"]
|
|
1203
|
-
}
|
|
1204
|
-
});
|
|
1205
|
-
packages.forEach((pkg) => {
|
|
1206
|
-
const versionCell = pkg.hasVersionConflict ? chalk.yellow(`ā ${pkg.allVersions.join(", ")} (multiple ā bundle impact)`) : pkg.version || "N/A";
|
|
1207
|
-
const row = [
|
|
1208
|
-
formatPackageName(pkg, getBannedViolation(pkg, violations)),
|
|
1209
|
-
versionCell,
|
|
1210
|
-
formatCount(pkg.componentCount),
|
|
1211
|
-
formatCount(pkg.usageCount),
|
|
1212
|
-
`${pkg.percentage.toFixed(1)}%`
|
|
1213
|
-
];
|
|
1214
|
-
if (hasReleaseAge) row.push(formatUpgradeCell(pkg.releaseAge));
|
|
1215
|
-
table.push(row);
|
|
1216
|
-
});
|
|
1217
|
-
console.log(table.toString());
|
|
1218
|
-
const totalComponents = packages.reduce((sum, p) => sum + p.componentCount, 0);
|
|
1219
|
-
const totalExternalUsage = packages.reduce((sum, p) => sum + p.usageCount, 0);
|
|
1220
|
-
console.log(chalk.gray(`\nTotal: ${formatCount(packages.length)} packages | ${formatCount(totalComponents)} unique components | ${formatCount(totalExternalUsage)} total usages`));
|
|
1221
1343
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1344
|
+
//#endregion
|
|
1345
|
+
//#region src/utils/package-rules.ts
|
|
1346
|
+
function detectBannedPackages(distribution, config) {
|
|
1347
|
+
const forbidRules = toArray(config?.rules.forbid_packages);
|
|
1348
|
+
if (forbidRules.length === 0) return [];
|
|
1349
|
+
const violations = [];
|
|
1350
|
+
for (const pkg of distribution) for (const rule of forbidRules) if (micromatch.isMatch(pkg.packageName, rule.patterns)) {
|
|
1351
|
+
violations.push({
|
|
1352
|
+
packageName: pkg.packageName,
|
|
1353
|
+
severity: rule.severity,
|
|
1354
|
+
message: rule.message
|
|
1355
|
+
});
|
|
1356
|
+
break;
|
|
1227
1357
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1358
|
+
return violations;
|
|
1359
|
+
}
|
|
1360
|
+
function detectRequiredPackages(distribution, versions, config) {
|
|
1361
|
+
const requireRules = toArray(config?.rules.require_packages);
|
|
1362
|
+
if (requireRules.length === 0) return [];
|
|
1363
|
+
const installedNames = /* @__PURE__ */ new Set([...Object.keys(versions), ...distribution.map((p) => p.packageName)]);
|
|
1364
|
+
const violations = [];
|
|
1365
|
+
for (const rule of requireRules) if (!rule.patterns.some((p) => [...installedNames].some((name) => micromatch.isMatch(name, p)))) violations.push({
|
|
1366
|
+
type: "require_packages",
|
|
1367
|
+
severity: rule.severity,
|
|
1368
|
+
patterns: rule.patterns,
|
|
1369
|
+
message: rule.message,
|
|
1370
|
+
matchedFiles: []
|
|
1237
1371
|
});
|
|
1372
|
+
return violations;
|
|
1238
1373
|
}
|
|
1239
1374
|
//#endregion
|
|
1240
|
-
//#region src/utils/
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1375
|
+
//#region src/utils/pattern-counter.ts
|
|
1376
|
+
function countPatterns(report, patternMap) {
|
|
1377
|
+
increment(patternMap, "imports.default", report.patterns.imports.default.length);
|
|
1378
|
+
increment(patternMap, "imports.named", report.patterns.imports.named.length);
|
|
1379
|
+
increment(patternMap, "imports.namespace", report.patterns.imports.namespace.length);
|
|
1380
|
+
increment(patternMap, "imports.aliased", report.patterns.imports.aliased.length);
|
|
1381
|
+
increment(patternMap, "usage.jsx", report.patterns.usage.jsx.length);
|
|
1382
|
+
increment(patternMap, "usage.variables", report.patterns.usage.variables.length);
|
|
1383
|
+
increment(patternMap, "usage.destructuring", report.patterns.usage.destructuring.length);
|
|
1384
|
+
increment(patternMap, "usage.conditional", report.patterns.usage.conditional.length);
|
|
1385
|
+
increment(patternMap, "usage.arrays", report.patterns.usage.arrays.length);
|
|
1386
|
+
increment(patternMap, "usage.objects", report.patterns.usage.objects.length);
|
|
1387
|
+
increment(patternMap, "advanced.lazy", report.patterns.advanced.lazy.length);
|
|
1388
|
+
increment(patternMap, "advanced.dynamic", report.patterns.advanced.dynamic.length);
|
|
1389
|
+
increment(patternMap, "advanced.hoc", report.patterns.advanced.hoc.length);
|
|
1390
|
+
increment(patternMap, "advanced.memo", report.patterns.advanced.memo.length);
|
|
1391
|
+
increment(patternMap, "advanced.forwardRef", report.patterns.advanced.forwardRef.length);
|
|
1392
|
+
increment(patternMap, "advanced.portal", report.patterns.advanced.portal.length);
|
|
1253
1393
|
}
|
|
1254
|
-
function
|
|
1255
|
-
|
|
1256
|
-
console.log(chalk.gray(` ${"ā".repeat(50)}`));
|
|
1257
|
-
const maxNameLen = Math.max(...result.entries.map((e) => e.packageName.length));
|
|
1258
|
-
for (const entry of result.entries) {
|
|
1259
|
-
const name = entry.packageName.padEnd(maxNameLen);
|
|
1260
|
-
const bar = renderBar(entry.percentage);
|
|
1261
|
-
const pct = chalk.bold(`${entry.percentage.toFixed(1)}%`);
|
|
1262
|
-
const usage = chalk.gray(`(${entry.count} usages)`);
|
|
1263
|
-
const components = entry.components.length > 0 ? chalk.gray(` ${formatComponents(entry.components)}`) : "";
|
|
1264
|
-
console.log(` ${name} ${bar} ${pct} ${usage}${components}`);
|
|
1265
|
-
}
|
|
1266
|
-
if (result.totalCount === 0) console.log(chalk.gray(" No usage detected for any package in this group."));
|
|
1267
|
-
console.log();
|
|
1394
|
+
function increment(map, key, value) {
|
|
1395
|
+
map.set(key, (map.get(key) || 0) + value);
|
|
1268
1396
|
}
|
|
1269
|
-
function
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1397
|
+
function getPatternDisplayName(patternType) {
|
|
1398
|
+
return {
|
|
1399
|
+
"imports.default": "Default Imports",
|
|
1400
|
+
"imports.named": "Named Imports",
|
|
1401
|
+
"imports.namespace": "Namespace Imports",
|
|
1402
|
+
"imports.aliased": "Aliased Imports",
|
|
1403
|
+
"usage.jsx": "JSX Usage",
|
|
1404
|
+
"usage.variables": "Variable Assignments",
|
|
1405
|
+
"usage.destructuring": "Destructuring",
|
|
1406
|
+
"usage.conditional": "Conditional Usage",
|
|
1407
|
+
"usage.arrays": "Array Mappings",
|
|
1408
|
+
"usage.objects": "Object Mappings",
|
|
1409
|
+
"advanced.lazy": "Lazy Loading",
|
|
1410
|
+
"advanced.dynamic": "Dynamic Imports",
|
|
1411
|
+
"advanced.hoc": "Higher-Order Components",
|
|
1412
|
+
"advanced.memo": "Memoized Components",
|
|
1413
|
+
"advanced.forwardRef": "Forward Refs",
|
|
1414
|
+
"advanced.portal": "Portal Usage"
|
|
1415
|
+
}[patternType] || patternType;
|
|
1273
1416
|
}
|
|
1274
1417
|
//#endregion
|
|
1275
|
-
//#region src/utils/
|
|
1276
|
-
function
|
|
1277
|
-
|
|
1278
|
-
case "forbid_files": return "forbid_files";
|
|
1279
|
-
case "require_files": return "require_files";
|
|
1280
|
-
case "allow_files": return "allow_files";
|
|
1281
|
-
case "forbid_packages": return "forbid_packages";
|
|
1282
|
-
case "require_packages": return "require_packages";
|
|
1283
|
-
case "require_scripts": return "require_scripts";
|
|
1284
|
-
case "require_package_fields": return "pkg_fields";
|
|
1285
|
-
case "engine_version": return "engine_version";
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
function ruleIcon(violation) {
|
|
1289
|
-
if (violation.severity === "error") return chalk.red("ā");
|
|
1290
|
-
return chalk.yellow("ā ");
|
|
1418
|
+
//#region src/utils/versus.ts
|
|
1419
|
+
function toPercentage(count, total) {
|
|
1420
|
+
return total > 0 ? count / total * 100 : 0;
|
|
1291
1421
|
}
|
|
1292
|
-
function
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
return
|
|
1308
|
-
|
|
1309
|
-
|
|
1422
|
+
function calculateVersusResults(distribution, versusConfigs) {
|
|
1423
|
+
const distMap = new Map(distribution.map((p) => [p.packageName, p]));
|
|
1424
|
+
return versusConfigs.map((vc) => {
|
|
1425
|
+
const entries = vc.packages.map((pkgName) => {
|
|
1426
|
+
const pkg = distMap.get(pkgName);
|
|
1427
|
+
return {
|
|
1428
|
+
packageName: pkgName,
|
|
1429
|
+
count: pkg?.usageCount ?? 0,
|
|
1430
|
+
percentage: 0,
|
|
1431
|
+
components: pkg?.components ?? []
|
|
1432
|
+
};
|
|
1433
|
+
});
|
|
1434
|
+
const totalCount = entries.reduce((sum, e) => sum + e.count, 0);
|
|
1435
|
+
for (const entry of entries) entry.percentage = toPercentage(entry.count, totalCount);
|
|
1436
|
+
entries.sort((a, b) => b.count - a.count);
|
|
1437
|
+
return {
|
|
1438
|
+
name: vc.name,
|
|
1439
|
+
packages: vc.packages,
|
|
1440
|
+
entries,
|
|
1441
|
+
totalCount
|
|
1442
|
+
};
|
|
1443
|
+
});
|
|
1310
1444
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1445
|
+
//#endregion
|
|
1446
|
+
//#region src/utils/aggregator-core.ts
|
|
1447
|
+
function aggregateReports(reports, versions = {}, config, multiVersions = {}) {
|
|
1448
|
+
const componentUsageMap = /* @__PURE__ */ new Map();
|
|
1449
|
+
let totalImports = 0;
|
|
1450
|
+
let totalUsagePatterns = 0;
|
|
1451
|
+
const patternCountMap = /* @__PURE__ */ new Map();
|
|
1452
|
+
const availablePackages = Object.keys(versions);
|
|
1453
|
+
for (const report of reports) {
|
|
1454
|
+
totalImports += report.summary.totalImports;
|
|
1455
|
+
totalUsagePatterns += report.summary.totalUsagePatterns;
|
|
1456
|
+
for (const jsx of report.patterns.usage.jsx) {
|
|
1457
|
+
const key = jsx.component;
|
|
1458
|
+
const existing = componentUsageMap.get(key);
|
|
1459
|
+
if (existing) existing.count++;
|
|
1460
|
+
else {
|
|
1461
|
+
const source = findComponentSource(jsx.component, report, availablePackages);
|
|
1462
|
+
componentUsageMap.set(key, {
|
|
1463
|
+
name: jsx.component,
|
|
1464
|
+
source,
|
|
1465
|
+
count: 1,
|
|
1466
|
+
files: /* @__PURE__ */ new Set()
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1334
1469
|
}
|
|
1470
|
+
countPatterns(report, patternCountMap);
|
|
1335
1471
|
}
|
|
1336
|
-
const
|
|
1337
|
-
const
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1472
|
+
const topComponents = Array.from(componentUsageMap.values()).sort((a, b) => b.count - a.count);
|
|
1473
|
+
const allComponents = Array.from(componentUsageMap.keys()).sort();
|
|
1474
|
+
const patternCounts = Array.from(patternCountMap.entries()).map(([type, count]) => ({
|
|
1475
|
+
patternType: type,
|
|
1476
|
+
displayName: getPatternDisplayName(type),
|
|
1477
|
+
count
|
|
1478
|
+
})).sort((a, b) => b.count - a.count);
|
|
1479
|
+
const packageDistribution = calculatePackageDistribution(componentUsageMap, versions, config, multiVersions);
|
|
1480
|
+
const versusResults = calculateVersusResults(packageDistribution, config?.versus ?? []);
|
|
1481
|
+
const bannedPackageViolations = detectBannedPackages(packageDistribution, config);
|
|
1482
|
+
const requiredPackageViolations = detectRequiredPackages(packageDistribution, versions, config);
|
|
1483
|
+
return {
|
|
1484
|
+
filesAnalyzed: reports.length,
|
|
1485
|
+
totalImports,
|
|
1486
|
+
totalComponents: componentUsageMap.size,
|
|
1487
|
+
totalUsagePatterns,
|
|
1488
|
+
patternCounts,
|
|
1489
|
+
componentUsage: componentUsageMap,
|
|
1490
|
+
topComponents,
|
|
1491
|
+
allComponents,
|
|
1492
|
+
packageDistribution,
|
|
1493
|
+
versusResults,
|
|
1494
|
+
ruleViolations: requiredPackageViolations,
|
|
1495
|
+
bannedPackageViolations,
|
|
1496
|
+
reports
|
|
1497
|
+
};
|
|
1342
1498
|
}
|
|
1343
1499
|
//#endregion
|
|
1344
1500
|
//#region src/utils/print-errors.ts
|
|
@@ -1352,28 +1508,6 @@ function printErrors(errors) {
|
|
|
1352
1508
|
console.log("");
|
|
1353
1509
|
}
|
|
1354
1510
|
//#endregion
|
|
1355
|
-
//#region src/utils/print-json.ts
|
|
1356
|
-
function printJson(aggregated) {
|
|
1357
|
-
const result = {
|
|
1358
|
-
summary: {
|
|
1359
|
-
filesAnalyzed: aggregated.filesAnalyzed,
|
|
1360
|
-
totalImports: aggregated.totalImports,
|
|
1361
|
-
totalComponents: aggregated.totalComponents,
|
|
1362
|
-
totalUsagePatterns: aggregated.totalUsagePatterns
|
|
1363
|
-
},
|
|
1364
|
-
packages: aggregated.packageDistribution,
|
|
1365
|
-
components: aggregated.topComponents.map((c) => ({
|
|
1366
|
-
...c,
|
|
1367
|
-
files: [...c.files]
|
|
1368
|
-
})),
|
|
1369
|
-
patterns: aggregated.patternCounts,
|
|
1370
|
-
versus: aggregated.versusResults,
|
|
1371
|
-
ruleViolations: aggregated.ruleViolations,
|
|
1372
|
-
bannedPackageViolations: aggregated.bannedPackageViolations
|
|
1373
|
-
};
|
|
1374
|
-
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1375
|
-
}
|
|
1376
|
-
//#endregion
|
|
1377
1511
|
//#region src/utils/file-utils.ts
|
|
1378
1512
|
/**
|
|
1379
1513
|
* Find files matching a glob pattern
|
|
@@ -1382,12 +1516,11 @@ function printJson(aggregated) {
|
|
|
1382
1516
|
* @returns Array of file paths
|
|
1383
1517
|
*/
|
|
1384
1518
|
async function findFiles(pattern, ignorePatterns) {
|
|
1385
|
-
return await glob(pattern, {
|
|
1519
|
+
return (await glob(pattern, {
|
|
1386
1520
|
ignore: ignorePatterns,
|
|
1387
1521
|
nodir: true,
|
|
1388
|
-
absolute: true,
|
|
1389
1522
|
windowsPathsNoEscape: true
|
|
1390
|
-
});
|
|
1523
|
+
})).map((f) => f.replace(/\\/g, "/"));
|
|
1391
1524
|
}
|
|
1392
1525
|
//#endregion
|
|
1393
1526
|
//#region src/lock-parser/patterns/npm.ts
|
|
@@ -1570,110 +1703,6 @@ function findAndParseLockfile(projectPath) {
|
|
|
1570
1703
|
throw new Error("No supported lockfile found");
|
|
1571
1704
|
}
|
|
1572
1705
|
//#endregion
|
|
1573
|
-
//#region src/config/schema.ts
|
|
1574
|
-
const RuleSeveritySchema = z.enum(["error", "warn"]);
|
|
1575
|
-
const RuleConfigSchema = z.object({
|
|
1576
|
-
severity: RuleSeveritySchema,
|
|
1577
|
-
patterns: z.array(z.string()),
|
|
1578
|
-
message: z.string().optional()
|
|
1579
|
-
});
|
|
1580
|
-
const RuleConfigOrArraySchema = z.union([RuleConfigSchema, z.array(RuleConfigSchema)]);
|
|
1581
|
-
const EngineVersionRuleSchema = z.object({
|
|
1582
|
-
severity: RuleSeveritySchema,
|
|
1583
|
-
range: z.string(),
|
|
1584
|
-
message: z.string().optional()
|
|
1585
|
-
});
|
|
1586
|
-
const ThresholdSchema = z.union([z.number(), z.literal(false)]);
|
|
1587
|
-
const HermexConfigSchema = z.object({
|
|
1588
|
-
includes: z.array(z.string()).default(["**/*.{tsx,jsx,ts,js}"]),
|
|
1589
|
-
excludes: z.array(z.string()).default([
|
|
1590
|
-
"**/node_modules/**",
|
|
1591
|
-
"**/dist/**",
|
|
1592
|
-
"**/build/**"
|
|
1593
|
-
]),
|
|
1594
|
-
packages: z.object({
|
|
1595
|
-
internal: z.array(z.string()).default([]),
|
|
1596
|
-
ignore: z.array(z.string()).default([])
|
|
1597
|
-
}).default(() => ({
|
|
1598
|
-
internal: [],
|
|
1599
|
-
ignore: []
|
|
1600
|
-
})),
|
|
1601
|
-
versus: z.array(z.object({
|
|
1602
|
-
name: z.string(),
|
|
1603
|
-
packages: z.array(z.string()).min(2)
|
|
1604
|
-
})).default([]),
|
|
1605
|
-
rules: z.object({
|
|
1606
|
-
forbid_files: RuleConfigOrArraySchema.default([]),
|
|
1607
|
-
require_files: RuleConfigOrArraySchema.default([]),
|
|
1608
|
-
allow_files: RuleConfigOrArraySchema.default([]),
|
|
1609
|
-
forbid_packages: RuleConfigOrArraySchema.default([]),
|
|
1610
|
-
require_packages: RuleConfigOrArraySchema.default([]),
|
|
1611
|
-
require_scripts: RuleConfigOrArraySchema.default([]),
|
|
1612
|
-
require_package_fields: RuleConfigOrArraySchema.default([]),
|
|
1613
|
-
engine_version: z.union([EngineVersionRuleSchema, z.array(EngineVersionRuleSchema)]).optional()
|
|
1614
|
-
}).default(() => ({
|
|
1615
|
-
forbid_files: [],
|
|
1616
|
-
require_files: [],
|
|
1617
|
-
allow_files: [],
|
|
1618
|
-
forbid_packages: [],
|
|
1619
|
-
require_packages: [],
|
|
1620
|
-
require_scripts: [],
|
|
1621
|
-
require_package_fields: []
|
|
1622
|
-
})),
|
|
1623
|
-
output: z.object({
|
|
1624
|
-
summary: z.union([z.literal("log"), z.literal(false)]).default("log"),
|
|
1625
|
-
components: z.union([z.enum(["table", "chart"]), z.literal(false)]).default("table"),
|
|
1626
|
-
packages: z.union([z.enum(["table", "chart"]), z.literal(false)]).default("table"),
|
|
1627
|
-
patterns: z.union([z.enum(["table", "chart"]), z.literal(false)]).default("table"),
|
|
1628
|
-
details: z.boolean().default(false),
|
|
1629
|
-
versus: z.boolean().default(true),
|
|
1630
|
-
rules: z.boolean().default(true),
|
|
1631
|
-
format: z.enum(["human", "json"]).default("human")
|
|
1632
|
-
}).default(() => ({
|
|
1633
|
-
summary: "log",
|
|
1634
|
-
components: "table",
|
|
1635
|
-
packages: "table",
|
|
1636
|
-
patterns: "table",
|
|
1637
|
-
details: false,
|
|
1638
|
-
versus: true,
|
|
1639
|
-
rules: true,
|
|
1640
|
-
format: "human"
|
|
1641
|
-
})),
|
|
1642
|
-
releaseAge: z.object({
|
|
1643
|
-
enabled: z.boolean().default(false),
|
|
1644
|
-
registry: z.string().default("https://registry.npmjs.org"),
|
|
1645
|
-
authToken: z.string().optional(),
|
|
1646
|
-
thresholds: z.object({
|
|
1647
|
-
patch: ThresholdSchema.default(30),
|
|
1648
|
-
minor: ThresholdSchema.default(45),
|
|
1649
|
-
major: ThresholdSchema.default(60)
|
|
1650
|
-
}).default(() => ({
|
|
1651
|
-
patch: 30,
|
|
1652
|
-
minor: 45,
|
|
1653
|
-
major: 60
|
|
1654
|
-
}))
|
|
1655
|
-
}).default(() => ({
|
|
1656
|
-
enabled: false,
|
|
1657
|
-
registry: "https://registry.npmjs.org",
|
|
1658
|
-
thresholds: {
|
|
1659
|
-
patch: 30,
|
|
1660
|
-
minor: 45,
|
|
1661
|
-
major: 60
|
|
1662
|
-
}
|
|
1663
|
-
}))
|
|
1664
|
-
});
|
|
1665
|
-
//#endregion
|
|
1666
|
-
//#region src/config/loader.ts
|
|
1667
|
-
async function loadConfig(cwd, explicitPath) {
|
|
1668
|
-
const configPath = explicitPath ? resolve(explicitPath) : join(cwd, "hermex.config.ts");
|
|
1669
|
-
if (explicitPath && !existsSync(configPath)) throw new Error(`Config file not found: ${configPath}`);
|
|
1670
|
-
if (existsSync(configPath)) {
|
|
1671
|
-
const mod = await import(pathToFileURL(configPath).href);
|
|
1672
|
-
return HermexConfigSchema.parse(mod.default ?? mod);
|
|
1673
|
-
}
|
|
1674
|
-
return HermexConfigSchema.parse({});
|
|
1675
|
-
}
|
|
1676
|
-
//#endregion
|
|
1677
1706
|
//#region src/npm-registry/client.ts
|
|
1678
1707
|
async function fetchPackageInfo(name, registryUrl, authToken) {
|
|
1679
1708
|
const url = `${registryUrl.replace(/\/$/, "")}/${encodeURIComponent(name).replace("%40", "@")}`;
|
|
@@ -1715,8 +1744,10 @@ function upgradeLevel(daysAgo, bump, thresholds) {
|
|
|
1715
1744
|
if (daysAgo > threshold) return bump === "major" ? "mandatory_upgrade" : "needs_upgrade";
|
|
1716
1745
|
return null;
|
|
1717
1746
|
}
|
|
1718
|
-
function computeReleaseAge(installedVersion, timeMap, deprecated, thresholds) {
|
|
1719
|
-
const
|
|
1747
|
+
function computeReleaseAge(installedVersion, timeMap, deprecated, thresholds, distTags, severity) {
|
|
1748
|
+
const byBump = /* @__PURE__ */ new Map();
|
|
1749
|
+
let minCompliantVersion;
|
|
1750
|
+
let minCompliantReleasedDaysAgo;
|
|
1720
1751
|
for (const [version, dateStr] of Object.entries(timeMap)) {
|
|
1721
1752
|
if (version === "created" || version === "modified") continue;
|
|
1722
1753
|
if (!semver.valid(version)) continue;
|
|
@@ -1724,26 +1755,45 @@ function computeReleaseAge(installedVersion, timeMap, deprecated, thresholds) {
|
|
|
1724
1755
|
const bump = classifyBump(installedVersion, version);
|
|
1725
1756
|
if (!bump) continue;
|
|
1726
1757
|
const daysAgo = daysSince(dateStr);
|
|
1727
|
-
const
|
|
1758
|
+
const list = byBump.get(bump) ?? [];
|
|
1759
|
+
list.push({
|
|
1760
|
+
version,
|
|
1761
|
+
daysAgo
|
|
1762
|
+
});
|
|
1763
|
+
byBump.set(bump, list);
|
|
1764
|
+
const threshold = thresholds.patch;
|
|
1765
|
+
if (bump === "patch" && threshold !== false && threshold !== void 0 && daysAgo <= threshold && (minCompliantReleasedDaysAgo === void 0 || daysAgo > minCompliantReleasedDaysAgo)) {
|
|
1766
|
+
minCompliantVersion = version;
|
|
1767
|
+
minCompliantReleasedDaysAgo = daysAgo;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
const upgrades = [];
|
|
1771
|
+
for (const [bump, versions] of byBump.entries()) {
|
|
1772
|
+
const level = upgradeLevel(Math.max(...versions.map((v) => v.daysAgo)), bump, thresholds);
|
|
1728
1773
|
if (!level) continue;
|
|
1774
|
+
const newest = versions.reduce((a, b) => a.daysAgo < b.daysAgo ? a : b);
|
|
1729
1775
|
upgrades.push({
|
|
1730
|
-
version,
|
|
1731
|
-
releasedDaysAgo: daysAgo,
|
|
1776
|
+
version: newest.version,
|
|
1777
|
+
releasedDaysAgo: newest.daysAgo,
|
|
1732
1778
|
semverBump: bump,
|
|
1733
1779
|
level
|
|
1734
1780
|
});
|
|
1735
1781
|
}
|
|
1736
|
-
const
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
const finalUpgrades = Array.from(worstPerBump.values()).sort((a, b) => b.releasedDaysAgo - a.releasedDaysAgo);
|
|
1782
|
+
const finalUpgrades = upgrades.sort((a, b) => b.releasedDaysAgo - a.releasedDaysAgo);
|
|
1783
|
+
const latestVersion = distTags?.["latest"];
|
|
1784
|
+
const latestEntry = latestVersion ? timeMap[latestVersion] : void 0;
|
|
1785
|
+
const latestReleasedDaysAgo = latestEntry ? daysSince(latestEntry) : void 0;
|
|
1786
|
+
for (const upgrade of finalUpgrades) if (latestVersion && upgrade.version === latestVersion) upgrade.isLatest = true;
|
|
1742
1787
|
return {
|
|
1743
1788
|
installedVersion,
|
|
1744
1789
|
upgrades: finalUpgrades,
|
|
1745
1790
|
worstLevel: finalUpgrades.some((u) => u.level === "mandatory_upgrade") ? "mandatory_upgrade" : finalUpgrades.length > 0 ? "needs_upgrade" : null,
|
|
1746
|
-
deprecated
|
|
1791
|
+
deprecated,
|
|
1792
|
+
latestVersion,
|
|
1793
|
+
latestReleasedDaysAgo,
|
|
1794
|
+
minCompliantVersion,
|
|
1795
|
+
minCompliantReleasedDaysAgo,
|
|
1796
|
+
severity
|
|
1747
1797
|
};
|
|
1748
1798
|
}
|
|
1749
1799
|
async function enrichWithReleaseAge(packages, config) {
|
|
@@ -1764,9 +1814,10 @@ async function enrichWithReleaseAge(packages, config) {
|
|
|
1764
1814
|
};
|
|
1765
1815
|
}
|
|
1766
1816
|
const deprecated = info.versions?.[pkg.version]?.deprecated ?? info.deprecated;
|
|
1817
|
+
const severity = config.enforceOn.length === 0 || micromatch.isMatch(pkg.packageName, config.enforceOn) ? "error" : "warn";
|
|
1767
1818
|
return {
|
|
1768
1819
|
pkg,
|
|
1769
|
-
entry: computeReleaseAge(pkg.version, info.time, typeof deprecated === "string" ? deprecated : void 0, config.thresholds)
|
|
1820
|
+
entry: computeReleaseAge(pkg.version, info.time, typeof deprecated === "string" ? deprecated : void 0, config.thresholds, info["dist-tags"], severity)
|
|
1770
1821
|
};
|
|
1771
1822
|
}));
|
|
1772
1823
|
for (const { pkg, entry } of results) {
|
|
@@ -1784,6 +1835,53 @@ async function enrichWithReleaseAge(packages, config) {
|
|
|
1784
1835
|
};
|
|
1785
1836
|
}
|
|
1786
1837
|
//#endregion
|
|
1838
|
+
//#region src/commands/pipeline.ts
|
|
1839
|
+
/**
|
|
1840
|
+
* Runs the shared parse ā aggregate ā rules ā release-age pipeline used by
|
|
1841
|
+
* both `scan` and `comply`. Returns `null` if no files matched (the spinner
|
|
1842
|
+
* has already reported the failure); throws on unexpected errors.
|
|
1843
|
+
*/
|
|
1844
|
+
async function runPipeline(config, spinner) {
|
|
1845
|
+
const lockfileResult = findAndParseLockfile(process.cwd());
|
|
1846
|
+
spinner.succeed(chalk.blue(`š¦ Found ${lockfileResult.lockfileType} lockfile (supports: ${lockfileResult.supportedVersions.join(", ")}) - ${Object.keys(lockfileResult.versions).length} packages`));
|
|
1847
|
+
spinner.start("Finding files...");
|
|
1848
|
+
const files = await findFiles(config.includes, config.excludes);
|
|
1849
|
+
if (files.length === 0) {
|
|
1850
|
+
spinner.fail(chalk.red(`No files found matching includes: ${config.includes.join(", ")}`));
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
spinner.succeed(chalk.green(` Found ${files.length} files`));
|
|
1854
|
+
spinner.start("Analyzing files...");
|
|
1855
|
+
const reports = [];
|
|
1856
|
+
const parseErrors = [];
|
|
1857
|
+
for (let i = 0; i < files.length; i++) {
|
|
1858
|
+
const file = files[i];
|
|
1859
|
+
spinner.text = `Analyzing files... (${i + 1}/${files.length})`;
|
|
1860
|
+
try {
|
|
1861
|
+
const report = parseFile(file);
|
|
1862
|
+
if (report) reports.push(report);
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1865
|
+
parseErrors.push({
|
|
1866
|
+
file,
|
|
1867
|
+
message
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
spinner.succeed(chalk.green(`Analysis complete! Analyzed ${reports.length}/${files.length} files`));
|
|
1872
|
+
printErrors(parseErrors);
|
|
1873
|
+
const aggregated = aggregateReports(reports, lockfileResult.versions, config, lockfileResult.multiVersions);
|
|
1874
|
+
const evaluatorViolations = evaluateRules(process.cwd(), config.rules, config.excludes);
|
|
1875
|
+
aggregated.ruleViolations = [...aggregated.ruleViolations, ...evaluatorViolations];
|
|
1876
|
+
if (config.releaseAge.enabled) {
|
|
1877
|
+
spinner.start("Fetching release age from registry...");
|
|
1878
|
+
const { enriched, skipped } = await enrichWithReleaseAge(aggregated.packageDistribution, config.releaseAge);
|
|
1879
|
+
aggregated.packageDistribution = enriched;
|
|
1880
|
+
spinner.succeed(chalk.blue(`š
Release age fetched${skipped > 0 ? chalk.gray(` (${skipped} packages skipped ā registry unreachable or not found)`) : ""}`));
|
|
1881
|
+
}
|
|
1882
|
+
return aggregated;
|
|
1883
|
+
}
|
|
1884
|
+
//#endregion
|
|
1787
1885
|
//#region src/commands/scan.ts
|
|
1788
1886
|
function registerScanCommand(program) {
|
|
1789
1887
|
program.command("scan").description("Scan and analyze local files").option("--config <path>", "Path to hermex config file (overrides CWD discovery)").action(async (options) => {
|
|
@@ -1791,53 +1889,17 @@ function registerScanCommand(program) {
|
|
|
1791
1889
|
});
|
|
1792
1890
|
}
|
|
1793
1891
|
async function executeScan(config) {
|
|
1794
|
-
const startTime = Date.now();
|
|
1795
1892
|
const isJson = config.output.format === "json";
|
|
1893
|
+
(isJson ? process.stderr : process.stdout).write(chalk.gray(`hermex v${getVersion()}\n`));
|
|
1796
1894
|
const spinner = ora({
|
|
1797
1895
|
text: "Parsing lockfile...",
|
|
1798
1896
|
stream: isJson ? process.stderr : process.stdout
|
|
1799
1897
|
}).start();
|
|
1800
1898
|
try {
|
|
1801
|
-
const
|
|
1802
|
-
|
|
1803
|
-
spinner.start("Finding files...");
|
|
1804
|
-
const files = await findFiles(config.includes, config.excludes);
|
|
1805
|
-
if (files.length === 0) {
|
|
1806
|
-
spinner.fail(chalk.red(`No files found matching includes: ${config.includes.join(", ")}`));
|
|
1807
|
-
return;
|
|
1808
|
-
}
|
|
1809
|
-
spinner.succeed(chalk.green(` Found ${files.length} files`));
|
|
1810
|
-
spinner.start("Analyzing files...");
|
|
1811
|
-
const reports = [];
|
|
1812
|
-
const parseErrors = [];
|
|
1813
|
-
for (let i = 0; i < files.length; i++) {
|
|
1814
|
-
const file = files[i];
|
|
1815
|
-
spinner.text = `Analyzing files... (${i + 1}/${files.length})`;
|
|
1816
|
-
try {
|
|
1817
|
-
const report = parseFile(file);
|
|
1818
|
-
if (report) reports.push(report);
|
|
1819
|
-
} catch (error) {
|
|
1820
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1821
|
-
parseErrors.push({
|
|
1822
|
-
file,
|
|
1823
|
-
message
|
|
1824
|
-
});
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
spinner.succeed(chalk.green(`Analysis complete! Analyzed ${reports.length}/${files.length} files`));
|
|
1828
|
-
printErrors(parseErrors);
|
|
1829
|
-
const elapsedTime = (Date.now() - startTime) / 1e3;
|
|
1830
|
-
const aggregated = aggregateReports(reports, lockfileResult.versions, config, lockfileResult.multiVersions);
|
|
1831
|
-
const evaluatorViolations = evaluateRules(process.cwd(), config.rules, config.excludes);
|
|
1832
|
-
aggregated.ruleViolations = [...aggregated.ruleViolations, ...evaluatorViolations];
|
|
1833
|
-
if (config.releaseAge.enabled) {
|
|
1834
|
-
spinner.start("Fetching release age from registry...");
|
|
1835
|
-
const { enriched, skipped } = await enrichWithReleaseAge(aggregated.packageDistribution, config.releaseAge);
|
|
1836
|
-
aggregated.packageDistribution = enriched;
|
|
1837
|
-
spinner.succeed(chalk.blue(`š
Release age fetched${skipped > 0 ? chalk.gray(` (${skipped} packages skipped ā registry unreachable or not found)`) : ""}`));
|
|
1838
|
-
}
|
|
1899
|
+
const aggregated = await runPipeline(config, spinner);
|
|
1900
|
+
if (!aggregated) return;
|
|
1839
1901
|
if (isJson) printJson(aggregated);
|
|
1840
|
-
else printScanResults(aggregated, config
|
|
1902
|
+
else printScanResults(aggregated, config);
|
|
1841
1903
|
} catch (error) {
|
|
1842
1904
|
const message = error instanceof Error ? error.message : String(error);
|
|
1843
1905
|
spinner.fail(chalk.red("Analysis failed: " + message));
|
|
@@ -1845,7 +1907,7 @@ async function executeScan(config) {
|
|
|
1845
1907
|
process.exit(1);
|
|
1846
1908
|
}
|
|
1847
1909
|
}
|
|
1848
|
-
function printScanResults(aggregated, config
|
|
1910
|
+
function printScanResults(aggregated, config) {
|
|
1849
1911
|
if (config.output.packages) printPackages(aggregated, config.output.packages);
|
|
1850
1912
|
if (config.output.versus) printVersus(aggregated);
|
|
1851
1913
|
if (config.output.rules) printRules(aggregated);
|
|
@@ -1855,13 +1917,85 @@ function printScanResults(aggregated, config, _elapsedTime) {
|
|
|
1855
1917
|
if (config.output.summary) printSummary(aggregated);
|
|
1856
1918
|
}
|
|
1857
1919
|
//#endregion
|
|
1858
|
-
//#region
|
|
1859
|
-
|
|
1920
|
+
//#region src/utils/print-compliance.ts
|
|
1921
|
+
function printComplianceVerdict(result) {
|
|
1922
|
+
const mandatoryCount = result.errorRuleViolations.length + result.errorBannedPackageViolations.length + result.mandatoryReleaseAgeViolations.length;
|
|
1923
|
+
if (result.compliant) {
|
|
1924
|
+
console.log(chalk.greenBright.bold("\nā COMPLIANT\n"));
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
console.log(chalk.redBright.bold(`\nā NOT COMPLIANT`));
|
|
1928
|
+
console.log(chalk.red(` ${mandatoryCount} mandatory violation${mandatoryCount > 1 ? "s" : ""} found`));
|
|
1929
|
+
if (result.mandatoryReleaseAgeViolations.length > 0) {
|
|
1930
|
+
console.log();
|
|
1931
|
+
for (const pkg of result.mandatoryReleaseAgeViolations) {
|
|
1932
|
+
const top = pkg.releaseAge?.upgrades[0];
|
|
1933
|
+
console.log(chalk.red(` ā releaseAge ${pkg.packageName} is ${top?.semverBump} version behind (${top?.releasedDaysAgo}d) ā mandatory upgrade [ERROR]`));
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
console.log();
|
|
1937
|
+
}
|
|
1938
|
+
//#endregion
|
|
1939
|
+
//#region src/utils/compliance.ts
|
|
1940
|
+
/**
|
|
1941
|
+
* A package is a mandatory compliance failure only when its releaseAge
|
|
1942
|
+
* severity is 'error' (i.e. it's in scope per `releaseAge.enforceOn`) AND
|
|
1943
|
+
* its worstLevel is 'mandatory_upgrade' ā 'needs_upgrade' (patch/minor) is
|
|
1944
|
+
* advisory even for enforced packages, matching the existing "mandatory"
|
|
1945
|
+
* vocabulary already used by upgradeLevel().
|
|
1946
|
+
*/
|
|
1947
|
+
function computeCompliance(aggregated) {
|
|
1948
|
+
const errorRuleViolations = aggregated.ruleViolations.filter((v) => v.severity === "error");
|
|
1949
|
+
const errorBannedPackageViolations = aggregated.bannedPackageViolations.filter((v) => v.severity === "error");
|
|
1950
|
+
const mandatoryReleaseAgeViolations = aggregated.packageDistribution.filter((p) => p.releaseAge?.severity === "error" && p.releaseAge?.worstLevel === "mandatory_upgrade");
|
|
1951
|
+
return {
|
|
1952
|
+
compliant: errorRuleViolations.length === 0 && errorBannedPackageViolations.length === 0 && mandatoryReleaseAgeViolations.length === 0,
|
|
1953
|
+
errorRuleViolations,
|
|
1954
|
+
errorBannedPackageViolations,
|
|
1955
|
+
mandatoryReleaseAgeViolations
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
//#endregion
|
|
1959
|
+
//#region src/commands/comply.ts
|
|
1960
|
+
function registerComplyCommand(program) {
|
|
1961
|
+
program.command("comply").description("Check compliance with hermex.config.ts rules and release-age policy (exits non-zero if not compliant)").option("--config <path>", "Path to hermex config file (overrides CWD discovery)").action(async (options) => {
|
|
1962
|
+
await executeComply(await loadConfig(process.cwd(), options.config));
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
async function executeComply(config) {
|
|
1966
|
+
const isJson = config.output.format === "json";
|
|
1967
|
+
(isJson ? process.stderr : process.stdout).write(chalk.gray(`hermex v${getVersion()}\n`));
|
|
1968
|
+
const spinner = ora({
|
|
1969
|
+
text: "Parsing lockfile...",
|
|
1970
|
+
stream: isJson ? process.stderr : process.stdout
|
|
1971
|
+
}).start();
|
|
1972
|
+
try {
|
|
1973
|
+
const aggregated = await runPipeline(config, spinner);
|
|
1974
|
+
if (!aggregated) {
|
|
1975
|
+
process.exitCode = 2;
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
const compliance = computeCompliance(aggregated);
|
|
1979
|
+
if (isJson) printJson(aggregated);
|
|
1980
|
+
else {
|
|
1981
|
+
printRules(aggregated);
|
|
1982
|
+
if (config.releaseAge.enabled) printPackages(aggregated, "table");
|
|
1983
|
+
printComplianceVerdict(compliance);
|
|
1984
|
+
}
|
|
1985
|
+
process.exitCode = compliance.compliant ? 0 : 1;
|
|
1986
|
+
} catch (error) {
|
|
1987
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1988
|
+
spinner.fail(chalk.red("Compliance check failed: " + message));
|
|
1989
|
+
console.error(error);
|
|
1990
|
+
process.exitCode = 2;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1860
1993
|
//#endregion
|
|
1861
1994
|
//#region src/cli.ts
|
|
1862
1995
|
const program = new Command();
|
|
1863
|
-
program.name("hermex").description("Analyze React component usage patterns in your codebase").version(
|
|
1996
|
+
program.name("hermex").description("Analyze React component usage patterns in your codebase").version(getVersion());
|
|
1864
1997
|
registerScanCommand(program);
|
|
1998
|
+
registerComplyCommand(program);
|
|
1865
1999
|
program.parse(process.argv);
|
|
1866
2000
|
//#endregion
|
|
1867
2001
|
export { program };
|