@georgewrmarshall/design-system-metrics 2.2.0 → 2.4.0

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/index.js CHANGED
@@ -7,6 +7,7 @@ const babelParser = require("@babel/parser");
7
7
  const traverse = require("@babel/traverse").default;
8
8
  const { program } = require("commander");
9
9
  const chalk = require("chalk");
10
+ const ExcelJS = require("exceljs");
10
11
 
11
12
  let config;
12
13
 
@@ -15,6 +16,7 @@ const loadConfig = async (configPath) => {
15
16
  try {
16
17
  const configContent = await fs.readFile(configPath, "utf8");
17
18
  config = JSON.parse(configContent);
19
+ validateConfig(config);
18
20
  } catch (err) {
19
21
  console.error(
20
22
  chalk.red(`Failed to load configuration file: ${err.message}`)
@@ -23,20 +25,33 @@ const loadConfig = async (configPath) => {
23
25
  }
24
26
  };
25
27
 
28
+ // Validate config structure
29
+ const validateConfig = (cfg) => {
30
+ if (!cfg.projects || typeof cfg.projects !== 'object') {
31
+ throw new Error('Config must have a "projects" object');
32
+ }
33
+
34
+ for (const [projectName, projectCfg] of Object.entries(cfg.projects)) {
35
+ if (projectCfg.deprecatedComponents && typeof projectCfg.deprecatedComponents !== 'object') {
36
+ throw new Error(`Project "${projectName}" deprecatedComponents must be an object`);
37
+ }
38
+
39
+ // Check if using old array format
40
+ if (Array.isArray(projectCfg.deprecatedComponents)) {
41
+ console.warn(chalk.yellow(`\nWarning: Project "${projectName}" is using the old array format for deprecatedComponents.`));
42
+ console.warn(chalk.yellow('Please migrate to the new object format with paths and replacement info.\n'));
43
+ }
44
+ }
45
+ };
46
+
26
47
  // Define CLI options using Commander
27
48
  program
28
- .version("2.1.0")
29
- .description("Design System Metrics CLI Tool - Track component usage from multiple sources")
49
+ .version("3.0.0")
50
+ .description("Design System Metrics CLI Tool - Track component usage and migration progress")
30
51
  .requiredOption(
31
52
  "-p, --project <name>",
32
53
  "Specify the project to audit (e.g., extension, mobile)"
33
54
  )
34
- .option("-f, --format <type>", "Output format (csv, json)", "csv")
35
- .option(
36
- "-s, --sources <types>",
37
- "Comma-separated list of sources to track (deprecated, current, all)",
38
- "all"
39
- )
40
55
  .option(
41
56
  "-c, --config <path>",
42
57
  "Path to custom config file",
@@ -47,34 +62,80 @@ program
47
62
  const options = program.opts();
48
63
 
49
64
  // Initialize component instances and file mappings
50
- // Structure: Map<componentName, Map<source, { count, files }>>
65
+ // Structure: Map<componentName, Map<source, Map<specificPath, { count, files }>>>
51
66
  let componentMetrics = new Map();
52
67
 
53
- // Helper function to track component usage by source
54
- const trackComponent = (componentName, source, filePath) => {
68
+ // Helper function to track component usage by source and specific path
69
+ const trackComponent = (componentName, source, specificPath, filePath) => {
55
70
  if (!componentMetrics.has(componentName)) {
56
71
  componentMetrics.set(componentName, new Map());
57
72
  }
58
73
 
59
74
  const sourceMetrics = componentMetrics.get(componentName);
60
75
  if (!sourceMetrics.has(source)) {
61
- sourceMetrics.set(source, { count: 0, files: [] });
76
+ sourceMetrics.set(source, new Map());
62
77
  }
63
78
 
64
- const metrics = sourceMetrics.get(source);
79
+ const pathMetrics = sourceMetrics.get(source);
80
+ if (!pathMetrics.has(specificPath)) {
81
+ pathMetrics.set(specificPath, { count: 0, files: [] });
82
+ }
83
+
84
+ const metrics = pathMetrics.get(specificPath);
65
85
  metrics.count++;
66
86
  metrics.files.push(filePath);
67
87
  };
68
88
 
89
+ // Helper function to check if import path matches any component paths
90
+ const matchComponentPath = (importPath, deprecatedComponents) => {
91
+ // Normalize the import path
92
+ const normalizedImportPath = importPath.replace(/\\/g, '/');
93
+
94
+ for (const [componentName, config] of Object.entries(deprecatedComponents)) {
95
+ for (const componentPath of config.paths) {
96
+ // Normalize the component path
97
+ const normalizedComponentPath = componentPath.replace(/\\/g, '/');
98
+
99
+ // Check for exact match
100
+ if (normalizedImportPath === normalizedComponentPath) {
101
+ return { componentName, matchedPath: componentPath };
102
+ }
103
+
104
+ // Check if import path ends with the component path (relative imports)
105
+ if (normalizedImportPath.endsWith(normalizedComponentPath)) {
106
+ return { componentName, matchedPath: componentPath };
107
+ }
108
+
109
+ // Check if import path includes key parts of component path
110
+ // e.g., "../../components/component-library" matches "*/component-library/*"
111
+ const pathParts = normalizedComponentPath.split('/');
112
+ const importParts = normalizedImportPath.split('/');
113
+
114
+ // If import includes "/component-library" and path includes "/component-library"
115
+ if (normalizedImportPath.includes('/component-library') &&
116
+ normalizedComponentPath.includes('/component-library')) {
117
+ return { componentName, matchedPath: componentPath };
118
+ }
119
+
120
+ // Check for package imports (e.g., react-native-vector-icons/*)
121
+ if (normalizedComponentPath.includes(normalizedImportPath)) {
122
+ return { componentName, matchedPath: componentPath };
123
+ }
124
+ }
125
+ }
126
+ return null;
127
+ };
128
+
69
129
  // Function to process a single file
70
130
  const processFile = async (
71
131
  filePath,
72
- deprecatedComponentsSet,
132
+ deprecatedComponents,
73
133
  currentComponentsSet,
74
134
  currentPackages
75
135
  ) => {
76
- // Track imports by source: Map<componentName, source>
136
+ // Track imports by source: Map<componentName, { source, specificPath }>
77
137
  const componentImports = new Map();
138
+
78
139
  try {
79
140
  const content = await fs.readFile(filePath, "utf8");
80
141
 
@@ -82,64 +143,61 @@ const processFile = async (
82
143
  const ast = babelParser.parse(content, {
83
144
  sourceType: "module",
84
145
  plugins: ["jsx", "typescript"],
85
- attachComment: true, // Enable comment attachment for JSDoc parsing
146
+ attachComment: true,
86
147
  });
87
148
 
88
149
  // Traverse the AST to find import declarations from multiple sources
89
150
  traverse(ast, {
90
151
  ImportDeclaration({ node }) {
91
152
  const importPath = node.source.value;
92
- console.log(`Import path detected: ${importPath}`);
93
153
 
94
154
  let source = null;
155
+ let specificPath = null;
95
156
 
96
- // Check if it's from local component library (DEPRECATED)
97
- if (importPath.includes("/component-library")) {
157
+ // Check if it's a deprecated component from local paths
158
+ const deprecatedMatch = matchComponentPath(importPath, deprecatedComponents);
159
+ if (deprecatedMatch) {
98
160
  source = "deprecated";
99
- }
100
- // Check if it's from current NPM packages
101
- else if (currentPackages && currentPackages.length > 0) {
102
- for (const pkg of currentPackages) {
103
- if (importPath === pkg || importPath.startsWith(`${pkg}/`)) {
104
- source = "current";
105
- break;
106
- }
107
- }
108
- }
161
+ specificPath = deprecatedMatch.matchedPath;
109
162
 
110
- // If we found a relevant import source, track the components
111
- if (source) {
112
163
  node.specifiers.forEach((specifier) => {
113
164
  let componentName = null;
114
165
 
115
166
  if (specifier.type === "ImportDefaultSpecifier") {
116
167
  componentName = specifier.local.name;
117
- console.log(
118
- `Default imported component: ${componentName} (${source})`
119
- );
120
168
  } else if (specifier.type === "ImportSpecifier") {
121
169
  componentName = specifier.local.name;
122
- console.log(`Named imported component: ${componentName} (${source})`);
123
170
  }
124
171
 
125
- if (componentName) {
126
- // Check if component is in deprecated list (for local imports)
127
- if (
128
- source === "deprecated" &&
129
- deprecatedComponentsSet.has(componentName)
130
- ) {
131
- componentImports.set(componentName, source);
132
- }
133
- // Check if component is in current list (for NPM imports)
134
- else if (
135
- source === "current" &&
136
- currentComponentsSet.has(componentName)
137
- ) {
138
- componentImports.set(componentName, source);
139
- }
172
+ if (componentName && deprecatedComponents[componentName]) {
173
+ componentImports.set(componentName, { source, specificPath });
140
174
  }
141
175
  });
142
176
  }
177
+ // Check if it's from current NPM packages
178
+ else if (currentPackages && currentPackages.length > 0) {
179
+ for (const pkg of currentPackages) {
180
+ if (importPath === pkg || importPath.startsWith(`${pkg}/`)) {
181
+ source = "current";
182
+ specificPath = pkg;
183
+
184
+ node.specifiers.forEach((specifier) => {
185
+ let componentName = null;
186
+
187
+ if (specifier.type === "ImportDefaultSpecifier") {
188
+ componentName = specifier.local.name;
189
+ } else if (specifier.type === "ImportSpecifier") {
190
+ componentName = specifier.local.name;
191
+ }
192
+
193
+ if (componentName && currentComponentsSet.has(componentName)) {
194
+ componentImports.set(componentName, { source, specificPath });
195
+ }
196
+ });
197
+ break;
198
+ }
199
+ }
200
+ }
143
201
  },
144
202
  });
145
203
 
@@ -166,16 +224,10 @@ const processFile = async (
166
224
  componentName = current.name;
167
225
  }
168
226
 
169
- console.log(`JSX component detected: ${componentName}`);
170
-
171
227
  // Check if this component was imported and track its usage
172
228
  if (componentImports.has(componentName)) {
173
- const source = componentImports.get(componentName);
174
- trackComponent(componentName, source, filePath);
175
-
176
- console.log(
177
- `Matched JSX component: ${componentName}, Source: ${source}`
178
- );
229
+ const { source, specificPath } = componentImports.get(componentName);
230
+ trackComponent(componentName, source, specificPath, filePath);
179
231
  }
180
232
  }
181
233
  },
@@ -208,12 +260,11 @@ const main = async () => {
208
260
  ignoreFolders,
209
261
  filePattern,
210
262
  outputFile,
211
- deprecatedComponents = [],
263
+ deprecatedComponents = {},
212
264
  currentComponents = [],
213
265
  currentPackages = [],
214
266
  } = projectConfig;
215
267
 
216
- const deprecatedComponentsSet = new Set(deprecatedComponents);
217
268
  const currentComponentsSet = new Set(currentComponents);
218
269
 
219
270
  console.log(chalk.blue(`\nStarting audit for project: ${projectName}\n`));
@@ -236,95 +287,207 @@ const main = async () => {
236
287
  files.map((file) =>
237
288
  processFile(
238
289
  file,
239
- deprecatedComponentsSet,
290
+ deprecatedComponents,
240
291
  currentComponentsSet,
241
292
  currentPackages
242
293
  )
243
294
  )
244
295
  );
245
296
 
246
- console.log(chalk.green("\nComponent Migration Metrics:"));
297
+ console.log(chalk.green("\nGenerating Component Migration Metrics...\n"));
298
+
299
+ // Collect metrics by component and calculate totals
300
+ const deprecatedMetrics = new Map(); // Map<componentName, { totalCount, pathBreakdown, files }>
301
+ const currentMetrics = new Map(); // Map<componentName, { count, files }>
302
+
303
+ // Aggregate deprecated metrics across all paths
304
+ for (const [componentName, componentConfig] of Object.entries(deprecatedComponents)) {
305
+ const componentSources = componentMetrics.get(componentName);
306
+ if (componentSources && componentSources.has("deprecated")) {
307
+ const pathMetrics = componentSources.get("deprecated");
308
+ let totalCount = 0;
309
+ const pathBreakdown = new Map();
310
+ const allFiles = [];
311
+
312
+ for (const [specificPath, metrics] of pathMetrics.entries()) {
313
+ totalCount += metrics.count;
314
+ pathBreakdown.set(specificPath, metrics);
315
+ allFiles.push(...metrics.files);
316
+ }
247
317
 
248
- // Parse sources filter from CLI
249
- let requestedSources = ["deprecated", "current"];
250
- if (options.sources && options.sources !== "all") {
251
- requestedSources = options.sources.split(",").map((s) => s.trim());
318
+ deprecatedMetrics.set(componentName, {
319
+ totalCount,
320
+ pathBreakdown,
321
+ files: allFiles,
322
+ replacement: componentConfig.replacement,
323
+ });
324
+ }
252
325
  }
253
326
 
254
- const sourceTypes = requestedSources;
255
-
256
- for (const sourceType of sourceTypes) {
257
- // Filter component metrics for this source type
258
- const sourceMetrics = new Map();
259
-
260
- // Determine which component set to use based on source type
261
- const componentSet =
262
- sourceType === "deprecated"
263
- ? deprecatedComponentsSet
264
- : currentComponentsSet;
265
-
266
- componentSet.forEach((componentName) => {
267
- const componentSources = componentMetrics.get(componentName);
268
- if (componentSources) {
269
- const metrics = componentSources.get(sourceType);
270
- if (metrics) {
271
- sourceMetrics.set(componentName, metrics);
272
- }
327
+ // Collect current (MMDS) metrics
328
+ currentComponentsSet.forEach((componentName) => {
329
+ const componentSources = componentMetrics.get(componentName);
330
+ if (componentSources && componentSources.has("current")) {
331
+ const pathMetrics = componentSources.get("current");
332
+ let totalCount = 0;
333
+ const allFiles = [];
334
+
335
+ for (const [, metrics] of pathMetrics.entries()) {
336
+ totalCount += metrics.count;
337
+ allFiles.push(...metrics.files);
273
338
  }
274
- });
275
339
 
276
- // Skip if no metrics for this source
277
- if (sourceMetrics.size === 0) {
278
- console.log(
279
- chalk.yellow(
280
- `No ${sourceType} components found, skipping report.`
281
- )
282
- );
283
- continue;
340
+ currentMetrics.set(componentName, { count: totalCount, files: allFiles });
284
341
  }
342
+ });
285
343
 
286
- // Generate output file name
287
- const baseFileName = outputFile.replace(/\.(csv|json)$/, "");
288
- const sourceOutputFile = `${baseFileName}-${sourceType}.${options.format.toLowerCase()}`;
344
+ // Generate XLSX file with multiple sheets using ExcelJS
345
+ const workbook = new ExcelJS.Workbook();
346
+
347
+ // Sheet 1: Migration Progress (components migrating to MMDS)
348
+ const migrationSheet = workbook.addWorksheet("Migration Progress");
349
+ migrationSheet.addRow([
350
+ "Deprecated Component",
351
+ "Source Paths",
352
+ "MMDS Component",
353
+ "Deprecated Instances",
354
+ "MMDS Instances",
355
+ "Migrated %",
356
+ ]);
357
+
358
+ const componentsWithMMDSReplacement = Array.from(deprecatedMetrics.entries())
359
+ .filter(([, metrics]) =>
360
+ metrics.replacement &&
361
+ metrics.replacement.package &&
362
+ (metrics.replacement.package.includes("@metamask/design-system"))
363
+ );
289
364
 
290
- console.log(
291
- chalk.blue(
292
- `\n${sourceType.toUpperCase()} Components (${sourceType === "deprecated" ? "Local component-library" : currentPackages.join(", ")}):`
293
- )
365
+ componentsWithMMDSReplacement.forEach(([componentName, metrics]) => {
366
+ const mmdsComp = metrics.replacement.component;
367
+ const deprecatedCount = metrics.totalCount;
368
+ const mmdsCount = currentMetrics.get(mmdsComp)?.count || 0;
369
+ const total = deprecatedCount + mmdsCount;
370
+ const percentage = total > 0 ? (mmdsCount / total) * 100 : 0;
371
+ const sourcePaths = deprecatedComponents[componentName].paths.join(", ");
372
+
373
+ migrationSheet.addRow([
374
+ componentName,
375
+ sourcePaths,
376
+ mmdsComp,
377
+ deprecatedCount,
378
+ mmdsCount,
379
+ `${percentage.toFixed(2)}%`,
380
+ ]);
381
+ });
382
+
383
+ console.log(
384
+ chalk.blue(`Migration Progress: ${componentsWithMMDSReplacement.length} components tracked`)
385
+ );
386
+
387
+ // Sheet 2: Intermediate Migrations (components migrating to component-library)
388
+ const intermediateSheet = workbook.addWorksheet("Intermediate Migrations");
389
+ intermediateSheet.addRow([
390
+ "Old Component",
391
+ "Old Path",
392
+ "New Component",
393
+ "New Package/Path",
394
+ "Instances",
395
+ ]);
396
+
397
+ const componentsWithIntermediateReplacement = Array.from(deprecatedMetrics.entries())
398
+ .filter(([, metrics]) =>
399
+ metrics.replacement &&
400
+ metrics.replacement.package === "component-library"
294
401
  );
295
402
 
296
- if (options.format.toLowerCase() === "json") {
297
- const jsonOutput = {};
298
- sourceMetrics.forEach((metrics, componentName) => {
299
- console.log(`${chalk.cyan(componentName)}: ${metrics.count}`);
300
- jsonOutput[componentName] = {
301
- instances: metrics.count,
302
- files: metrics.files,
303
- };
304
- });
403
+ componentsWithIntermediateReplacement.forEach(([componentName, metrics]) => {
404
+ const oldPaths = deprecatedComponents[componentName].paths.join(", ");
405
+ const newComponent = metrics.replacement.component;
406
+ const newPath = metrics.replacement.path || metrics.replacement.package;
407
+
408
+ intermediateSheet.addRow([
409
+ componentName,
410
+ oldPaths,
411
+ newComponent,
412
+ newPath,
413
+ metrics.totalCount,
414
+ ]);
415
+ });
305
416
 
306
- await fs.writeFile(
307
- sourceOutputFile,
308
- JSON.stringify(jsonOutput, null, 2)
309
- );
310
- } else {
311
- // CSV format
312
- let csvContent = "Component,Instances,File Paths\n";
313
-
314
- sourceMetrics.forEach((metrics, componentName) => {
315
- console.log(`${chalk.cyan(componentName)}: ${metrics.count}`);
316
- csvContent += `"${componentName}",${metrics.count},"${metrics.files.join(", ")}"\n`;
317
- });
417
+ console.log(
418
+ chalk.blue(`Intermediate Migrations: ${componentsWithIntermediateReplacement.length} components tracked`)
419
+ );
318
420
 
319
- await fs.writeFile(sourceOutputFile, csvContent);
320
- }
421
+ // Sheet 3: Path-Level Detail
422
+ const pathDetailSheet = workbook.addWorksheet("Path-Level Detail");
423
+ pathDetailSheet.addRow([
424
+ "Component",
425
+ "Specific Path",
426
+ "Instances",
427
+ "File Paths",
428
+ ]);
429
+
430
+ deprecatedMetrics.forEach((metrics, componentName) => {
431
+ metrics.pathBreakdown.forEach((pathMetrics, specificPath) => {
432
+ pathDetailSheet.addRow([
433
+ componentName,
434
+ specificPath,
435
+ pathMetrics.count,
436
+ pathMetrics.files.join(", "),
437
+ ]);
438
+ });
439
+ });
321
440
 
322
- console.log(chalk.green(`Metrics written to ${sourceOutputFile}`));
323
- }
441
+ console.log(
442
+ chalk.blue(`Path-Level Detail: ${deprecatedMetrics.size} components with path breakdowns`)
443
+ );
444
+
445
+ // Sheet 4: MMDS Usage
446
+ const mmdsSheet = workbook.addWorksheet("MMDS Usage");
447
+ mmdsSheet.addRow(["Component", "Instances", "File Paths"]);
448
+
449
+ currentMetrics.forEach((metrics, componentName) => {
450
+ console.log(`${chalk.cyan(componentName)}: ${metrics.count} (MMDS)`);
451
+ mmdsSheet.addRow([
452
+ componentName,
453
+ metrics.count,
454
+ metrics.files.join(", "),
455
+ ]);
456
+ });
457
+
458
+ // Sheet 5: No Replacement Components
459
+ const noReplacementSheet = workbook.addWorksheet("No Replacement");
460
+ noReplacementSheet.addRow(["Component", "Path", "Instances", "File Paths"]);
461
+
462
+ const componentsWithNoReplacement = Array.from(deprecatedMetrics.entries())
463
+ .filter(([, metrics]) => !metrics.replacement);
464
+
465
+ componentsWithNoReplacement.forEach(([componentName, metrics]) => {
466
+ const paths = deprecatedComponents[componentName].paths.join(", ");
467
+ noReplacementSheet.addRow([
468
+ componentName,
469
+ paths,
470
+ metrics.totalCount,
471
+ metrics.files.join(", "),
472
+ ]);
473
+ });
474
+
475
+ console.log(
476
+ chalk.blue(`No Replacement: ${componentsWithNoReplacement.length} components`)
477
+ );
478
+
479
+ // Create output directory if it doesn't exist
480
+ const outputDir = path.dirname(outputFile);
481
+ await fs.mkdir(outputDir, { recursive: true });
482
+
483
+ // Write the XLSX file
484
+ await workbook.xlsx.writeFile(outputFile);
324
485
 
325
- console.log(chalk.green("\n✓ All reports generated successfully!\n"));
486
+ console.log(chalk.green(`\n✓ Metrics written to ${outputFile}`));
487
+ console.log(chalk.green("✓ All reports generated successfully!\n"));
326
488
  } catch (err) {
327
- console.error(chalk.red(`Error reading files: ${err.message}`));
489
+ console.error(chalk.red(`Error: ${err.message}`));
490
+ console.error(chalk.red(err.stack));
328
491
  }
329
492
  };
330
493
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@georgewrmarshall/design-system-metrics",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "A CLI tool to audit design system component usage from local libraries, NPM packages, and track deprecated components across MetaMask codebases",
5
5
  "main": "index.js",
6
6
  "packageManager": "yarn@4.3.1",
@@ -25,6 +25,7 @@
25
25
  "brace-expansion": "^4.0.0",
26
26
  "chalk": "^4.1.2",
27
27
  "commander": "^12.1.0",
28
+ "exceljs": "^4.4.0",
28
29
  "glob": "^11.0.0"
29
30
  },
30
31
  "devDependencies": {