hermex 1.1.2 → 1.3.0-beta.1

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