hermex 1.3.0-beta.4 → 2.0.0-beta.2

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