@georgewrmarshall/design-system-metrics 1.0.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/.editorconfig ADDED
@@ -0,0 +1,10 @@
1
+ root = true
2
+
3
+ [*]
4
+ end_of_line = lf
5
+ insert_final_newline = true
6
+
7
+ [*.{js,json,yml}]
8
+ charset = utf-8
9
+ indent_style = space
10
+ indent_size = 2
package/.gitattributes ADDED
@@ -0,0 +1,4 @@
1
+ /.yarn/** linguist-vendored
2
+ /.yarn/releases/* binary
3
+ /.yarn/plugins/**/* binary
4
+ /.pnp.* binary linguist-generated
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # **@georgewrmarshall/design-system-metrics**
2
+
3
+ A CLI tool to audit design system component usage across multiple MetaMask codebases
4
+
5
+ ## **Getting Started**
6
+
7
+ - [Extension](#extension)
8
+ - [Mobile](#mobile)
9
+ - [CLI Options](#cli-options)
10
+ - [Requirements](#requirements)
11
+
12
+ ---
13
+
14
+ ### **Extension**
15
+
16
+ 1. **Clone the [MetaMask Extension](https://github.com/MetaMask/metamask-extension)** repository if you haven’t already:
17
+
18
+ ```bash
19
+ git clone https://github.com/MetaMask/metamask-extension.git
20
+ ```
21
+
22
+ 2. **Run the CLI tool from the `@georgewrmarshall/design-system-metrics` package:**
23
+
24
+ Navigate to the `@georgewrmarshall/design-system-metrics` package directory:
25
+
26
+ ```bash
27
+ cd /path/to/@georgewrmarshall/design-system-metrics
28
+ ```
29
+
30
+ 3. **Run the CLI tool for the MetaMask Extension:**
31
+
32
+ ```bash
33
+ yarn node index.js --project extension
34
+ ```
35
+
36
+ 4. A file called `extension-component-adoption-metrics.csv` will be generated in the current working directory if there are no errors.
37
+
38
+ ---
39
+
40
+ ### **Mobile**
41
+
42
+ 1. **Clone the [MetaMask Mobile](https://github.com/MetaMask/metamask-mobile)** repository if you haven’t already:
43
+
44
+ ```bash
45
+ git clone https://github.com/MetaMask/metamask-mobile.git
46
+ ```
47
+
48
+ 2. **Run the CLI tool from the `@georgewrmarshall/design-system-metrics` package:**
49
+
50
+ Navigate to the `@georgewrmarshall/design-system-metrics` package directory:
51
+
52
+ ```bash
53
+ cd /path/to/@georgewrmarshall/design-system-metrics
54
+ ```
55
+
56
+ 3. **Run the CLI tool for the MetaMask Mobile project:**
57
+
58
+ ```bash
59
+ yarn node index.js --project mobile
60
+ ```
61
+
62
+ 4. A file called `mobile-component-adoption-metrics.csv` will be generated in the current working directory if there are no errors.
63
+
64
+ ---
65
+
66
+ ### **CLI Options**
67
+
68
+ - **`--project` (Required)**: Specify the project to audit. Options are:
69
+
70
+ - `extension`: For MetaMask Extension
71
+ - `mobile`: For MetaMask Mobile
72
+
73
+ Example:
74
+
75
+ ```bash
76
+ yarn node index.js --project extension
77
+ ```
78
+
79
+ - **`--format` (Optional)**: Specify the output format. Options are `csv` (default) or `json`.
80
+
81
+ Example:
82
+
83
+ ```bash
84
+ yarn node index.js --project extension --format json
85
+ ```
86
+
87
+ - **Custom Configuration**: By default, the tool uses a `config.json` file to define the component list and ignore patterns. You can also pass a custom configuration file by adding the `--config` option:
88
+
89
+ ```bash
90
+ yarn node index.js --project extension --config /path/to/custom-config.json
91
+ ```
92
+
93
+ ---
94
+
95
+ ### **Requirements**
96
+
97
+ - The tool **ignores deprecated components** (e.g., `component-library/deprecated`).
98
+ - The tool **does not count duplicate components** when imported from different locations (e.g., `<Button` from `../../ui/button` versus `<Button` from `../../component-library`).
99
+ - Components **inside JSDoc comments** are not counted (e.g., `@deprecated <Box /> is deprecated in favour of <Box />`).
100
+
101
+ ---
102
+
103
+ ### **Example Output**
104
+
105
+ Upon running the CLI tool, a CSV file will be generated in the root of the repository (e.g., `extension-component-adoption-metrics.csv` or `mobile-component-adoption-metrics.csv`), listing the components and the number of instances where they are used.
106
+
107
+ ---
108
+
109
+ ### **Contributing**
110
+
111
+ If you wish to contribute to the tool, ensure you are running the latest version of **Yarn (v4.x)** and **Node.js**. You can make adjustments to the `config.json` file or update the CLI logic for tracking additional components or repositories.
112
+
113
+ ---
114
+
115
+ ### **License**
116
+
117
+ This project is licensed under the [MIT License](LICENSE).
package/config.json ADDED
@@ -0,0 +1,123 @@
1
+ {
2
+ "projects": {
3
+ "extension": {
4
+ "rootFolder": "ui",
5
+ "ignoreFolders": ["ui/components/component-library"],
6
+ "filePattern": "ui/**/*.{js,tsx}",
7
+ "outputFile": "extension-component-adoption-metrics.csv",
8
+ "components": [
9
+ "AvatarAccount",
10
+ "AvatarBase",
11
+ "AvatarFavicon",
12
+ "AvatarIcon",
13
+ "AvatarNetwork",
14
+ "AvatarToken",
15
+ "BadgeWrapper",
16
+ "BannerAlert",
17
+ "BannerBase",
18
+ "BannerTip",
19
+ "Box",
20
+ "Button",
21
+ "ButtonBase",
22
+ "ButtonIcon",
23
+ "ButtonLink",
24
+ "ButtonPrimary",
25
+ "ButtonSecondary",
26
+ "Checkbox",
27
+ "Container",
28
+ "FormTextField",
29
+ "HeaderBase",
30
+ "HelpText",
31
+ "Icon",
32
+ "Input",
33
+ "Label",
34
+ "Modal",
35
+ "ModalBody",
36
+ "ModalContent",
37
+ "ModalFocus",
38
+ "ModalFooter",
39
+ "ModalHeader",
40
+ "ModalOverlay",
41
+ "PickerNetwork",
42
+ "Popover",
43
+ "PopoverHeader",
44
+ "SelectButton",
45
+ "SelectOption",
46
+ "SelectWrapper",
47
+ "Tag",
48
+ "TagUrl",
49
+ "Text",
50
+ "TextField",
51
+ "TextFieldSearch"
52
+ ]
53
+ },
54
+ "mobile": {
55
+ "rootFolder": "app",
56
+ "ignoreFolders": ["app/component-library"],
57
+ "filePattern": "app/components/**/*.{js,tsx}",
58
+ "outputFile": "mobile-component-adoption-metrics.csv",
59
+ "components": [
60
+ "Accordion",
61
+ "Avatar",
62
+ "AvatarAccount",
63
+ "AvatarBase",
64
+ "AvatarFavicon",
65
+ "AvatarGroup",
66
+ "AvatarIcon",
67
+ "AvatarNetwork",
68
+ "AvatarToken",
69
+ "Badge",
70
+ "BadgeBase",
71
+ "BadgeNetwork",
72
+ "BadgeStatus",
73
+ "BadgeWrapper",
74
+ "Banner",
75
+ "BannerAlert",
76
+ "BannerBase",
77
+ "BannerTip",
78
+ "BottomSheet",
79
+ "BottomSheetFooter",
80
+ "BottomSheetHeader",
81
+ "Button",
82
+ "ButtonBase",
83
+ "ButtonIcon",
84
+ "ButtonLink",
85
+ "ButtonPrimary",
86
+ "ButtonSecondary",
87
+ "Card",
88
+ "Cell",
89
+ "CellBase",
90
+ "CellDisplay",
91
+ "CellMultiSelect",
92
+ "CellSelect",
93
+ "Checkbox",
94
+ "Header",
95
+ "HelpText",
96
+ "Icon",
97
+ "Input",
98
+ "Label",
99
+ "ListItem",
100
+ "ListItemColumn",
101
+ "ModalConfirmation",
102
+ "ModalMadatory",
103
+ "MultiSelectItem",
104
+ "Overlay",
105
+ "PickerAccount",
106
+ "PickerBase",
107
+ "PickerNetwork",
108
+ "SelectItem",
109
+ "SheetBottom",
110
+ "SheetHeader",
111
+ "TabBar",
112
+ "TabBarItem",
113
+ "Tag",
114
+ "TagUrl",
115
+ "Text",
116
+ "TextField",
117
+ "TextFieldSearch",
118
+ "TextWithPrefixIcon",
119
+ "Toast"
120
+ ]
121
+ }
122
+ }
123
+ }
package/index.js ADDED
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs").promises;
4
+ const { glob } = require("glob");
5
+ const path = require("path");
6
+ const babelParser = require("@babel/parser");
7
+ const traverse = require("@babel/traverse").default;
8
+ const { program } = require("commander");
9
+ const chalk = require("chalk");
10
+
11
+ // Load configuration
12
+ const CONFIG_PATH = path.join(__dirname, "config.json");
13
+ let config;
14
+
15
+ // Function to load and parse the configuration file
16
+ const loadConfig = async () => {
17
+ try {
18
+ const configContent = await fs.readFile(CONFIG_PATH, "utf8");
19
+ config = JSON.parse(configContent);
20
+ } catch (err) {
21
+ console.error(
22
+ chalk.red(`Failed to load configuration file: ${err.message}`)
23
+ );
24
+ process.exit(1);
25
+ }
26
+ };
27
+
28
+ // Define CLI options using Commander
29
+ program
30
+ .version("1.0.0")
31
+ .description("Design System Metrics CLI Tool")
32
+ .requiredOption(
33
+ "-p, --project <name>",
34
+ "Specify the project to audit (e.g., extension, mobile)"
35
+ )
36
+ .option("-f, --format <type>", "Output format (csv, json)", "csv")
37
+ .parse(process.argv);
38
+
39
+ const options = program.opts();
40
+
41
+ // Initialize component instances and file mappings
42
+ let componentInstances = new Map();
43
+ let componentFiles = new Map();
44
+
45
+ // Function to process a single file
46
+ const processFile = async (filePath, componentsSet, importedComponentsSet) => {
47
+ try {
48
+ const content = await fs.readFile(filePath, "utf8");
49
+
50
+ // Parse the file content into an AST
51
+ const ast = babelParser.parse(content, {
52
+ sourceType: "module",
53
+ plugins: ["jsx", "typescript"],
54
+ });
55
+
56
+ // Traverse the AST to find import declarations from the component library
57
+ traverse(ast, {
58
+ ImportDeclaration({ node }) {
59
+ const importPath = node.source.value;
60
+ if (importPath.includes("/component-library")) {
61
+ node.specifiers.forEach((specifier) => {
62
+ if (specifier.type === "ImportSpecifier") {
63
+ importedComponentsSet.add(specifier.imported.name);
64
+ }
65
+ });
66
+ }
67
+ },
68
+ });
69
+
70
+ // Traverse the AST to find JSX elements
71
+ traverse(ast, {
72
+ JSXElement({ node }) {
73
+ const openingElement = node.openingElement;
74
+ if (
75
+ openingElement &&
76
+ openingElement.name &&
77
+ (openingElement.name.type === "JSXIdentifier" ||
78
+ openingElement.name.type === "JSXMemberExpression")
79
+ ) {
80
+ let componentName = "";
81
+
82
+ if (openingElement.name.type === "JSXIdentifier") {
83
+ componentName = openingElement.name.name;
84
+ } else if (openingElement.name.type === "JSXMemberExpression") {
85
+ // Handle namespaced components like <UI.Button>
86
+ let current = openingElement.name;
87
+ while (current.object) {
88
+ current = current.object;
89
+ }
90
+ componentName = current.name;
91
+ }
92
+
93
+ if (
94
+ componentsSet.has(componentName) &&
95
+ importedComponentsSet.has(componentName)
96
+ ) {
97
+ const count = componentInstances.get(componentName) || 0;
98
+ componentInstances.set(componentName, count + 1);
99
+
100
+ const files = componentFiles.get(componentName) || [];
101
+ files.push(filePath);
102
+ componentFiles.set(componentName, files);
103
+ }
104
+ }
105
+ },
106
+ });
107
+ } catch (err) {
108
+ console.error(
109
+ chalk.yellow(`Error processing file ${filePath}: ${err.message}`)
110
+ );
111
+ }
112
+ };
113
+
114
+ // Main function to coordinate the audit
115
+ const main = async () => {
116
+ await loadConfig();
117
+
118
+ const projectName = options.project;
119
+ const projectConfig = config.projects[projectName];
120
+
121
+ if (!projectConfig) {
122
+ console.error(
123
+ chalk.red(
124
+ `Project "${projectName}" is not defined in the configuration file.`
125
+ )
126
+ );
127
+ process.exit(1);
128
+ }
129
+
130
+ const { rootFolder, ignoreFolders, filePattern, outputFile, components } =
131
+ projectConfig;
132
+
133
+ const componentsSet = new Set(components);
134
+
135
+ console.log(chalk.blue(`\nStarting audit for project: ${projectName}\n`));
136
+
137
+ try {
138
+ const files = await glob(filePattern, {
139
+ ignore: [
140
+ ...ignoreFolders.map((folder) => path.join(folder, "**")),
141
+ `${rootFolder}/**/*.test.{js,tsx}`,
142
+ ],
143
+ });
144
+
145
+ if (files.length === 0) {
146
+ console.log(chalk.yellow("No files matched the provided pattern."));
147
+ return;
148
+ }
149
+
150
+ // Process files concurrently
151
+ await Promise.all(
152
+ files.map((file) => processFile(file, componentsSet, new Set()))
153
+ );
154
+
155
+ console.log(chalk.green("\nComponent Adoption Metrics:"));
156
+
157
+ let csvContent = "Component,Instances,File Paths\n";
158
+ let jsonOutput = {};
159
+
160
+ componentsSet.forEach((componentName) => {
161
+ const instanceCount = componentInstances.get(componentName) || 0;
162
+ const filePaths = componentFiles.get(componentName) || [];
163
+ console.log(`${chalk.cyan(componentName)}: ${instanceCount}`);
164
+
165
+ if (options.format.toLowerCase() === "json") {
166
+ jsonOutput[componentName] = {
167
+ instances: instanceCount,
168
+ files: filePaths,
169
+ };
170
+ } else {
171
+ csvContent += `"${componentName}",${instanceCount},"${filePaths.join(
172
+ ", "
173
+ )}"\n`;
174
+ }
175
+ });
176
+
177
+ // Write the metrics to the specified output file
178
+ try {
179
+ if (options.format.toLowerCase() === "json") {
180
+ await fs.writeFile(outputFile, JSON.stringify(jsonOutput, null, 2));
181
+ } else {
182
+ await fs.writeFile(outputFile, csvContent);
183
+ }
184
+ console.log(
185
+ chalk.green(`\nComponent metrics written to ${outputFile}\n`)
186
+ );
187
+ } catch (err) {
188
+ console.error(chalk.red(`Error writing to file: ${err.message}`));
189
+ }
190
+ } catch (err) {
191
+ console.error(chalk.red(`Error reading files: ${err.message}`));
192
+ }
193
+ };
194
+
195
+ main();
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@georgewrmarshall/design-system-metrics",
3
+ "version": "1.0.0",
4
+ "description": "A CLI tool to audit design system component usage across multiple MetaMask codebases",
5
+ "main": "index.js",
6
+ "packageManager": "yarn@4.3.1",
7
+ "bin": "./index.js",
8
+ "scripts": {
9
+ "start": "node index.js --project extension"
10
+ },
11
+ "keywords": [
12
+ "cli",
13
+ "audit",
14
+ "components",
15
+ "ast",
16
+ "babel"
17
+ ],
18
+ "author": "George Marshall",
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "@babel/parser": "^7.25.7",
22
+ "@babel/traverse": "^7.25.7",
23
+ "chalk": "^4.1.2",
24
+ "commander": "^12.1.0",
25
+ "glob": "^11.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "jest": "^29.7.0"
29
+ }
30
+ }