@civicactions/cmsds-open-data-components 4.0.7 → 4.0.8-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.0",
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,119 @@
1
+ # Component Inventory Generator
2
+
3
+ This script automatically generates a comprehensive markdown inventory report for the cmsds-open-data-components library.
4
+
5
+ ## Usage
6
+
7
+ Run the script using npm:
8
+
9
+ ```bash
10
+ npm run generate:inventory
11
+ ```
12
+
13
+ Or run it directly:
14
+
15
+ ```bash
16
+ node scripts/generate-inventory.cjs
17
+ ```
18
+
19
+ ## Output
20
+
21
+ The script generates `COMPONENTS_INVENTORY.md` in the root directory containing:
22
+
23
+ - **Inventory Table**: Complete list of all components, services, templates, hooks, contexts, utilities, types, and assets
24
+ - **Public Export Status**: Indicates which items are publicly exported vs internal-only
25
+ - **Storybook Status**: Shows which items have Storybook stories for visual documentation
26
+ - **Unit Test Status**: Shows which items have unit test coverage
27
+ - **Quality Metrics**: Summary statistics for documentation and testing coverage
28
+ - **Export Summary**: Count of public vs internal items by category
29
+
30
+ ## What It Scans
31
+
32
+ The script automatically scans the following directories:
33
+
34
+ - `src/components/` - React components
35
+ - `src/templates/` - Page-level templates
36
+ - `src/services/` - API service hooks
37
+ - `src/utilities/` - Utility functions
38
+ - `src/types/` - TypeScript type definitions
39
+ - `src/assets/` - Static assets and data files
40
+
41
+ It also automatically detects:
42
+ - **Hooks**: Directories or files starting with `use` (e.g., `useScrollToTop`)
43
+ - **Contexts**: Files ending with `Context` that contain `createContext()` calls
44
+
45
+ ## How It Works
46
+
47
+ 1. **Reads `src/index.ts`** to determine which items are publicly exported
48
+ 2. **Scans each directory** for subdirectories and files
49
+ 3. **Dynamically discovers hooks and contexts** by naming patterns and file content
50
+ 4. **Checks for `.stories.*` files** to determine Storybook coverage
51
+ 5. **Checks for `.test.*` and `.spec.*` files** to determine unit test coverage
52
+ 6. **Calculates statistics** for overall project quality metrics
53
+ 7. **Generates markdown** with formatted tables and summaries
54
+
55
+ ## Maintenance
56
+
57
+ The script is designed to automatically adapt to changes in the codebase:
58
+
59
+ - New components/templates/services/hooks/contexts are automatically discovered
60
+ - Public export status updates when `src/index.ts` changes
61
+ - Story and test status updates as files are added/removed
62
+ - Statistics recalculate automatically
63
+
64
+ No manual updates needed when adding or removing hooks and contexts!
65
+
66
+ ### Hook and Context Detection
67
+
68
+ **Hooks** are detected by:
69
+ - Directory names starting with `use` (e.g., `src/components/useScrollToTop/`)
70
+ - File names starting with `use` (e.g., `useAddLoginLink.ts`)
71
+
72
+ **Contexts** are detected by:
73
+ - File names ending with `Context` (e.g., `HeaderContext.tsx`)
74
+ - File must contain a `createContext()` call
75
+ - Export name is extracted from `export default` or `export const` statements
76
+
77
+ The scanner automatically excludes test and story files to prevent false positives.
78
+
79
+ ### Known Special Cases
80
+
81
+ The script handles several special cases:
82
+
83
+ - **DatasetAdditionalInformation**: Exports `buildRows` function
84
+ - **DatasetTableTab**: Exported as `DatasetTable`
85
+ - **Datatable**: Exported as `DataTable` (case difference)
86
+ - **frequencyMap**: Commented out in exports
87
+ - **aca.ts**: Exports `acaToParams` function
88
+
89
+ To add new special cases, update the scanning functions in `generate-inventory.cjs`.
90
+
91
+ ## File Structure
92
+
93
+ ```
94
+ scripts/
95
+ generate-inventory.cjs # Main script file
96
+ README.md # This file
97
+
98
+ COMPONENTS_INVENTORY.md # Generated output file (root directory)
99
+ ```
100
+
101
+ ## Requirements
102
+
103
+ - Node.js v14 or higher
104
+ - No additional dependencies required (uses Node.js built-in `fs` and `path` modules)
105
+
106
+ ## Troubleshooting
107
+
108
+ **Error: "require is not defined"**
109
+ - The script uses `.cjs` extension to work with the ES module package type
110
+ - Make sure the file is named `generate-inventory.cjs` (not `.js`)
111
+
112
+ **Missing components in output**
113
+ - Ensure new components follow the standard directory structure
114
+ - Component directories should be in `src/components/`
115
+ - Each component should have an `index.tsx` or similar entry file
116
+
117
+ **Incorrect public export status**
118
+ - Check if the component is exported in `src/index.ts`
119
+ - For special export names, add them to the scanning logic in the script
@@ -0,0 +1,445 @@
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
+ // Match ES6 imports: import { Component1, Component2 } from '@civicactions/cmsds-open-data-components'
129
+ const namedImportRegex = new RegExp(
130
+ `import\\s+\\{([^}]+)\\}\\s+from\\s+['"]${PACKAGE_NAME}['"]`,
131
+ 'g'
132
+ );
133
+
134
+ let match;
135
+ while ((match = namedImportRegex.exec(content)) !== null) {
136
+ const names = match[1]
137
+ .split(',')
138
+ .map(n => n.trim())
139
+ .filter(n => n.length > 0);
140
+
141
+ names.forEach(name => {
142
+ imports.push({
143
+ name,
144
+ type: 'named',
145
+ line: content.substring(0, match.index).split('\n').length
146
+ });
147
+ });
148
+ }
149
+
150
+ // Match default imports: import Component from '@civicactions/cmsds-open-data-components'
151
+ const defaultImportRegex = new RegExp(
152
+ `import\\s+(\\w+)\\s+from\\s+['"]${PACKAGE_NAME}['"]`,
153
+ 'g'
154
+ );
155
+
156
+ while ((match = defaultImportRegex.exec(content)) !== null) {
157
+ imports.push({
158
+ name: match[1],
159
+ type: 'default',
160
+ line: content.substring(0, match.index).split('\n').length
161
+ });
162
+ }
163
+
164
+ return imports;
165
+ } catch (error) {
166
+ return [];
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Scan the project for component usage
172
+ */
173
+ function scanProjectForUsage() {
174
+ console.log('šŸ” Scanning project for component usage...');
175
+
176
+ const usageMap = new Map();
177
+ const scanDirs = DEFAULT_SCAN_DIRS
178
+ .map(dir => path.join(projectRoot, dir))
179
+ .filter(dir => fs.existsSync(dir));
180
+
181
+ if (scanDirs.length === 0) {
182
+ console.warn('āš ļø No standard source directories found. Scanning entire project...');
183
+ scanDirs.push(projectRoot);
184
+ }
185
+
186
+ let filesScanned = 0;
187
+
188
+ for (const scanDir of scanDirs) {
189
+ const files = findFiles(scanDir, FILE_EXTENSIONS);
190
+ filesScanned += files.length;
191
+
192
+ for (const file of files) {
193
+ const imports = parseFileForImports(file);
194
+
195
+ if (imports.length > 0) {
196
+ const relativePath = path.relative(projectRoot, file);
197
+
198
+ for (const imp of imports) {
199
+ if (!usageMap.has(imp.name)) {
200
+ usageMap.set(imp.name, []);
201
+ }
202
+
203
+ usageMap.get(imp.name).push({
204
+ file: relativePath,
205
+ line: imp.line,
206
+ type: imp.type
207
+ });
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ console.log(` Scanned ${filesScanned} files`);
214
+ console.log(` Found ${usageMap.size} unique components in use`);
215
+
216
+ return usageMap;
217
+ }
218
+
219
+ /**
220
+ * Categorize components by type based on naming conventions
221
+ */
222
+ function categorizeComponent(name) {
223
+ if (name.startsWith('use')) return 'Hook';
224
+ if (name.endsWith('Context') || name.endsWith('Provider')) return 'Context';
225
+ if (name.endsWith('Page') || name.endsWith('Template')) return 'Template';
226
+ if (name.includes('Service') || name.startsWith('fetch') || name.startsWith('get')) return 'Service';
227
+ if (name.endsWith('Util') || name.endsWith('Helper')) return 'Utility';
228
+ return 'Component';
229
+ }
230
+
231
+ /**
232
+ * Generate markdown table row
233
+ */
234
+ function generateUsageRow(name, usages, category) {
235
+ const count = usages.length;
236
+ const files = [...new Set(usages.map(u => u.file))];
237
+ const fileList = files.length <= 3
238
+ ? files.map(f => `\`${f}\``).join(', ')
239
+ : `${files.slice(0, 3).map(f => `\`${f}\``).join(', ')} and ${files.length - 3} more`;
240
+
241
+ return `| ${name} | ${category} | ${count} | ${fileList} |`;
242
+ }
243
+
244
+ /**
245
+ * Generate the usage report
246
+ */
247
+ function generateReport(usageMap, availableComponents) {
248
+ const lines = [];
249
+
250
+ // Get project info
251
+ const projectPkgPath = path.join(projectRoot, 'package.json');
252
+ let projectName = 'Unknown Project';
253
+ let projectVersion = '';
254
+
255
+ if (fs.existsSync(projectPkgPath)) {
256
+ const projectPkg = JSON.parse(fs.readFileSync(projectPkgPath, 'utf8'));
257
+ projectName = projectPkg.name || path.basename(projectRoot);
258
+
259
+ // Get the installed version
260
+ const deps = { ...projectPkg.dependencies, ...projectPkg.devDependencies };
261
+ projectVersion = deps[PACKAGE_NAME] || availableComponents.version;
262
+ }
263
+
264
+ // Header
265
+ lines.push(`# ${projectName} Component Usage Report`);
266
+ lines.push('');
267
+ lines.push(`Analysis of \`${PACKAGE_NAME}\` usage in this project.`);
268
+ lines.push('');
269
+ lines.push(`**Library Version**: \`${PACKAGE_NAME}: ${projectVersion}\` `);
270
+ lines.push(`**Generated**: ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}`);
271
+ lines.push('');
272
+ lines.push('---');
273
+ lines.push('');
274
+
275
+ // Summary statistics
276
+ const totalUsages = Array.from(usageMap.values()).reduce((sum, usages) => sum + usages.length, 0);
277
+ const uniqueComponents = usageMap.size;
278
+ const filesUsingComponents = new Set();
279
+
280
+ usageMap.forEach(usages => {
281
+ usages.forEach(usage => filesUsingComponents.add(usage.file));
282
+ });
283
+
284
+ lines.push('## Summary Statistics');
285
+ lines.push('');
286
+ lines.push(`- **Unique Components Used**: ${uniqueComponents}`);
287
+ lines.push(`- **Total Import Statements**: ${totalUsages}`);
288
+ lines.push(`- **Files Using Components**: ${filesUsingComponents.size}`);
289
+ lines.push('');
290
+
291
+ // Categorize components
292
+ const categorized = new Map();
293
+ usageMap.forEach((usages, name) => {
294
+ const category = categorizeComponent(name);
295
+ if (!categorized.has(category)) {
296
+ categorized.set(category, []);
297
+ }
298
+ categorized.get(category).push({ name, usages });
299
+ });
300
+
301
+ // Category breakdown
302
+ lines.push('### By Category');
303
+ lines.push('');
304
+ lines.push('| Category | Count |');
305
+ lines.push('|----------|-------|');
306
+
307
+ const categories = ['Component', 'Template', 'Hook', 'Context', 'Service', 'Utility'];
308
+ categories.forEach(cat => {
309
+ const count = categorized.get(cat)?.length || 0;
310
+ if (count > 0) {
311
+ lines.push(`| ${cat}s | ${count} |`);
312
+ }
313
+ });
314
+
315
+ lines.push('');
316
+ lines.push('---');
317
+ lines.push('');
318
+
319
+ // Detailed usage table
320
+ lines.push('## Component Usage Details');
321
+ lines.push('');
322
+ lines.push('| Component | Type | Usage Count | Used In |');
323
+ lines.push('|-----------|------|-------------|---------|');
324
+
325
+ // Sort by usage count (descending)
326
+ const sortedComponents = Array.from(usageMap.entries())
327
+ .sort((a, b) => b[1].length - a[1].length);
328
+
329
+ sortedComponents.forEach(([name, usages]) => {
330
+ const category = categorizeComponent(name);
331
+ lines.push(generateUsageRow(name, usages, category));
332
+ });
333
+
334
+ lines.push('');
335
+ lines.push('---');
336
+ lines.push('');
337
+
338
+ // Most used components
339
+ lines.push('## Most Used Components');
340
+ lines.push('');
341
+ const topComponents = sortedComponents.slice(0, 10);
342
+
343
+ topComponents.forEach(([name, usages], index) => {
344
+ lines.push(`${index + 1}. **${name}** - ${usages.length} usage${usages.length > 1 ? 's' : ''}`);
345
+ });
346
+
347
+ lines.push('');
348
+ lines.push('---');
349
+ lines.push('');
350
+
351
+ // Detailed file listings
352
+ lines.push('## Detailed File Usage');
353
+ lines.push('');
354
+
355
+ categorized.forEach((items, category) => {
356
+ if (items.length > 0) {
357
+ lines.push(`### ${category}s`);
358
+ lines.push('');
359
+
360
+ items.sort((a, b) => a.name.localeCompare(b.name));
361
+
362
+ items.forEach(({ name, usages }) => {
363
+ lines.push(`#### ${name}`);
364
+ lines.push('');
365
+
366
+ // Group by file
367
+ const fileGroups = new Map();
368
+ usages.forEach(usage => {
369
+ if (!fileGroups.has(usage.file)) {
370
+ fileGroups.set(usage.file, []);
371
+ }
372
+ fileGroups.get(usage.file).push(usage);
373
+ });
374
+
375
+ fileGroups.forEach((fileUsages, file) => {
376
+ const lineNumbers = fileUsages.map(u => u.line).join(', ');
377
+ lines.push(`- \`${file}\` (line${fileUsages.length > 1 ? 's' : ''} ${lineNumbers})`);
378
+ });
379
+
380
+ lines.push('');
381
+ });
382
+ }
383
+ });
384
+
385
+ lines.push('---');
386
+ lines.push('');
387
+ lines.push(`*Generated by ${PACKAGE_NAME} usage report tool* `);
388
+ lines.push(`*Report generated: ${new Date().toISOString()}*`);
389
+ lines.push('');
390
+
391
+ return lines.join('\n');
392
+ }
393
+
394
+ /**
395
+ * Main execution
396
+ */
397
+ function main() {
398
+ console.log('šŸ“Š Generating Component Usage Report...\n');
399
+
400
+ try {
401
+ // Check if package is installed
402
+ const packagePath = path.join(projectRoot, 'node_modules', PACKAGE_NAME);
403
+ if (!fs.existsSync(packagePath)) {
404
+ console.error(`āŒ Error: ${PACKAGE_NAME} is not installed in this project`);
405
+ console.error(' Make sure to run this from a project that depends on cmsds-open-data-components');
406
+ process.exit(1);
407
+ }
408
+
409
+ // Get available components
410
+ const availableComponents = getAvailableComponents();
411
+ console.log(` Library version: ${availableComponents.version}`);
412
+
413
+ // Scan for usage
414
+ const usageMap = scanProjectForUsage();
415
+
416
+ if (usageMap.size === 0) {
417
+ console.warn('āš ļø No component usage found in this project');
418
+ console.warn(' Make sure you are running this from the correct directory');
419
+ process.exit(0);
420
+ }
421
+
422
+ // Generate report
423
+ const report = generateReport(usageMap, availableComponents);
424
+
425
+ // Write to file
426
+ const outputPath = path.join(projectRoot, outputFile);
427
+ fs.writeFileSync(outputPath, report, 'utf8');
428
+
429
+ console.log(`\nāœ… Usage report generated successfully!`);
430
+ console.log(`šŸ“„ Output: ${outputPath}`);
431
+ console.log(`šŸ“Š File size: ${(Buffer.byteLength(report, 'utf8') / 1024).toFixed(2)} KB`);
432
+ console.log(`šŸ“¦ Components tracked: ${usageMap.size}`);
433
+ } catch (error) {
434
+ console.error('āŒ Error generating report:', error.message);
435
+ console.error(error.stack);
436
+ process.exit(1);
437
+ }
438
+ }
439
+
440
+ // Run if called directly
441
+ if (require.main === module) {
442
+ main();
443
+ }
444
+
445
+ module.exports = { main, scanProjectForUsage, generateReport };