@soulbatical/tetra-dev-toolkit 1.20.26 → 1.21.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.
File without changes
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { readFileSync, writeFileSync, existsSync } from 'fs'
14
- import { join } from 'path'
14
+ import { join, resolve, dirname } from 'path'
15
15
  import { execSync } from 'child_process'
16
16
  import { glob } from 'glob'
17
17
 
@@ -419,7 +419,107 @@ function checkDarkMode(files) {
419
419
  }
420
420
 
421
421
  /**
422
- * HIGH 7: tetra-core version should be recent.
422
+ * CRITICAL 7: All @source directives in globals.css must resolve to existing paths,
423
+ * and at least one must point to @soulbatical/tetra-ui/dist so Tailwind v4 generates
424
+ * AppShell layout utility classes (ml-64, ml-16, etc.).
425
+ *
426
+ * Root cause this prevents: a path with one too few `../` segments silently scans
427
+ * nothing, causing AppShell margins to never be generated and pages to render
428
+ * underneath the fixed sidebar.
429
+ */
430
+ function checkTetraUiSourcePath(files, projectRoot) {
431
+ if (!files.globalsCss) {
432
+ return {
433
+ id: 'tetraUiSourcePath',
434
+ severity: 'critical',
435
+ label: 'tetra-ui @source path',
436
+ pass: false,
437
+ detail: 'globals.css not found',
438
+ fixable: false,
439
+ }
440
+ }
441
+
442
+ const globalsCssFullPath = join(projectRoot, files.globalsCss.path)
443
+ const globalsCssDir = dirname(globalsCssFullPath)
444
+ const content = files.globalsCss.content
445
+
446
+ // Collect all @source directives
447
+ const sourceRegex = /@source\s+["']([^"']+)["']/g
448
+ const sourcePaths = []
449
+ let match
450
+ while ((match = sourceRegex.exec(content)) !== null) {
451
+ sourcePaths.push(match[1])
452
+ }
453
+
454
+ if (sourcePaths.length === 0) {
455
+ // No @source directives — skip to avoid false positives on minimal setups
456
+ return {
457
+ id: 'tetraUiSourcePath',
458
+ severity: 'critical',
459
+ label: 'tetra-ui @source path',
460
+ pass: true,
461
+ detail: `no @source directives in ${files.globalsCss.path} (skipped)`,
462
+ fixable: false,
463
+ }
464
+ }
465
+
466
+ // Check every @source path resolves to an existing directory/file
467
+ const broken = []
468
+ for (const sourcePath of sourcePaths) {
469
+ const resolvedPath = resolve(globalsCssDir, sourcePath)
470
+ if (!existsSync(resolvedPath)) {
471
+ broken.push({ raw: sourcePath, resolved: resolvedPath })
472
+ }
473
+ }
474
+
475
+ if (broken.length > 0) {
476
+ const first = broken[0]
477
+ const extra = broken.length > 1 ? ` (and ${broken.length - 1} more)` : ''
478
+ return {
479
+ id: 'tetraUiSourcePath',
480
+ severity: 'critical',
481
+ label: 'tetra-ui @source path',
482
+ pass: false,
483
+ detail: `@source "${first.raw}" resolves to non-existent path: ${first.resolved}${extra}` +
484
+ ` — fix: @source "../../../node_modules/@soulbatical/tetra-ui/dist"` +
485
+ ` (adjust depth to reach node_modules from ${files.globalsCss.path})`,
486
+ fixable: false,
487
+ }
488
+ }
489
+
490
+ // Check at least one @source entry covers tetra-ui/dist so AppShell
491
+ // margin classes (ml-64, ml-16, etc.) are generated by Tailwind v4
492
+ const tetraUiDistPaths = sourcePaths.filter(sourcePath => {
493
+ const resolvedPath = resolve(globalsCssDir, sourcePath)
494
+ return resolvedPath.includes('@soulbatical/tetra-ui') && resolvedPath.includes('dist')
495
+ })
496
+
497
+ if (tetraUiDistPaths.length === 0) {
498
+ return {
499
+ id: 'tetraUiSourcePath',
500
+ severity: 'critical',
501
+ label: 'tetra-ui @source path',
502
+ pass: false,
503
+ detail: `no @source entry pointing to @soulbatical/tetra-ui/dist found in ${files.globalsCss.path}` +
504
+ ` — AppShell layout classes (ml-64, ml-16, etc.) will not be generated by Tailwind v4.` +
505
+ ` Add: @source "../../../node_modules/@soulbatical/tetra-ui/dist"` +
506
+ ` (adjust relative depth as needed)`,
507
+ fixable: false,
508
+ }
509
+ }
510
+
511
+ return {
512
+ id: 'tetraUiSourcePath',
513
+ severity: 'critical',
514
+ label: 'tetra-ui @source path',
515
+ pass: true,
516
+ detail: `@source "${tetraUiDistPaths[0]}" exists in ${files.globalsCss.path}`,
517
+ fixable: false,
518
+ }
519
+ }
520
+
521
+ /**
522
+ * HIGH 8: tetra-core version should be recent.
423
523
  */
424
524
  function checkTetraCoreVersion(files) {
425
525
  const pkgContent = files.backendPackageJson?.content
@@ -771,6 +871,7 @@ export async function runDoctorAudit(projectRoot) {
771
871
  checkNextThemes(projectRoot),
772
872
  checkCssTokens(files),
773
873
  checkDarkMode(files),
874
+ checkTetraUiSourcePath(files, projectRoot),
774
875
  // HIGH
775
876
  checkTetraCoreVersion(files),
776
877
  appShellCheck,
@@ -33,46 +33,46 @@ export async function run(config, projectRoot) {
33
33
  }
34
34
  }
35
35
 
36
- // Find all route and controller files
37
- const patterns = [
38
- '**/routes/**/*.ts',
39
- '**/routes/**/*.js',
40
- '**/controllers/**/*.ts',
41
- '**/controllers/**/*.js',
42
- '**/features/**/controllers/**/*.ts',
43
- '**/features/**/*.controller.ts'
44
- ]
45
-
46
- let files = []
47
- for (const pattern of patterns) {
48
- const found = await glob(pattern, {
49
- cwd: projectRoot,
50
- ignore: [
51
- ...(config.ignore || []),
52
- 'node_modules/**',
53
- '**/node_modules/**',
54
- 'dist/**',
55
- '**/dist/**',
56
- 'build/**',
57
- '**/build/**',
58
- '**/*.test.*',
59
- '**/*.spec.*',
60
- '**/*.d.ts',
61
- '**/*.js.map'
62
- ]
63
- })
64
- files.push(...found)
65
- }
66
-
67
- // Deduplicate
68
- files = [...new Set(files)]
36
+ // Discover ALL backend source files (no path conventions assumed).
37
+ // Strategy: scan every .ts/.js file and detect Express route handlers by content.
38
+ // This catches feature folders, custom layouts, monorepo backends, etc.
39
+ const candidates = await glob('**/*.{ts,js,mts,cts,mjs,cjs}', {
40
+ cwd: projectRoot,
41
+ ignore: [
42
+ ...(config.ignore || []),
43
+ 'node_modules/**',
44
+ '**/node_modules/**',
45
+ 'dist/**',
46
+ '**/dist/**',
47
+ 'build/**',
48
+ '**/build/**',
49
+ '.next/**',
50
+ '**/.next/**',
51
+ 'coverage/**',
52
+ '**/coverage/**',
53
+ '**/*.test.*',
54
+ '**/*.spec.*',
55
+ '**/*.d.ts',
56
+ '**/*.js.map',
57
+ // Frontend folders – these are not Express backends
58
+ 'frontend/**',
59
+ 'web/**',
60
+ 'app/**',
61
+ 'pages/**',
62
+ 'src/app/**',
63
+ 'src/pages/**'
64
+ ]
65
+ })
66
+
67
+ const files = [...new Set(candidates)]
69
68
 
70
69
  if (files.length === 0) {
71
70
  results.skipped = true
72
- results.skipReason = 'No route or controller files found'
71
+ results.skipReason = 'No source files found to scan'
73
72
  return results
74
73
  }
75
74
 
75
+ let backendFilesScanned = 0
76
76
  for (const file of files) {
77
77
  const filePath = `${projectRoot}/${file}`
78
78
  let content
@@ -82,10 +82,21 @@ export async function run(config, projectRoot) {
82
82
  continue
83
83
  }
84
84
 
85
+ if (!isExpressRouteFile(content)) continue
86
+
87
+ backendFilesScanned++
85
88
  const lines = content.split('\n')
86
89
  analyzeFile(file, lines, results)
87
90
  }
88
91
 
92
+ results.info.backendFilesScanned = backendFilesScanned
93
+
94
+ if (backendFilesScanned === 0) {
95
+ results.skipped = true
96
+ results.skipReason = 'No Express route handlers detected in scanned files'
97
+ return results
98
+ }
99
+
89
100
  results.passed = results.findings.filter(f => f.severity === 'critical' || f.severity === 'high').length === 0
90
101
  results.info.violations = results.findings.length
91
102
  results.info.compliant = results.info.totalEndpoints - results.info.violations
@@ -93,6 +104,30 @@ export async function run(config, projectRoot) {
93
104
  return results
94
105
  }
95
106
 
107
+ /**
108
+ * Heuristic: does this file contain Express-style route handlers that
109
+ * eventually call res.json() or res.status().json()?
110
+ *
111
+ * We require BOTH:
112
+ * 1. An indicator that this is server code (express import / Request type / router/app verb)
113
+ * 2. At least one res.json() / res.status(...).json() call
114
+ *
115
+ * This avoids scanning frontend files that happen to use the variable name `res`.
116
+ */
117
+ function isExpressRouteFile(content) {
118
+ const hasResJsonCall = /\bres\s*(?:\.\s*status\s*\([^)]*\)\s*)?\.\s*json\s*\(/.test(content)
119
+ if (!hasResJsonCall) return false
120
+
121
+ const looksLikeExpress =
122
+ /from\s+['"]express['"]/.test(content) ||
123
+ /require\s*\(\s*['"]express['"]\s*\)/.test(content) ||
124
+ /\b(?:Request|Response|NextFunction|Router)\b/.test(content) ||
125
+ /\b(?:router|app)\s*\.\s*(?:get|post|put|patch|delete|all|use)\s*\(/.test(content) ||
126
+ /\bexpress\s*\(\s*\)/.test(content)
127
+
128
+ return looksLikeExpress
129
+ }
130
+
96
131
  function analyzeFile(file, lines, results) {
97
132
  for (let i = 0; i < lines.length; i++) {
98
133
  const line = lines[i]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.20.26",
3
+ "version": "1.21.1",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },