@jmruthers/pace-core 0.6.7 → 0.6.9

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 (117) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/audit-tool/00-dependencies.cjs +215 -9
  3. package/audit-tool/audits/02-project-structure.cjs +41 -53
  4. package/audit-tool/audits/03-architecture.cjs +34 -6
  5. package/audit-tool/audits/06-security-rbac.cjs +10 -0
  6. package/audit-tool/audits/07-api-tech-stack.cjs +55 -1
  7. package/audit-tool/index.cjs +23 -19
  8. package/audit-tool/utils/report-utils.cjs +141 -2
  9. package/dist/{DataTable-7PMH7XN7.js → DataTable-SOAFXIWY.js} +5 -5
  10. package/dist/{PublicPageProvider-DlsCaR5v.d.ts → PublicPageProvider-CIGSujI2.d.ts} +14 -8
  11. package/dist/{UnifiedAuthProvider-ZT6TIGM7.js → UnifiedAuthProvider-7SNDOWYD.js} +2 -2
  12. package/dist/{api-Y4MQWOFW.js → api-7P7DI652.js} +1 -1
  13. package/dist/{chunk-L4XMVJKY.js → chunk-4DDCYDQ3.js} +8 -7
  14. package/dist/{chunk-JGWDVX64.js → chunk-5HNSDQWH.js} +125 -55
  15. package/dist/{chunk-ZKAWKYT4.js → chunk-5W2A3DRC.js} +2 -1
  16. package/dist/{chunk-IUBRCBSY.js → chunk-C7ZQ5O4C.js} +11 -5
  17. package/dist/{chunk-VBCS3DUA.js → chunk-EF2UGZWY.js} +3 -3
  18. package/dist/{chunk-BM4CQ5P3.js → chunk-GS5672WG.js} +6 -6
  19. package/dist/{chunk-Q7Q7V5NV.js → chunk-J2U36LHD.js} +72 -9
  20. package/dist/{chunk-ZFYPMX46.js → chunk-LX6U42O3.js} +1 -1
  21. package/dist/{chunk-5X4QLXRG.js → chunk-MPBLMWVR.js} +5 -3
  22. package/dist/{chunk-6F3IILHI.js → chunk-S6ZQKDY6.js} +1 -1
  23. package/dist/{chunk-FTCRZOG2.js → chunk-T5CVK4R3.js} +5 -5
  24. package/dist/{chunk-GHYHJTYV.js → chunk-Z2FNRKF3.js} +13 -13
  25. package/dist/components.d.ts +1 -1
  26. package/dist/components.js +12 -12
  27. package/dist/{database.generated-CcnC_DRc.d.ts → database.generated-DT8JTZiP.d.ts} +12 -12
  28. package/dist/eslint-rules/rules/04-code-quality.cjs +66 -10
  29. package/dist/eslint-rules/rules/06-security-rbac.cjs +8 -3
  30. package/dist/eslint-rules/rules/07-api-tech-stack.cjs +190 -68
  31. package/dist/{functions-DHebl8-F.d.ts → functions-lBy5L2ry.d.ts} +1 -1
  32. package/dist/hooks.d.ts +3 -3
  33. package/dist/hooks.js +7 -7
  34. package/dist/index.d.ts +6 -6
  35. package/dist/index.js +16 -16
  36. package/dist/providers.js +2 -2
  37. package/dist/rbac/index.d.ts +2 -2
  38. package/dist/rbac/index.js +6 -6
  39. package/dist/theming/runtime.d.ts +48 -1
  40. package/dist/theming/runtime.js +1 -1
  41. package/dist/{timezone-BZe_eUxx.d.ts → timezone-0AyangqX.d.ts} +1 -1
  42. package/dist/types.d.ts +3 -3
  43. package/dist/{usePublicRouteParams-MamNgwqe.d.ts → usePublicRouteParams-DQLrDqDb.d.ts} +1 -1
  44. package/dist/utils.d.ts +3 -3
  45. package/dist/utils.js +3 -3
  46. package/docs/api/modules.md +64 -15
  47. package/docs/api-reference/rpc-functions.md +3 -3
  48. package/docs/getting-started/dependencies.md +23 -0
  49. package/docs/implementation-guides/app-layout.md +1 -1
  50. package/docs/implementation-guides/data-tables.md +67 -1
  51. package/docs/standards/1-pace-core-compliance-standards.md +38 -1
  52. package/eslint-config-pace-core.cjs +30 -11
  53. package/package.json +45 -15
  54. package/scripts/eslint-audit.cjs +123 -0
  55. package/scripts/install-eslint-config.cjs +67 -2
  56. package/scripts/validate-dependencies.cjs +248 -0
  57. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +20 -8
  58. package/src/__tests__/templates/accessibility.test.template.tsx +1 -0
  59. package/src/components/AddressField/AddressField.tsx +26 -1
  60. package/src/components/Alert/Alert.test.tsx +86 -22
  61. package/src/components/Alert/Alert.tsx +19 -11
  62. package/src/components/Badge/Badge.tsx +1 -1
  63. package/src/components/Checkbox/Checkbox.test.tsx +2 -1
  64. package/src/components/ContextSelector/ContextSelector.tsx +39 -41
  65. package/src/components/DataTable/DataTable.tsx +1 -19
  66. package/src/components/DataTable/__tests__/DataTable.select-label-display.test.tsx +483 -0
  67. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +6 -10
  68. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +18 -9
  69. package/src/components/DataTable/__tests__/pagination.modes.test.tsx +3 -2
  70. package/src/components/DataTable/components/EmptyState.tsx +1 -1
  71. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +1 -1
  72. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +3 -3
  73. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +33 -29
  74. package/src/components/DataTable/hooks/__tests__/useTableColumns.test.ts +224 -0
  75. package/src/components/DataTable/hooks/useTableColumns.ts +23 -1
  76. package/src/components/DataTable/utils/__tests__/selectFieldUtils.test.ts +207 -0
  77. package/src/components/DataTable/utils/index.ts +1 -0
  78. package/src/components/DataTable/utils/selectFieldUtils.ts +134 -0
  79. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +1 -2
  80. package/src/components/FileUpload/FileUpload.test.tsx +22 -31
  81. package/src/components/FileUpload/FileUpload.tsx +29 -0
  82. package/src/components/NavigationMenu/NavigationMenu.test.tsx +48 -12
  83. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +9 -9
  84. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +30 -30
  85. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +4 -4
  86. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +7 -1
  87. package/src/components/UserMenu/UserMenu.tsx +3 -5
  88. package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +8 -5
  89. package/src/hooks/__tests__/useFileUrl.unit.test.ts +4 -0
  90. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +3 -3
  91. package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +45 -8
  92. package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +22 -2
  93. package/src/hooks/public/usePublicRouteParams.ts +8 -4
  94. package/src/hooks/useAddressAutocomplete.test.ts +18 -18
  95. package/src/hooks/useEventTheme.ts +5 -1
  96. package/src/hooks/useFileUrl.ts +52 -8
  97. package/src/hooks/useOrganisationSecurity.test.ts +2 -1
  98. package/src/providers/__tests__/ProviderLifecycle.test.tsx +1 -1
  99. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +15 -6
  100. package/src/rbac/__tests__/rbac-functions.test.ts +3 -3
  101. package/src/rbac/api.test.ts +104 -0
  102. package/src/rbac/engine.ts +1 -1
  103. package/src/rbac/hooks/useCan.test.ts +2 -2
  104. package/src/rbac/secureClient.ts +1 -1
  105. package/src/rbac/types/functions.ts +1 -1
  106. package/src/theming/__tests__/parseEventColours.test.ts +117 -8
  107. package/src/theming/parseEventColours.ts +56 -2
  108. package/src/types/database.generated.ts +9 -9
  109. package/src/types/supabase.ts +2 -3
  110. package/src/utils/__tests__/bundleAnalysis.unit.test.ts +9 -9
  111. package/src/utils/file-reference/__tests__/file-reference.test.ts +4 -0
  112. package/src/utils/formatting/formatDate.test.ts +3 -2
  113. package/src/utils/formatting/formatDateTime.test.ts +2 -2
  114. package/src/utils/google-places/googlePlacesUtils.test.ts +36 -24
  115. package/src/utils/storage/__tests__/helpers.unit.test.ts +19 -12
  116. package/src/utils/storage/helpers.test.ts +69 -3
  117. package/src/utils/supabase/createBaseClient.ts +25 -7
package/CHANGELOG.md CHANGED
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+ - **DataTable: Automatic label display for select fields** - Select fields with `fieldType: 'select'` and `fieldOptions` now automatically display labels (e.g., "Mobile") instead of raw values (e.g., `1`) in read mode. This eliminates the need for custom cell renderers for common select field patterns. The feature supports simple options, grouped options, type coercion, and gracefully falls back to raw values when labels aren't found. Custom cell renderers are preserved if already defined.
12
+
10
13
  ### Breaking Changes
11
14
  - **@supabase/supabase-js is now an included dependency (security enforcement)**: Moved from peer dependency to included dependency to enforce security rules. Consuming apps can no longer import `createClient` directly.
12
15
  - **Action Required**:
@@ -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}`);
@@ -127,6 +127,8 @@ function checkImportPaths(consumingAppPath) {
127
127
 
128
128
  /**
129
129
  * Check test file colocation
130
+ * Only flags tests that are in the wrong location (e.g., in __tests__/ when they should be colocated).
131
+ * Does not flag missing test files - use test coverage tools for that.
130
132
  */
131
133
  function checkTestColocation(consumingAppPath) {
132
134
  const issues = [];
@@ -136,9 +138,9 @@ function checkTestColocation(consumingAppPath) {
136
138
  return issues;
137
139
  }
138
140
 
139
- // Find all source files
140
- const sourceFiles = [];
141
- function findFiles(dir) {
141
+ // Find all test files
142
+ const testFiles = [];
143
+ function findTestFiles(dir) {
142
144
  if (!fs.existsSync(dir)) return;
143
145
 
144
146
  const files = fs.readdirSync(dir);
@@ -148,47 +150,48 @@ function checkTestColocation(consumingAppPath) {
148
150
 
149
151
  if (stat.isDirectory()) {
150
152
  if (!['node_modules', 'dist', 'build', '.git'].includes(file)) {
151
- findFiles(filePath);
153
+ findTestFiles(filePath);
152
154
  }
153
- } else if (/\.(ts|tsx|js|jsx)$/.test(file) && !file.includes('.test.') && !file.includes('.spec.')) {
154
- sourceFiles.push(filePath);
155
+ } else if (/\.(ts|tsx|js|jsx)$/.test(file) && (file.includes('.test.') || file.includes('.spec.'))) {
156
+ testFiles.push(filePath);
155
157
  }
156
158
  });
157
159
  }
158
160
 
159
- findFiles(srcDir);
161
+ findTestFiles(srcDir);
160
162
 
161
- // Check if test files are colocated
162
- sourceFiles.forEach(sourceFile => {
163
- const dir = path.dirname(sourceFile);
164
- const basename = path.basename(sourceFile, path.extname(sourceFile));
165
- const ext = path.extname(sourceFile);
163
+ // Check if test files are in the wrong location
164
+ testFiles.forEach(testFile => {
165
+ const testDir = path.dirname(testFile);
166
+ const testBasename = path.basename(testFile);
166
167
 
167
- // Look for test file
168
- const testFile = path.join(dir, `${basename}.test${ext}`);
169
- const testFileTsx = path.join(dir, `${basename}.test.tsx`);
168
+ // Extract the source file name from the test file name
169
+ // e.g., "Component.test.tsx" -> "Component.tsx"
170
+ const sourceBasename = testBasename.replace(/\.(test|spec)\./, '.');
171
+ const sourceFile = path.join(testDir, sourceBasename);
170
172
 
171
- // Skip if it's a test file itself or if test file exists
172
- if (fileExists(testFile) || fileExists(testFileTsx)) {
173
- return;
174
- }
175
-
176
- // Check if file is in a test directory (acceptable alternative)
177
- if (dir.includes('/__tests__/') || dir.includes('/tests/')) {
178
- return;
179
- }
173
+ // Check if test is in a __tests__ or tests directory
174
+ const isInTestDirectory = testDir.includes('/__tests__/') || testDir.includes('/tests/') ||
175
+ testDir.endsWith('/__tests__') || testDir.endsWith('/tests');
180
176
 
181
- // For components, hooks, and utils, suggest colocation
182
- if (dir.includes('/components/') || dir.includes('/hooks/') || dir.includes('/utils/')) {
183
- const relativePath = getRelativePath(sourceFile, consumingAppPath);
184
- issues.push({
185
- type: 'testColocation',
186
- file: relativePath,
187
- line: 1,
188
- message: `Test file not colocated with source file. Consider creating ${path.basename(testFile)}`,
189
- severity: 'info',
190
- fix: `Create test file: ${path.basename(testFile)} in the same directory`,
191
- });
177
+ // If test is in a test directory, check if the source file exists in the parent directory
178
+ if (isInTestDirectory) {
179
+ // Get the parent directory (should be where the source file is)
180
+ const parentDir = path.dirname(testDir);
181
+ const expectedSourceFile = path.join(parentDir, sourceBasename);
182
+
183
+ // If source file exists in parent, flag that test should be colocated
184
+ if (fileExists(expectedSourceFile)) {
185
+ const relativePath = getRelativePath(testFile, consumingAppPath);
186
+ issues.push({
187
+ type: 'testColocation',
188
+ file: relativePath,
189
+ line: 1,
190
+ message: `Test file is in a test directory but should be colocated with source file. Move to ${parentDir}`,
191
+ severity: 'warning',
192
+ fix: `Move test file to ${parentDir} to colocate with source file`,
193
+ });
194
+ }
192
195
  }
193
196
  });
194
197
 
@@ -197,28 +200,13 @@ function checkTestColocation(consumingAppPath) {
197
200
 
198
201
  /**
199
202
  * Check Supabase structure
203
+ * Note: Consuming apps don't need migrations directory - only pace-core handles migrations
200
204
  */
201
205
  function checkSupabaseStructure(consumingAppPath) {
202
206
  const issues = [];
203
207
 
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
- }
208
+ // Consuming apps don't need supabase/migrations - only pace-core handles migrations
209
+ // This check has been removed as it's not applicable to consuming apps
222
210
 
223
211
  return issues;
224
212
  }
@@ -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();