@civicactions/cmsds-open-data-components 4.0.7 ā 4.0.8-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/package.json +5 -1
- package/scripts/README.md +264 -0
- package/scripts/generate-usage-report.cjs +607 -0
package/README.md
CHANGED
|
@@ -62,7 +62,36 @@ This creates `COMPONENTS_INVENTORY.md` in the root directory with:
|
|
|
62
62
|
|
|
63
63
|
The inventory is automatically updated on every commit via a pre-commit hook.
|
|
64
64
|
|
|
65
|
-
For more details, see the [
|
|
65
|
+
For more details, see the [Scripts Documentation](scripts/README.md).
|
|
66
|
+
|
|
67
|
+
## Component Usage Report
|
|
68
|
+
|
|
69
|
+
This project includes a script that can be run in projects that use cmsds-open-data-components as a dependency to analyze component usage.
|
|
70
|
+
|
|
71
|
+
### Running in Dependent Projects
|
|
72
|
+
|
|
73
|
+
From a project that has `@civicactions/cmsds-open-data-components` as a dependency:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx generate-usage-report
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This generates `COMPONENT_USAGE_REPORT.md` showing:
|
|
80
|
+
- Which components from the library are being used
|
|
81
|
+
- Where each component is imported in the project
|
|
82
|
+
- Summary statistics and category breakdown
|
|
83
|
+
- GitHub links to component source code
|
|
84
|
+
|
|
85
|
+
### Generating Sample Report (Library Development)
|
|
86
|
+
|
|
87
|
+
To generate a sample report using Storybook files in this repository:
|
|
88
|
+
```bash
|
|
89
|
+
npm run generate:usage-report
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This creates `SAMPLE_COMPONENT_USAGE_REPORT.md` demonstrating the report format.
|
|
93
|
+
|
|
94
|
+
For more details, see the [Scripts Documentation](scripts/README.md).
|
|
66
95
|
|
|
67
96
|
## Publishing new versions
|
|
68
97
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@civicactions/cmsds-open-data-components",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.8-alpha.1",
|
|
4
4
|
"description": "Components for the open data catalog frontend using CMS Design System",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"source": "src/index.ts",
|
|
7
7
|
"types": "dist/types.d.ts",
|
|
8
8
|
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"generate-usage-report": "./scripts/generate-usage-report.cjs"
|
|
11
|
+
},
|
|
9
12
|
"scripts": {
|
|
10
13
|
"build": "parcel build",
|
|
11
14
|
"build:report": "parcel build --reporter @parcel/reporter-bundle-analyzer",
|
|
@@ -16,6 +19,7 @@
|
|
|
16
19
|
"storybook": "storybook dev -p 6006",
|
|
17
20
|
"build-storybook": "storybook build",
|
|
18
21
|
"generate:inventory": "node scripts/generate-inventory.cjs",
|
|
22
|
+
"generate:usage-report": "node scripts/generate-usage-report.cjs",
|
|
19
23
|
"prepare": "husky"
|
|
20
24
|
},
|
|
21
25
|
"author": "",
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Scripts Documentation
|
|
2
|
+
|
|
3
|
+
This directory contains utility scripts for managing and analyzing the cmsds-open-data-components library.
|
|
4
|
+
|
|
5
|
+
## Component Inventory Generator
|
|
6
|
+
|
|
7
|
+
This script automatically generates a comprehensive markdown inventory report for the cmsds-open-data-components library.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Run the script using npm:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm run generate:inventory
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or run it directly:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
node scripts/generate-inventory.cjs
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Output
|
|
24
|
+
|
|
25
|
+
The script generates `COMPONENTS_INVENTORY.md` in the root directory containing:
|
|
26
|
+
|
|
27
|
+
- **Inventory Table**: Complete list of all components, services, templates, hooks, contexts, utilities, types, and assets
|
|
28
|
+
- **Public Export Status**: Indicates which items are publicly exported vs internal-only
|
|
29
|
+
- **Storybook Status**: Shows which items have Storybook stories for visual documentation
|
|
30
|
+
- **Unit Test Status**: Shows which items have unit test coverage
|
|
31
|
+
- **Quality Metrics**: Summary statistics for documentation and testing coverage
|
|
32
|
+
- **Export Summary**: Count of public vs internal items by category
|
|
33
|
+
|
|
34
|
+
## What It Scans
|
|
35
|
+
|
|
36
|
+
The script automatically scans the following directories:
|
|
37
|
+
|
|
38
|
+
- `src/components/` - React components
|
|
39
|
+
- `src/templates/` - Page-level templates
|
|
40
|
+
- `src/services/` - API service hooks
|
|
41
|
+
- `src/utilities/` - Utility functions
|
|
42
|
+
- `src/types/` - TypeScript type definitions
|
|
43
|
+
- `src/assets/` - Static assets and data files
|
|
44
|
+
|
|
45
|
+
It also automatically detects:
|
|
46
|
+
- **Hooks**: Directories or files starting with `use` (e.g., `useScrollToTop`)
|
|
47
|
+
- **Contexts**: Files ending with `Context` that contain `createContext()` calls
|
|
48
|
+
|
|
49
|
+
## How It Works
|
|
50
|
+
|
|
51
|
+
1. **Reads `src/index.ts`** to determine which items are publicly exported
|
|
52
|
+
2. **Scans each directory** for subdirectories and files
|
|
53
|
+
3. **Dynamically discovers hooks and contexts** by naming patterns and file content
|
|
54
|
+
4. **Checks for `.stories.*` files** to determine Storybook coverage
|
|
55
|
+
5. **Checks for `.test.*` and `.spec.*` files** to determine unit test coverage
|
|
56
|
+
6. **Calculates statistics** for overall project quality metrics
|
|
57
|
+
7. **Generates markdown** with formatted tables and summaries
|
|
58
|
+
|
|
59
|
+
## Maintenance
|
|
60
|
+
|
|
61
|
+
The script is designed to automatically adapt to changes in the codebase:
|
|
62
|
+
|
|
63
|
+
- New components/templates/services/hooks/contexts are automatically discovered
|
|
64
|
+
- Public export status updates when `src/index.ts` changes
|
|
65
|
+
- Story and test status updates as files are added/removed
|
|
66
|
+
- Statistics recalculate automatically
|
|
67
|
+
|
|
68
|
+
No manual updates needed when adding or removing hooks and contexts!
|
|
69
|
+
|
|
70
|
+
### Hook and Context Detection
|
|
71
|
+
|
|
72
|
+
**Hooks** are detected by:
|
|
73
|
+
- Directory names starting with `use` (e.g., `src/components/useScrollToTop/`)
|
|
74
|
+
- File names starting with `use` (e.g., `useAddLoginLink.ts`)
|
|
75
|
+
|
|
76
|
+
**Contexts** are detected by:
|
|
77
|
+
- File names ending with `Context` (e.g., `HeaderContext.tsx`)
|
|
78
|
+
- File must contain a `createContext()` call
|
|
79
|
+
- Export name is extracted from `export default` or `export const` statements
|
|
80
|
+
|
|
81
|
+
The scanner automatically excludes test and story files to prevent false positives.
|
|
82
|
+
|
|
83
|
+
### Known Special Cases
|
|
84
|
+
|
|
85
|
+
The script handles several special cases:
|
|
86
|
+
|
|
87
|
+
- **DatasetAdditionalInformation**: Exports `buildRows` function
|
|
88
|
+
- **DatasetTableTab**: Exported as `DatasetTable`
|
|
89
|
+
- **Datatable**: Exported as `DataTable` (case difference)
|
|
90
|
+
- **frequencyMap**: Commented out in exports
|
|
91
|
+
- **aca.ts**: Exports `acaToParams` function
|
|
92
|
+
|
|
93
|
+
To add new special cases, update the scanning functions in `generate-inventory.cjs`.
|
|
94
|
+
|
|
95
|
+
## File Structure
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
scripts/
|
|
99
|
+
generate-inventory.cjs # Component inventory script
|
|
100
|
+
generate-inventory.test.js # Tests for inventory script
|
|
101
|
+
generate-usage-report.cjs # Component usage report script
|
|
102
|
+
generate-usage-report.test.js # Tests for usage report script
|
|
103
|
+
README.md # This file
|
|
104
|
+
|
|
105
|
+
COMPONENTS_INVENTORY.md # Generated inventory (root directory)
|
|
106
|
+
COMPONENT_USAGE_REPORT.md # Generated usage report (root directory, when run in dependent projects)
|
|
107
|
+
SAMPLE_COMPONENT_USAGE_REPORT.md # Sample usage report (root directory, when run locally)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
# Component Usage Report Generator
|
|
113
|
+
|
|
114
|
+
This script analyzes projects that use cmsds-open-data-components as a dependency and generates a comprehensive report showing which components are being used and where.
|
|
115
|
+
|
|
116
|
+
## Usage
|
|
117
|
+
|
|
118
|
+
### In Projects Using cmsds-open-data-components
|
|
119
|
+
|
|
120
|
+
From a project that has `@civicactions/cmsds-open-data-components` as a dependency:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npx generate-usage-report
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Or add to your project's scripts in `package.json`:
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"scripts": {
|
|
130
|
+
"usage-report": "generate-usage-report"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Then run:
|
|
136
|
+
```bash
|
|
137
|
+
npm run usage-report
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### In the Library Repository
|
|
141
|
+
|
|
142
|
+
Run the script directly in this repository to generate a sample report:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm run generate:usage-report
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Or run it directly:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
node scripts/generate-usage-report.cjs
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
When run in the library repository, it generates a sample report (`SAMPLE_COMPONENT_USAGE_REPORT.md`) by analyzing Storybook files.
|
|
155
|
+
|
|
156
|
+
## Output
|
|
157
|
+
|
|
158
|
+
The script generates a markdown report containing:
|
|
159
|
+
|
|
160
|
+
- **Library Version**: Version of cmsds-open-data-components being analyzed
|
|
161
|
+
- **Summary Statistics**: Total unique components, import statements, and files analyzed
|
|
162
|
+
- **Category Breakdown**: Components grouped by type (Component, Template, Hook, Context, Service, Utility)
|
|
163
|
+
- **Component Usage Details**: Table showing each component with:
|
|
164
|
+
- Component name (linked to GitHub repository)
|
|
165
|
+
- Category/type
|
|
166
|
+
- Number of times imported
|
|
167
|
+
- List of files where it's used
|
|
168
|
+
|
|
169
|
+
## What It Scans
|
|
170
|
+
|
|
171
|
+
The script scans the following directories in your project:
|
|
172
|
+
|
|
173
|
+
- `src/` - Main source directory
|
|
174
|
+
- `app/` - Next.js app directory
|
|
175
|
+
- `pages/` - Next.js pages directory
|
|
176
|
+
- `components/` - Components directory
|
|
177
|
+
- `templates/` - Templates directory
|
|
178
|
+
|
|
179
|
+
It searches for these file types:
|
|
180
|
+
- `.js`, `.jsx` - JavaScript/React files
|
|
181
|
+
- `.ts`, `.tsx` - TypeScript/React files
|
|
182
|
+
|
|
183
|
+
The script automatically excludes:
|
|
184
|
+
- `node_modules/` - Dependencies
|
|
185
|
+
- `dist/`, `build/` - Build output directories
|
|
186
|
+
- `.next/`, `.cache/` - Framework cache directories
|
|
187
|
+
|
|
188
|
+
## How It Works
|
|
189
|
+
|
|
190
|
+
1. **Detects execution context**: Determines if running in the library repo or a dependent project
|
|
191
|
+
2. **Scans project files**: Recursively searches for JavaScript/TypeScript files
|
|
192
|
+
3. **Parses imports**: Uses regex to find imports from `@civicactions/cmsds-open-data-components`
|
|
193
|
+
4. **Tracks usage**: Records each component, where it's imported, and at what line number
|
|
194
|
+
5. **Categorizes components**: Automatically categorizes by naming patterns:
|
|
195
|
+
- Hooks: Names starting with `use`
|
|
196
|
+
- Contexts: Names ending with `Context`
|
|
197
|
+
- Templates: From `templates/` directory
|
|
198
|
+
- Services: From `services/` directory
|
|
199
|
+
- Utilities: From `utilities/` directory
|
|
200
|
+
- Components: Everything else
|
|
201
|
+
6. **Generates report**: Creates formatted markdown with statistics and detailed usage tables
|
|
202
|
+
7. **Adds GitHub links**: Links each component name to its source in the GitHub repository
|
|
203
|
+
|
|
204
|
+
## Component Categories
|
|
205
|
+
|
|
206
|
+
Components are automatically categorized based on naming conventions and source location:
|
|
207
|
+
|
|
208
|
+
- **Hook**: Components starting with `use` (e.g., `useScrollToTop`, `useDatastore`)
|
|
209
|
+
- **Context**: Components ending with `Context` (e.g., `ACAContext`)
|
|
210
|
+
- **Template**: Components from the `templates/` directory
|
|
211
|
+
- **Service**: Components from the `services/` directory
|
|
212
|
+
- **Utility**: Components from the `utilities/` directory
|
|
213
|
+
- **Component**: All other React components
|
|
214
|
+
|
|
215
|
+
## Sample vs Real Reports
|
|
216
|
+
|
|
217
|
+
**In the library repository**: Generates `SAMPLE_COMPONENT_USAGE_REPORT.md` by analyzing how components are used in Storybook files. This serves as an example of the report format.
|
|
218
|
+
|
|
219
|
+
**In dependent projects**: Generates `COMPONENT_USAGE_REPORT.md` by analyzing actual component usage throughout the project's codebase.
|
|
220
|
+
|
|
221
|
+
## Requirements
|
|
222
|
+
|
|
223
|
+
- Node.js v14 or higher
|
|
224
|
+
- No additional dependencies required (uses Node.js built-in `fs` and `path` modules)
|
|
225
|
+
|
|
226
|
+
## Troubleshooting
|
|
227
|
+
|
|
228
|
+
**Error: "require is not defined"**
|
|
229
|
+
- The script uses `.cjs` extension to work with the ES module package type
|
|
230
|
+
- Make sure the file is named `generate-usage-report.cjs` (not `.js`)
|
|
231
|
+
|
|
232
|
+
**No components found in scan**
|
|
233
|
+
- Verify that `@civicactions/cmsds-open-data-components` is installed as a dependency
|
|
234
|
+
- Check that your import statements use the full package name
|
|
235
|
+
- Ensure source files are in scanned directories (`src/`, `app/`, `pages/`, `components/`, `templates/`)
|
|
236
|
+
|
|
237
|
+
**Missing some component usages**
|
|
238
|
+
- The script only detects standard ES6 import syntax
|
|
239
|
+
- Dynamic imports (e.g., `import()`) are not currently detected
|
|
240
|
+
- Ensure imports use the package name: `from '@civicactions/cmsds-open-data-components'`
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## File Structure
|
|
245
|
+
|
|
246
|
+
## Requirements
|
|
247
|
+
|
|
248
|
+
- Node.js v14 or higher
|
|
249
|
+
- No additional dependencies required (uses Node.js built-in `fs` and `path` modules)
|
|
250
|
+
|
|
251
|
+
## Troubleshooting
|
|
252
|
+
|
|
253
|
+
**Error: "require is not defined"**
|
|
254
|
+
- The script uses `.cjs` extension to work with the ES module package type
|
|
255
|
+
- Make sure the file is named `generate-inventory.cjs` (not `.js`)
|
|
256
|
+
|
|
257
|
+
**Missing components in output**
|
|
258
|
+
- Ensure new components follow the standard directory structure
|
|
259
|
+
- Component directories should be in `src/components/`
|
|
260
|
+
- Each component should have an `index.tsx` or similar entry file
|
|
261
|
+
|
|
262
|
+
**Incorrect public export status**
|
|
263
|
+
- Check if the component is exported in `src/index.ts`
|
|
264
|
+
- For special export names, add them to the scanning logic in the script
|
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component Usage Report Generator
|
|
5
|
+
*
|
|
6
|
+
* Scans a project that uses cmsds-open-data-components as a dependency and generates
|
|
7
|
+
* a comprehensive markdown usage report including:
|
|
8
|
+
* - Which components, templates, services, hooks, and utilities are being used
|
|
9
|
+
* - Where they are imported/used in the codebase
|
|
10
|
+
* - Usage frequency and patterns
|
|
11
|
+
* - Unused available components
|
|
12
|
+
*
|
|
13
|
+
* This script should be run from the root of a project that depends on
|
|
14
|
+
* @civicactions/cmsds-open-data-components
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
// Configuration
|
|
21
|
+
const DEFAULT_SCAN_DIRS = ['src', 'app', 'pages', 'components', 'templates'];
|
|
22
|
+
const FILE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx'];
|
|
23
|
+
const PACKAGE_NAME = '@civicactions/cmsds-open-data-components';
|
|
24
|
+
|
|
25
|
+
// Command line arguments
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const outputFile = args[0] || 'COMPONENT_USAGE_REPORT.md';
|
|
28
|
+
const projectRoot = process.cwd();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the list of public exports from the installed package
|
|
32
|
+
*/
|
|
33
|
+
function getAvailableComponents() {
|
|
34
|
+
try {
|
|
35
|
+
const packagePath = path.join(projectRoot, 'node_modules', PACKAGE_NAME);
|
|
36
|
+
const indexPath = path.join(packagePath, 'dist', 'index.d.ts');
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(indexPath)) {
|
|
39
|
+
// Fallback to package.json exports
|
|
40
|
+
const pkgJsonPath = path.join(packagePath, 'package.json');
|
|
41
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
42
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
43
|
+
return {
|
|
44
|
+
version: pkgJson.version,
|
|
45
|
+
exports: new Set()
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
throw new Error('Could not find package index');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const content = fs.readFileSync(indexPath, 'utf8');
|
|
52
|
+
const exports = new Set();
|
|
53
|
+
|
|
54
|
+
// Match export declarations
|
|
55
|
+
const exportMatches = content.matchAll(/export\s+(?:declare\s+)?(?:const|function|class|interface|type)\s+(\w+)/g);
|
|
56
|
+
for (const match of exportMatches) {
|
|
57
|
+
exports.add(match[1]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Match default exports
|
|
61
|
+
const defaultMatches = content.matchAll(/export\s+\{\s*default\s+as\s+(\w+)\s*\}/g);
|
|
62
|
+
for (const match of defaultMatches) {
|
|
63
|
+
exports.add(match[1]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Get version
|
|
67
|
+
const pkgJsonPath = path.join(packagePath, 'package.json');
|
|
68
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
version: pkgJson.version,
|
|
72
|
+
exports
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.warn('ā ļø Could not read package exports, will detect from usage');
|
|
76
|
+
return {
|
|
77
|
+
version: 'unknown',
|
|
78
|
+
exports: new Set()
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Recursively find all files with specified extensions in a directory
|
|
85
|
+
*/
|
|
86
|
+
function findFiles(dir, extensions, results = []) {
|
|
87
|
+
if (!fs.existsSync(dir)) {
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
93
|
+
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
const fullPath = path.join(dir, entry.name);
|
|
96
|
+
|
|
97
|
+
// Skip node_modules, .git, build directories
|
|
98
|
+
if (entry.name === 'node_modules' ||
|
|
99
|
+
entry.name === '.git' ||
|
|
100
|
+
entry.name === 'build' ||
|
|
101
|
+
entry.name === 'dist' ||
|
|
102
|
+
entry.name === '.next' ||
|
|
103
|
+
entry.name === 'coverage') {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (entry.isDirectory()) {
|
|
108
|
+
findFiles(fullPath, extensions, results);
|
|
109
|
+
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
110
|
+
results.push(fullPath);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// Skip directories we can't read
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return results;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse a file for imports from cmsds-open-data-components
|
|
122
|
+
*/
|
|
123
|
+
function parseFileForImports(filePath) {
|
|
124
|
+
try {
|
|
125
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
126
|
+
const imports = [];
|
|
127
|
+
|
|
128
|
+
const getLineNumber = (index) => content.substring(0, index).split('\n').length;
|
|
129
|
+
|
|
130
|
+
// Match named imports: import { Component1, Component2 } from 'package'
|
|
131
|
+
const namedImportRegex = new RegExp(
|
|
132
|
+
`import\\s+\\{([^}]+)\\}\\s+from\\s+['"]${PACKAGE_NAME}['"]`,
|
|
133
|
+
'g'
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
for (const match of content.matchAll(namedImportRegex)) {
|
|
137
|
+
const names = match[1]
|
|
138
|
+
.split(',')
|
|
139
|
+
.map(n => n.trim())
|
|
140
|
+
.filter(n => n.length > 0);
|
|
141
|
+
|
|
142
|
+
names.forEach(name => {
|
|
143
|
+
imports.push({
|
|
144
|
+
name,
|
|
145
|
+
type: 'named',
|
|
146
|
+
line: getLineNumber(match.index)
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Match default imports: import Component from 'package'
|
|
152
|
+
const defaultImportRegex = new RegExp(
|
|
153
|
+
`import\\s+(\\w+)\\s+from\\s+['"]${PACKAGE_NAME}['"]`,
|
|
154
|
+
'g'
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
for (const match of content.matchAll(defaultImportRegex)) {
|
|
158
|
+
imports.push({
|
|
159
|
+
name: match[1],
|
|
160
|
+
type: 'default',
|
|
161
|
+
line: getLineNumber(match.index)
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return imports;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Scan the project for component usage
|
|
173
|
+
*/
|
|
174
|
+
function scanProjectForUsage() {
|
|
175
|
+
console.log('š Scanning project for component usage...');
|
|
176
|
+
|
|
177
|
+
const usageMap = new Map();
|
|
178
|
+
const scanDirs = DEFAULT_SCAN_DIRS
|
|
179
|
+
.map(dir => path.join(projectRoot, dir))
|
|
180
|
+
.filter(dir => fs.existsSync(dir));
|
|
181
|
+
|
|
182
|
+
if (scanDirs.length === 0) {
|
|
183
|
+
console.warn('ā ļø No standard source directories found. Scanning entire project...');
|
|
184
|
+
scanDirs.push(projectRoot);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let filesScanned = 0;
|
|
188
|
+
|
|
189
|
+
for (const scanDir of scanDirs) {
|
|
190
|
+
const files = findFiles(scanDir, FILE_EXTENSIONS);
|
|
191
|
+
filesScanned += files.length;
|
|
192
|
+
|
|
193
|
+
for (const file of files) {
|
|
194
|
+
const imports = parseFileForImports(file);
|
|
195
|
+
|
|
196
|
+
if (imports.length > 0) {
|
|
197
|
+
const relativePath = path.relative(projectRoot, file);
|
|
198
|
+
|
|
199
|
+
for (const imp of imports) {
|
|
200
|
+
if (!usageMap.has(imp.name)) {
|
|
201
|
+
usageMap.set(imp.name, []);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
usageMap.get(imp.name).push({
|
|
205
|
+
file: relativePath,
|
|
206
|
+
line: imp.line,
|
|
207
|
+
type: imp.type
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
console.log(` Scanned ${filesScanned} files`);
|
|
215
|
+
console.log(` Found ${usageMap.size} unique components in use`);
|
|
216
|
+
|
|
217
|
+
return usageMap;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Categorize components by type based on naming conventions
|
|
222
|
+
*/
|
|
223
|
+
function categorizeComponent(name) {
|
|
224
|
+
if (name.startsWith('use')) return 'Hook';
|
|
225
|
+
if (name.endsWith('Context') || name.endsWith('Provider')) return 'Context';
|
|
226
|
+
if (name.endsWith('Page') || name.endsWith('Template')) return 'Template';
|
|
227
|
+
if (name.includes('Service') || name.startsWith('fetch') || name.startsWith('get')) return 'Service';
|
|
228
|
+
if (name.endsWith('Util') || name.endsWith('Helper')) return 'Utility';
|
|
229
|
+
return 'Component';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Generate markdown table row with GitHub link
|
|
234
|
+
*/
|
|
235
|
+
function generateUsageRow(name, usages, category) {
|
|
236
|
+
const count = usages.length;
|
|
237
|
+
const files = [...new Set(usages.map(u => u.file))];
|
|
238
|
+
const fileList = files.map(f => `\`${f}\``).join('<br>');
|
|
239
|
+
|
|
240
|
+
const githubPath = getGitHubPath(name, category);
|
|
241
|
+
const githubUrl = `https://github.com/GetDKAN/cmsds-open-data-components/tree/main/${githubPath}`;
|
|
242
|
+
const nameLink = `[${name}](${githubUrl})`;
|
|
243
|
+
|
|
244
|
+
return `| ${nameLink} | ${category} | ${count} | ${fileList} |`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Determine GitHub path based on component category and name
|
|
249
|
+
*/
|
|
250
|
+
function getGitHubPath(name, category) {
|
|
251
|
+
// Standard categories map directly to directories
|
|
252
|
+
const categoryPaths = {
|
|
253
|
+
'Component': `src/components/${name}`,
|
|
254
|
+
'Hook': `src/components/${name}`,
|
|
255
|
+
'Template': `src/templates/${name}`,
|
|
256
|
+
'Service': `src/services/${name}`,
|
|
257
|
+
'Utility': `src/utilities/${name}`
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
if (categoryPaths[category]) {
|
|
261
|
+
return categoryPaths[category];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Context locations vary, check by name
|
|
265
|
+
if (category === 'Context') {
|
|
266
|
+
if (name.includes('DataTable')) {
|
|
267
|
+
return `src/components/DatasetTableTab/${name}.tsx`;
|
|
268
|
+
}
|
|
269
|
+
if (name === 'HeaderContext') {
|
|
270
|
+
return `src/templates/Header/${name}.tsx`;
|
|
271
|
+
}
|
|
272
|
+
if (name === 'ACAContext') {
|
|
273
|
+
return `src/utilities/${name}.ts`;
|
|
274
|
+
}
|
|
275
|
+
return `src/templates/Dataset/${name}.tsx`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return `src/components/${name}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Generate the usage report
|
|
283
|
+
*/
|
|
284
|
+
function generateReport(usageMap, availableComponents) {
|
|
285
|
+
const lines = [];
|
|
286
|
+
const projectInfo = getProjectInfo(availableComponents);
|
|
287
|
+
|
|
288
|
+
// Header
|
|
289
|
+
addReportHeader(lines, projectInfo);
|
|
290
|
+
|
|
291
|
+
// Summary statistics
|
|
292
|
+
addSummaryStatistics(lines, usageMap);
|
|
293
|
+
|
|
294
|
+
// Categorize and add category breakdown
|
|
295
|
+
const categorized = categorizeUsages(usageMap);
|
|
296
|
+
addCategoryBreakdown(lines, categorized);
|
|
297
|
+
|
|
298
|
+
// Detailed usage table
|
|
299
|
+
addUsageTable(lines, usageMap);
|
|
300
|
+
|
|
301
|
+
// Footer
|
|
302
|
+
addReportFooter(lines);
|
|
303
|
+
|
|
304
|
+
return lines.join('\n');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get project information
|
|
309
|
+
*/
|
|
310
|
+
function getProjectInfo(availableComponents) {
|
|
311
|
+
const projectPkgPath = path.join(projectRoot, 'package.json');
|
|
312
|
+
|
|
313
|
+
if (!fs.existsSync(projectPkgPath)) {
|
|
314
|
+
return {
|
|
315
|
+
name: 'Unknown Project',
|
|
316
|
+
version: availableComponents.version
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const projectPkg = JSON.parse(fs.readFileSync(projectPkgPath, 'utf8'));
|
|
321
|
+
const deps = { ...projectPkg.dependencies, ...projectPkg.devDependencies };
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
name: projectPkg.name || path.basename(projectRoot),
|
|
325
|
+
version: deps[PACKAGE_NAME] || availableComponents.version
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Add report header section
|
|
331
|
+
*/
|
|
332
|
+
function addReportHeader(lines, projectInfo) {
|
|
333
|
+
lines.push(`# ${projectInfo.name} Component Usage Report`);
|
|
334
|
+
lines.push('');
|
|
335
|
+
lines.push(`Analysis of \`${PACKAGE_NAME}\` usage in this project.`);
|
|
336
|
+
lines.push('');
|
|
337
|
+
lines.push(`**Library Version**: \`${PACKAGE_NAME}: ${projectInfo.version}\` `);
|
|
338
|
+
lines.push('');
|
|
339
|
+
lines.push('---');
|
|
340
|
+
lines.push('');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Add summary statistics section
|
|
345
|
+
*/
|
|
346
|
+
function addSummaryStatistics(lines, usageMap) {
|
|
347
|
+
const totalUsages = Array.from(usageMap.values()).reduce((sum, usages) => sum + usages.length, 0);
|
|
348
|
+
const uniqueComponents = usageMap.size;
|
|
349
|
+
const filesUsingComponents = new Set();
|
|
350
|
+
|
|
351
|
+
usageMap.forEach(usages => {
|
|
352
|
+
usages.forEach(usage => filesUsingComponents.add(usage.file));
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
lines.push('## Summary Statistics');
|
|
356
|
+
lines.push('');
|
|
357
|
+
lines.push(`- **Unique Components Used**: ${uniqueComponents}`);
|
|
358
|
+
lines.push(`- **Total Import Statements**: ${totalUsages}`);
|
|
359
|
+
lines.push(`- **Files Using Components**: ${filesUsingComponents.size}`);
|
|
360
|
+
lines.push('');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Categorize components by type
|
|
365
|
+
*/
|
|
366
|
+
function categorizeUsages(usageMap) {
|
|
367
|
+
const categorized = new Map();
|
|
368
|
+
|
|
369
|
+
usageMap.forEach((usages, name) => {
|
|
370
|
+
const category = categorizeComponent(name);
|
|
371
|
+
if (!categorized.has(category)) {
|
|
372
|
+
categorized.set(category, []);
|
|
373
|
+
}
|
|
374
|
+
categorized.get(category).push({ name, usages });
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
return categorized;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Add category breakdown section
|
|
382
|
+
*/
|
|
383
|
+
function addCategoryBreakdown(lines, categorized) {
|
|
384
|
+
lines.push('### By Category');
|
|
385
|
+
lines.push('');
|
|
386
|
+
lines.push('| Category | Count |');
|
|
387
|
+
lines.push('|----------|-------|');
|
|
388
|
+
|
|
389
|
+
const categories = ['Component', 'Template', 'Hook', 'Context', 'Service', 'Utility'];
|
|
390
|
+
categories.forEach(cat => {
|
|
391
|
+
const count = categorized.get(cat)?.length || 0;
|
|
392
|
+
if (count > 0) {
|
|
393
|
+
lines.push(`| ${cat}s | ${count} |`);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
lines.push('');
|
|
398
|
+
lines.push('---');
|
|
399
|
+
lines.push('');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Add detailed usage table section
|
|
404
|
+
*/
|
|
405
|
+
function addUsageTable(lines, usageMap) {
|
|
406
|
+
lines.push('## Component Usage Details');
|
|
407
|
+
lines.push('');
|
|
408
|
+
lines.push('| Component | Type | Usage Count | Used In |');
|
|
409
|
+
lines.push('|-----------|------|-------------|---------|');
|
|
410
|
+
|
|
411
|
+
// Sort by usage count (descending)
|
|
412
|
+
const sortedComponents = Array.from(usageMap.entries())
|
|
413
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
414
|
+
|
|
415
|
+
sortedComponents.forEach(([name, usages]) => {
|
|
416
|
+
const category = categorizeComponent(name);
|
|
417
|
+
lines.push(generateUsageRow(name, usages, category));
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
lines.push('');
|
|
421
|
+
lines.push('---');
|
|
422
|
+
lines.push('');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Add report footer
|
|
427
|
+
*/
|
|
428
|
+
function addReportFooter(lines) {
|
|
429
|
+
const now = new Date();
|
|
430
|
+
const dateStr = now.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
431
|
+
|
|
432
|
+
lines.push(`*Last updated: ${dateStr}* `);
|
|
433
|
+
lines.push(`*Generated by ${PACKAGE_NAME} usage report tool*`);
|
|
434
|
+
lines.push('');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Check if running in the library repo itself
|
|
439
|
+
*/
|
|
440
|
+
function isLibraryRepo() {
|
|
441
|
+
const pkgJsonPath = path.join(projectRoot, 'package.json');
|
|
442
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
443
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
444
|
+
return pkgJson.name === PACKAGE_NAME;
|
|
445
|
+
}
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Generate sample report for the library repo using Storybook files
|
|
451
|
+
*/
|
|
452
|
+
function generateSampleReport() {
|
|
453
|
+
console.log('š Detected library repository - generating sample usage report...\n');
|
|
454
|
+
|
|
455
|
+
const usageMap = scanStoryFiles();
|
|
456
|
+
|
|
457
|
+
console.log(` Found ${usageMap.size} unique components used in stories`);
|
|
458
|
+
|
|
459
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
|
|
460
|
+
const availableComponents = {
|
|
461
|
+
version: pkgJson.version,
|
|
462
|
+
exports: new Set()
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const report = generateReport(usageMap, availableComponents);
|
|
466
|
+
const sampleNote = createSampleNote();
|
|
467
|
+
|
|
468
|
+
return sampleNote + report;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Scan Storybook files for component usage
|
|
473
|
+
*/
|
|
474
|
+
function scanStoryFiles() {
|
|
475
|
+
const usageMap = new Map();
|
|
476
|
+
const srcPath = path.join(projectRoot, 'src');
|
|
477
|
+
const storyFiles = findFiles(srcPath, ['.stories.js', '.stories.jsx', '.stories.ts', '.stories.tsx']);
|
|
478
|
+
|
|
479
|
+
console.log(` Found ${storyFiles.length} Storybook files to analyze`);
|
|
480
|
+
|
|
481
|
+
storyFiles.forEach(file => {
|
|
482
|
+
const relativePath = path.relative(projectRoot, file);
|
|
483
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
484
|
+
|
|
485
|
+
extractLocalImports(content, relativePath, usageMap);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
return usageMap;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Extract local imports from file content
|
|
493
|
+
*/
|
|
494
|
+
function extractLocalImports(content, relativePath, usageMap) {
|
|
495
|
+
const getLineNumber = (index) => content.substring(0, index).split('\n').length;
|
|
496
|
+
|
|
497
|
+
// Match local imports like: import { Component } from './Component'
|
|
498
|
+
const localImportRegex = /import\s+\{([^}]+)\}\s+from\s+['"]\.\//g;
|
|
499
|
+
const defaultImportRegex = /import\s+(\w+)\s+from\s+['"]\.\//g;
|
|
500
|
+
|
|
501
|
+
for (const match of content.matchAll(localImportRegex)) {
|
|
502
|
+
const names = match[1].split(',').map(n => n.trim()).filter(n => n.length > 0);
|
|
503
|
+
names.forEach(name => {
|
|
504
|
+
if (!usageMap.has(name)) {
|
|
505
|
+
usageMap.set(name, []);
|
|
506
|
+
}
|
|
507
|
+
usageMap.get(name).push({
|
|
508
|
+
file: relativePath,
|
|
509
|
+
line: getLineNumber(match.index),
|
|
510
|
+
type: 'named'
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
for (const match of content.matchAll(defaultImportRegex)) {
|
|
516
|
+
const name = match[1];
|
|
517
|
+
if (!usageMap.has(name)) {
|
|
518
|
+
usageMap.set(name, []);
|
|
519
|
+
}
|
|
520
|
+
usageMap.get(name).push({
|
|
521
|
+
file: relativePath,
|
|
522
|
+
line: getLineNumber(match.index),
|
|
523
|
+
type: 'default'
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Create sample note for library repo reports
|
|
530
|
+
*/
|
|
531
|
+
function createSampleNote() {
|
|
532
|
+
return [
|
|
533
|
+
'> **Note**: This is a sample usage report generated from the cmsds-open-data-components library repository.',
|
|
534
|
+
'> It shows how components are used in Storybook files within this repository.',
|
|
535
|
+
'> When run in a project that depends on this library, the report will show actual component usage.',
|
|
536
|
+
'',
|
|
537
|
+
'---',
|
|
538
|
+
''
|
|
539
|
+
].join('\n');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Main execution
|
|
544
|
+
*/
|
|
545
|
+
function main() {
|
|
546
|
+
console.log('š Generating Component Usage Report...\n');
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
// Check if running in the library repo itself
|
|
550
|
+
if (isLibraryRepo()) {
|
|
551
|
+
const report = generateSampleReport();
|
|
552
|
+
const outputPath = path.join(projectRoot, 'SAMPLE_' + outputFile);
|
|
553
|
+
fs.writeFileSync(outputPath, report, 'utf8');
|
|
554
|
+
|
|
555
|
+
console.log(`\nā
Sample usage report generated successfully!`);
|
|
556
|
+
console.log(`š Output: ${outputPath}`);
|
|
557
|
+
console.log(`š File size: ${(Buffer.byteLength(report, 'utf8') / 1024).toFixed(2)} KB`);
|
|
558
|
+
console.log(`\nš” This sample report shows component usage in Storybook files.`);
|
|
559
|
+
console.log(` To see real usage, run this command in a project that depends on this library.`);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Check if package is installed
|
|
564
|
+
const packagePath = path.join(projectRoot, 'node_modules', PACKAGE_NAME);
|
|
565
|
+
if (!fs.existsSync(packagePath)) {
|
|
566
|
+
console.error(`ā Error: ${PACKAGE_NAME} is not installed in this project`);
|
|
567
|
+
console.error(' Make sure to run this from a project that depends on cmsds-open-data-components');
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Get available components
|
|
572
|
+
const availableComponents = getAvailableComponents();
|
|
573
|
+
console.log(` Library version: ${availableComponents.version}`);
|
|
574
|
+
|
|
575
|
+
// Scan for usage
|
|
576
|
+
const usageMap = scanProjectForUsage();
|
|
577
|
+
|
|
578
|
+
if (usageMap.size === 0) {
|
|
579
|
+
console.warn('ā ļø No component usage found in this project');
|
|
580
|
+
console.warn(' Make sure you are running this from the correct directory');
|
|
581
|
+
process.exit(0);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Generate report
|
|
585
|
+
const report = generateReport(usageMap, availableComponents);
|
|
586
|
+
|
|
587
|
+
// Write to file
|
|
588
|
+
const outputPath = path.join(projectRoot, outputFile);
|
|
589
|
+
fs.writeFileSync(outputPath, report, 'utf8');
|
|
590
|
+
|
|
591
|
+
console.log(`\nā
Usage report generated successfully!`);
|
|
592
|
+
console.log(`š Output: ${outputPath}`);
|
|
593
|
+
console.log(`š File size: ${(Buffer.byteLength(report, 'utf8') / 1024).toFixed(2)} KB`);
|
|
594
|
+
console.log(`š¦ Components tracked: ${usageMap.size}`);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
console.error('ā Error generating report:', error.message);
|
|
597
|
+
console.error(error.stack);
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Run if called directly
|
|
603
|
+
if (require.main === module) {
|
|
604
|
+
main();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
module.exports = { main, scanProjectForUsage, generateReport };
|