@jmruthers/pace-core 0.6.7 → 0.6.8

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.
Files changed (100) hide show
  1. package/audit-tool/00-dependencies.cjs +215 -9
  2. package/audit-tool/audits/02-project-structure.cjs +3 -18
  3. package/audit-tool/audits/03-architecture.cjs +34 -6
  4. package/audit-tool/audits/06-security-rbac.cjs +10 -0
  5. package/audit-tool/audits/07-api-tech-stack.cjs +55 -1
  6. package/audit-tool/index.cjs +23 -19
  7. package/audit-tool/utils/report-utils.cjs +141 -2
  8. package/dist/{DataTable-7PMH7XN7.js → DataTable-6RMSCQJ6.js} +5 -5
  9. package/dist/{PublicPageProvider-DlsCaR5v.d.ts → PublicPageProvider-CIGSujI2.d.ts} +14 -8
  10. package/dist/{UnifiedAuthProvider-ZT6TIGM7.js → UnifiedAuthProvider-7SNDOWYD.js} +2 -2
  11. package/dist/{api-Y4MQWOFW.js → api-7P7DI652.js} +1 -1
  12. package/dist/{chunk-L4XMVJKY.js → chunk-4DDCYDQ3.js} +8 -7
  13. package/dist/{chunk-ZKAWKYT4.js → chunk-5W2A3DRC.js} +2 -1
  14. package/dist/{chunk-VBCS3DUA.js → chunk-EF2UGZWY.js} +3 -3
  15. package/dist/{chunk-JGWDVX64.js → chunk-EURB7QFZ.js} +123 -53
  16. package/dist/{chunk-BM4CQ5P3.js → chunk-GS5672WG.js} +6 -6
  17. package/dist/{chunk-ZFYPMX46.js → chunk-LX6U42O3.js} +1 -1
  18. package/dist/{chunk-5X4QLXRG.js → chunk-MPBLMWVR.js} +5 -3
  19. package/dist/{chunk-Q7Q7V5NV.js → chunk-NKHKXPI4.js} +7 -7
  20. package/dist/{chunk-6F3IILHI.js → chunk-S6ZQKDY6.js} +1 -1
  21. package/dist/{chunk-FTCRZOG2.js → chunk-T5CVK4R3.js} +5 -5
  22. package/dist/{chunk-GHYHJTYV.js → chunk-Z2FNRKF3.js} +13 -13
  23. package/dist/components.d.ts +1 -1
  24. package/dist/components.js +12 -12
  25. package/dist/eslint-rules/rules/04-code-quality.cjs +66 -10
  26. package/dist/eslint-rules/rules/06-security-rbac.cjs +8 -3
  27. package/dist/eslint-rules/rules/07-api-tech-stack.cjs +190 -68
  28. package/dist/{functions-DHebl8-F.d.ts → functions-lBy5L2ry.d.ts} +1 -1
  29. package/dist/hooks.js +7 -7
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +15 -15
  32. package/dist/providers.js +2 -2
  33. package/dist/rbac/index.d.ts +1 -1
  34. package/dist/rbac/index.js +6 -6
  35. package/dist/theming/runtime.d.ts +48 -1
  36. package/dist/theming/runtime.js +1 -1
  37. package/dist/types.d.ts +2 -2
  38. package/dist/utils.js +1 -1
  39. package/docs/api/modules.md +63 -14
  40. package/docs/getting-started/dependencies.md +23 -0
  41. package/docs/implementation-guides/app-layout.md +1 -1
  42. package/docs/implementation-guides/data-tables.md +1 -1
  43. package/docs/standards/1-pace-core-compliance-standards.md +38 -1
  44. package/eslint-config-pace-core.cjs +30 -11
  45. package/package.json +45 -15
  46. package/scripts/eslint-audit.cjs +123 -0
  47. package/scripts/install-eslint-config.cjs +67 -2
  48. package/scripts/validate-dependencies.cjs +248 -0
  49. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +20 -8
  50. package/src/__tests__/templates/accessibility.test.template.tsx +1 -0
  51. package/src/components/AddressField/AddressField.tsx +26 -1
  52. package/src/components/Alert/Alert.test.tsx +86 -22
  53. package/src/components/Alert/Alert.tsx +19 -11
  54. package/src/components/Badge/Badge.tsx +1 -1
  55. package/src/components/Checkbox/Checkbox.test.tsx +2 -1
  56. package/src/components/ContextSelector/ContextSelector.tsx +39 -41
  57. package/src/components/DataTable/DataTable.tsx +1 -19
  58. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +6 -10
  59. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +18 -9
  60. package/src/components/DataTable/__tests__/pagination.modes.test.tsx +3 -2
  61. package/src/components/DataTable/components/EmptyState.tsx +1 -1
  62. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +1 -1
  63. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +3 -3
  64. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +33 -29
  65. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +1 -2
  66. package/src/components/FileUpload/FileUpload.test.tsx +22 -31
  67. package/src/components/FileUpload/FileUpload.tsx +29 -0
  68. package/src/components/NavigationMenu/NavigationMenu.test.tsx +48 -12
  69. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +9 -9
  70. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +30 -30
  71. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +4 -4
  72. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +7 -1
  73. package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +8 -5
  74. package/src/hooks/__tests__/useFileUrl.unit.test.ts +4 -0
  75. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +3 -3
  76. package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +45 -8
  77. package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +22 -2
  78. package/src/hooks/public/usePublicRouteParams.ts +8 -4
  79. package/src/hooks/useAddressAutocomplete.test.ts +18 -18
  80. package/src/hooks/useEventTheme.ts +5 -1
  81. package/src/hooks/useFileUrl.ts +52 -8
  82. package/src/hooks/useOrganisationSecurity.test.ts +2 -1
  83. package/src/providers/__tests__/ProviderLifecycle.test.tsx +1 -1
  84. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +15 -6
  85. package/src/rbac/__tests__/rbac-functions.test.ts +3 -3
  86. package/src/rbac/api.test.ts +104 -0
  87. package/src/rbac/engine.ts +1 -1
  88. package/src/rbac/hooks/useCan.test.ts +2 -2
  89. package/src/rbac/secureClient.ts +1 -1
  90. package/src/rbac/types/functions.ts +1 -1
  91. package/src/theming/__tests__/parseEventColours.test.ts +117 -8
  92. package/src/theming/parseEventColours.ts +56 -2
  93. package/src/types/supabase.ts +2 -3
  94. package/src/utils/__tests__/bundleAnalysis.unit.test.ts +9 -9
  95. package/src/utils/file-reference/__tests__/file-reference.test.ts +4 -0
  96. package/src/utils/formatting/formatDate.test.ts +3 -2
  97. package/src/utils/formatting/formatDateTime.test.ts +2 -2
  98. package/src/utils/google-places/googlePlacesUtils.test.ts +36 -24
  99. package/src/utils/storage/__tests__/helpers.unit.test.ts +19 -12
  100. package/src/utils/storage/helpers.test.ts +69 -3
@@ -80,6 +80,24 @@ function matchesVersionRange(version, range) {
80
80
  return version.startsWith(range.replace(/[\^~]/, ''));
81
81
  }
82
82
 
83
+ // Compare two version strings (e.g., "4.1.8" vs "4.1.16")
84
+ // Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
85
+ function compareVersions(v1, v2) {
86
+ const parts1 = v1.split('.').map(Number);
87
+ const parts2 = v2.split('.').map(Number);
88
+ const maxLength = Math.max(parts1.length, parts2.length);
89
+
90
+ for (let i = 0; i < maxLength; i++) {
91
+ const part1 = parts1[i] || 0;
92
+ const part2 = parts2[i] || 0;
93
+
94
+ if (part1 < part2) return -1;
95
+ if (part1 > part2) return 1;
96
+ }
97
+
98
+ return 0;
99
+ }
100
+
83
101
  // Check version compatibility
84
102
  function checkVersion(installed, required) {
85
103
  if (!required) return { valid: true };
@@ -88,18 +106,46 @@ function checkVersion(installed, required) {
88
106
  const installedVersion = installed.replace(/[\^~]/, '');
89
107
  const requiredVersion = required.replace(/[\^~]/, '');
90
108
 
91
- // Check major version
92
- const installedMajor = parseInt(installedVersion.split('.')[0]);
93
- const requiredMajor = parseInt(requiredVersion.split('.')[0]);
109
+ // Parse version parts
110
+ const installedParts = installedVersion.split('.').map(Number);
111
+ const requiredParts = requiredVersion.split('.').map(Number);
112
+
113
+ const installedMajor = installedParts[0] || 0;
114
+ const requiredMajor = requiredParts[0] || 0;
94
115
 
95
116
  if (required.startsWith('^')) {
117
+ // Caret range: ^4.1.16 means >= 4.1.16 and < 5.0.0
118
+ // Check if installed version is >= required version
119
+ const versionComparison = compareVersions(installedVersion, requiredVersion);
120
+
121
+ // Must be same major version and >= required version
122
+ const valid = installedMajor === requiredMajor && versionComparison >= 0;
123
+
96
124
  return {
97
- valid: installedMajor >= requiredMajor,
125
+ valid,
98
126
  installed,
99
127
  required,
100
128
  };
101
129
  }
102
130
 
131
+ if (required.startsWith('~')) {
132
+ // Tilde range: ~4.1.16 means >= 4.1.16 and < 4.2.0
133
+ const versionComparison = compareVersions(installedVersion, requiredVersion);
134
+ const installedMinor = installedParts[1] || 0;
135
+ const requiredMinor = requiredParts[1] || 0;
136
+
137
+ const valid = installedMajor === requiredMajor &&
138
+ installedMinor === requiredMinor &&
139
+ versionComparison >= 0;
140
+
141
+ return {
142
+ valid,
143
+ installed,
144
+ required,
145
+ };
146
+ }
147
+
148
+ // Exact match or no prefix - check major version
103
149
  return {
104
150
  valid: installedMajor === requiredMajor,
105
151
  installed,
@@ -114,7 +160,7 @@ function runDependencyAudit(consumingAppPath = process.cwd()) {
114
160
  if (!fs.existsSync(packageJsonPath)) {
115
161
  return {
116
162
  error: `package.json not found at ${packageJsonPath}`,
117
- issues: { includedDeps: [], missingRequired: [], missingOptional: [], versionIssues: [], wrongLocation: [] }
163
+ issues: { includedDeps: [], missingRequired: [], missingOptional: [], versionIssues: [], wrongLocation: [], missingDevDeps: [], devVersionIssues: [] }
118
164
  };
119
165
  }
120
166
 
@@ -123,22 +169,41 @@ function runDependencyAudit(consumingAppPath = process.cwd()) {
123
169
  if (!paceCorePath) {
124
170
  return {
125
171
  error: 'Could not find pace-core package.json. Make sure @jmruthers/pace-core is installed in your project.',
126
- issues: { includedDeps: [], missingRequired: [], missingOptional: [], versionIssues: [], wrongLocation: [] }
172
+ issues: { includedDeps: [], missingRequired: [], missingOptional: [], versionIssues: [], wrongLocation: [], missingDevDeps: [], devVersionIssues: [] }
127
173
  };
128
174
  }
129
175
 
130
176
  const paceCorePkg = JSON.parse(fs.readFileSync(paceCorePath, 'utf8'));
131
177
  const INCLUDED_DEPS = Object.keys(paceCorePkg.dependencies || {});
132
178
  const PEER_DEPS = paceCorePkg.peerDependencies || {};
133
- const REQUIRED_PEERS = ['react', 'react-dom', 'react-router-dom', 'tailwindcss'];
134
- const OPTIONAL_PEERS = Object.keys(PEER_DEPS).filter(dep => !REQUIRED_PEERS.includes(dep));
179
+ const PEER_META = paceCorePkg.peerDependenciesMeta || {};
180
+
181
+ // Required peers are those NOT marked as optional in peerDependenciesMeta
182
+ const REQUIRED_PEERS = Object.keys(PEER_DEPS).filter(
183
+ dep => !PEER_META[dep]?.optional
184
+ );
185
+
186
+ // Optional peers are those marked as optional in peerDependenciesMeta
187
+ const OPTIONAL_PEERS = Object.keys(PEER_DEPS).filter(
188
+ dep => PEER_META[dep]?.optional
189
+ );
190
+
191
+ // Validation warnings: check if any peer dependency is missing from peerDependenciesMeta
192
+ const missingMeta = Object.keys(PEER_DEPS).filter(
193
+ dep => !PEER_META[dep]
194
+ );
195
+
196
+ if (missingMeta.length > 0) {
197
+ console.warn(`${colors.yellow}Warning: The following peer dependencies are missing from peerDependenciesMeta: ${missingMeta.join(', ')}${colors.reset}`);
198
+ console.warn(`${colors.yellow}They will be treated as required. Add them to peerDependenciesMeta to mark them as optional.${colors.reset}`);
199
+ }
135
200
 
136
201
  // Verify pace-core is installed
137
202
  const paceCoreInNodeModules = path.join(consumingAppPath, 'node_modules', '@jmruthers', 'pace-core', 'package.json');
138
203
  if (!fs.existsSync(paceCoreInNodeModules)) {
139
204
  return {
140
205
  error: '@jmruthers/pace-core not found in node_modules. Please run: npm install @jmruthers/pace-core',
141
- issues: { includedDeps: [], missingRequired: [], missingOptional: [], versionIssues: [], wrongLocation: [] }
206
+ issues: { includedDeps: [], missingRequired: [], missingOptional: [], versionIssues: [], wrongLocation: [], missingDevDeps: [], devVersionIssues: [] }
142
207
  };
143
208
  }
144
209
 
@@ -154,6 +219,8 @@ function runDependencyAudit(consumingAppPath = process.cwd()) {
154
219
  missingOptional: [],
155
220
  versionIssues: [],
156
221
  wrongLocation: [],
222
+ missingDevDeps: [],
223
+ devVersionIssues: [],
157
224
  };
158
225
 
159
226
  // Check for included dependencies
@@ -216,6 +283,107 @@ function runDependencyAudit(consumingAppPath = process.cwd()) {
216
283
  });
217
284
  }
218
285
 
286
+ // Check required dev dependencies
287
+ // Get pace-core's dev dependencies to use as reference versions
288
+ const paceCoreDevDeps = paceCorePkg.devDependencies || {};
289
+ const devDeps = consumingPkg.devDependencies || {};
290
+
291
+ // Build list of required dev dependencies dynamically from pace-core's devDependencies
292
+ // Exclude packages that consuming apps don't need:
293
+ // 1. pace-core-specific build tools (tsup, typedoc, etc.)
294
+ // 2. Testing libraries (consuming apps may use different testing setups)
295
+ // 3. Type definitions (@types/* - consuming apps manage their own)
296
+ // 4. Packages already checked as peer dependencies (except tailwindcss which we check in both)
297
+ // 5. Packages already included in pace-core's dependencies
298
+
299
+ const requiredDevDeps = {};
300
+
301
+ Object.entries(paceCoreDevDeps).forEach(([dep, version]) => {
302
+ // Skip pace-core-specific build tools
303
+ if (dep === 'tsup' ||
304
+ dep === 'typedoc' ||
305
+ dep.startsWith('typedoc-plugin-') ||
306
+ dep === 'esbuild' ||
307
+ dep === '@vitest/coverage-v8') {
308
+ return;
309
+ }
310
+
311
+ // Skip testing libraries (consuming apps may use different testing)
312
+ if (dep.includes('testing-library') ||
313
+ dep === 'jsdom' ||
314
+ dep === 'vitest') {
315
+ return;
316
+ }
317
+
318
+ // Skip type definitions (consuming apps manage their own @types/*)
319
+ if (dep.startsWith('@types/')) {
320
+ return;
321
+ }
322
+
323
+ // Skip if it's already a peer dependency (checked separately)
324
+ // Exception: tailwindcss - we check both peer and dev dependency versions
325
+ if (PEER_DEPS[dep] && dep !== 'tailwindcss') {
326
+ return;
327
+ }
328
+
329
+ // Skip if it's already in pace-core's dependencies (included, not needed by consuming apps)
330
+ if (INCLUDED_DEPS.includes(dep)) {
331
+ return;
332
+ }
333
+
334
+ // Skip if it's in pace-core's dependencies (shouldn't be in devDependencies)
335
+ if (paceCorePkg.dependencies?.[dep]) {
336
+ return;
337
+ }
338
+
339
+ // Add to required dev dependencies - versions come directly from pace-core's package.json
340
+ requiredDevDeps[dep] = version;
341
+ });
342
+
343
+ // Override tailwindcss version with peer dependency version if available
344
+ // This ensures we use the peer dependency requirement, not the dev dependency version
345
+ if (PEER_DEPS.tailwindcss) {
346
+ requiredDevDeps.tailwindcss = PEER_DEPS.tailwindcss;
347
+ }
348
+
349
+ // Check for missing required dev dependencies
350
+ Object.entries(requiredDevDeps).forEach(([dep, requiredVersion]) => {
351
+ if (!requiredVersion) return; // Skip if pace-core doesn't specify a version
352
+
353
+ if (!devDeps[dep] && !consumingPkg.dependencies?.[dep]) {
354
+ issues.missingDevDeps.push({
355
+ package: dep,
356
+ required: requiredVersion,
357
+ });
358
+ } else {
359
+ // Check version if installed
360
+ const installedVersion = devDeps[dep] || consumingPkg.dependencies?.[dep];
361
+ if (installedVersion) {
362
+ const versionCheck = checkVersion(installedVersion, requiredVersion);
363
+ if (!versionCheck.valid) {
364
+ issues.devVersionIssues.push({
365
+ package: dep,
366
+ installed: versionCheck.installed,
367
+ required: versionCheck.required,
368
+ location: devDeps[dep] ? 'devDependencies' : 'dependencies',
369
+ });
370
+ }
371
+ }
372
+ }
373
+ });
374
+
375
+ // Check for dev dependencies in wrong location (should be in devDependencies, not dependencies)
376
+ // This is dynamic - check all required dev dependencies
377
+ Object.keys(requiredDevDeps).forEach(dep => {
378
+ if (consumingPkg.dependencies?.[dep] && !devDeps[dep]) {
379
+ issues.wrongLocation.push({
380
+ package: dep,
381
+ current: 'dependencies',
382
+ shouldBe: 'devDependencies',
383
+ });
384
+ }
385
+ });
386
+
219
387
  // Find pace-core version - check dependencies, devDependencies, or installed package
220
388
  let paceCoreVersion = consumingPkg.dependencies?.['@jmruthers/pace-core'] ||
221
389
  consumingPkg.devDependencies?.['@jmruthers/pace-core'];
@@ -315,6 +483,28 @@ function auditDependencies(consumingAppPath = process.cwd()) {
315
483
  console.log();
316
484
  }
317
485
 
486
+ // Missing dev dependencies (errors)
487
+ if (issues.missingDevDeps.length > 0) {
488
+ hasErrors = true;
489
+ console.log(`${colors.red}❌ MISSING REQUIRED DEV DEPENDENCIES:${colors.reset}`);
490
+ issues.missingDevDeps.forEach(issue => {
491
+ console.log(` - ${colors.red}${issue.package}${colors.reset} (required: ${issue.required})`);
492
+ });
493
+ console.log();
494
+ }
495
+
496
+ // Dev version issues (errors)
497
+ if (issues.devVersionIssues.length > 0) {
498
+ hasErrors = true;
499
+ console.log(`${colors.yellow}⚠️ DEV DEPENDENCY VERSION ISSUES:${colors.reset}`);
500
+ issues.devVersionIssues.forEach(issue => {
501
+ console.log(` - ${colors.yellow}${issue.package}${colors.reset} (in ${issue.location})`);
502
+ console.log(` Installed: ${issue.installed}`);
503
+ console.log(` Required: ${issue.required}`);
504
+ });
505
+ console.log();
506
+ }
507
+
318
508
  // Success message
319
509
  if (!hasErrors && issues.missingOptional.length === 0 && issues.wrongLocation.length === 0) {
320
510
  console.log(`${colors.green}✅ All dependencies are correctly configured!${colors.reset}\n`);
@@ -339,6 +529,14 @@ function auditDependencies(consumingAppPath = process.cwd()) {
339
529
  console.log(`npm install ${depsToInstall}\n`);
340
530
  }
341
531
 
532
+ if (issues.missingDevDeps.length > 0) {
533
+ const devDepsToInstall = issues.missingDevDeps
534
+ .map(i => `${i.package}@${i.required}`)
535
+ .join(' ');
536
+ console.log(`${colors.cyan}# Install missing required dev dependencies${colors.reset}`);
537
+ console.log(`npm install -D ${devDepsToInstall}\n`);
538
+ }
539
+
342
540
  if (issues.versionIssues.length > 0) {
343
541
  const depsToFix = issues.versionIssues
344
542
  .map(i => `${i.package}@${i.required}`)
@@ -347,6 +545,14 @@ function auditDependencies(consumingAppPath = process.cwd()) {
347
545
  console.log(`npm install ${depsToFix}\n`);
348
546
  }
349
547
 
548
+ if (issues.devVersionIssues.length > 0) {
549
+ const devDepsToFix = issues.devVersionIssues
550
+ .map(i => `${i.package}@${i.required}`)
551
+ .join(' ');
552
+ console.log(`${colors.cyan}# Fix dev dependency version ranges${colors.reset}`);
553
+ console.log(`npm install -D ${devDepsToFix}\n`);
554
+ }
555
+
350
556
  if (issues.wrongLocation.length > 0) {
351
557
  issues.wrongLocation.forEach(issue => {
352
558
  console.log(`${colors.cyan}# Move ${issue.package} to devDependencies${colors.reset}`);
@@ -197,28 +197,13 @@ function checkTestColocation(consumingAppPath) {
197
197
 
198
198
  /**
199
199
  * Check Supabase structure
200
+ * Note: Consuming apps don't need migrations directory - only pace-core handles migrations
200
201
  */
201
202
  function checkSupabaseStructure(consumingAppPath) {
202
203
  const issues = [];
203
204
 
204
- const supabaseDir = path.join(consumingAppPath, 'supabase');
205
- if (!directoryExists(supabaseDir)) {
206
- // Supabase structure is optional
207
- return issues;
208
- }
209
-
210
- // Check for migrations directory
211
- const migrationsDir = path.join(supabaseDir, 'migrations');
212
- if (!directoryExists(migrationsDir)) {
213
- issues.push({
214
- type: 'supabaseStructure',
215
- file: 'supabase/migrations/',
216
- line: 0,
217
- message: 'supabase/migrations/ directory not found. Database migrations should be stored here.',
218
- severity: 'warning',
219
- fix: 'Create supabase/migrations/ directory for database migrations',
220
- });
221
- }
205
+ // Consuming apps don't need supabase/migrations - only pace-core handles migrations
206
+ // This check has been removed as it's not applicable to consuming apps
222
207
 
223
208
  return issues;
224
209
  }
@@ -25,8 +25,26 @@ function checkComponentBoundaries(consumingAppPath) {
25
25
  }
26
26
 
27
27
  const componentFiles = findSourceFiles(srcDir).filter(file => {
28
+ // Only check actual component files, not test files or utility files
29
+ const isTestFile = file.includes('.test.') || file.includes('.spec.');
30
+ const isUtilityFile = file.endsWith('Utils.ts') ||
31
+ file.endsWith('Utils.tsx') ||
32
+ file.endsWith('Helpers.ts') ||
33
+ file.endsWith('Helpers.tsx') ||
34
+ file.includes('testUtils') ||
35
+ file.includes('testHelpers') ||
36
+ file.includes('testAssertions') ||
37
+ file.includes('testSetup');
38
+
39
+ if (isTestFile || isUtilityFile) {
40
+ return false;
41
+ }
42
+
28
43
  const dir = path.dirname(file);
29
- return dir.includes('/components/') || dir.includes('/pages/');
44
+ const isComponentDir = dir.includes('/components/') || dir.includes('/pages/');
45
+ const isComponentFile = file.endsWith('.tsx') || (file.endsWith('.ts') && !isUtilityFile);
46
+
47
+ return isComponentDir && isComponentFile;
30
48
  });
31
49
 
32
50
  componentFiles.forEach(filePath => {
@@ -71,18 +89,28 @@ function checkComponentBoundaries(consumingAppPath) {
71
89
  });
72
90
 
73
91
  // Check for complex business logic in components
92
+ // Only flag actual business logic patterns, not UI text or comments
74
93
  const businessLogicPatterns = [
75
- /if\s*\([^)]*permission/i,
76
- /if\s*\([^)]*role/i,
77
- /calculate|compute|process/i,
94
+ /if\s*\([^)]*permission[^)]*\)/i, // Permission checks
95
+ /if\s*\([^)]*role[^)]*\)/i, // Role checks
96
+ /calculate\w*\s*\(/i, // Calculation functions
97
+ /compute\w*\s*\(/i, // Computation functions
98
+ /process\w+\s*\(/i, // Process functions (not "Processing..." text)
78
99
  ];
79
100
 
80
101
  // This is a heuristic - look for complex logic that should be in hooks/utils
102
+ // Exclude common UI text patterns
103
+ const hasUIOnlyText = /Processing\.\.\.|processing\.\.\./i.test(content);
81
104
  const hasComplexLogic = businessLogicPatterns.some(pattern => pattern.test(content));
82
- if (hasComplexLogic) {
105
+
106
+ if (hasComplexLogic && !hasUIOnlyText) {
83
107
  // Check if logic is in a hook call (acceptable)
84
108
  const hasHookCalls = /use[A-Z]\w*\s*\(/.test(content);
85
- if (!hasHookCalls) {
109
+ // Check if it's just a simple conditional render (acceptable)
110
+ const isSimpleConditional = /return\s*\([^)]*\?[^)]*:[^)]*\)/.test(content) ||
111
+ /\{[^}]*\?[^}]*:[^}]*\}/.test(content);
112
+
113
+ if (!hasHookCalls && !isSimpleConditional) {
86
114
  issues.push({
87
115
  type: 'componentBoundary',
88
116
  file: relativePath,
@@ -377,6 +377,16 @@ function checkPagePermissionGuardCoverage(consumingAppPath) {
377
377
  }
378
378
 
379
379
  const relativePath = getRelativePath(filePath, consumingAppPath);
380
+ const fileName = path.basename(filePath, path.extname(filePath));
381
+
382
+ // Exclude public/error pages that don't need RBAC protection
383
+ // NotFound, 404, Error, Unauthorized, Forbidden, AccessDenied pages are public
384
+ const isPublicPage = /NotFound|not.*found|404|Error|Unauthorized|Forbidden|AccessDenied/i.test(fileName) ||
385
+ /public|error|unauthorized|forbidden|access.*denied/i.test(relativePath);
386
+
387
+ if (isPublicPage) {
388
+ return; // Skip public/error pages - they don't need PagePermissionGuard
389
+ }
380
390
 
381
391
  // Check if PagePermissionGuard is used
382
392
  const hasPagePermissionGuard = /<PagePermissionGuard/.test(content) ||
@@ -256,7 +256,61 @@ function checkViteConfig(consumingAppPath) {
256
256
  }
257
257
 
258
258
  // Check for resolve.dedupe (should dedupe React dependencies)
259
- const hasResolveDedupe = /resolve\s*:\s*\{[^}]*dedupe/.test(content);
259
+ // Need to handle nested objects, so look for dedupe anywhere after resolve: {
260
+ // Match resolve: { ... and then look for dedupe before the matching closing brace
261
+ // Use a more flexible pattern that handles nested braces
262
+ const resolvePattern = /resolve\s*:\s*\{/;
263
+ const resolveMatch = content.match(resolvePattern);
264
+
265
+ let hasResolveDedupe = false;
266
+ if (resolveMatch) {
267
+ const resolveStart = resolveMatch.index + resolveMatch[0].length;
268
+ // Find the matching closing brace for the resolve object
269
+ let braceCount = 1;
270
+ let i = resolveStart;
271
+ let resolveEnd = -1;
272
+
273
+ while (i < content.length && braceCount > 0) {
274
+ if (content[i] === '{') braceCount++;
275
+ if (content[i] === '}') braceCount--;
276
+ if (braceCount === 0) {
277
+ resolveEnd = i;
278
+ break;
279
+ }
280
+ i++;
281
+ }
282
+
283
+ if (resolveEnd > 0) {
284
+ const resolveBody = content.substring(resolveStart, resolveEnd);
285
+ // Check if dedupe exists in the resolve object body
286
+ hasResolveDedupe = /dedupe\s*:/.test(resolveBody);
287
+
288
+ // Also verify it includes the required dependencies
289
+ if (hasResolveDedupe) {
290
+ const dedupePattern = /dedupe\s*:\s*\[([^\]]+)\]/;
291
+ const dedupeMatch = resolveBody.match(dedupePattern);
292
+ if (dedupeMatch) {
293
+ const dedupeArray = dedupeMatch[1];
294
+ const hasReact = /['"]react['"]/.test(dedupeArray);
295
+ const hasReactDom = /['"]react-dom['"]/.test(dedupeArray);
296
+ const hasReactRouter = /['"]react-router-dom['"]/.test(dedupeArray);
297
+
298
+ // If any required dependency is missing, flag it
299
+ if (!hasReact || !hasReactDom || !hasReactRouter) {
300
+ issues.push({
301
+ type: 'viteConfig',
302
+ file: relativePath,
303
+ line: 1,
304
+ message: 'vite.config.ts resolve.dedupe is missing required dependencies. Should include: react, react-dom, react-router-dom',
305
+ severity: 'warning',
306
+ fix: 'Update resolve.dedupe to include: [\'react\', \'react-dom\', \'react-router-dom\']',
307
+ });
308
+ }
309
+ }
310
+ }
311
+ }
312
+ }
313
+
260
314
  if (!hasResolveDedupe) {
261
315
  issues.push({
262
316
  type: 'viteConfig',
@@ -164,6 +164,25 @@ function main() {
164
164
  // Display audit results summary
165
165
  console.log(`\n${colors.bold}Audit Results:${colors.reset}\n`);
166
166
 
167
+ // Display dependency audit first
168
+ if (dependencyResult.error) {
169
+ console.log(` ${colors.red}❌ Dependency Audit: Error - ${dependencyResult.error}${colors.reset}`);
170
+ } else {
171
+ const depIssues = dependencyResult.issues || {};
172
+ const depCount = (depIssues.includedDeps?.length || 0) +
173
+ (depIssues.missingRequired?.length || 0) +
174
+ (depIssues.versionIssues?.length || 0) +
175
+ (depIssues.wrongLocation?.length || 0) +
176
+ (depIssues.missingDevDeps?.length || 0) +
177
+ (depIssues.devVersionIssues?.length || 0);
178
+
179
+ if (depCount === 0) {
180
+ console.log(` ${colors.green}✅ Dependency Audit: 0 issues${colors.reset}`);
181
+ } else {
182
+ console.log(` ${colors.red}❌ Dependency Audit: ${depCount} issue(s)${colors.reset}`);
183
+ }
184
+ }
185
+
167
186
  // Display each standard
168
187
  Object.entries(standardResults).forEach(([key, result]) => {
169
188
  const standardNames = {
@@ -189,34 +208,19 @@ function main() {
189
208
  }
190
209
  });
191
210
 
192
- // Display dependency audit
193
- if (dependencyResult.error) {
194
- console.log(` ${colors.red}❌ Dependency Audit: Error - ${dependencyResult.error}${colors.reset}`);
195
- } else {
196
- const depIssues = dependencyResult.issues || {};
197
- const depCount = (depIssues.includedDeps?.length || 0) +
198
- (depIssues.missingRequired?.length || 0) +
199
- (depIssues.versionIssues?.length || 0) +
200
- (depIssues.wrongLocation?.length || 0);
201
-
202
- if (depCount === 0) {
203
- console.log(` ${colors.green}✅ Dependency Audit: 0 issues${colors.reset}`);
204
- } else {
205
- console.log(` ${colors.red}❌ Dependency Audit: ${depCount} issue(s)${colors.reset}`);
206
- }
207
- }
208
-
209
211
  // Total summary
210
212
  const totalIssues = summary.total + (dependencyResult.error ? 0 :
211
213
  ((dependencyResult.issues?.includedDeps?.length || 0) +
212
214
  (dependencyResult.issues?.missingRequired?.length || 0) +
213
215
  (dependencyResult.issues?.versionIssues?.length || 0) +
214
- (dependencyResult.issues?.wrongLocation?.length || 0)));
216
+ (dependencyResult.issues?.wrongLocation?.length || 0) +
217
+ (dependencyResult.issues?.missingDevDeps?.length || 0) +
218
+ (dependencyResult.issues?.devVersionIssues?.length || 0)));
215
219
 
216
220
  console.log(`\n${colors.bold}Total Issues: ${totalIssues === 0 ? colors.green + '0 (All checks passed!)' : colors.red + totalIssues}${colors.reset}\n`);
217
221
 
218
222
  // Generate and save markdown report
219
- const markdownReport = generateMarkdownReport(standardResults, consumingAppPath);
223
+ const markdownReport = generateMarkdownReport(standardResults, consumingAppPath, dependencyResult);
220
224
 
221
225
  // Generate timestamp in yyyymmddHHMM format
222
226
  const now = new Date();