hermex 1.3.0-beta.3 → 2.0.0-beta.1

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