@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 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 [Inventory Generator Documentation](scripts/README.md).
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.7",
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 };