@soulbatical/tetra-dev-toolkit 1.3.3 → 1.4.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/bin/tetra-setup.js +463 -2
- package/lib/checks/health/conventional-commits.js +164 -0
- package/lib/checks/health/coverage-thresholds.js +199 -0
- package/lib/checks/health/dependency-cruiser.js +113 -0
- package/lib/checks/health/eslint-security.js +172 -0
- package/lib/checks/health/index.js +7 -1
- package/lib/checks/health/prettier.js +162 -0
- package/lib/checks/health/scanner.js +14 -2
- package/lib/checks/health/types.js +1 -1
- package/lib/checks/health/typescript-strict.js +143 -0
- package/package.json +5 -3
package/bin/tetra-setup.js
CHANGED
|
@@ -9,10 +9,16 @@
|
|
|
9
9
|
* - Configuration file
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
12
|
-
* tetra-setup # Interactive setup
|
|
12
|
+
* tetra-setup # Interactive setup (hooks + ci + config)
|
|
13
13
|
* tetra-setup hooks # Setup Husky hooks only
|
|
14
14
|
* tetra-setup ci # Setup GitHub Actions only
|
|
15
15
|
* tetra-setup config # Create .tetra-quality.json
|
|
16
|
+
* tetra-setup prettier # Setup Prettier + lint-staged
|
|
17
|
+
* tetra-setup eslint-security # Setup ESLint security plugins
|
|
18
|
+
* tetra-setup coverage # Setup test coverage thresholds
|
|
19
|
+
* tetra-setup lighthouse # Setup Lighthouse CI budgets
|
|
20
|
+
* tetra-setup commitlint # Setup commitlint + commit-msg hook
|
|
21
|
+
* tetra-setup depcruiser # Setup dependency-cruiser
|
|
16
22
|
*/
|
|
17
23
|
|
|
18
24
|
import { program } from 'commander'
|
|
@@ -26,7 +32,7 @@ program
|
|
|
26
32
|
.name('tetra-setup')
|
|
27
33
|
.description('Setup Tetra Dev Toolkit in your project')
|
|
28
34
|
.version('1.2.0')
|
|
29
|
-
.argument('[component]', 'Component to setup: hooks, ci, config, or all
|
|
35
|
+
.argument('[component]', 'Component to setup: hooks, ci, config, prettier, eslint-security, coverage, lighthouse, commitlint, depcruiser, or all')
|
|
30
36
|
.option('-f, --force', 'Overwrite existing files')
|
|
31
37
|
.action(async (component, options) => {
|
|
32
38
|
console.log('')
|
|
@@ -49,8 +55,27 @@ program
|
|
|
49
55
|
case 'config':
|
|
50
56
|
await setupConfig(options)
|
|
51
57
|
break
|
|
58
|
+
case 'prettier':
|
|
59
|
+
await setupPrettier(options)
|
|
60
|
+
break
|
|
61
|
+
case 'eslint-security':
|
|
62
|
+
await setupEslintSecurity(options)
|
|
63
|
+
break
|
|
64
|
+
case 'coverage':
|
|
65
|
+
await setupCoverage(options)
|
|
66
|
+
break
|
|
67
|
+
case 'lighthouse':
|
|
68
|
+
await setupLighthouse(options)
|
|
69
|
+
break
|
|
70
|
+
case 'commitlint':
|
|
71
|
+
await setupCommitlint(options)
|
|
72
|
+
break
|
|
73
|
+
case 'depcruiser':
|
|
74
|
+
await setupDepCruiser(options)
|
|
75
|
+
break
|
|
52
76
|
default:
|
|
53
77
|
console.log(`Unknown component: ${comp}`)
|
|
78
|
+
console.log('Available: hooks, ci, config, prettier, eslint-security, coverage, lighthouse, commitlint, depcruiser')
|
|
54
79
|
}
|
|
55
80
|
}
|
|
56
81
|
|
|
@@ -258,4 +283,440 @@ async function setupConfig(options) {
|
|
|
258
283
|
}
|
|
259
284
|
}
|
|
260
285
|
|
|
286
|
+
// ─── Prettier ───────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
async function setupPrettier(options) {
|
|
289
|
+
console.log('🎨 Setting up Prettier + lint-staged...')
|
|
290
|
+
|
|
291
|
+
const prettierPath = join(projectRoot, '.prettierrc.json')
|
|
292
|
+
if (!existsSync(prettierPath) || options.force) {
|
|
293
|
+
const prettierConfig = {
|
|
294
|
+
semi: true,
|
|
295
|
+
trailingComma: 'es5',
|
|
296
|
+
singleQuote: true,
|
|
297
|
+
printWidth: 100,
|
|
298
|
+
tabWidth: 2,
|
|
299
|
+
useTabs: false,
|
|
300
|
+
arrowParens: 'always',
|
|
301
|
+
endOfLine: 'lf'
|
|
302
|
+
}
|
|
303
|
+
writeFileSync(prettierPath, JSON.stringify(prettierConfig, null, 2) + '\n')
|
|
304
|
+
console.log(' ✅ Created .prettierrc.json')
|
|
305
|
+
} else {
|
|
306
|
+
console.log(' ⏭️ .prettierrc.json already exists')
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const ignorePath = join(projectRoot, '.prettierignore')
|
|
310
|
+
if (!existsSync(ignorePath) || options.force) {
|
|
311
|
+
const ignoreContent = `node_modules
|
|
312
|
+
dist
|
|
313
|
+
build
|
|
314
|
+
.next
|
|
315
|
+
coverage
|
|
316
|
+
*.min.js
|
|
317
|
+
*.min.css
|
|
318
|
+
package-lock.json
|
|
319
|
+
`
|
|
320
|
+
writeFileSync(ignorePath, ignoreContent)
|
|
321
|
+
console.log(' ✅ Created .prettierignore')
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Add lint-staged config
|
|
325
|
+
const lintStagedPath = join(projectRoot, '.lintstagedrc.json')
|
|
326
|
+
if (!existsSync(lintStagedPath) || options.force) {
|
|
327
|
+
const lintStagedConfig = {
|
|
328
|
+
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
|
|
329
|
+
'*.{json,css,scss,md,yml,yaml}': ['prettier --write']
|
|
330
|
+
}
|
|
331
|
+
writeFileSync(lintStagedPath, JSON.stringify(lintStagedConfig, null, 2) + '\n')
|
|
332
|
+
console.log(' ✅ Created .lintstagedrc.json')
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Add scripts to package.json
|
|
336
|
+
const packagePath = join(projectRoot, 'package.json')
|
|
337
|
+
if (existsSync(packagePath)) {
|
|
338
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
|
|
339
|
+
pkg.scripts = pkg.scripts || {}
|
|
340
|
+
let changed = false
|
|
341
|
+
|
|
342
|
+
if (!pkg.scripts.format) {
|
|
343
|
+
pkg.scripts.format = 'prettier --write .'
|
|
344
|
+
changed = true
|
|
345
|
+
}
|
|
346
|
+
if (!pkg.scripts['format:check']) {
|
|
347
|
+
pkg.scripts['format:check'] = 'prettier --check .'
|
|
348
|
+
changed = true
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (changed) {
|
|
352
|
+
writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
|
|
353
|
+
console.log(' ✅ Added format scripts to package.json')
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.log(' 📦 Run: npm install --save-dev prettier lint-staged')
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ─── ESLint Security ────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
async function setupEslintSecurity(options) {
|
|
363
|
+
console.log('🔒 Setting up ESLint security plugins...')
|
|
364
|
+
|
|
365
|
+
// Check for existing ESLint config
|
|
366
|
+
const eslintConfigs = [
|
|
367
|
+
'.eslintrc.js', '.eslintrc.cjs', '.eslintrc.json',
|
|
368
|
+
'eslint.config.js', 'eslint.config.mjs',
|
|
369
|
+
'backend/.eslintrc.js', 'backend/eslint.config.js'
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
let hasEslint = false
|
|
373
|
+
for (const config of eslintConfigs) {
|
|
374
|
+
if (existsSync(join(projectRoot, config))) {
|
|
375
|
+
hasEslint = true
|
|
376
|
+
console.log(` Found existing config: ${config}`)
|
|
377
|
+
break
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!hasEslint) {
|
|
382
|
+
// Create a new ESLint flat config with security plugins
|
|
383
|
+
const eslintPath = join(projectRoot, 'eslint.config.js')
|
|
384
|
+
if (!existsSync(eslintPath) || options.force) {
|
|
385
|
+
const eslintContent = `import js from '@eslint/js'
|
|
386
|
+
import security from 'eslint-plugin-security'
|
|
387
|
+
import sonarjs from 'eslint-plugin-sonarjs'
|
|
388
|
+
|
|
389
|
+
export default [
|
|
390
|
+
js.configs.recommended,
|
|
391
|
+
{
|
|
392
|
+
plugins: {
|
|
393
|
+
security,
|
|
394
|
+
sonarjs
|
|
395
|
+
},
|
|
396
|
+
rules: {
|
|
397
|
+
// Security rules
|
|
398
|
+
'security/detect-unsafe-regex': 'error',
|
|
399
|
+
'security/detect-eval-with-expression': 'error',
|
|
400
|
+
'security/detect-no-csrf-before-method-override': 'error',
|
|
401
|
+
'security/detect-non-literal-regexp': 'warn',
|
|
402
|
+
'security/detect-object-injection': 'off', // too many false positives
|
|
403
|
+
'security/detect-possible-timing-attacks': 'warn',
|
|
404
|
+
|
|
405
|
+
// SonarJS rules
|
|
406
|
+
'sonarjs/cognitive-complexity': ['warn', 25],
|
|
407
|
+
'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }],
|
|
408
|
+
'sonarjs/slow-regex': 'warn',
|
|
409
|
+
|
|
410
|
+
// General safety
|
|
411
|
+
'no-eval': 'error',
|
|
412
|
+
'no-implied-eval': 'error',
|
|
413
|
+
'no-new-func': 'error'
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
]
|
|
417
|
+
`
|
|
418
|
+
writeFileSync(eslintPath, eslintContent)
|
|
419
|
+
console.log(' ✅ Created eslint.config.js with security plugins')
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
console.log(' ⚠️ ESLint config exists — manually add these plugins:')
|
|
423
|
+
console.log(' - eslint-plugin-security')
|
|
424
|
+
console.log(' - eslint-plugin-sonarjs')
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
console.log(' 📦 Run: npm install --save-dev eslint eslint-plugin-security eslint-plugin-sonarjs')
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ─── Coverage Thresholds ────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
async function setupCoverage(options) {
|
|
433
|
+
console.log('📊 Setting up test coverage thresholds...')
|
|
434
|
+
|
|
435
|
+
// Detect test framework
|
|
436
|
+
const packagePath = join(projectRoot, 'package.json')
|
|
437
|
+
let framework = null
|
|
438
|
+
|
|
439
|
+
const packageLocations = ['package.json', 'backend/package.json']
|
|
440
|
+
for (const loc of packageLocations) {
|
|
441
|
+
const pkgPath = join(projectRoot, loc)
|
|
442
|
+
if (!existsSync(pkgPath)) continue
|
|
443
|
+
|
|
444
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
445
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
446
|
+
if (allDeps.vitest) { framework = 'vitest'; break }
|
|
447
|
+
if (allDeps.jest) { framework = 'jest'; break }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (!framework) {
|
|
451
|
+
console.log(' ⚠️ No test framework detected. Install vitest or jest first.')
|
|
452
|
+
console.log(' 📦 Recommended: npm install --save-dev vitest @vitest/coverage-v8')
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (framework === 'vitest') {
|
|
457
|
+
const vitestPath = join(projectRoot, 'vitest.config.ts')
|
|
458
|
+
if (!existsSync(vitestPath) || options.force) {
|
|
459
|
+
const vitestContent = `import { defineConfig } from 'vitest/config'
|
|
460
|
+
|
|
461
|
+
export default defineConfig({
|
|
462
|
+
test: {
|
|
463
|
+
coverage: {
|
|
464
|
+
provider: 'v8',
|
|
465
|
+
reporter: ['text', 'lcov', 'html'],
|
|
466
|
+
reportsDirectory: './coverage',
|
|
467
|
+
thresholds: {
|
|
468
|
+
statements: 70,
|
|
469
|
+
branches: 60,
|
|
470
|
+
functions: 70,
|
|
471
|
+
lines: 70
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
`
|
|
477
|
+
writeFileSync(vitestPath, vitestContent)
|
|
478
|
+
console.log(' ✅ Created vitest.config.ts with coverage thresholds (70/60/70/70)')
|
|
479
|
+
}
|
|
480
|
+
console.log(' 📦 Run: npm install --save-dev @vitest/coverage-v8')
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (framework === 'jest') {
|
|
484
|
+
console.log(' ℹ️ Add to your jest.config.js:')
|
|
485
|
+
console.log(`
|
|
486
|
+
coverageThreshold: {
|
|
487
|
+
global: {
|
|
488
|
+
statements: 70,
|
|
489
|
+
branches: 60,
|
|
490
|
+
functions: 70,
|
|
491
|
+
lines: 70
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
`)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Add coverage script
|
|
498
|
+
if (existsSync(packagePath)) {
|
|
499
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
|
|
500
|
+
pkg.scripts = pkg.scripts || {}
|
|
501
|
+
if (!pkg.scripts['test:coverage']) {
|
|
502
|
+
pkg.scripts['test:coverage'] = framework === 'vitest'
|
|
503
|
+
? 'vitest run --coverage'
|
|
504
|
+
: 'jest --coverage'
|
|
505
|
+
writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
|
|
506
|
+
console.log(' ✅ Added test:coverage script to package.json')
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ─── Lighthouse ─────────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
async function setupLighthouse(options) {
|
|
514
|
+
console.log('🏎️ Setting up Lighthouse CI...')
|
|
515
|
+
|
|
516
|
+
const lighthousercPath = join(projectRoot, 'lighthouserc.json')
|
|
517
|
+
if (!existsSync(lighthousercPath) || options.force) {
|
|
518
|
+
const config = {
|
|
519
|
+
ci: {
|
|
520
|
+
collect: {
|
|
521
|
+
numberOfRuns: 3,
|
|
522
|
+
settings: {
|
|
523
|
+
preset: 'desktop'
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
assert: {
|
|
527
|
+
assertions: {
|
|
528
|
+
'categories:performance': ['warn', { minScore: 0.7 }],
|
|
529
|
+
'categories:seo': ['error', { minScore: 0.9 }],
|
|
530
|
+
'categories:accessibility': ['warn', { minScore: 0.85 }],
|
|
531
|
+
'categories:best-practices': ['warn', { minScore: 0.8 }],
|
|
532
|
+
'first-contentful-paint': ['warn', { maxNumericValue: 2500 }],
|
|
533
|
+
'largest-contentful-paint': ['warn', { maxNumericValue: 3500 }],
|
|
534
|
+
'cumulative-layout-shift': ['warn', { maxNumericValue: 0.1 }],
|
|
535
|
+
'total-blocking-time': ['warn', { maxNumericValue: 500 }]
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
writeFileSync(lighthousercPath, JSON.stringify(config, null, 2) + '\n')
|
|
541
|
+
console.log(' ✅ Created lighthouserc.json')
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const budgetPath = join(projectRoot, 'lighthouse-budget.json')
|
|
545
|
+
if (!existsSync(budgetPath) || options.force) {
|
|
546
|
+
const budget = [{
|
|
547
|
+
path: '/*',
|
|
548
|
+
resourceSizes: [
|
|
549
|
+
{ resourceType: 'script', budget: 500 },
|
|
550
|
+
{ resourceType: 'image', budget: 300 },
|
|
551
|
+
{ resourceType: 'stylesheet', budget: 100 },
|
|
552
|
+
{ resourceType: 'font', budget: 100 },
|
|
553
|
+
{ resourceType: 'total', budget: 1500 }
|
|
554
|
+
],
|
|
555
|
+
resourceCounts: [
|
|
556
|
+
{ resourceType: 'third-party', budget: 15 }
|
|
557
|
+
]
|
|
558
|
+
}]
|
|
559
|
+
writeFileSync(budgetPath, JSON.stringify(budget, null, 2) + '\n')
|
|
560
|
+
console.log(' ✅ Created lighthouse-budget.json')
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Create GitHub Actions workflow
|
|
564
|
+
const workflowDir = join(projectRoot, '.github/workflows')
|
|
565
|
+
if (!existsSync(workflowDir)) mkdirSync(workflowDir, { recursive: true })
|
|
566
|
+
|
|
567
|
+
const workflowPath = join(workflowDir, 'lighthouse.yml')
|
|
568
|
+
if (!existsSync(workflowPath) || options.force) {
|
|
569
|
+
const workflow = `name: Lighthouse CI
|
|
570
|
+
|
|
571
|
+
on:
|
|
572
|
+
push:
|
|
573
|
+
branches: [main, master]
|
|
574
|
+
pull_request:
|
|
575
|
+
branches: [main, master]
|
|
576
|
+
|
|
577
|
+
jobs:
|
|
578
|
+
lighthouse:
|
|
579
|
+
name: Lighthouse Audit
|
|
580
|
+
runs-on: ubuntu-latest
|
|
581
|
+
steps:
|
|
582
|
+
- uses: actions/checkout@v4
|
|
583
|
+
- name: Audit URLs
|
|
584
|
+
uses: treosh/lighthouse-ci-action@v12
|
|
585
|
+
with:
|
|
586
|
+
configPath: ./lighthouserc.json
|
|
587
|
+
budgetPath: ./lighthouse-budget.json
|
|
588
|
+
uploadArtifacts: true
|
|
589
|
+
temporaryPublicStorage: true
|
|
590
|
+
`
|
|
591
|
+
writeFileSync(workflowPath, workflow)
|
|
592
|
+
console.log(' ✅ Created .github/workflows/lighthouse.yml')
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
console.log(' ℹ️ Update lighthouserc.json with your production URLs')
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ─── Commitlint ─────────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
async function setupCommitlint(options) {
|
|
601
|
+
console.log('📝 Setting up commitlint...')
|
|
602
|
+
|
|
603
|
+
const commitlintPath = join(projectRoot, 'commitlint.config.js')
|
|
604
|
+
if (!existsSync(commitlintPath) || options.force) {
|
|
605
|
+
const content = `export default {
|
|
606
|
+
extends: ['@commitlint/config-conventional'],
|
|
607
|
+
rules: {
|
|
608
|
+
'type-enum': [2, 'always', [
|
|
609
|
+
'feat', 'fix', 'docs', 'style', 'refactor',
|
|
610
|
+
'perf', 'test', 'build', 'ci', 'chore', 'security'
|
|
611
|
+
]],
|
|
612
|
+
'subject-case': [2, 'never', ['start-case', 'pascal-case', 'upper-case']],
|
|
613
|
+
'header-max-length': [2, 'always', 100]
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
`
|
|
617
|
+
writeFileSync(commitlintPath, content)
|
|
618
|
+
console.log(' ✅ Created commitlint.config.js')
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Add commit-msg hook
|
|
622
|
+
const huskyDir = join(projectRoot, '.husky')
|
|
623
|
+
if (existsSync(huskyDir)) {
|
|
624
|
+
const commitMsgPath = join(huskyDir, 'commit-msg')
|
|
625
|
+
if (!existsSync(commitMsgPath) || options.force) {
|
|
626
|
+
const hookContent = `#!/bin/sh
|
|
627
|
+
npx --no -- commitlint --edit \${1}
|
|
628
|
+
`
|
|
629
|
+
writeFileSync(commitMsgPath, hookContent)
|
|
630
|
+
execSync(`chmod +x ${commitMsgPath}`)
|
|
631
|
+
console.log(' ✅ Created .husky/commit-msg hook')
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
console.log(' ⚠️ No .husky directory — run "tetra-setup hooks" first')
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
console.log(' 📦 Run: npm install --save-dev @commitlint/cli @commitlint/config-conventional')
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ─── Dependency Cruiser ─────────────────────────────────────────
|
|
641
|
+
|
|
642
|
+
async function setupDepCruiser(options) {
|
|
643
|
+
console.log('🔍 Setting up dependency-cruiser...')
|
|
644
|
+
|
|
645
|
+
const configPath = join(projectRoot, '.dependency-cruiser.cjs')
|
|
646
|
+
if (!existsSync(configPath) || options.force) {
|
|
647
|
+
const content = `/** @type {import('dependency-cruiser').IConfiguration} */
|
|
648
|
+
module.exports = {
|
|
649
|
+
forbidden: [
|
|
650
|
+
{
|
|
651
|
+
name: 'no-circular',
|
|
652
|
+
severity: 'error',
|
|
653
|
+
comment: 'Circular dependencies cause maintenance problems and unexpected behavior',
|
|
654
|
+
from: {},
|
|
655
|
+
to: { circular: true }
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
name: 'no-orphans',
|
|
659
|
+
severity: 'info',
|
|
660
|
+
comment: 'Modules without incoming or outgoing connections',
|
|
661
|
+
from: { orphan: true, pathNot: ['\\\\.(test|spec)\\\\.(ts|js)$', 'types\\\\.ts$'] },
|
|
662
|
+
to: {}
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
name: 'no-dev-deps-in-src',
|
|
666
|
+
severity: 'error',
|
|
667
|
+
comment: 'devDependencies should not be imported in production code',
|
|
668
|
+
from: { path: '^src', pathNot: '\\\\.(test|spec)\\\\.' },
|
|
669
|
+
to: { dependencyTypes: ['npm-dev'] }
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: 'no-deprecated-core',
|
|
673
|
+
severity: 'warn',
|
|
674
|
+
comment: 'Deprecated Node.js core modules',
|
|
675
|
+
from: {},
|
|
676
|
+
to: { dependencyTypes: ['core'], path: '^(punycode|domain|constants|sys|_linklist|_stream_wrap)$' }
|
|
677
|
+
}
|
|
678
|
+
],
|
|
679
|
+
options: {
|
|
680
|
+
doNotFollow: { path: 'node_modules' },
|
|
681
|
+
tsPreCompilationDeps: true,
|
|
682
|
+
tsConfig: { fileName: 'tsconfig.json' },
|
|
683
|
+
enhancedResolveOptions: {
|
|
684
|
+
exportsFields: ['exports'],
|
|
685
|
+
conditionNames: ['import', 'require', 'node', 'default']
|
|
686
|
+
},
|
|
687
|
+
reporterOptions: {
|
|
688
|
+
dot: { collapsePattern: 'node_modules/[^/]+' }
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
`
|
|
693
|
+
writeFileSync(configPath, content)
|
|
694
|
+
console.log(' ✅ Created .dependency-cruiser.cjs')
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Add scripts
|
|
698
|
+
const packagePath = join(projectRoot, 'package.json')
|
|
699
|
+
if (existsSync(packagePath)) {
|
|
700
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
|
|
701
|
+
pkg.scripts = pkg.scripts || {}
|
|
702
|
+
let changed = false
|
|
703
|
+
|
|
704
|
+
if (!pkg.scripts['check:deps']) {
|
|
705
|
+
pkg.scripts['check:deps'] = 'depcruise src --config .dependency-cruiser.cjs'
|
|
706
|
+
changed = true
|
|
707
|
+
}
|
|
708
|
+
if (!pkg.scripts['check:circular']) {
|
|
709
|
+
pkg.scripts['check:circular'] = 'depcruise src --config .dependency-cruiser.cjs --output-type err-only'
|
|
710
|
+
changed = true
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (changed) {
|
|
714
|
+
writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
|
|
715
|
+
console.log(' ✅ Added check:deps and check:circular scripts')
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
console.log(' 📦 Run: npm install --save-dev dependency-cruiser')
|
|
720
|
+
}
|
|
721
|
+
|
|
261
722
|
program.parse()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: Conventional Commits
|
|
3
|
+
*
|
|
4
|
+
* Checks for commit message convention enforcement and changelog generation.
|
|
5
|
+
* Score: up to 2 points:
|
|
6
|
+
* +1 for commitlint config OR standard-version/semantic-release config
|
|
7
|
+
* +1 for commit-msg hook OR release script
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from 'fs'
|
|
11
|
+
import { join } from 'path'
|
|
12
|
+
import { createCheck } from './types.js'
|
|
13
|
+
|
|
14
|
+
export async function check(projectPath) {
|
|
15
|
+
const result = createCheck('conventional-commits', 2, {
|
|
16
|
+
hasCommitlint: false,
|
|
17
|
+
commitlintConfig: null,
|
|
18
|
+
hasVersionTool: false,
|
|
19
|
+
versionTool: null,
|
|
20
|
+
hasCommitMsgHook: false,
|
|
21
|
+
hasReleaseScript: false,
|
|
22
|
+
releaseScript: null,
|
|
23
|
+
hasChangelog: false,
|
|
24
|
+
message: ''
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Check for commitlint config
|
|
28
|
+
const commitlintConfigs = [
|
|
29
|
+
'commitlint.config.js',
|
|
30
|
+
'commitlint.config.cjs',
|
|
31
|
+
'commitlint.config.mjs',
|
|
32
|
+
'commitlint.config.ts',
|
|
33
|
+
'.commitlintrc',
|
|
34
|
+
'.commitlintrc.json',
|
|
35
|
+
'.commitlintrc.yml',
|
|
36
|
+
'.commitlintrc.js',
|
|
37
|
+
'.commitlintrc.cjs'
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
for (const config of commitlintConfigs) {
|
|
41
|
+
if (existsSync(join(projectPath, config))) {
|
|
42
|
+
result.details.hasCommitlint = true
|
|
43
|
+
result.details.commitlintConfig = config
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check package.json for commitlint key
|
|
49
|
+
const pkgPath = join(projectPath, 'package.json')
|
|
50
|
+
let rootPkg = null
|
|
51
|
+
if (existsSync(pkgPath)) {
|
|
52
|
+
try {
|
|
53
|
+
rootPkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
54
|
+
if (rootPkg.commitlint) {
|
|
55
|
+
result.details.hasCommitlint = true
|
|
56
|
+
result.details.commitlintConfig = 'package.json (commitlint key)'
|
|
57
|
+
}
|
|
58
|
+
} catch { /* ignore */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check for versioning tools
|
|
62
|
+
const versionConfigs = [
|
|
63
|
+
{ file: '.versionrc', tool: 'standard-version' },
|
|
64
|
+
{ file: '.versionrc.json', tool: 'standard-version' },
|
|
65
|
+
{ file: '.versionrc.js', tool: 'standard-version' },
|
|
66
|
+
{ file: 'backend/.versionrc.json', tool: 'standard-version' },
|
|
67
|
+
{ file: '.releaserc', tool: 'semantic-release' },
|
|
68
|
+
{ file: '.releaserc.json', tool: 'semantic-release' },
|
|
69
|
+
{ file: '.releaserc.js', tool: 'semantic-release' },
|
|
70
|
+
{ file: 'release.config.js', tool: 'semantic-release' },
|
|
71
|
+
{ file: '.changeset/config.json', tool: 'changesets' }
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
for (const { file, tool } of versionConfigs) {
|
|
75
|
+
if (existsSync(join(projectPath, file))) {
|
|
76
|
+
result.details.hasVersionTool = true
|
|
77
|
+
result.details.versionTool = tool
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check dependencies for version tools
|
|
83
|
+
const packageLocations = ['package.json', 'backend/package.json']
|
|
84
|
+
for (const loc of packageLocations) {
|
|
85
|
+
const path = join(projectPath, loc)
|
|
86
|
+
if (!existsSync(path)) continue
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const pkg = JSON.parse(readFileSync(path, 'utf-8'))
|
|
90
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
91
|
+
|
|
92
|
+
if (allDeps['standard-version'] && !result.details.hasVersionTool) {
|
|
93
|
+
result.details.hasVersionTool = true
|
|
94
|
+
result.details.versionTool = 'standard-version'
|
|
95
|
+
}
|
|
96
|
+
if (allDeps['semantic-release'] && !result.details.hasVersionTool) {
|
|
97
|
+
result.details.hasVersionTool = true
|
|
98
|
+
result.details.versionTool = 'semantic-release'
|
|
99
|
+
}
|
|
100
|
+
if (allDeps['@changesets/cli'] && !result.details.hasVersionTool) {
|
|
101
|
+
result.details.hasVersionTool = true
|
|
102
|
+
result.details.versionTool = 'changesets'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check for release scripts
|
|
106
|
+
const scripts = pkg.scripts || {}
|
|
107
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
108
|
+
if (typeof cmd !== 'string') continue
|
|
109
|
+
if (name.includes('release') || cmd.includes('standard-version') ||
|
|
110
|
+
cmd.includes('semantic-release') || cmd.includes('changeset')) {
|
|
111
|
+
result.details.hasReleaseScript = true
|
|
112
|
+
result.details.releaseScript = `${loc} → ${name}`
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch { /* ignore */ }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check for commit-msg hook
|
|
120
|
+
const commitMsgHooks = [
|
|
121
|
+
'.husky/commit-msg',
|
|
122
|
+
'backend/.husky/commit-msg'
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
for (const hook of commitMsgHooks) {
|
|
126
|
+
if (existsSync(join(projectPath, hook))) {
|
|
127
|
+
result.details.hasCommitMsgHook = true
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check for CHANGELOG
|
|
133
|
+
if (existsSync(join(projectPath, 'CHANGELOG.md')) ||
|
|
134
|
+
existsSync(join(projectPath, 'backend/CHANGELOG.md'))) {
|
|
135
|
+
result.details.hasChangelog = true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Scoring
|
|
139
|
+
if (result.details.hasCommitlint || result.details.hasVersionTool) {
|
|
140
|
+
result.score += 1
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (result.details.hasCommitMsgHook || result.details.hasReleaseScript) {
|
|
144
|
+
result.score += 1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
result.score = Math.min(result.score, result.maxScore)
|
|
148
|
+
|
|
149
|
+
// Determine status
|
|
150
|
+
if (result.score === 0) {
|
|
151
|
+
result.status = 'warning'
|
|
152
|
+
result.details.message = 'No commit convention or release management configured'
|
|
153
|
+
} else if (result.score < 2) {
|
|
154
|
+
result.status = 'warning'
|
|
155
|
+
const tool = result.details.hasCommitlint ? 'commitlint' :
|
|
156
|
+
result.details.versionTool || 'commit tool'
|
|
157
|
+
result.details.message = `${tool} configured but no enforcement hook or release script`
|
|
158
|
+
} else {
|
|
159
|
+
const tool = result.details.versionTool || 'commitlint'
|
|
160
|
+
result.details.message = `Commit conventions enforced via ${tool}`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result
|
|
164
|
+
}
|