@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/.claude/settings.local.json +6 -1
- package/CHANGELOG.md +19 -0
- package/README.md +160 -106
- package/__tests__/fixtures/test-config.json +35 -8
- package/__tests__/metrics.test.js +186 -95
- package/config.json +1190 -163
- package/index.js +291 -128
- package/package.json +2 -1
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("
|
|
29
|
-
.description("Design System Metrics CLI Tool - Track component usage
|
|
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,
|
|
76
|
+
sourceMetrics.set(source, new Map());
|
|
62
77
|
}
|
|
63
78
|
|
|
64
|
-
const
|
|
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
|
-
|
|
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,
|
|
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
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
+
deprecatedComponents,
|
|
240
291
|
currentComponentsSet,
|
|
241
292
|
currentPackages
|
|
242
293
|
)
|
|
243
294
|
)
|
|
244
295
|
);
|
|
245
296
|
|
|
246
|
-
console.log(chalk.green("\
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
318
|
+
deprecatedMetrics.set(componentName, {
|
|
319
|
+
totalCount,
|
|
320
|
+
pathBreakdown,
|
|
321
|
+
files: allFiles,
|
|
322
|
+
replacement: componentConfig.replacement,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
252
325
|
}
|
|
253
326
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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.
|
|
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": {
|