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