hermex 1.3.0-beta.4 ā 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 +1502 -1398
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -2,1351 +2,1499 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import ora from "ora";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import
|
|
5
|
+
import Table from "cli-table3";
|
|
6
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
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
16
|
import { load } from "js-yaml";
|
|
16
17
|
import lockfile from "@yarnpkg/lockfile";
|
|
17
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
431
304
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
});
|
|
305
|
+
function ruleIcon(violation) {
|
|
306
|
+
if (violation.severity === "error") return chalk.red("ā");
|
|
307
|
+
if (violation.severity === "info") return chalk.blue("ā¹");
|
|
308
|
+
return chalk.yellow("ā ");
|
|
441
309
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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}`;
|
|
325
|
+
}
|
|
326
|
+
return `${patterns} not present${suffix}`;
|
|
447
327
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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(", ")}`));
|
|
453
359
|
}
|
|
360
|
+
//#endregion
|
|
361
|
+
//#region src/utils/version.ts
|
|
362
|
+
let cachedVersion;
|
|
454
363
|
/**
|
|
455
|
-
*
|
|
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.
|
|
456
367
|
*/
|
|
457
|
-
function
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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;
|
|
461
376
|
}
|
|
462
377
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
return
|
|
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;
|
|
468
383
|
}
|
|
469
384
|
//#endregion
|
|
470
|
-
//#region src/
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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);
|
|
519
556
|
break;
|
|
520
|
-
case "
|
|
521
|
-
|
|
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
|
-
});
|
|
557
|
+
case "ImportNamespaceSpecifier":
|
|
558
|
+
analyzeNamespaceImport(spec, source, node, state);
|
|
531
559
|
break;
|
|
532
|
-
|
|
533
|
-
|
|
560
|
+
case "ImportSpecifier":
|
|
561
|
+
analyzeNamedImport(spec, source, node, state);
|
|
534
562
|
break;
|
|
535
563
|
}
|
|
536
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
|
|
537
601
|
/**
|
|
538
|
-
*
|
|
602
|
+
* Extracts the name from a JSX element (handles identifiers and member expressions)
|
|
539
603
|
*/
|
|
540
|
-
function
|
|
541
|
-
if (
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
else if (node.callee?.property?.value === "forwardRef") analyzeForwardRefUsage(node, state);
|
|
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 "";
|
|
547
610
|
}
|
|
548
|
-
if (node.callee?.property?.value === "createPortal" || node.callee?.value === "createPortal") analyzePortalUsage(node, state);
|
|
549
|
-
visitChildren(node, state, context);
|
|
550
611
|
}
|
|
551
612
|
/**
|
|
552
|
-
*
|
|
613
|
+
* Checks if a JSX member expression is a known component
|
|
553
614
|
*/
|
|
554
|
-
function
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if (Array.isArray(value)) {
|
|
559
|
-
for (const item of value) if (item && typeof item === "object") visitNode(item, state, {
|
|
560
|
-
...context,
|
|
561
|
-
parent: node
|
|
562
|
-
});
|
|
563
|
-
} else if (value && typeof value === "object" && value.type) visitNode(value, state, {
|
|
564
|
-
...context,
|
|
565
|
-
parent: node
|
|
566
|
-
});
|
|
615
|
+
function isMemberExpressionComponent(nameNode, state) {
|
|
616
|
+
if (nameNode?.type === "JSXMemberExpression") {
|
|
617
|
+
const objectName = getJSXElementName(nameNode.object);
|
|
618
|
+
return state.allIdentifiers.has(objectName);
|
|
567
619
|
}
|
|
620
|
+
return false;
|
|
568
621
|
}
|
|
569
|
-
//#endregion
|
|
570
|
-
//#region src/swc-parser/core/report.ts
|
|
571
622
|
/**
|
|
572
|
-
*
|
|
623
|
+
* Extracts props from JSX attributes
|
|
573
624
|
*/
|
|
574
|
-
function
|
|
575
|
-
return
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
usage: {
|
|
589
|
-
jsx: Array.from(state.usagePatterns.jsxUsage.values()),
|
|
590
|
-
variables: Array.from(state.usagePatterns.variableAssignments.entries()).map(([key, value]) => ({
|
|
591
|
-
variable: key,
|
|
592
|
-
assignment: value.assignment
|
|
593
|
-
})),
|
|
594
|
-
destructuring: Array.from(state.usagePatterns.destructuredUsage),
|
|
595
|
-
conditional: Array.from(state.usagePatterns.conditionalUsage),
|
|
596
|
-
arrays: Array.from(state.usagePatterns.arrayMappings),
|
|
597
|
-
objects: Array.from(state.usagePatterns.objectMappings)
|
|
598
|
-
},
|
|
599
|
-
advanced: {
|
|
600
|
-
lazy: Array.from(state.usagePatterns.lazyImports),
|
|
601
|
-
dynamic: Array.from(state.usagePatterns.dynamicImports),
|
|
602
|
-
hoc: Array.from(state.usagePatterns.hocUsage),
|
|
603
|
-
memo: Array.from(state.usagePatterns.memoizedComponents),
|
|
604
|
-
forwardRef: Array.from(state.usagePatterns.forwardedRefs),
|
|
605
|
-
portal: Array.from(state.usagePatterns.portalUsage)
|
|
606
|
-
},
|
|
607
|
-
props: Array.from(state.usagePatterns.propsAnalysis.entries()).map(([component, analysis]) => ({
|
|
608
|
-
component,
|
|
609
|
-
analysis
|
|
610
|
-
}))
|
|
611
|
-
},
|
|
612
|
-
components: Array.from(state.componentNames).sort()
|
|
613
|
-
};
|
|
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);
|
|
614
639
|
}
|
|
615
640
|
/**
|
|
616
|
-
*
|
|
641
|
+
* Extracts value from JSX attribute
|
|
617
642
|
*/
|
|
618
|
-
function
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
return
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
//#endregion
|
|
625
|
-
//#region src/swc-parser/index.ts
|
|
626
|
-
function swcOptionsForFile(filePath) {
|
|
627
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
628
|
-
if (ext === ".ts") return {
|
|
629
|
-
syntax: "typescript",
|
|
630
|
-
tsx: false,
|
|
631
|
-
decorators: true,
|
|
632
|
-
dynamicImport: true
|
|
633
|
-
};
|
|
634
|
-
if (ext === ".tsx") return {
|
|
635
|
-
syntax: "typescript",
|
|
636
|
-
tsx: true,
|
|
637
|
-
decorators: true,
|
|
638
|
-
dynamicImport: true
|
|
639
|
-
};
|
|
640
|
-
if (ext === ".jsx") return {
|
|
641
|
-
syntax: "ecmascript",
|
|
642
|
-
jsx: true,
|
|
643
|
-
decorators: true,
|
|
644
|
-
importAssertions: true
|
|
645
|
-
};
|
|
646
|
-
return {
|
|
647
|
-
syntax: "ecmascript",
|
|
648
|
-
jsx: true,
|
|
649
|
-
decorators: true,
|
|
650
|
-
importAssertions: true
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
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/utils/package-distribution.ts
|
|
663
|
-
function resolvePackageFromImportPath(importPath, availablePackages) {
|
|
664
|
-
if (importPath.startsWith(".") || importPath.startsWith("/")) return "local";
|
|
665
|
-
const sortedPackages = [...availablePackages].sort((a, b) => b.length - a.length);
|
|
666
|
-
for (const pkg of sortedPackages) {
|
|
667
|
-
if (importPath === pkg) return pkg;
|
|
668
|
-
if (importPath.startsWith(`${pkg}/`)) return pkg;
|
|
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]";
|
|
669
649
|
}
|
|
670
|
-
return "unknown";
|
|
671
|
-
}
|
|
672
|
-
function findComponentSource(componentName, report, availablePackages) {
|
|
673
|
-
const namedImport = report.patterns.imports.named.find((imp) => imp.name === componentName);
|
|
674
|
-
if (namedImport) return resolvePackageFromImportPath(namedImport.source, availablePackages);
|
|
675
|
-
const defaultImport = report.patterns.imports.default.find((imp) => imp.name === componentName);
|
|
676
|
-
if (defaultImport) return resolvePackageFromImportPath(defaultImport.source, availablePackages);
|
|
677
|
-
const aliasedImport = report.patterns.imports.aliased.find((imp) => imp.local === componentName);
|
|
678
|
-
if (aliasedImport) return resolvePackageFromImportPath(aliasedImport.source, availablePackages);
|
|
679
|
-
return "unknown";
|
|
680
650
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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]";
|
|
692
666
|
}
|
|
693
|
-
return null;
|
|
694
667
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
} else {
|
|
708
|
-
const isInternal = internalPatterns.length > 0 ? micromatch.isMatch(component.source, internalPatterns) : false;
|
|
709
|
-
const allVersions = multiVersions[component.source] ?? [];
|
|
710
|
-
const hasVersionConflict = allVersions.length > 1;
|
|
711
|
-
packageMap.set(component.source, {
|
|
712
|
-
packageName: component.source,
|
|
713
|
-
version: getPackageVersion(component.source, versions),
|
|
714
|
-
componentCount: 1,
|
|
715
|
-
usageCount: component.count,
|
|
716
|
-
percentage: 0,
|
|
717
|
-
components: [component.name],
|
|
718
|
-
internal: isInternal,
|
|
719
|
-
hasVersionConflict,
|
|
720
|
-
allVersions
|
|
721
|
-
});
|
|
722
|
-
}
|
|
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";
|
|
723
680
|
}
|
|
724
|
-
const distribution = Array.from(packageMap.values());
|
|
725
|
-
const totalExternalUsage = distribution.reduce((sum, pkg) => sum + pkg.usageCount, 0);
|
|
726
|
-
for (const pkg of distribution) pkg.percentage = totalExternalUsage > 0 ? pkg.usageCount / totalExternalUsage * 100 : 0;
|
|
727
|
-
return distribution.sort((a, b) => b.usageCount - a.usageCount);
|
|
728
681
|
}
|
|
729
682
|
//#endregion
|
|
730
|
-
//#region src/
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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"
|
|
742
719
|
});
|
|
743
|
-
|
|
720
|
+
analysis.hasComplexProps = true;
|
|
744
721
|
}
|
|
745
|
-
|
|
722
|
+
state.usagePatterns.propsAnalysis.set(componentName, analysis);
|
|
723
|
+
return analysis;
|
|
746
724
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
+
}
|
|
754
749
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
patterns: rule.patterns,
|
|
765
|
-
message: rule.message,
|
|
766
|
-
matchedFiles: matches
|
|
767
|
-
});
|
|
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";
|
|
768
759
|
}
|
|
769
|
-
|
|
770
|
-
type: "require_files",
|
|
771
|
-
severity: rule.severity,
|
|
772
|
-
patterns: rule.patterns,
|
|
773
|
-
message: rule.message,
|
|
774
|
-
matchedFiles: []
|
|
775
|
-
});
|
|
776
|
-
for (const rule of toArray(rulesConfig.allow_files)) if (findMatches(rule.patterns, repoPath, excludes).length === 0) violations.push({
|
|
777
|
-
type: "allow_files",
|
|
778
|
-
severity: rule.severity,
|
|
779
|
-
patterns: rule.patterns,
|
|
780
|
-
message: rule.message,
|
|
781
|
-
matchedFiles: []
|
|
782
|
-
});
|
|
783
|
-
return violations;
|
|
760
|
+
return false;
|
|
784
761
|
}
|
|
785
762
|
//#endregion
|
|
786
|
-
//#region src/
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
return rules.filter((rule) => !rule.patterns.some((p) => scriptKeys.some((k) => micromatch.isMatch(k, p)))).map((rule) => ({
|
|
793
|
-
type: "require_scripts",
|
|
794
|
-
severity: rule.severity,
|
|
795
|
-
patterns: rule.patterns,
|
|
796
|
-
message: rule.message,
|
|
797
|
-
matchedFiles: []
|
|
798
|
-
}));
|
|
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);
|
|
799
769
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
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);
|
|
814
785
|
}
|
|
815
786
|
//#endregion
|
|
816
|
-
//#region src/
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
return
|
|
822
|
-
if (!nodeRange) return [{
|
|
823
|
-
type: "engine_version",
|
|
824
|
-
severity: rule.severity,
|
|
825
|
-
patterns: [],
|
|
826
|
-
message: rule.message ?? "engines.node not specified in package.json",
|
|
827
|
-
matchedFiles: [],
|
|
828
|
-
requiredRange: rule.range
|
|
829
|
-
}];
|
|
830
|
-
const minVer = semver.minVersion(nodeRange);
|
|
831
|
-
if (!minVer || !semver.satisfies(minVer, rule.range)) return [{
|
|
832
|
-
type: "engine_version",
|
|
833
|
-
severity: rule.severity,
|
|
834
|
-
patterns: [],
|
|
835
|
-
message: rule.message,
|
|
836
|
-
matchedFiles: [],
|
|
837
|
-
installedRange: nodeRange,
|
|
838
|
-
requiredRange: rule.range
|
|
839
|
-
}];
|
|
840
|
-
return [];
|
|
841
|
-
});
|
|
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);
|
|
842
793
|
}
|
|
843
794
|
//#endregion
|
|
844
|
-
//#region src/
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
+
}
|
|
852
817
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
if (
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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
|
+
}
|
|
866
833
|
}
|
|
867
|
-
return violations;
|
|
868
834
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
matchedFiles: []
|
|
880
|
-
});
|
|
881
|
-
return violations;
|
|
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
|
+
}
|
|
882
845
|
}
|
|
883
846
|
//#endregion
|
|
884
|
-
//#region src/
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
increment(patternMap, "advanced.lazy", report.patterns.advanced.lazy.length);
|
|
897
|
-
increment(patternMap, "advanced.dynamic", report.patterns.advanced.dynamic.length);
|
|
898
|
-
increment(patternMap, "advanced.hoc", report.patterns.advanced.hoc.length);
|
|
899
|
-
increment(patternMap, "advanced.memo", report.patterns.advanced.memo.length);
|
|
900
|
-
increment(patternMap, "advanced.forwardRef", report.patterns.advanced.forwardRef.length);
|
|
901
|
-
increment(patternMap, "advanced.portal", report.patterns.advanced.portal.length);
|
|
902
|
-
}
|
|
903
|
-
function increment(map, key, value) {
|
|
904
|
-
map.set(key, (map.get(key) || 0) + value);
|
|
905
|
-
}
|
|
906
|
-
function getPatternDisplayName(patternType) {
|
|
907
|
-
return {
|
|
908
|
-
"imports.default": "Default Imports",
|
|
909
|
-
"imports.named": "Named Imports",
|
|
910
|
-
"imports.namespace": "Namespace Imports",
|
|
911
|
-
"imports.aliased": "Aliased Imports",
|
|
912
|
-
"usage.jsx": "JSX Usage",
|
|
913
|
-
"usage.variables": "Variable Assignments",
|
|
914
|
-
"usage.destructuring": "Destructuring",
|
|
915
|
-
"usage.conditional": "Conditional Usage",
|
|
916
|
-
"usage.arrays": "Array Mappings",
|
|
917
|
-
"usage.objects": "Object Mappings",
|
|
918
|
-
"advanced.lazy": "Lazy Loading",
|
|
919
|
-
"advanced.dynamic": "Dynamic Imports",
|
|
920
|
-
"advanced.hoc": "Higher-Order Components",
|
|
921
|
-
"advanced.memo": "Memoized Components",
|
|
922
|
-
"advanced.forwardRef": "Forward Refs",
|
|
923
|
-
"advanced.portal": "Portal Usage"
|
|
924
|
-
}[patternType] || patternType;
|
|
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
|
+
});
|
|
925
859
|
}
|
|
926
860
|
//#endregion
|
|
927
|
-
//#region src/
|
|
928
|
-
|
|
929
|
-
|
|
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
|
+
});
|
|
930
873
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
entries.sort((a, b) => b.count - a.count);
|
|
946
|
-
return {
|
|
947
|
-
name: vc.name,
|
|
948
|
-
packages: vc.packages,
|
|
949
|
-
entries,
|
|
950
|
-
totalCount
|
|
951
|
-
};
|
|
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
|
|
952
888
|
});
|
|
953
889
|
}
|
|
954
890
|
//#endregion
|
|
955
|
-
//#region src/
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
else {
|
|
970
|
-
const source = findComponentSource(jsx.component, report, availablePackages);
|
|
971
|
-
componentUsageMap.set(key, {
|
|
972
|
-
name: jsx.component,
|
|
973
|
-
source,
|
|
974
|
-
count: 1,
|
|
975
|
-
files: /* @__PURE__ */ new Set()
|
|
976
|
-
});
|
|
977
|
-
}
|
|
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
|
+
});
|
|
978
905
|
}
|
|
979
|
-
countPatterns(report, patternCountMap);
|
|
980
906
|
}
|
|
981
|
-
const topComponents = Array.from(componentUsageMap.values()).sort((a, b) => b.count - a.count);
|
|
982
|
-
const allComponents = Array.from(componentUsageMap.keys()).sort();
|
|
983
|
-
const patternCounts = Array.from(patternCountMap.entries()).map(([type, count]) => ({
|
|
984
|
-
patternType: type,
|
|
985
|
-
displayName: getPatternDisplayName(type),
|
|
986
|
-
count
|
|
987
|
-
})).sort((a, b) => b.count - a.count);
|
|
988
|
-
const packageDistribution = calculatePackageDistribution(componentUsageMap, versions, config, multiVersions);
|
|
989
|
-
const versusResults = calculateVersusResults(packageDistribution, config?.versus ?? []);
|
|
990
|
-
const bannedPackageViolations = detectBannedPackages(packageDistribution, config);
|
|
991
|
-
const requiredPackageViolations = detectRequiredPackages(packageDistribution, versions, config);
|
|
992
|
-
return {
|
|
993
|
-
filesAnalyzed: reports.length,
|
|
994
|
-
totalImports,
|
|
995
|
-
totalComponents: componentUsageMap.size,
|
|
996
|
-
totalUsagePatterns,
|
|
997
|
-
patternCounts,
|
|
998
|
-
componentUsage: componentUsageMap,
|
|
999
|
-
topComponents,
|
|
1000
|
-
allComponents,
|
|
1001
|
-
packageDistribution,
|
|
1002
|
-
versusResults,
|
|
1003
|
-
ruleViolations: requiredPackageViolations,
|
|
1004
|
-
bannedPackageViolations,
|
|
1005
|
-
reports
|
|
1006
|
-
};
|
|
1007
907
|
}
|
|
1008
|
-
//#endregion
|
|
1009
|
-
//#region src/utils/format-utils.ts
|
|
1010
908
|
/**
|
|
1011
|
-
*
|
|
1012
|
-
* @param num - Number to format
|
|
1013
|
-
* @returns Formatted string (e.g., 1,234,567)
|
|
909
|
+
* Analyzes dynamic import() calls
|
|
1014
910
|
*/
|
|
1015
|
-
function
|
|
1016
|
-
|
|
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
|
+
});
|
|
1017
917
|
}
|
|
1018
918
|
//#endregion
|
|
1019
|
-
//#region src/
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
+
});
|
|
1022
929
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
|
1031
938
|
});
|
|
1032
|
-
const externalComponents = aggregated.topComponents.filter((comp) => comp.source !== "unknown" && comp.source !== "local").length;
|
|
1033
|
-
const totalExternalUsage = aggregated.packageDistribution.reduce((sum, pkg) => sum + pkg.usageCount, 0);
|
|
1034
|
-
table.push(["Files Analyzed", formatCount(aggregated.filesAnalyzed)], ["External Packages", formatCount(aggregated.packageDistribution.length)], ["External Components", formatCount(externalComponents)], ["Total Usages", formatCount(totalExternalUsage)]);
|
|
1035
|
-
console.log(table.toString());
|
|
1036
939
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
940
|
+
/**
|
|
941
|
+
* Analyzes React.forwardRef() usage
|
|
942
|
+
*/
|
|
943
|
+
function analyzeForwardRefUsage(node, state) {
|
|
944
|
+
state.usagePatterns.forwardedRefs.add({ line: node.span?.start || 0 });
|
|
1041
945
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
946
|
+
/**
|
|
947
|
+
* Analyzes ReactDOM.createPortal() usage
|
|
948
|
+
*/
|
|
949
|
+
function analyzePortalUsage(node, state) {
|
|
950
|
+
state.usagePatterns.portalUsage.add({ line: node.span?.start || 0 });
|
|
1046
951
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
}
|
|
1055
|
-
const maxValue = Math.max(...data.map((d) => d.value));
|
|
1056
|
-
if (maxValue === 0) {
|
|
1057
|
-
console.log(chalk.gray(" All values are zero"));
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
const maxLabelLength = Math.max(...data.map((d) => d.label.length));
|
|
1061
|
-
for (const item of data) {
|
|
1062
|
-
const percentage = item.value / maxValue;
|
|
1063
|
-
const barLength = Math.round(percentage * maxWidth);
|
|
1064
|
-
const emptyLength = maxWidth - barLength;
|
|
1065
|
-
const paddedLabel = item.label.padEnd(maxLabelLength, " ");
|
|
1066
|
-
const bar = chalk.green(barChar.repeat(barLength)) + chalk.gray(emptyChar.repeat(emptyLength));
|
|
1067
|
-
const valueStr = showValues ? ` ${formatCount(item.value)}` : "";
|
|
1068
|
-
console.log(`${paddedLabel} ${bar}${valueStr}\n`);
|
|
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);
|
|
1069
959
|
}
|
|
1070
960
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
function printComponents(aggregated, mode) {
|
|
1077
|
-
const components = aggregated.topComponents;
|
|
1078
|
-
if (mode === "table") printComponentsTable(components);
|
|
1079
|
-
else if (mode === "chart") printComponentsChart(components);
|
|
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));
|
|
1080
966
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
+
}
|
|
1107
1034
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1035
|
+
/**
|
|
1036
|
+
* Analyzes call expressions and routes to specific analyzers
|
|
1037
|
+
*/
|
|
1038
|
+
function analyzeCallExpression(node, state, context) {
|
|
1039
|
+
if (node.callee?.value === "lazy" || node.callee?.object?.value === "React" && node.callee?.property?.value === "lazy") analyzeLazyImport(node, state);
|
|
1040
|
+
if (node.callee?.type === "Import") analyzeDynamicImport(node, state);
|
|
1041
|
+
if (isHOCPattern(node, state)) analyzeHOCUsage(node, state);
|
|
1042
|
+
if (node.callee?.object?.value === "React") {
|
|
1043
|
+
if (node.callee?.property?.value === "memo") analyzeMemoUsage(node, state);
|
|
1044
|
+
else if (node.callee?.property?.value === "forwardRef") analyzeForwardRefUsage(node, state);
|
|
1045
|
+
}
|
|
1046
|
+
if (node.callee?.property?.value === "createPortal" || node.callee?.value === "createPortal") analyzePortalUsage(node, state);
|
|
1047
|
+
visitChildren(node, state, context);
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Visits all children of a node
|
|
1051
|
+
*/
|
|
1052
|
+
function visitChildren(node, state, context) {
|
|
1053
|
+
if (!node) return;
|
|
1054
|
+
for (const key in node) {
|
|
1055
|
+
const value = node[key];
|
|
1056
|
+
if (Array.isArray(value)) {
|
|
1057
|
+
for (const item of value) if (item && typeof item === "object") visitNode(item, state, {
|
|
1058
|
+
...context,
|
|
1059
|
+
parent: node
|
|
1060
|
+
});
|
|
1061
|
+
} else if (value && typeof value === "object" && value.type) visitNode(value, state, {
|
|
1062
|
+
...context,
|
|
1063
|
+
parent: node
|
|
1064
|
+
});
|
|
1114
1065
|
}
|
|
1115
|
-
renderBarChart(externalComponents.map((comp) => ({
|
|
1116
|
-
label: comp.name,
|
|
1117
|
-
value: comp.count
|
|
1118
|
-
})), { maxWidth: 50 });
|
|
1119
1066
|
}
|
|
1120
1067
|
//#endregion
|
|
1121
|
-
//#region src/
|
|
1122
|
-
|
|
1123
|
-
|
|
1068
|
+
//#region src/swc-parser/core/report.ts
|
|
1069
|
+
/**
|
|
1070
|
+
* Generates a comprehensive usage report from parser state
|
|
1071
|
+
*/
|
|
1072
|
+
function generateReport(state) {
|
|
1073
|
+
return {
|
|
1074
|
+
summary: {
|
|
1075
|
+
totalImports: state.usagePatterns.defaultImports.size + state.usagePatterns.namedImports.size + state.usagePatterns.namespaceImports.size,
|
|
1076
|
+
totalComponents: state.componentNames.size,
|
|
1077
|
+
totalUsagePatterns: calculateTotalPatterns(state)
|
|
1078
|
+
},
|
|
1079
|
+
patterns: {
|
|
1080
|
+
imports: {
|
|
1081
|
+
default: Array.from(state.usagePatterns.defaultImports),
|
|
1082
|
+
named: Array.from(state.usagePatterns.namedImports),
|
|
1083
|
+
namespace: Array.from(state.usagePatterns.namespaceImports),
|
|
1084
|
+
aliased: Array.from(state.usagePatterns.aliasedImports.values())
|
|
1085
|
+
},
|
|
1086
|
+
usage: {
|
|
1087
|
+
jsx: Array.from(state.usagePatterns.jsxUsage.values()),
|
|
1088
|
+
variables: Array.from(state.usagePatterns.variableAssignments.entries()).map(([key, value]) => ({
|
|
1089
|
+
variable: key,
|
|
1090
|
+
assignment: value.assignment
|
|
1091
|
+
})),
|
|
1092
|
+
destructuring: Array.from(state.usagePatterns.destructuredUsage),
|
|
1093
|
+
conditional: Array.from(state.usagePatterns.conditionalUsage),
|
|
1094
|
+
arrays: Array.from(state.usagePatterns.arrayMappings),
|
|
1095
|
+
objects: Array.from(state.usagePatterns.objectMappings)
|
|
1096
|
+
},
|
|
1097
|
+
advanced: {
|
|
1098
|
+
lazy: Array.from(state.usagePatterns.lazyImports),
|
|
1099
|
+
dynamic: Array.from(state.usagePatterns.dynamicImports),
|
|
1100
|
+
hoc: Array.from(state.usagePatterns.hocUsage),
|
|
1101
|
+
memo: Array.from(state.usagePatterns.memoizedComponents),
|
|
1102
|
+
forwardRef: Array.from(state.usagePatterns.forwardedRefs),
|
|
1103
|
+
portal: Array.from(state.usagePatterns.portalUsage)
|
|
1104
|
+
},
|
|
1105
|
+
props: Array.from(state.usagePatterns.propsAnalysis.entries()).map(([component, analysis]) => ({
|
|
1106
|
+
component,
|
|
1107
|
+
analysis
|
|
1108
|
+
}))
|
|
1109
|
+
},
|
|
1110
|
+
components: Array.from(state.componentNames).sort()
|
|
1111
|
+
};
|
|
1124
1112
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1113
|
+
/**
|
|
1114
|
+
* Calculates total number of usage patterns found
|
|
1115
|
+
*/
|
|
1116
|
+
function calculateTotalPatterns(state) {
|
|
1117
|
+
return Object.values(state.usagePatterns).reduce((sum, collection) => {
|
|
1118
|
+
if (collection instanceof Set || collection instanceof Map) return sum + collection.size;
|
|
1119
|
+
return sum;
|
|
1120
|
+
}, 0);
|
|
1129
1121
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1122
|
+
//#endregion
|
|
1123
|
+
//#region src/swc-parser/index.ts
|
|
1124
|
+
function swcOptionsForFile(filePath) {
|
|
1125
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1126
|
+
if (ext === ".ts") return {
|
|
1127
|
+
syntax: "typescript",
|
|
1128
|
+
tsx: false,
|
|
1129
|
+
decorators: true,
|
|
1130
|
+
dynamicImport: true
|
|
1131
|
+
};
|
|
1132
|
+
if (ext === ".tsx") return {
|
|
1133
|
+
syntax: "typescript",
|
|
1134
|
+
tsx: true,
|
|
1135
|
+
decorators: true,
|
|
1136
|
+
dynamicImport: true
|
|
1137
|
+
};
|
|
1138
|
+
if (ext === ".jsx") return {
|
|
1139
|
+
syntax: "ecmascript",
|
|
1140
|
+
jsx: true,
|
|
1141
|
+
decorators: true,
|
|
1142
|
+
importAssertions: true
|
|
1143
|
+
};
|
|
1144
|
+
return {
|
|
1145
|
+
syntax: "ecmascript",
|
|
1146
|
+
jsx: true,
|
|
1147
|
+
decorators: true,
|
|
1148
|
+
importAssertions: true
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
function parseCode(code, filePath = "file.tsx") {
|
|
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);
|
|
1158
|
+
}
|
|
1159
|
+
//#endregion
|
|
1160
|
+
//#region src/utils/package-distribution.ts
|
|
1161
|
+
function resolvePackageFromImportPath(importPath, availablePackages) {
|
|
1162
|
+
if (importPath.startsWith(".") || importPath.startsWith("/")) return "local";
|
|
1163
|
+
const sortedPackages = [...availablePackages].sort((a, b) => b.length - a.length);
|
|
1164
|
+
for (const pkg of sortedPackages) {
|
|
1165
|
+
if (importPath === pkg) return pkg;
|
|
1166
|
+
if (importPath.startsWith(`${pkg}/`)) return pkg;
|
|
1135
1167
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1168
|
+
return "unknown";
|
|
1169
|
+
}
|
|
1170
|
+
function findComponentSource(componentName, report, availablePackages) {
|
|
1171
|
+
const namedImport = report.patterns.imports.named.find((imp) => imp.name === componentName);
|
|
1172
|
+
if (namedImport) return resolvePackageFromImportPath(namedImport.source, availablePackages);
|
|
1173
|
+
const defaultImport = report.patterns.imports.default.find((imp) => imp.name === componentName);
|
|
1174
|
+
if (defaultImport) return resolvePackageFromImportPath(defaultImport.source, availablePackages);
|
|
1175
|
+
const aliasedImport = report.patterns.imports.aliased.find((imp) => imp.local === componentName);
|
|
1176
|
+
if (aliasedImport) return resolvePackageFromImportPath(aliasedImport.source, availablePackages);
|
|
1177
|
+
return "unknown";
|
|
1178
|
+
}
|
|
1179
|
+
function getPackageVersion(packageName, versions) {
|
|
1180
|
+
if (versions[packageName]) return versions[packageName];
|
|
1181
|
+
if (packageName.includes("/")) {
|
|
1182
|
+
const parts = packageName.split("/");
|
|
1183
|
+
if (packageName.startsWith("@") && parts.length > 2) {
|
|
1184
|
+
const basePackage = `${parts[0]}/${parts[1]}`;
|
|
1185
|
+
if (versions[basePackage]) return versions[basePackage];
|
|
1141
1186
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
const totalPatterns = patterns.reduce((sum, p) => sum + p.count, 0);
|
|
1148
|
-
console.log(chalk.gray(`\nTotal: ${totalPatterns} patterns detected`));
|
|
1187
|
+
if (!packageName.startsWith("@") && parts.length > 1) {
|
|
1188
|
+
if (versions[parts[0]]) return versions[parts[0]];
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return null;
|
|
1149
1192
|
}
|
|
1150
|
-
function
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1193
|
+
function calculatePackageDistribution(componentUsageMap, versions, config, multiVersions = {}) {
|
|
1194
|
+
const ignorePatterns = config?.packages.ignore ?? [];
|
|
1195
|
+
const internalPatterns = config?.packages.internal ?? [];
|
|
1196
|
+
const packageMap = /* @__PURE__ */ new Map();
|
|
1197
|
+
for (const component of componentUsageMap.values()) {
|
|
1198
|
+
if (component.source === "unknown" || component.source === "local") continue;
|
|
1199
|
+
if (ignorePatterns.length > 0 && micromatch.isMatch(component.source, ignorePatterns)) continue;
|
|
1200
|
+
const existing = packageMap.get(component.source);
|
|
1201
|
+
if (existing) {
|
|
1202
|
+
existing.componentCount++;
|
|
1203
|
+
existing.usageCount += component.count;
|
|
1204
|
+
existing.components.push(component.name);
|
|
1205
|
+
} else {
|
|
1206
|
+
const isInternal = internalPatterns.length > 0 ? micromatch.isMatch(component.source, internalPatterns) : false;
|
|
1207
|
+
const allVersions = multiVersions[component.source] ?? [];
|
|
1208
|
+
const hasVersionConflict = allVersions.length > 1;
|
|
1209
|
+
packageMap.set(component.source, {
|
|
1210
|
+
packageName: component.source,
|
|
1211
|
+
version: getPackageVersion(component.source, versions),
|
|
1212
|
+
componentCount: 1,
|
|
1213
|
+
usageCount: component.count,
|
|
1214
|
+
percentage: 0,
|
|
1215
|
+
components: [component.name],
|
|
1216
|
+
internal: isInternal,
|
|
1217
|
+
hasVersionConflict,
|
|
1218
|
+
allVersions
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1155
1221
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1222
|
+
const distribution = Array.from(packageMap.values());
|
|
1223
|
+
const totalExternalUsage = distribution.reduce((sum, pkg) => sum + pkg.usageCount, 0);
|
|
1224
|
+
for (const pkg of distribution) pkg.percentage = totalExternalUsage > 0 ? pkg.usageCount / totalExternalUsage * 100 : 0;
|
|
1225
|
+
return distribution.sort((a, b) => b.usageCount - a.usageCount);
|
|
1160
1226
|
}
|
|
1161
1227
|
//#endregion
|
|
1162
|
-
//#region src/
|
|
1163
|
-
function
|
|
1164
|
-
|
|
1228
|
+
//#region src/rules/shared.ts
|
|
1229
|
+
function toArray(val) {
|
|
1230
|
+
if (!val) return [];
|
|
1231
|
+
return Array.isArray(val) ? val : [val];
|
|
1165
1232
|
}
|
|
1166
|
-
function
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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, "/")));
|
|
1242
|
+
}
|
|
1243
|
+
return [...new Set(matches)];
|
|
1172
1244
|
}
|
|
1173
|
-
function
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
return chalk.yellow(`ā ${top.semverBump} ${top.version} (${top.releasedDaysAgo}d)`);
|
|
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;
|
|
1251
|
+
}
|
|
1181
1252
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1253
|
+
//#endregion
|
|
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
|
+
});
|
|
1266
|
+
}
|
|
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: []
|
|
1273
|
+
});
|
|
1274
|
+
return violations;
|
|
1184
1275
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1276
|
+
//#endregion
|
|
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
|
+
}));
|
|
1290
|
+
}
|
|
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
|
+
}));
|
|
1305
|
+
}
|
|
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
|
+
});
|
|
1190
1333
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
"Package",
|
|
1200
|
-
"Version",
|
|
1201
|
-
"Components",
|
|
1202
|
-
"Usage",
|
|
1203
|
-
"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)
|
|
1204
1342
|
];
|
|
1205
|
-
if (hasReleaseAge) head.push("Upgrades");
|
|
1206
|
-
const table = new Table({
|
|
1207
|
-
head,
|
|
1208
|
-
style: {
|
|
1209
|
-
head: ["cyan"],
|
|
1210
|
-
border: ["gray"]
|
|
1211
|
-
}
|
|
1212
|
-
});
|
|
1213
|
-
packages.forEach((pkg) => {
|
|
1214
|
-
const versionCell = pkg.hasVersionConflict ? chalk.yellow(`ā ${pkg.allVersions.join(", ")} (multiple ā bundle impact)`) : pkg.version || "N/A";
|
|
1215
|
-
const row = [
|
|
1216
|
-
formatPackageName(pkg, getBannedViolation(pkg, violations)),
|
|
1217
|
-
versionCell,
|
|
1218
|
-
formatCount(pkg.componentCount),
|
|
1219
|
-
formatCount(pkg.usageCount),
|
|
1220
|
-
`${pkg.percentage.toFixed(1)}%`
|
|
1221
|
-
];
|
|
1222
|
-
if (hasReleaseAge) row.push(formatUpgradeCell(pkg.releaseAge));
|
|
1223
|
-
table.push(row);
|
|
1224
|
-
});
|
|
1225
|
-
console.log(table.toString());
|
|
1226
|
-
const totalComponents = packages.reduce((sum, p) => sum + p.componentCount, 0);
|
|
1227
|
-
const totalExternalUsage = packages.reduce((sum, p) => sum + p.usageCount, 0);
|
|
1228
|
-
console.log(chalk.gray(`\nTotal: ${formatCount(packages.length)} packages | ${formatCount(totalComponents)} unique components | ${formatCount(totalExternalUsage)} total usages`));
|
|
1229
1343
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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;
|
|
1235
1357
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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: []
|
|
1245
1371
|
});
|
|
1372
|
+
return violations;
|
|
1246
1373
|
}
|
|
1247
1374
|
//#endregion
|
|
1248
|
-
//#region src/utils/
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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);
|
|
1261
1393
|
}
|
|
1262
|
-
function
|
|
1263
|
-
|
|
1264
|
-
console.log(chalk.gray(` ${"ā".repeat(50)}`));
|
|
1265
|
-
const maxNameLen = Math.max(...result.entries.map((e) => e.packageName.length));
|
|
1266
|
-
for (const entry of result.entries) {
|
|
1267
|
-
const name = entry.packageName.padEnd(maxNameLen);
|
|
1268
|
-
const bar = renderBar(entry.percentage);
|
|
1269
|
-
const pct = chalk.bold(`${entry.percentage.toFixed(1)}%`);
|
|
1270
|
-
const usage = chalk.gray(`(${entry.count} usages)`);
|
|
1271
|
-
const components = entry.components.length > 0 ? chalk.gray(` ${formatComponents(entry.components)}`) : "";
|
|
1272
|
-
console.log(` ${name} ${bar} ${pct} ${usage}${components}`);
|
|
1273
|
-
}
|
|
1274
|
-
if (result.totalCount === 0) console.log(chalk.gray(" No usage detected for any package in this group."));
|
|
1275
|
-
console.log();
|
|
1394
|
+
function increment(map, key, value) {
|
|
1395
|
+
map.set(key, (map.get(key) || 0) + value);
|
|
1276
1396
|
}
|
|
1277
|
-
function
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
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;
|
|
1281
1416
|
}
|
|
1282
1417
|
//#endregion
|
|
1283
|
-
//#region src/utils/
|
|
1284
|
-
function
|
|
1285
|
-
|
|
1286
|
-
case "forbid_files": return "forbid_files";
|
|
1287
|
-
case "require_files": return "require_files";
|
|
1288
|
-
case "allow_files": return "allow_files";
|
|
1289
|
-
case "forbid_packages": return "forbid_packages";
|
|
1290
|
-
case "require_packages": return "require_packages";
|
|
1291
|
-
case "require_scripts": return "require_scripts";
|
|
1292
|
-
case "require_package_fields": return "pkg_fields";
|
|
1293
|
-
case "engine_version": return "engine_version";
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
function ruleIcon(violation) {
|
|
1297
|
-
if (violation.severity === "error") return chalk.red("ā");
|
|
1298
|
-
return chalk.yellow("ā ");
|
|
1418
|
+
//#region src/utils/versus.ts
|
|
1419
|
+
function toPercentage(count, total) {
|
|
1420
|
+
return total > 0 ? count / total * 100 : 0;
|
|
1299
1421
|
}
|
|
1300
|
-
function
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
return
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
+
});
|
|
1318
1444
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
+
}
|
|
1342
1469
|
}
|
|
1470
|
+
countPatterns(report, patternCountMap);
|
|
1343
1471
|
}
|
|
1344
|
-
const
|
|
1345
|
-
const
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
+
};
|
|
1350
1498
|
}
|
|
1351
1499
|
//#endregion
|
|
1352
1500
|
//#region src/utils/print-errors.ts
|
|
@@ -1360,53 +1508,6 @@ function printErrors(errors) {
|
|
|
1360
1508
|
console.log("");
|
|
1361
1509
|
}
|
|
1362
1510
|
//#endregion
|
|
1363
|
-
//#region src/utils/version.ts
|
|
1364
|
-
let cachedVersion;
|
|
1365
|
-
/**
|
|
1366
|
-
* Walks up from `dir` to find the nearest package.json.
|
|
1367
|
-
* Needed because this module's own path differs between the tsdown-bundled
|
|
1368
|
-
* single-file `dist/cli.mjs` and the unbundled source used by tests.
|
|
1369
|
-
*/
|
|
1370
|
-
function findPackageJson(dir) {
|
|
1371
|
-
let current = dir;
|
|
1372
|
-
while (true) {
|
|
1373
|
-
const candidate = path.join(current, "package.json");
|
|
1374
|
-
if (existsSync(candidate)) return candidate;
|
|
1375
|
-
const parent = path.dirname(current);
|
|
1376
|
-
if (parent === current) throw new Error(`Could not locate package.json above ${dir}`);
|
|
1377
|
-
current = parent;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
function getVersion() {
|
|
1381
|
-
if (cachedVersion) return cachedVersion;
|
|
1382
|
-
const pkgPath = findPackageJson(path.dirname(fileURLToPath(import.meta.url)));
|
|
1383
|
-
cachedVersion = JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
1384
|
-
return cachedVersion;
|
|
1385
|
-
}
|
|
1386
|
-
//#endregion
|
|
1387
|
-
//#region src/utils/print-json.ts
|
|
1388
|
-
function printJson(aggregated) {
|
|
1389
|
-
const result = {
|
|
1390
|
-
version: getVersion(),
|
|
1391
|
-
summary: {
|
|
1392
|
-
filesAnalyzed: aggregated.filesAnalyzed,
|
|
1393
|
-
totalImports: aggregated.totalImports,
|
|
1394
|
-
totalComponents: aggregated.totalComponents,
|
|
1395
|
-
totalUsagePatterns: aggregated.totalUsagePatterns
|
|
1396
|
-
},
|
|
1397
|
-
packages: aggregated.packageDistribution,
|
|
1398
|
-
components: aggregated.topComponents.map((c) => ({
|
|
1399
|
-
...c,
|
|
1400
|
-
files: [...c.files]
|
|
1401
|
-
})),
|
|
1402
|
-
patterns: aggregated.patternCounts,
|
|
1403
|
-
versus: aggregated.versusResults,
|
|
1404
|
-
ruleViolations: aggregated.ruleViolations,
|
|
1405
|
-
bannedPackageViolations: aggregated.bannedPackageViolations
|
|
1406
|
-
};
|
|
1407
|
-
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1408
|
-
}
|
|
1409
|
-
//#endregion
|
|
1410
1511
|
//#region src/utils/file-utils.ts
|
|
1411
1512
|
/**
|
|
1412
1513
|
* Find files matching a glob pattern
|
|
@@ -1602,110 +1703,6 @@ function findAndParseLockfile(projectPath) {
|
|
|
1602
1703
|
throw new Error("No supported lockfile found");
|
|
1603
1704
|
}
|
|
1604
1705
|
//#endregion
|
|
1605
|
-
//#region src/config/schema.ts
|
|
1606
|
-
const RuleSeveritySchema = z.enum(["error", "warn"]);
|
|
1607
|
-
const RuleConfigSchema = z.object({
|
|
1608
|
-
severity: RuleSeveritySchema,
|
|
1609
|
-
patterns: z.array(z.string()),
|
|
1610
|
-
message: z.string().optional()
|
|
1611
|
-
});
|
|
1612
|
-
const RuleConfigOrArraySchema = z.union([RuleConfigSchema, z.array(RuleConfigSchema)]);
|
|
1613
|
-
const EngineVersionRuleSchema = z.object({
|
|
1614
|
-
severity: RuleSeveritySchema,
|
|
1615
|
-
range: z.string(),
|
|
1616
|
-
message: z.string().optional()
|
|
1617
|
-
});
|
|
1618
|
-
const ThresholdSchema = z.union([z.number(), z.literal(false)]);
|
|
1619
|
-
const HermexConfigSchema = z.object({
|
|
1620
|
-
includes: z.array(z.string()).default(["**/*.{tsx,jsx,ts,js}"]),
|
|
1621
|
-
excludes: z.array(z.string()).default([
|
|
1622
|
-
"**/node_modules/**",
|
|
1623
|
-
"**/dist/**",
|
|
1624
|
-
"**/build/**"
|
|
1625
|
-
]),
|
|
1626
|
-
packages: z.object({
|
|
1627
|
-
internal: z.array(z.string()).default([]),
|
|
1628
|
-
ignore: z.array(z.string()).default([])
|
|
1629
|
-
}).default(() => ({
|
|
1630
|
-
internal: [],
|
|
1631
|
-
ignore: []
|
|
1632
|
-
})),
|
|
1633
|
-
versus: z.array(z.object({
|
|
1634
|
-
name: z.string(),
|
|
1635
|
-
packages: z.array(z.string()).min(2)
|
|
1636
|
-
})).default([]),
|
|
1637
|
-
rules: z.object({
|
|
1638
|
-
forbid_files: RuleConfigOrArraySchema.default([]),
|
|
1639
|
-
require_files: RuleConfigOrArraySchema.default([]),
|
|
1640
|
-
allow_files: RuleConfigOrArraySchema.default([]),
|
|
1641
|
-
forbid_packages: RuleConfigOrArraySchema.default([]),
|
|
1642
|
-
require_packages: RuleConfigOrArraySchema.default([]),
|
|
1643
|
-
require_scripts: RuleConfigOrArraySchema.default([]),
|
|
1644
|
-
require_package_fields: RuleConfigOrArraySchema.default([]),
|
|
1645
|
-
engine_version: z.union([EngineVersionRuleSchema, z.array(EngineVersionRuleSchema)]).optional()
|
|
1646
|
-
}).default(() => ({
|
|
1647
|
-
forbid_files: [],
|
|
1648
|
-
require_files: [],
|
|
1649
|
-
allow_files: [],
|
|
1650
|
-
forbid_packages: [],
|
|
1651
|
-
require_packages: [],
|
|
1652
|
-
require_scripts: [],
|
|
1653
|
-
require_package_fields: []
|
|
1654
|
-
})),
|
|
1655
|
-
output: z.object({
|
|
1656
|
-
summary: z.union([z.literal("log"), z.literal(false)]).default("log"),
|
|
1657
|
-
components: z.union([z.enum(["table", "chart"]), z.literal(false)]).default("table"),
|
|
1658
|
-
packages: z.union([z.enum(["table", "chart"]), z.literal(false)]).default("table"),
|
|
1659
|
-
patterns: z.union([z.enum(["table", "chart"]), z.literal(false)]).default("table"),
|
|
1660
|
-
details: z.boolean().default(false),
|
|
1661
|
-
versus: z.boolean().default(true),
|
|
1662
|
-
rules: z.boolean().default(true),
|
|
1663
|
-
format: z.enum(["human", "json"]).default("human")
|
|
1664
|
-
}).default(() => ({
|
|
1665
|
-
summary: "log",
|
|
1666
|
-
components: "table",
|
|
1667
|
-
packages: "table",
|
|
1668
|
-
patterns: "table",
|
|
1669
|
-
details: false,
|
|
1670
|
-
versus: true,
|
|
1671
|
-
rules: true,
|
|
1672
|
-
format: "human"
|
|
1673
|
-
})),
|
|
1674
|
-
releaseAge: z.object({
|
|
1675
|
-
enabled: z.boolean().default(false),
|
|
1676
|
-
registry: z.string().default("https://registry.npmjs.org"),
|
|
1677
|
-
authToken: z.string().optional(),
|
|
1678
|
-
thresholds: z.object({
|
|
1679
|
-
patch: ThresholdSchema.default(30),
|
|
1680
|
-
minor: ThresholdSchema.default(45),
|
|
1681
|
-
major: ThresholdSchema.default(60)
|
|
1682
|
-
}).default(() => ({
|
|
1683
|
-
patch: 30,
|
|
1684
|
-
minor: 45,
|
|
1685
|
-
major: 60
|
|
1686
|
-
}))
|
|
1687
|
-
}).default(() => ({
|
|
1688
|
-
enabled: false,
|
|
1689
|
-
registry: "https://registry.npmjs.org",
|
|
1690
|
-
thresholds: {
|
|
1691
|
-
patch: 30,
|
|
1692
|
-
minor: 45,
|
|
1693
|
-
major: 60
|
|
1694
|
-
}
|
|
1695
|
-
}))
|
|
1696
|
-
});
|
|
1697
|
-
//#endregion
|
|
1698
|
-
//#region src/config/loader.ts
|
|
1699
|
-
async function loadConfig(cwd, explicitPath) {
|
|
1700
|
-
const configPath = explicitPath ? resolve(explicitPath) : join(cwd, "hermex.config.ts");
|
|
1701
|
-
if (explicitPath && !existsSync(configPath)) throw new Error(`Config file not found: ${configPath}`);
|
|
1702
|
-
if (existsSync(configPath)) {
|
|
1703
|
-
const mod = await import(pathToFileURL(configPath).href);
|
|
1704
|
-
return HermexConfigSchema.parse(mod.default ?? mod);
|
|
1705
|
-
}
|
|
1706
|
-
return HermexConfigSchema.parse({});
|
|
1707
|
-
}
|
|
1708
|
-
//#endregion
|
|
1709
1706
|
//#region src/npm-registry/client.ts
|
|
1710
1707
|
async function fetchPackageInfo(name, registryUrl, authToken) {
|
|
1711
1708
|
const url = `${registryUrl.replace(/\/$/, "")}/${encodeURIComponent(name).replace("%40", "@")}`;
|
|
@@ -1747,8 +1744,10 @@ function upgradeLevel(daysAgo, bump, thresholds) {
|
|
|
1747
1744
|
if (daysAgo > threshold) return bump === "major" ? "mandatory_upgrade" : "needs_upgrade";
|
|
1748
1745
|
return null;
|
|
1749
1746
|
}
|
|
1750
|
-
function computeReleaseAge(installedVersion, timeMap, deprecated, thresholds) {
|
|
1751
|
-
const
|
|
1747
|
+
function computeReleaseAge(installedVersion, timeMap, deprecated, thresholds, distTags, severity) {
|
|
1748
|
+
const byBump = /* @__PURE__ */ new Map();
|
|
1749
|
+
let minCompliantVersion;
|
|
1750
|
+
let minCompliantReleasedDaysAgo;
|
|
1752
1751
|
for (const [version, dateStr] of Object.entries(timeMap)) {
|
|
1753
1752
|
if (version === "created" || version === "modified") continue;
|
|
1754
1753
|
if (!semver.valid(version)) continue;
|
|
@@ -1756,26 +1755,45 @@ function computeReleaseAge(installedVersion, timeMap, deprecated, thresholds) {
|
|
|
1756
1755
|
const bump = classifyBump(installedVersion, version);
|
|
1757
1756
|
if (!bump) continue;
|
|
1758
1757
|
const daysAgo = daysSince(dateStr);
|
|
1759
|
-
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);
|
|
1760
1773
|
if (!level) continue;
|
|
1774
|
+
const newest = versions.reduce((a, b) => a.daysAgo < b.daysAgo ? a : b);
|
|
1761
1775
|
upgrades.push({
|
|
1762
|
-
version,
|
|
1763
|
-
releasedDaysAgo: daysAgo,
|
|
1776
|
+
version: newest.version,
|
|
1777
|
+
releasedDaysAgo: newest.daysAgo,
|
|
1764
1778
|
semverBump: bump,
|
|
1765
1779
|
level
|
|
1766
1780
|
});
|
|
1767
1781
|
}
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
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;
|
|
1774
1787
|
return {
|
|
1775
1788
|
installedVersion,
|
|
1776
1789
|
upgrades: finalUpgrades,
|
|
1777
1790
|
worstLevel: finalUpgrades.some((u) => u.level === "mandatory_upgrade") ? "mandatory_upgrade" : finalUpgrades.length > 0 ? "needs_upgrade" : null,
|
|
1778
|
-
deprecated
|
|
1791
|
+
deprecated,
|
|
1792
|
+
latestVersion,
|
|
1793
|
+
latestReleasedDaysAgo,
|
|
1794
|
+
minCompliantVersion,
|
|
1795
|
+
minCompliantReleasedDaysAgo,
|
|
1796
|
+
severity
|
|
1779
1797
|
};
|
|
1780
1798
|
}
|
|
1781
1799
|
async function enrichWithReleaseAge(packages, config) {
|
|
@@ -1796,9 +1814,10 @@ async function enrichWithReleaseAge(packages, config) {
|
|
|
1796
1814
|
};
|
|
1797
1815
|
}
|
|
1798
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";
|
|
1799
1818
|
return {
|
|
1800
1819
|
pkg,
|
|
1801
|
-
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)
|
|
1802
1821
|
};
|
|
1803
1822
|
}));
|
|
1804
1823
|
for (const { pkg, entry } of results) {
|
|
@@ -1816,6 +1835,53 @@ async function enrichWithReleaseAge(packages, config) {
|
|
|
1816
1835
|
};
|
|
1817
1836
|
}
|
|
1818
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
|
|
1819
1885
|
//#region src/commands/scan.ts
|
|
1820
1886
|
function registerScanCommand(program) {
|
|
1821
1887
|
program.command("scan").description("Scan and analyze local files").option("--config <path>", "Path to hermex config file (overrides CWD discovery)").action(async (options) => {
|
|
@@ -1823,7 +1889,6 @@ function registerScanCommand(program) {
|
|
|
1823
1889
|
});
|
|
1824
1890
|
}
|
|
1825
1891
|
async function executeScan(config) {
|
|
1826
|
-
const startTime = Date.now();
|
|
1827
1892
|
const isJson = config.output.format === "json";
|
|
1828
1893
|
(isJson ? process.stderr : process.stdout).write(chalk.gray(`hermex v${getVersion()}\n`));
|
|
1829
1894
|
const spinner = ora({
|
|
@@ -1831,46 +1896,10 @@ async function executeScan(config) {
|
|
|
1831
1896
|
stream: isJson ? process.stderr : process.stdout
|
|
1832
1897
|
}).start();
|
|
1833
1898
|
try {
|
|
1834
|
-
const
|
|
1835
|
-
|
|
1836
|
-
spinner.start("Finding files...");
|
|
1837
|
-
const files = await findFiles(config.includes, config.excludes);
|
|
1838
|
-
if (files.length === 0) {
|
|
1839
|
-
spinner.fail(chalk.red(`No files found matching includes: ${config.includes.join(", ")}`));
|
|
1840
|
-
return;
|
|
1841
|
-
}
|
|
1842
|
-
spinner.succeed(chalk.green(` Found ${files.length} files`));
|
|
1843
|
-
spinner.start("Analyzing files...");
|
|
1844
|
-
const reports = [];
|
|
1845
|
-
const parseErrors = [];
|
|
1846
|
-
for (let i = 0; i < files.length; i++) {
|
|
1847
|
-
const file = files[i];
|
|
1848
|
-
spinner.text = `Analyzing files... (${i + 1}/${files.length})`;
|
|
1849
|
-
try {
|
|
1850
|
-
const report = parseFile(file);
|
|
1851
|
-
if (report) reports.push(report);
|
|
1852
|
-
} catch (error) {
|
|
1853
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1854
|
-
parseErrors.push({
|
|
1855
|
-
file,
|
|
1856
|
-
message
|
|
1857
|
-
});
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
spinner.succeed(chalk.green(`Analysis complete! Analyzed ${reports.length}/${files.length} files`));
|
|
1861
|
-
printErrors(parseErrors);
|
|
1862
|
-
const elapsedTime = (Date.now() - startTime) / 1e3;
|
|
1863
|
-
const aggregated = aggregateReports(reports, lockfileResult.versions, config, lockfileResult.multiVersions);
|
|
1864
|
-
const evaluatorViolations = evaluateRules(process.cwd(), config.rules, config.excludes);
|
|
1865
|
-
aggregated.ruleViolations = [...aggregated.ruleViolations, ...evaluatorViolations];
|
|
1866
|
-
if (config.releaseAge.enabled) {
|
|
1867
|
-
spinner.start("Fetching release age from registry...");
|
|
1868
|
-
const { enriched, skipped } = await enrichWithReleaseAge(aggregated.packageDistribution, config.releaseAge);
|
|
1869
|
-
aggregated.packageDistribution = enriched;
|
|
1870
|
-
spinner.succeed(chalk.blue(`š
Release age fetched${skipped > 0 ? chalk.gray(` (${skipped} packages skipped ā registry unreachable or not found)`) : ""}`));
|
|
1871
|
-
}
|
|
1899
|
+
const aggregated = await runPipeline(config, spinner);
|
|
1900
|
+
if (!aggregated) return;
|
|
1872
1901
|
if (isJson) printJson(aggregated);
|
|
1873
|
-
else printScanResults(aggregated, config
|
|
1902
|
+
else printScanResults(aggregated, config);
|
|
1874
1903
|
} catch (error) {
|
|
1875
1904
|
const message = error instanceof Error ? error.message : String(error);
|
|
1876
1905
|
spinner.fail(chalk.red("Analysis failed: " + message));
|
|
@@ -1878,7 +1907,7 @@ async function executeScan(config) {
|
|
|
1878
1907
|
process.exit(1);
|
|
1879
1908
|
}
|
|
1880
1909
|
}
|
|
1881
|
-
function printScanResults(aggregated, config
|
|
1910
|
+
function printScanResults(aggregated, config) {
|
|
1882
1911
|
if (config.output.packages) printPackages(aggregated, config.output.packages);
|
|
1883
1912
|
if (config.output.versus) printVersus(aggregated);
|
|
1884
1913
|
if (config.output.rules) printRules(aggregated);
|
|
@@ -1888,10 +1917,85 @@ function printScanResults(aggregated, config, _elapsedTime) {
|
|
|
1888
1917
|
if (config.output.summary) printSummary(aggregated);
|
|
1889
1918
|
}
|
|
1890
1919
|
//#endregion
|
|
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
|
+
}
|
|
1993
|
+
//#endregion
|
|
1891
1994
|
//#region src/cli.ts
|
|
1892
1995
|
const program = new Command();
|
|
1893
1996
|
program.name("hermex").description("Analyze React component usage patterns in your codebase").version(getVersion());
|
|
1894
1997
|
registerScanCommand(program);
|
|
1998
|
+
registerComplyCommand(program);
|
|
1895
1999
|
program.parse(process.argv);
|
|
1896
2000
|
//#endregion
|
|
1897
2001
|
export { program };
|