@georgewrmarshall/design-system-metrics 1.0.2 → 2.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.
@@ -0,0 +1,17 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(tree:*)",
5
+ "Bash(node:*)",
6
+ "Bash(yarn install)",
7
+ "Bash(yarn node:*)",
8
+ "WebFetch(domain:github.com)",
9
+ "Bash(yarn test:*)",
10
+ "Bash(npm view:*)",
11
+ "Bash(npm whoami:*)",
12
+ "Bash(npm publish:*)"
13
+ ],
14
+ "deny": [],
15
+ "ask": []
16
+ }
17
+ }
package/README.md CHANGED
@@ -1,108 +1,210 @@
1
1
  # **@georgewrmarshall/design-system-metrics**
2
2
 
3
- A CLI tool to audit design system component usage across multiple MetaMask codebases
3
+ A CLI tool to audit design system component usage and track migration progress across MetaMask codebases. Identifies deprecated local component usage and measures adoption of the new MetaMask Design System (MMDS) NPM packages.
4
4
 
5
5
  ## **Getting Started**
6
6
 
7
7
  - [Extension](#extension)
8
8
  - [Mobile](#mobile)
9
+ - [Design System Packages](#design-system-packages)
9
10
  - [CLI Options](#cli-options)
11
+ - [Features](#features)
12
+ - [Output Files](#output-files)
10
13
  - [Requirements](#requirements)
11
14
 
12
15
  ---
13
16
 
14
17
  ### **Extension**
15
18
 
16
- 1. **Clone the [MetaMask Extension](https://github.com/MetaMask/metamask-extension)** repository if you havent already:
19
+ 1. **Clone the [MetaMask Extension](https://github.com/MetaMask/metamask-extension)** repository if you haven't already:
17
20
 
18
21
  ```bash
19
22
  git clone https://github.com/MetaMask/metamask-extension.git
20
23
  ```
21
24
 
22
- 2. **Run the CLI tool from the `@georgewrmarshall/design-system-metrics` package:**
25
+ 2. **Run the CLI tool using npx:**
23
26
 
24
- Navigate to the `@georgewrmarshall/design-system-metrics` package directory:
27
+ ```bash
28
+ npx @georgewrmarshall/design-system-metrics --project extension
29
+ ```
30
+
31
+ 3. Two files will be generated in the current working directory:
32
+ - `extension-component-metrics-deprecated.csv` - Old local component-library usage (needs migration)
33
+ - `extension-component-metrics-current.csv` - New MMDS NPM package usage (@metamask/design-system-react)
34
+
35
+ ---
36
+
37
+ ### **Mobile**
38
+
39
+ 1. **Clone the [MetaMask Mobile](https://github.com/MetaMask/metamask-mobile)** repository if you haven't already:
25
40
 
26
41
  ```bash
27
- cd /path/to/@georgewrmarshall/design-system-metrics
42
+ git clone https://github.com/MetaMask/metamask-mobile.git
28
43
  ```
29
44
 
30
- 3. **Run the CLI tool for the MetaMask Extension:**
45
+ 2. **Run the CLI tool using npx:**
31
46
 
32
47
  ```bash
33
- yarn node index.js --project extension
48
+ npx @georgewrmarshall/design-system-metrics --project mobile
34
49
  ```
35
50
 
36
- 4. A file called `extension-component-adoption-metrics.csv` will be generated in the current working directory if there are no errors.
51
+ 3. Two files will be generated in the current working directory:
52
+ - `mobile-component-metrics-deprecated.csv` - Old local component-library usage (needs migration)
53
+ - `mobile-component-metrics-current.csv` - New MMDS NPM package usage (@metamask/design-system-react-native)
37
54
 
38
55
  ---
39
56
 
40
- ### **Mobile**
57
+ ### **Design System Packages**
58
+
59
+ You can also audit the design system NPM packages themselves to track component usage within the design system repos.
41
60
 
42
- 1. **Clone the [MetaMask Mobile](https://github.com/MetaMask/metamask-mobile)** repository if you haven’t already:
61
+ #### **design-system-react**
62
+
63
+ 1. **Clone the [design-system-react](https://github.com/MetaMask/design-system-react)** repository:
43
64
 
44
65
  ```bash
45
- git clone https://github.com/MetaMask/metamask-mobile.git
66
+ git clone https://github.com/MetaMask/design-system-react.git
67
+ ```
68
+
69
+ 2. **Run the CLI tool:**
70
+
71
+ ```bash
72
+ npx @georgewrmarshall/design-system-metrics --project design-system-react
46
73
  ```
47
74
 
48
- 2. **Run the CLI tool from the `@georgewrmarshall/design-system-metrics` package:**
75
+ 3. One file will be generated:
76
+ - `design-system-react-metrics.csv` - Component usage within the design system package
77
+
78
+ #### **design-system-react-native**
49
79
 
50
- Navigate to the `@georgewrmarshall/design-system-metrics` package directory:
80
+ 1. **Clone the [design-system-react-native](https://github.com/MetaMask/design-system-react-native)** repository:
51
81
 
52
82
  ```bash
53
- cd /path/to/@georgewrmarshall/design-system-metrics
83
+ git clone https://github.com/MetaMask/design-system-react-native.git
54
84
  ```
55
85
 
56
- 3. **Run the CLI tool for the MetaMask Mobile project:**
86
+ 2. **Run the CLI tool:**
57
87
 
58
88
  ```bash
59
- yarn node index.js --project mobile
89
+ npx @georgewrmarshall/design-system-metrics --project design-system-react-native
60
90
  ```
61
91
 
62
- 4. A file called `mobile-component-adoption-metrics.csv` will be generated in the current working directory if there are no errors.
92
+ 3. One file will be generated:
93
+ - `design-system-react-native-metrics.csv` - Component usage within the design system package
63
94
 
64
95
  ---
65
96
 
66
97
  ### **CLI Options**
67
98
 
68
99
  - **`--project` (Required)**: Specify the project to audit. Options are:
100
+ - `extension`: For MetaMask Extension
101
+ - `mobile`: For MetaMask Mobile
102
+ - `design-system-react`: For design-system-react package
103
+ - `design-system-react-native`: For design-system-react-native package
69
104
 
70
- - `extension`: For MetaMask Extension
71
- - `mobile`: For MetaMask Mobile
105
+ Example:
106
+ ```bash
107
+ npx @georgewrmarshall/design-system-metrics --project extension
108
+ ```
72
109
 
73
- Example:
110
+ - **`--format` (Optional)**: Specify the output format. Options are `csv` (default) or `json`.
74
111
 
75
- ```bash
76
- yarn node index.js --project extension
77
- ```
112
+ Example:
113
+ ```bash
114
+ npx @georgewrmarshall/design-system-metrics --project extension --format json
115
+ ```
78
116
 
79
- - **`--format` (Optional)**: Specify the output format. Options are `csv` (default) or `json`.
117
+ - **`--sources` (Optional)**: Specify which sources to track. Options are:
118
+ - `all` (default): Track both deprecated and current sources
119
+ - `deprecated`: Only track old local component-library usage
120
+ - `current`: Only track new MMDS NPM package usage
121
+ - Comma-separated: `deprecated,current` (same as `all`)
80
122
 
81
- Example:
123
+ Example:
124
+ ```bash
125
+ npx @georgewrmarshall/design-system-metrics --project extension --sources deprecated
126
+ ```
82
127
 
83
- ```bash
84
- yarn node index.js --project extension --format json
85
- ```
128
+ - **`--config` (Optional)**: Path to custom configuration file
86
129
 
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:
130
+ Example:
131
+ ```bash
132
+ npx @georgewrmarshall/design-system-metrics --project extension --config ./custom-config.json
133
+ ```
88
134
 
89
- ```bash
90
- yarn node index.js --project extension --config /path/to/custom-config.json
91
- ```
135
+ ---
136
+
137
+ ### **Features**
138
+
139
+ #### **Multi-Source Tracking (Extension & Mobile)**
140
+ For Extension and Mobile projects, the tool tracks component usage from two distinct sources:
141
+
142
+ 1. **Deprecated**: Components imported from old local `/component-library` paths (needs migration)
143
+ 2. **Current**: Components imported from new MMDS NPM packages:
144
+ - Extension: `@metamask/design-system-react`
145
+ - Mobile: `@metamask/design-system-react-native`
146
+
147
+ This helps you track **migration progress** from deprecated local components to the new design system packages.
148
+
149
+ #### **Design System Package Auditing**
150
+ For design system packages themselves (`design-system-react` and `design-system-react-native`), the tool generates a single report showing all component usage within the package. This helps you understand which components are being used internally.
151
+
152
+ #### **Flexible Configuration**
153
+ Four project configurations are available in `config.json`:
154
+ - `extension` - Tracks Extension repo (deprecated = local, current = @metamask/design-system-react)
155
+ - `mobile` - Tracks Mobile repo (deprecated = local, current = @metamask/design-system-react-native)
156
+ - `design-system-react` - Tracks the design-system-react package itself
157
+ - `design-system-react-native` - Tracks the design-system-react-native package itself
158
+
159
+ Each project can be configured with:
160
+ - Custom file patterns and ignore rules
161
+ - Component lists to audit
162
+ - NPM packages to track (for extension/mobile)
92
163
 
93
164
  ---
94
165
 
95
- ### **Requirements**
166
+ ### **Output Files**
167
+
168
+ #### **Extension & Mobile Projects**
169
+ Two separate CSV or JSON files are generated to track migration progress:
170
+
171
+ **CSV Format:**
172
+ ```csv
173
+ Component,Instances,File Paths
174
+ "Button",42,"ui/pages/send/send.js, ui/pages/home/home.js"
175
+ ```
176
+
177
+ **Files Generated:**
178
+ - `{project}-component-metrics-deprecated.csv` - Old local component usage
179
+ - `{project}-component-metrics-current.csv` - New NPM package usage
180
+
181
+ **JSON Format:**
182
+ ```json
183
+ {
184
+ "Button": {
185
+ "instances": 42,
186
+ "files": ["ui/pages/send/send.js", "ui/pages/home/home.js"]
187
+ }
188
+ }
189
+ ```
96
190
 
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 />`).
191
+ #### **Design System Package Projects**
192
+ One aggregated report showing all component usage:
193
+
194
+ **Files Generated:**
195
+ - `design-system-react-metrics.csv` - All component usage in design-system-react
196
+ - `design-system-react-native-metrics.csv` - All component usage in design-system-react-native
100
197
 
101
198
  ---
102
199
 
103
- ### **Example Output**
200
+ ### **Requirements**
104
201
 
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.
202
+ - The tool **only counts components that are imported** from tracked sources:
203
+ - For Extension/Mobile: Local `/component-library` (deprecated) or configured NPM packages (current)
204
+ - For Design System packages: Any imports within the package
205
+ - Components **inside JSDoc comments** are not counted as usage
206
+ - **Test files** are automatically excluded (`*.test.{js,tsx}`)
207
+ - **Node.js** v14 or higher is required
106
208
 
107
209
  ---
108
210
 
@@ -0,0 +1,24 @@
1
+ {
2
+ "projects": {
3
+ "test-project": {
4
+ "rootFolder": "__tests__/fixtures/test-project/ui",
5
+ "ignoreFolders": ["__tests__/fixtures/test-project/ui/components/component-library"],
6
+ "filePattern": "__tests__/fixtures/test-project/ui/**/*.{js,tsx}",
7
+ "outputFile": "__tests__/output/test-metrics.csv",
8
+ "currentPackages": ["@metamask/design-system-react"],
9
+ "deprecatedComponents": [
10
+ "Button",
11
+ "Icon",
12
+ "Modal",
13
+ "TextField",
14
+ "BannerAlert"
15
+ ],
16
+ "currentComponents": [
17
+ "Button",
18
+ "Icon",
19
+ "Text",
20
+ "Box"
21
+ ]
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { Button, Icon, Text, Box } from '@metamask/design-system-react';
3
+
4
+ export const CurrentPage = () => {
5
+ return (
6
+ <Box>
7
+ <Button>New Button</Button>
8
+ <Icon name="check" />
9
+ <Text>Welcome</Text>
10
+ <Button variant="primary">Submit</Button>
11
+ </Box>
12
+ );
13
+ };
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { Button, Icon, Modal, TextField } from '../../components/component-library';
3
+
4
+ export const DeprecatedPage = () => {
5
+ return (
6
+ <div>
7
+ <Button>Old Button</Button>
8
+ <Icon name="check" />
9
+ <Modal>
10
+ <TextField label="Name" />
11
+ <Button type="submit">Submit</Button>
12
+ </Modal>
13
+ </div>
14
+ );
15
+ };
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { Button as OldButton } from '../../components/component-library';
3
+ import { Button, Text } from '@metamask/design-system-react';
4
+
5
+ export const MixedPage = () => {
6
+ return (
7
+ <div>
8
+ <OldButton>Old Button</OldButton>
9
+ <Button>New Button</Button>
10
+ <Text>Some text</Text>
11
+ </div>
12
+ );
13
+ };
@@ -0,0 +1,189 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ const TEST_CONFIG = path.join(__dirname, 'fixtures/test-config.json');
6
+ const OUTPUT_DIR = path.join(__dirname, 'output');
7
+ const DEPRECATED_OUTPUT = path.join(OUTPUT_DIR, 'test-metrics-deprecated.csv');
8
+ const CURRENT_OUTPUT = path.join(OUTPUT_DIR, 'test-metrics-current.csv');
9
+
10
+ describe('Design System Metrics', () => {
11
+ beforeAll(() => {
12
+ // Clean output directory before tests
13
+ try {
14
+ execSync(`rm -rf ${OUTPUT_DIR}/*`);
15
+ } catch (err) {
16
+ // Directory might not exist yet
17
+ }
18
+ });
19
+
20
+ afterAll(async () => {
21
+ // Clean up output files after tests
22
+ try {
23
+ await fs.rm(OUTPUT_DIR, { recursive: true, force: true });
24
+ } catch (err) {
25
+ // Ignore errors
26
+ }
27
+ });
28
+
29
+ describe('Deprecated Component Tracking', () => {
30
+ test('should track deprecated components from local component-library', async () => {
31
+ execSync(
32
+ `yarn node index.js --project test-project --config ${TEST_CONFIG} --sources deprecated`,
33
+ { stdio: 'pipe' }
34
+ );
35
+
36
+ const content = await fs.readFile(DEPRECATED_OUTPUT, 'utf8');
37
+ const lines = content.trim().split('\n');
38
+
39
+ // Check header
40
+ expect(lines[0]).toBe('Component,Instances,File Paths');
41
+
42
+ // Check that deprecated components are tracked
43
+ expect(content).toContain('Button');
44
+ expect(content).toContain('Icon');
45
+ expect(content).toContain('Modal');
46
+ expect(content).toContain('TextField');
47
+
48
+ // Parse CSV and verify counts
49
+ const buttonLine = lines.find(line => line.startsWith('"Button"'));
50
+ expect(buttonLine).toBeDefined();
51
+ // Button appears 2 times in deprecated-page.js
52
+ expect(buttonLine).toContain(',2,');
53
+ });
54
+
55
+ test('should only track components in deprecatedComponents list', async () => {
56
+ execSync(
57
+ `yarn node index.js --project test-project --config ${TEST_CONFIG} --sources deprecated`,
58
+ { stdio: 'pipe' }
59
+ );
60
+
61
+ const content = await fs.readFile(DEPRECATED_OUTPUT, 'utf8');
62
+
63
+ // BannerAlert is in the list but not used, should still appear with 0 or not tracked
64
+ // Only used components should appear
65
+ expect(content).toContain('Button');
66
+ expect(content).toContain('Icon');
67
+ expect(content).toContain('Modal');
68
+ expect(content).toContain('TextField');
69
+ });
70
+ });
71
+
72
+ describe('Current Component Tracking', () => {
73
+ test('should track current components from NPM package', async () => {
74
+ execSync(
75
+ `yarn node index.js --project test-project --config ${TEST_CONFIG} --sources current`,
76
+ { stdio: 'pipe' }
77
+ );
78
+
79
+ const content = await fs.readFile(CURRENT_OUTPUT, 'utf8');
80
+ const lines = content.trim().split('\n');
81
+
82
+ // Check header
83
+ expect(lines[0]).toBe('Component,Instances,File Paths');
84
+
85
+ // Check that current components are tracked
86
+ expect(content).toContain('Button');
87
+ expect(content).toContain('Icon');
88
+ expect(content).toContain('Text');
89
+ expect(content).toContain('Box');
90
+ });
91
+
92
+ test('should count multiple instances correctly', async () => {
93
+ execSync(
94
+ `yarn node index.js --project test-project --config ${TEST_CONFIG} --sources current`,
95
+ { stdio: 'pipe' }
96
+ );
97
+
98
+ const content = await fs.readFile(CURRENT_OUTPUT, 'utf8');
99
+ const lines = content.trim().split('\n');
100
+
101
+ // Button appears 3 times total in current files (2 in current-page.js, 1 in mixed-page.js)
102
+ const buttonLine = lines.find(line => line.startsWith('"Button"'));
103
+ expect(buttonLine).toBeDefined();
104
+ expect(buttonLine).toContain(',3,');
105
+ });
106
+ });
107
+
108
+ describe('Source Separation', () => {
109
+ test('should generate both deprecated and current reports by default', async () => {
110
+ execSync(
111
+ `yarn node index.js --project test-project --config ${TEST_CONFIG}`,
112
+ { stdio: 'pipe' }
113
+ );
114
+
115
+ // Both files should exist
116
+ const deprecatedExists = await fs
117
+ .access(DEPRECATED_OUTPUT)
118
+ .then(() => true)
119
+ .catch(() => false);
120
+ const currentExists = await fs
121
+ .access(CURRENT_OUTPUT)
122
+ .then(() => true)
123
+ .catch(() => false);
124
+
125
+ expect(deprecatedExists).toBe(true);
126
+ expect(currentExists).toBe(true);
127
+ });
128
+
129
+ test('should separate Button usage by source', async () => {
130
+ execSync(
131
+ `yarn node index.js --project test-project --config ${TEST_CONFIG}`,
132
+ { stdio: 'pipe' }
133
+ );
134
+
135
+ const deprecatedContent = await fs.readFile(DEPRECATED_OUTPUT, 'utf8');
136
+ const currentContent = await fs.readFile(CURRENT_OUTPUT, 'utf8');
137
+
138
+ // Deprecated Button count (2 in deprecated-page.js)
139
+ // Note: mixed-page.js uses "Button as OldButton" which doesn't match "Button" component name
140
+ const deprecatedLines = deprecatedContent.trim().split('\n');
141
+ const deprecatedButtonLine = deprecatedLines.find(line =>
142
+ line.startsWith('"Button"')
143
+ );
144
+ expect(deprecatedButtonLine).toContain(',2,'); // 2 in deprecated-page
145
+
146
+ // Current Button count (2 in current-page.js + 1 in mixed-page.js)
147
+ const currentLines = currentContent.trim().split('\n');
148
+ const currentButtonLine = currentLines.find(line =>
149
+ line.startsWith('"Button"')
150
+ );
151
+ expect(currentButtonLine).toContain(',3,'); // 2 in current-page + 1 in mixed-page
152
+ });
153
+ });
154
+
155
+ describe('File Path Tracking', () => {
156
+ test('should track which files contain each component', async () => {
157
+ execSync(
158
+ `yarn node index.js --project test-project --config ${TEST_CONFIG} --sources deprecated`,
159
+ { stdio: 'pipe' }
160
+ );
161
+
162
+ const content = await fs.readFile(DEPRECATED_OUTPUT, 'utf8');
163
+
164
+ // Button should reference the files where it's used
165
+ expect(content).toContain('deprecated-page.js');
166
+ });
167
+ });
168
+
169
+ describe('JSON Output Format', () => {
170
+ test('should generate JSON format when specified', async () => {
171
+ execSync(
172
+ `yarn node index.js --project test-project --config ${TEST_CONFIG} --sources deprecated --format json`,
173
+ { stdio: 'pipe' }
174
+ );
175
+
176
+ const jsonOutput = path.join(OUTPUT_DIR, 'test-metrics-deprecated.json');
177
+ const content = await fs.readFile(jsonOutput, 'utf8');
178
+ const data = JSON.parse(content);
179
+
180
+ // Should be valid JSON
181
+ expect(typeof data).toBe('object');
182
+
183
+ // Should have Button data
184
+ expect(data.Button).toBeDefined();
185
+ expect(data.Button.instances).toBeGreaterThan(0);
186
+ expect(Array.isArray(data.Button.files)).toBe(true);
187
+ });
188
+ });
189
+ });
package/config.json CHANGED
@@ -4,8 +4,9 @@
4
4
  "rootFolder": "ui",
5
5
  "ignoreFolders": ["ui/components/component-library"],
6
6
  "filePattern": "ui/**/*.{js,tsx}",
7
- "outputFile": "extension-component-adoption-metrics.csv",
8
- "components": [
7
+ "outputFile": "extension-component-metrics.csv",
8
+ "currentPackages": ["@metamask/design-system-react"],
9
+ "deprecatedComponents": [
9
10
  "AvatarAccount",
10
11
  "AvatarBase",
11
12
  "AvatarFavicon",
@@ -50,15 +51,47 @@
50
51
  "Textarea",
51
52
  "TextField",
52
53
  "TextFieldSearch"
54
+ ],
55
+ "currentComponents": [
56
+ "AvatarAccount",
57
+ "AvatarBase",
58
+ "AvatarFavicon",
59
+ "AvatarGroup",
60
+ "AvatarIcon",
61
+ "AvatarNetwork",
62
+ "AvatarToken",
63
+ "BadgeCount",
64
+ "BadgeIcon",
65
+ "BadgeNetwork",
66
+ "BadgeStatus",
67
+ "BadgeWrapper",
68
+ "Blockies",
69
+ "Box",
70
+ "Button",
71
+ "ButtonBase",
72
+ "ButtonIcon",
73
+ "ButtonHero",
74
+ "Checkbox",
75
+ "Icon",
76
+ "Jazzicon",
77
+ "Maskicon",
78
+ "Text",
79
+ "TextButton"
53
80
  ]
54
81
  },
55
82
  "mobile": {
56
83
  "rootFolder": "app",
57
84
  "ignoreFolders": ["app/component-library"],
58
85
  "filePattern": "app/components/**/*.{js,jsx,ts,tsx}",
59
- "outputFile": "mobile-component-adoption-metrics.csv",
60
- "components": [
86
+ "outputFile": "mobile-component-metrics.csv",
87
+ "currentPackages": ["@metamask/design-system-react-native"],
88
+ "deprecatedComponents": [
61
89
  "Accordion",
90
+ "AccountBalance",
91
+ "AccountBase",
92
+ "AccountCell",
93
+ "ActionListItem",
94
+ "AggregatedPercentage",
62
95
  "Avatar",
63
96
  "AvatarAccount",
64
97
  "AvatarBase",
@@ -81,43 +114,105 @@
81
114
  "BottomSheetHeader",
82
115
  "Button",
83
116
  "ButtonBase",
117
+ "ButtonFilter",
118
+ "ButtonHero",
84
119
  "ButtonIcon",
85
120
  "ButtonLink",
121
+ "ButtonPill",
86
122
  "ButtonPrimary",
87
123
  "ButtonSecondary",
124
+ "ButtonSemantic",
125
+ "ButtonToggle",
88
126
  "Card",
89
127
  "Cell",
90
128
  "CellBase",
91
129
  "CellDisplay",
92
130
  "CellMultiSelect",
93
131
  "CellSelect",
132
+ "CellSelectWithMenu",
94
133
  "Checkbox",
134
+ "ConditionalScrollView",
135
+ "ContractBox",
136
+ "ContractBoxBase",
137
+ "CustomInput",
138
+ "CustomSpendCap",
95
139
  "Header",
140
+ "HeaderCenter",
141
+ "HeaderWithTitleLeft",
142
+ "HeaderWithTitleLeftScrollable",
96
143
  "HelpText",
97
144
  "Icon",
98
145
  "Input",
146
+ "KeyValueRow",
99
147
  "Label",
100
148
  "ListItem",
101
149
  "ListItemColumn",
150
+ "ListItemMultiSelectButton",
151
+ "ListItemMultiSelectWithMenuButton",
152
+ "Loader",
153
+ "MainActionButton",
102
154
  "ModalConfirmation",
103
- "ModalMadatory",
155
+ "ModalMandatory",
156
+ "MultichainAccountSelectorList",
157
+ "MultichainAddressRow",
158
+ "MultichainAddressRowsList",
159
+ "MultichainAddWalletActions",
104
160
  "MultiSelectItem",
105
161
  "Overlay",
162
+ "PercentageChange",
106
163
  "PickerAccount",
107
164
  "PickerBase",
108
165
  "PickerNetwork",
166
+ "QuickActionButton",
167
+ "QuickActionButtons",
168
+ "SegmentedControl",
109
169
  "SelectItem",
170
+ "SheetActions",
110
171
  "SheetBottom",
111
172
  "SheetHeader",
173
+ "Tab",
112
174
  "TabBar",
113
175
  "TabBarItem",
176
+ "TabEmptyState",
177
+ "Tabs",
178
+ "TabsBar",
179
+ "TabsList",
114
180
  "Tag",
181
+ "TagColored",
115
182
  "TagUrl",
116
183
  "Text",
117
184
  "TextField",
118
185
  "TextFieldSearch",
119
186
  "TextWithPrefixIcon",
187
+ "TitleLeft",
120
188
  "Toast"
189
+ ],
190
+ "currentComponents": [
191
+ "AvatarAccount",
192
+ "AvatarBase",
193
+ "AvatarFavicon",
194
+ "AvatarGroup",
195
+ "AvatarIcon",
196
+ "AvatarNetwork",
197
+ "AvatarToken",
198
+ "BadgeCount",
199
+ "BadgeIcon",
200
+ "BadgeNetwork",
201
+ "BadgeStatus",
202
+ "BadgeWrapper",
203
+ "Blockies",
204
+ "Box",
205
+ "ButtonAnimated",
206
+ "ButtonBase",
207
+ "Button",
208
+ "ButtonIcon",
209
+ "Checkbox",
210
+ "Icon",
211
+ "Jazzicon",
212
+ "Maskicon",
213
+ "TextButton",
214
+ "Text",
215
+ "TextOrChildren"
121
216
  ]
122
217
  }
123
218
  }
package/index.js CHANGED
@@ -8,14 +8,12 @@ const traverse = require("@babel/traverse").default;
8
8
  const { program } = require("commander");
9
9
  const chalk = require("chalk");
10
10
 
11
- // Load configuration
12
- const CONFIG_PATH = path.join(__dirname, "config.json");
13
11
  let config;
14
12
 
15
13
  // Function to load and parse the configuration file
16
- const loadConfig = async () => {
14
+ const loadConfig = async (configPath) => {
17
15
  try {
18
- const configContent = await fs.readFile(CONFIG_PATH, "utf8");
16
+ const configContent = await fs.readFile(configPath, "utf8");
19
17
  config = JSON.parse(configContent);
20
18
  } catch (err) {
21
19
  console.error(
@@ -27,23 +25,56 @@ const loadConfig = async () => {
27
25
 
28
26
  // Define CLI options using Commander
29
27
  program
30
- .version("1.0.0")
31
- .description("Design System Metrics CLI Tool")
28
+ .version("2.0.0")
29
+ .description("Design System Metrics CLI Tool - Track component usage from multiple sources")
32
30
  .requiredOption(
33
31
  "-p, --project <name>",
34
32
  "Specify the project to audit (e.g., extension, mobile)"
35
33
  )
36
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
+ .option(
41
+ "-c, --config <path>",
42
+ "Path to custom config file",
43
+ path.join(__dirname, "config.json")
44
+ )
37
45
  .parse(process.argv);
38
46
 
39
47
  const options = program.opts();
40
48
 
41
49
  // Initialize component instances and file mappings
42
- let componentInstances = new Map();
43
- let componentFiles = new Map();
50
+ // Structure: Map<componentName, Map<source, { count, files }>>
51
+ let componentMetrics = new Map();
52
+
53
+ // Helper function to track component usage by source
54
+ const trackComponent = (componentName, source, filePath) => {
55
+ if (!componentMetrics.has(componentName)) {
56
+ componentMetrics.set(componentName, new Map());
57
+ }
58
+
59
+ const sourceMetrics = componentMetrics.get(componentName);
60
+ if (!sourceMetrics.has(source)) {
61
+ sourceMetrics.set(source, { count: 0, files: [] });
62
+ }
63
+
64
+ const metrics = sourceMetrics.get(source);
65
+ metrics.count++;
66
+ metrics.files.push(filePath);
67
+ };
44
68
 
45
69
  // Function to process a single file
46
- const processFile = async (filePath, componentsSet, importedComponentsSet) => {
70
+ const processFile = async (
71
+ filePath,
72
+ deprecatedComponentsSet,
73
+ currentComponentsSet,
74
+ currentPackages
75
+ ) => {
76
+ // Track imports by source: Map<componentName, source>
77
+ const componentImports = new Map();
47
78
  try {
48
79
  const content = await fs.readFile(filePath, "utf8");
49
80
 
@@ -51,23 +82,61 @@ const processFile = async (filePath, componentsSet, importedComponentsSet) => {
51
82
  const ast = babelParser.parse(content, {
52
83
  sourceType: "module",
53
84
  plugins: ["jsx", "typescript"],
85
+ attachComment: true, // Enable comment attachment for JSDoc parsing
54
86
  });
55
87
 
56
- // Traverse the AST to find import declarations from the component library
88
+ // Traverse the AST to find import declarations from multiple sources
57
89
  traverse(ast, {
58
90
  ImportDeclaration({ node }) {
59
91
  const importPath = node.source.value;
60
92
  console.log(`Import path detected: ${importPath}`);
93
+
94
+ let source = null;
95
+
96
+ // Check if it's from local component library (DEPRECATED)
61
97
  if (importPath.includes("/component-library")) {
98
+ 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
+ }
109
+
110
+ // If we found a relevant import source, track the components
111
+ if (source) {
62
112
  node.specifiers.forEach((specifier) => {
113
+ let componentName = null;
114
+
63
115
  if (specifier.type === "ImportDefaultSpecifier") {
64
- importedComponentsSet.add(specifier.local.name);
116
+ componentName = specifier.local.name;
65
117
  console.log(
66
- `Default imported component: ${specifier.local.name}`
118
+ `Default imported component: ${componentName} (${source})`
67
119
  );
68
120
  } else if (specifier.type === "ImportSpecifier") {
69
- importedComponentsSet.add(specifier.local.name);
70
- console.log(`Named imported component: ${specifier.local.name}`);
121
+ componentName = specifier.local.name;
122
+ console.log(`Named imported component: ${componentName} (${source})`);
123
+ }
124
+
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
+ }
71
140
  }
72
141
  });
73
142
  }
@@ -99,19 +168,13 @@ const processFile = async (filePath, componentsSet, importedComponentsSet) => {
99
168
 
100
169
  console.log(`JSX component detected: ${componentName}`);
101
170
 
102
- if (
103
- componentsSet.has(componentName) &&
104
- importedComponentsSet.has(componentName)
105
- ) {
106
- const count = componentInstances.get(componentName) || 0;
107
- componentInstances.set(componentName, count + 1);
108
-
109
- const files = componentFiles.get(componentName) || [];
110
- files.push(filePath);
111
- componentFiles.set(componentName, files);
171
+ // Check if this component was imported and track its usage
172
+ if (componentImports.has(componentName)) {
173
+ const source = componentImports.get(componentName);
174
+ trackComponent(componentName, source, filePath);
112
175
 
113
176
  console.log(
114
- `Matched JSX component: ${componentName}, Count: ${count + 1}`
177
+ `Matched JSX component: ${componentName}, Source: ${source}`
115
178
  );
116
179
  }
117
180
  }
@@ -126,7 +189,7 @@ const processFile = async (filePath, componentsSet, importedComponentsSet) => {
126
189
 
127
190
  // Main function to coordinate the audit
128
191
  const main = async () => {
129
- await loadConfig();
192
+ await loadConfig(options.config);
130
193
 
131
194
  const projectName = options.project;
132
195
  const projectConfig = config.projects[projectName];
@@ -140,10 +203,18 @@ const main = async () => {
140
203
  process.exit(1);
141
204
  }
142
205
 
143
- const { rootFolder, ignoreFolders, filePattern, outputFile, components } =
144
- projectConfig;
206
+ const {
207
+ rootFolder,
208
+ ignoreFolders,
209
+ filePattern,
210
+ outputFile,
211
+ deprecatedComponents = [],
212
+ currentComponents = [],
213
+ currentPackages = [],
214
+ } = projectConfig;
145
215
 
146
- const componentsSet = new Set(components);
216
+ const deprecatedComponentsSet = new Set(deprecatedComponents);
217
+ const currentComponentsSet = new Set(currentComponents);
147
218
 
148
219
  console.log(chalk.blue(`\nStarting audit for project: ${projectName}\n`));
149
220
 
@@ -162,44 +233,96 @@ const main = async () => {
162
233
 
163
234
  // Process files concurrently
164
235
  await Promise.all(
165
- files.map((file) => processFile(file, componentsSet, new Set()))
236
+ files.map((file) =>
237
+ processFile(
238
+ file,
239
+ deprecatedComponentsSet,
240
+ currentComponentsSet,
241
+ currentPackages
242
+ )
243
+ )
166
244
  );
167
245
 
168
- console.log(chalk.green("\nComponent Adoption Metrics:"));
246
+ console.log(chalk.green("\nComponent Migration Metrics:"));
169
247
 
170
- let csvContent = "Component,Instances,File Paths\n";
171
- let jsonOutput = {};
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());
252
+ }
172
253
 
173
- componentsSet.forEach((componentName) => {
174
- const instanceCount = componentInstances.get(componentName) || 0;
175
- const filePaths = componentFiles.get(componentName) || [];
176
- console.log(`${chalk.cyan(componentName)}: ${instanceCount}`);
254
+ const sourceTypes = requestedSources;
177
255
 
178
- if (options.format.toLowerCase() === "json") {
179
- jsonOutput[componentName] = {
180
- instances: instanceCount,
181
- files: filePaths,
182
- };
183
- } else {
184
- csvContent += `"${componentName}",${instanceCount},"${filePaths.join(
185
- ", "
186
- )}"\n`;
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
+ }
273
+ }
274
+ });
275
+
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;
187
284
  }
188
- });
189
285
 
190
- // Write the metrics to the specified output file
191
- try {
286
+ // Generate output file name
287
+ const baseFileName = outputFile.replace(/\.(csv|json)$/, "");
288
+ const sourceOutputFile = `${baseFileName}-${sourceType}.${options.format.toLowerCase()}`;
289
+
290
+ console.log(
291
+ chalk.blue(
292
+ `\n${sourceType.toUpperCase()} Components (${sourceType === "deprecated" ? "Local component-library" : currentPackages.join(", ")}):`
293
+ )
294
+ );
295
+
192
296
  if (options.format.toLowerCase() === "json") {
193
- await fs.writeFile(outputFile, JSON.stringify(jsonOutput, null, 2));
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
+ });
305
+
306
+ await fs.writeFile(
307
+ sourceOutputFile,
308
+ JSON.stringify(jsonOutput, null, 2)
309
+ );
194
310
  } else {
195
- await fs.writeFile(outputFile, csvContent);
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
+ });
318
+
319
+ await fs.writeFile(sourceOutputFile, csvContent);
196
320
  }
197
- console.log(
198
- chalk.green(`\nComponent metrics written to ${outputFile}\n`)
199
- );
200
- } catch (err) {
201
- console.error(chalk.red(`Error writing to file: ${err.message}`));
321
+
322
+ console.log(chalk.green(`Metrics written to ${sourceOutputFile}`));
202
323
  }
324
+
325
+ console.log(chalk.green("\n✓ All reports generated successfully!\n"));
203
326
  } catch (err) {
204
327
  console.error(chalk.red(`Error reading files: ${err.message}`));
205
328
  }
package/jest.config.js ADDED
@@ -0,0 +1,14 @@
1
+ module.exports = {
2
+ testEnvironment: 'node',
3
+ testMatch: ['**/__tests__/**/*.test.js'],
4
+ testPathIgnorePatterns: [
5
+ '/node_modules/',
6
+ '/__tests__/fixtures/',
7
+ '/__tests__/output/',
8
+ ],
9
+ coveragePathIgnorePatterns: [
10
+ '/node_modules/',
11
+ '/__tests__/fixtures/',
12
+ '/__tests__/output/',
13
+ ],
14
+ };
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@georgewrmarshall/design-system-metrics",
3
- "version": "1.0.2",
4
- "description": "A CLI tool to audit design system component usage across multiple MetaMask codebases",
3
+ "version": "2.0.0",
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",
7
7
  "bin": "./index.js",
8
8
  "scripts": {
9
- "start": "node index.js --project extension"
9
+ "start": "node index.js --project extension",
10
+ "test": "jest",
11
+ "test:watch": "jest --watch"
10
12
  },
11
13
  "keywords": [
12
14
  "cli",
@@ -28,4 +30,4 @@
28
30
  "devDependencies": {
29
31
  "jest": "^29.7.0"
30
32
  }
31
- }
33
+ }