@soulbatical/tetra-dev-toolkit 1.3.3 → 1.5.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 +555 -2
- package/lib/checks/health/bundle-size.js +156 -0
- package/lib/checks/health/conventional-commits.js +164 -0
- package/lib/checks/health/coverage-thresholds.js +199 -0
- package/lib/checks/health/dependency-automation.js +116 -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 +12 -1
- package/lib/checks/health/knip.js +135 -0
- package/lib/checks/health/license-audit.js +133 -0
- package/lib/checks/health/prettier.js +162 -0
- package/lib/checks/health/sast.js +166 -0
- package/lib/checks/health/scanner.js +24 -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,18 @@
|
|
|
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
|
|
22
|
+
* tetra-setup knip # Setup Knip (dead code detection)
|
|
23
|
+
* tetra-setup license-audit # Setup license compliance checking
|
|
16
24
|
*/
|
|
17
25
|
|
|
18
26
|
import { program } from 'commander'
|
|
@@ -26,7 +34,7 @@ program
|
|
|
26
34
|
.name('tetra-setup')
|
|
27
35
|
.description('Setup Tetra Dev Toolkit in your project')
|
|
28
36
|
.version('1.2.0')
|
|
29
|
-
.argument('[component]', 'Component to setup: hooks, ci, config, or all
|
|
37
|
+
.argument('[component]', 'Component to setup: hooks, ci, config, prettier, eslint-security, coverage, lighthouse, commitlint, depcruiser, knip, license-audit, or all')
|
|
30
38
|
.option('-f, --force', 'Overwrite existing files')
|
|
31
39
|
.action(async (component, options) => {
|
|
32
40
|
console.log('')
|
|
@@ -49,8 +57,33 @@ program
|
|
|
49
57
|
case 'config':
|
|
50
58
|
await setupConfig(options)
|
|
51
59
|
break
|
|
60
|
+
case 'prettier':
|
|
61
|
+
await setupPrettier(options)
|
|
62
|
+
break
|
|
63
|
+
case 'eslint-security':
|
|
64
|
+
await setupEslintSecurity(options)
|
|
65
|
+
break
|
|
66
|
+
case 'coverage':
|
|
67
|
+
await setupCoverage(options)
|
|
68
|
+
break
|
|
69
|
+
case 'lighthouse':
|
|
70
|
+
await setupLighthouse(options)
|
|
71
|
+
break
|
|
72
|
+
case 'commitlint':
|
|
73
|
+
await setupCommitlint(options)
|
|
74
|
+
break
|
|
75
|
+
case 'depcruiser':
|
|
76
|
+
await setupDepCruiser(options)
|
|
77
|
+
break
|
|
78
|
+
case 'knip':
|
|
79
|
+
await setupKnip(options)
|
|
80
|
+
break
|
|
81
|
+
case 'license-audit':
|
|
82
|
+
await setupLicenseAudit(options)
|
|
83
|
+
break
|
|
52
84
|
default:
|
|
53
85
|
console.log(`Unknown component: ${comp}`)
|
|
86
|
+
console.log('Available: hooks, ci, config, prettier, eslint-security, coverage, lighthouse, commitlint, depcruiser, knip, license-audit')
|
|
54
87
|
}
|
|
55
88
|
}
|
|
56
89
|
|
|
@@ -258,4 +291,524 @@ async function setupConfig(options) {
|
|
|
258
291
|
}
|
|
259
292
|
}
|
|
260
293
|
|
|
294
|
+
// ─── Prettier ───────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
async function setupPrettier(options) {
|
|
297
|
+
console.log('🎨 Setting up Prettier + lint-staged...')
|
|
298
|
+
|
|
299
|
+
const prettierPath = join(projectRoot, '.prettierrc.json')
|
|
300
|
+
if (!existsSync(prettierPath) || options.force) {
|
|
301
|
+
const prettierConfig = {
|
|
302
|
+
semi: true,
|
|
303
|
+
trailingComma: 'es5',
|
|
304
|
+
singleQuote: true,
|
|
305
|
+
printWidth: 100,
|
|
306
|
+
tabWidth: 2,
|
|
307
|
+
useTabs: false,
|
|
308
|
+
arrowParens: 'always',
|
|
309
|
+
endOfLine: 'lf'
|
|
310
|
+
}
|
|
311
|
+
writeFileSync(prettierPath, JSON.stringify(prettierConfig, null, 2) + '\n')
|
|
312
|
+
console.log(' ✅ Created .prettierrc.json')
|
|
313
|
+
} else {
|
|
314
|
+
console.log(' ⏭️ .prettierrc.json already exists')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const ignorePath = join(projectRoot, '.prettierignore')
|
|
318
|
+
if (!existsSync(ignorePath) || options.force) {
|
|
319
|
+
const ignoreContent = `node_modules
|
|
320
|
+
dist
|
|
321
|
+
build
|
|
322
|
+
.next
|
|
323
|
+
coverage
|
|
324
|
+
*.min.js
|
|
325
|
+
*.min.css
|
|
326
|
+
package-lock.json
|
|
327
|
+
`
|
|
328
|
+
writeFileSync(ignorePath, ignoreContent)
|
|
329
|
+
console.log(' ✅ Created .prettierignore')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Add lint-staged config
|
|
333
|
+
const lintStagedPath = join(projectRoot, '.lintstagedrc.json')
|
|
334
|
+
if (!existsSync(lintStagedPath) || options.force) {
|
|
335
|
+
const lintStagedConfig = {
|
|
336
|
+
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
|
|
337
|
+
'*.{json,css,scss,md,yml,yaml}': ['prettier --write']
|
|
338
|
+
}
|
|
339
|
+
writeFileSync(lintStagedPath, JSON.stringify(lintStagedConfig, null, 2) + '\n')
|
|
340
|
+
console.log(' ✅ Created .lintstagedrc.json')
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Add scripts to package.json
|
|
344
|
+
const packagePath = join(projectRoot, 'package.json')
|
|
345
|
+
if (existsSync(packagePath)) {
|
|
346
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
|
|
347
|
+
pkg.scripts = pkg.scripts || {}
|
|
348
|
+
let changed = false
|
|
349
|
+
|
|
350
|
+
if (!pkg.scripts.format) {
|
|
351
|
+
pkg.scripts.format = 'prettier --write .'
|
|
352
|
+
changed = true
|
|
353
|
+
}
|
|
354
|
+
if (!pkg.scripts['format:check']) {
|
|
355
|
+
pkg.scripts['format:check'] = 'prettier --check .'
|
|
356
|
+
changed = true
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (changed) {
|
|
360
|
+
writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
|
|
361
|
+
console.log(' ✅ Added format scripts to package.json')
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.log(' 📦 Run: npm install --save-dev prettier lint-staged')
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ─── ESLint Security ────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
async function setupEslintSecurity(options) {
|
|
371
|
+
console.log('🔒 Setting up ESLint security plugins...')
|
|
372
|
+
|
|
373
|
+
// Check for existing ESLint config
|
|
374
|
+
const eslintConfigs = [
|
|
375
|
+
'.eslintrc.js', '.eslintrc.cjs', '.eslintrc.json',
|
|
376
|
+
'eslint.config.js', 'eslint.config.mjs',
|
|
377
|
+
'backend/.eslintrc.js', 'backend/eslint.config.js'
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
let hasEslint = false
|
|
381
|
+
for (const config of eslintConfigs) {
|
|
382
|
+
if (existsSync(join(projectRoot, config))) {
|
|
383
|
+
hasEslint = true
|
|
384
|
+
console.log(` Found existing config: ${config}`)
|
|
385
|
+
break
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!hasEslint) {
|
|
390
|
+
// Create a new ESLint flat config with security plugins
|
|
391
|
+
const eslintPath = join(projectRoot, 'eslint.config.js')
|
|
392
|
+
if (!existsSync(eslintPath) || options.force) {
|
|
393
|
+
const eslintContent = `import js from '@eslint/js'
|
|
394
|
+
import security from 'eslint-plugin-security'
|
|
395
|
+
import sonarjs from 'eslint-plugin-sonarjs'
|
|
396
|
+
|
|
397
|
+
export default [
|
|
398
|
+
js.configs.recommended,
|
|
399
|
+
{
|
|
400
|
+
plugins: {
|
|
401
|
+
security,
|
|
402
|
+
sonarjs
|
|
403
|
+
},
|
|
404
|
+
rules: {
|
|
405
|
+
// Security rules
|
|
406
|
+
'security/detect-unsafe-regex': 'error',
|
|
407
|
+
'security/detect-eval-with-expression': 'error',
|
|
408
|
+
'security/detect-no-csrf-before-method-override': 'error',
|
|
409
|
+
'security/detect-non-literal-regexp': 'warn',
|
|
410
|
+
'security/detect-object-injection': 'off', // too many false positives
|
|
411
|
+
'security/detect-possible-timing-attacks': 'warn',
|
|
412
|
+
|
|
413
|
+
// SonarJS rules
|
|
414
|
+
'sonarjs/cognitive-complexity': ['warn', 25],
|
|
415
|
+
'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }],
|
|
416
|
+
'sonarjs/slow-regex': 'warn',
|
|
417
|
+
|
|
418
|
+
// General safety
|
|
419
|
+
'no-eval': 'error',
|
|
420
|
+
'no-implied-eval': 'error',
|
|
421
|
+
'no-new-func': 'error'
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
]
|
|
425
|
+
`
|
|
426
|
+
writeFileSync(eslintPath, eslintContent)
|
|
427
|
+
console.log(' ✅ Created eslint.config.js with security plugins')
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
console.log(' ⚠️ ESLint config exists — manually add these plugins:')
|
|
431
|
+
console.log(' - eslint-plugin-security')
|
|
432
|
+
console.log(' - eslint-plugin-sonarjs')
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
console.log(' 📦 Run: npm install --save-dev eslint eslint-plugin-security eslint-plugin-sonarjs')
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ─── Coverage Thresholds ────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
async function setupCoverage(options) {
|
|
441
|
+
console.log('📊 Setting up test coverage thresholds...')
|
|
442
|
+
|
|
443
|
+
// Detect test framework
|
|
444
|
+
const packagePath = join(projectRoot, 'package.json')
|
|
445
|
+
let framework = null
|
|
446
|
+
|
|
447
|
+
const packageLocations = ['package.json', 'backend/package.json']
|
|
448
|
+
for (const loc of packageLocations) {
|
|
449
|
+
const pkgPath = join(projectRoot, loc)
|
|
450
|
+
if (!existsSync(pkgPath)) continue
|
|
451
|
+
|
|
452
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
453
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
454
|
+
if (allDeps.vitest) { framework = 'vitest'; break }
|
|
455
|
+
if (allDeps.jest) { framework = 'jest'; break }
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!framework) {
|
|
459
|
+
console.log(' ⚠️ No test framework detected. Install vitest or jest first.')
|
|
460
|
+
console.log(' 📦 Recommended: npm install --save-dev vitest @vitest/coverage-v8')
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (framework === 'vitest') {
|
|
465
|
+
const vitestPath = join(projectRoot, 'vitest.config.ts')
|
|
466
|
+
if (!existsSync(vitestPath) || options.force) {
|
|
467
|
+
const vitestContent = `import { defineConfig } from 'vitest/config'
|
|
468
|
+
|
|
469
|
+
export default defineConfig({
|
|
470
|
+
test: {
|
|
471
|
+
coverage: {
|
|
472
|
+
provider: 'v8',
|
|
473
|
+
reporter: ['text', 'lcov', 'html'],
|
|
474
|
+
reportsDirectory: './coverage',
|
|
475
|
+
thresholds: {
|
|
476
|
+
statements: 70,
|
|
477
|
+
branches: 60,
|
|
478
|
+
functions: 70,
|
|
479
|
+
lines: 70
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
})
|
|
484
|
+
`
|
|
485
|
+
writeFileSync(vitestPath, vitestContent)
|
|
486
|
+
console.log(' ✅ Created vitest.config.ts with coverage thresholds (70/60/70/70)')
|
|
487
|
+
}
|
|
488
|
+
console.log(' 📦 Run: npm install --save-dev @vitest/coverage-v8')
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (framework === 'jest') {
|
|
492
|
+
console.log(' ℹ️ Add to your jest.config.js:')
|
|
493
|
+
console.log(`
|
|
494
|
+
coverageThreshold: {
|
|
495
|
+
global: {
|
|
496
|
+
statements: 70,
|
|
497
|
+
branches: 60,
|
|
498
|
+
functions: 70,
|
|
499
|
+
lines: 70
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
`)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Add coverage script
|
|
506
|
+
if (existsSync(packagePath)) {
|
|
507
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
|
|
508
|
+
pkg.scripts = pkg.scripts || {}
|
|
509
|
+
if (!pkg.scripts['test:coverage']) {
|
|
510
|
+
pkg.scripts['test:coverage'] = framework === 'vitest'
|
|
511
|
+
? 'vitest run --coverage'
|
|
512
|
+
: 'jest --coverage'
|
|
513
|
+
writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
|
|
514
|
+
console.log(' ✅ Added test:coverage script to package.json')
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ─── Lighthouse ─────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
async function setupLighthouse(options) {
|
|
522
|
+
console.log('🏎️ Setting up Lighthouse CI...')
|
|
523
|
+
|
|
524
|
+
const lighthousercPath = join(projectRoot, 'lighthouserc.json')
|
|
525
|
+
if (!existsSync(lighthousercPath) || options.force) {
|
|
526
|
+
const config = {
|
|
527
|
+
ci: {
|
|
528
|
+
collect: {
|
|
529
|
+
numberOfRuns: 3,
|
|
530
|
+
settings: {
|
|
531
|
+
preset: 'desktop'
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
assert: {
|
|
535
|
+
assertions: {
|
|
536
|
+
'categories:performance': ['warn', { minScore: 0.7 }],
|
|
537
|
+
'categories:seo': ['error', { minScore: 0.9 }],
|
|
538
|
+
'categories:accessibility': ['warn', { minScore: 0.85 }],
|
|
539
|
+
'categories:best-practices': ['warn', { minScore: 0.8 }],
|
|
540
|
+
'first-contentful-paint': ['warn', { maxNumericValue: 2500 }],
|
|
541
|
+
'largest-contentful-paint': ['warn', { maxNumericValue: 3500 }],
|
|
542
|
+
'cumulative-layout-shift': ['warn', { maxNumericValue: 0.1 }],
|
|
543
|
+
'total-blocking-time': ['warn', { maxNumericValue: 500 }]
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
writeFileSync(lighthousercPath, JSON.stringify(config, null, 2) + '\n')
|
|
549
|
+
console.log(' ✅ Created lighthouserc.json')
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const budgetPath = join(projectRoot, 'lighthouse-budget.json')
|
|
553
|
+
if (!existsSync(budgetPath) || options.force) {
|
|
554
|
+
const budget = [{
|
|
555
|
+
path: '/*',
|
|
556
|
+
resourceSizes: [
|
|
557
|
+
{ resourceType: 'script', budget: 500 },
|
|
558
|
+
{ resourceType: 'image', budget: 300 },
|
|
559
|
+
{ resourceType: 'stylesheet', budget: 100 },
|
|
560
|
+
{ resourceType: 'font', budget: 100 },
|
|
561
|
+
{ resourceType: 'total', budget: 1500 }
|
|
562
|
+
],
|
|
563
|
+
resourceCounts: [
|
|
564
|
+
{ resourceType: 'third-party', budget: 15 }
|
|
565
|
+
]
|
|
566
|
+
}]
|
|
567
|
+
writeFileSync(budgetPath, JSON.stringify(budget, null, 2) + '\n')
|
|
568
|
+
console.log(' ✅ Created lighthouse-budget.json')
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Create GitHub Actions workflow
|
|
572
|
+
const workflowDir = join(projectRoot, '.github/workflows')
|
|
573
|
+
if (!existsSync(workflowDir)) mkdirSync(workflowDir, { recursive: true })
|
|
574
|
+
|
|
575
|
+
const workflowPath = join(workflowDir, 'lighthouse.yml')
|
|
576
|
+
if (!existsSync(workflowPath) || options.force) {
|
|
577
|
+
const workflow = `name: Lighthouse CI
|
|
578
|
+
|
|
579
|
+
on:
|
|
580
|
+
push:
|
|
581
|
+
branches: [main, master]
|
|
582
|
+
pull_request:
|
|
583
|
+
branches: [main, master]
|
|
584
|
+
|
|
585
|
+
jobs:
|
|
586
|
+
lighthouse:
|
|
587
|
+
name: Lighthouse Audit
|
|
588
|
+
runs-on: ubuntu-latest
|
|
589
|
+
steps:
|
|
590
|
+
- uses: actions/checkout@v4
|
|
591
|
+
- name: Audit URLs
|
|
592
|
+
uses: treosh/lighthouse-ci-action@v12
|
|
593
|
+
with:
|
|
594
|
+
configPath: ./lighthouserc.json
|
|
595
|
+
budgetPath: ./lighthouse-budget.json
|
|
596
|
+
uploadArtifacts: true
|
|
597
|
+
temporaryPublicStorage: true
|
|
598
|
+
`
|
|
599
|
+
writeFileSync(workflowPath, workflow)
|
|
600
|
+
console.log(' ✅ Created .github/workflows/lighthouse.yml')
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
console.log(' ℹ️ Update lighthouserc.json with your production URLs')
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ─── Commitlint ─────────────────────────────────────────────────
|
|
607
|
+
|
|
608
|
+
async function setupCommitlint(options) {
|
|
609
|
+
console.log('📝 Setting up commitlint...')
|
|
610
|
+
|
|
611
|
+
const commitlintPath = join(projectRoot, 'commitlint.config.js')
|
|
612
|
+
if (!existsSync(commitlintPath) || options.force) {
|
|
613
|
+
const content = `export default {
|
|
614
|
+
extends: ['@commitlint/config-conventional'],
|
|
615
|
+
rules: {
|
|
616
|
+
'type-enum': [2, 'always', [
|
|
617
|
+
'feat', 'fix', 'docs', 'style', 'refactor',
|
|
618
|
+
'perf', 'test', 'build', 'ci', 'chore', 'security'
|
|
619
|
+
]],
|
|
620
|
+
'subject-case': [2, 'never', ['start-case', 'pascal-case', 'upper-case']],
|
|
621
|
+
'header-max-length': [2, 'always', 100]
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
`
|
|
625
|
+
writeFileSync(commitlintPath, content)
|
|
626
|
+
console.log(' ✅ Created commitlint.config.js')
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Add commit-msg hook
|
|
630
|
+
const huskyDir = join(projectRoot, '.husky')
|
|
631
|
+
if (existsSync(huskyDir)) {
|
|
632
|
+
const commitMsgPath = join(huskyDir, 'commit-msg')
|
|
633
|
+
if (!existsSync(commitMsgPath) || options.force) {
|
|
634
|
+
const hookContent = `#!/bin/sh
|
|
635
|
+
npx --no -- commitlint --edit \${1}
|
|
636
|
+
`
|
|
637
|
+
writeFileSync(commitMsgPath, hookContent)
|
|
638
|
+
execSync(`chmod +x ${commitMsgPath}`)
|
|
639
|
+
console.log(' ✅ Created .husky/commit-msg hook')
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
642
|
+
console.log(' ⚠️ No .husky directory — run "tetra-setup hooks" first')
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
console.log(' 📦 Run: npm install --save-dev @commitlint/cli @commitlint/config-conventional')
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ─── Dependency Cruiser ─────────────────────────────────────────
|
|
649
|
+
|
|
650
|
+
async function setupDepCruiser(options) {
|
|
651
|
+
console.log('🔍 Setting up dependency-cruiser...')
|
|
652
|
+
|
|
653
|
+
const configPath = join(projectRoot, '.dependency-cruiser.cjs')
|
|
654
|
+
if (!existsSync(configPath) || options.force) {
|
|
655
|
+
const content = `/** @type {import('dependency-cruiser').IConfiguration} */
|
|
656
|
+
module.exports = {
|
|
657
|
+
forbidden: [
|
|
658
|
+
{
|
|
659
|
+
name: 'no-circular',
|
|
660
|
+
severity: 'error',
|
|
661
|
+
comment: 'Circular dependencies cause maintenance problems and unexpected behavior',
|
|
662
|
+
from: {},
|
|
663
|
+
to: { circular: true }
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: 'no-orphans',
|
|
667
|
+
severity: 'info',
|
|
668
|
+
comment: 'Modules without incoming or outgoing connections',
|
|
669
|
+
from: { orphan: true, pathNot: ['\\\\.(test|spec)\\\\.(ts|js)$', 'types\\\\.ts$'] },
|
|
670
|
+
to: {}
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
name: 'no-dev-deps-in-src',
|
|
674
|
+
severity: 'error',
|
|
675
|
+
comment: 'devDependencies should not be imported in production code',
|
|
676
|
+
from: { path: '^src', pathNot: '\\\\.(test|spec)\\\\.' },
|
|
677
|
+
to: { dependencyTypes: ['npm-dev'] }
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
name: 'no-deprecated-core',
|
|
681
|
+
severity: 'warn',
|
|
682
|
+
comment: 'Deprecated Node.js core modules',
|
|
683
|
+
from: {},
|
|
684
|
+
to: { dependencyTypes: ['core'], path: '^(punycode|domain|constants|sys|_linklist|_stream_wrap)$' }
|
|
685
|
+
}
|
|
686
|
+
],
|
|
687
|
+
options: {
|
|
688
|
+
doNotFollow: { path: 'node_modules' },
|
|
689
|
+
tsPreCompilationDeps: true,
|
|
690
|
+
tsConfig: { fileName: 'tsconfig.json' },
|
|
691
|
+
enhancedResolveOptions: {
|
|
692
|
+
exportsFields: ['exports'],
|
|
693
|
+
conditionNames: ['import', 'require', 'node', 'default']
|
|
694
|
+
},
|
|
695
|
+
reporterOptions: {
|
|
696
|
+
dot: { collapsePattern: 'node_modules/[^/]+' }
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
`
|
|
701
|
+
writeFileSync(configPath, content)
|
|
702
|
+
console.log(' ✅ Created .dependency-cruiser.cjs')
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Add scripts
|
|
706
|
+
const packagePath = join(projectRoot, 'package.json')
|
|
707
|
+
if (existsSync(packagePath)) {
|
|
708
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
|
|
709
|
+
pkg.scripts = pkg.scripts || {}
|
|
710
|
+
let changed = false
|
|
711
|
+
|
|
712
|
+
if (!pkg.scripts['check:deps']) {
|
|
713
|
+
pkg.scripts['check:deps'] = 'depcruise src --config .dependency-cruiser.cjs'
|
|
714
|
+
changed = true
|
|
715
|
+
}
|
|
716
|
+
if (!pkg.scripts['check:circular']) {
|
|
717
|
+
pkg.scripts['check:circular'] = 'depcruise src --config .dependency-cruiser.cjs --output-type err-only'
|
|
718
|
+
changed = true
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (changed) {
|
|
722
|
+
writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
|
|
723
|
+
console.log(' ✅ Added check:deps and check:circular scripts')
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
console.log(' 📦 Run: npm install --save-dev dependency-cruiser')
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ─── Knip (Dead Code Detection) ─────────────────────────────
|
|
731
|
+
|
|
732
|
+
async function setupKnip(options) {
|
|
733
|
+
console.log('🔍 Setting up Knip (dead code detection)...')
|
|
734
|
+
|
|
735
|
+
const knipPath = join(projectRoot, 'knip.config.ts')
|
|
736
|
+
if (!existsSync(knipPath) || options.force) {
|
|
737
|
+
const content = `import type { KnipConfig } from 'knip'
|
|
738
|
+
|
|
739
|
+
const config: KnipConfig = {
|
|
740
|
+
entry: ['src/index.{ts,js}', 'src/main.{ts,tsx}'],
|
|
741
|
+
project: ['src/**/*.{ts,tsx,js,jsx}'],
|
|
742
|
+
ignore: [
|
|
743
|
+
'**/*.test.{ts,tsx}',
|
|
744
|
+
'**/*.spec.{ts,tsx}',
|
|
745
|
+
'**/test/**',
|
|
746
|
+
'**/tests/**'
|
|
747
|
+
],
|
|
748
|
+
ignoreDependencies: [
|
|
749
|
+
// Add dependencies that are used but not detected by Knip
|
|
750
|
+
]
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export default config
|
|
754
|
+
`
|
|
755
|
+
writeFileSync(knipPath, content)
|
|
756
|
+
console.log(' ✅ Created knip.config.ts')
|
|
757
|
+
} else {
|
|
758
|
+
console.log(' ⏭️ knip.config.ts already exists')
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Add scripts
|
|
762
|
+
const packagePath = join(projectRoot, 'package.json')
|
|
763
|
+
if (existsSync(packagePath)) {
|
|
764
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
|
|
765
|
+
pkg.scripts = pkg.scripts || {}
|
|
766
|
+
let changed = false
|
|
767
|
+
|
|
768
|
+
if (!pkg.scripts.knip && !pkg.scripts['check:unused']) {
|
|
769
|
+
pkg.scripts.knip = 'knip'
|
|
770
|
+
pkg.scripts['knip:fix'] = 'knip --fix'
|
|
771
|
+
changed = true
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (changed) {
|
|
775
|
+
writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
|
|
776
|
+
console.log(' ✅ Added knip scripts to package.json')
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
console.log(' 📦 Run: npm install --save-dev knip')
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ─── License Audit ──────────────────────────────────────────
|
|
784
|
+
|
|
785
|
+
async function setupLicenseAudit(options) {
|
|
786
|
+
console.log('📜 Setting up license compliance checking...')
|
|
787
|
+
|
|
788
|
+
// Add license check script
|
|
789
|
+
const packagePath = join(projectRoot, 'package.json')
|
|
790
|
+
if (existsSync(packagePath)) {
|
|
791
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
|
|
792
|
+
pkg.scripts = pkg.scripts || {}
|
|
793
|
+
let changed = false
|
|
794
|
+
|
|
795
|
+
if (!pkg.scripts['check:licenses']) {
|
|
796
|
+
pkg.scripts['check:licenses'] = 'license-checker --production --failOn "GPL-2.0;GPL-3.0;AGPL-1.0;AGPL-3.0" --excludePrivatePackages'
|
|
797
|
+
changed = true
|
|
798
|
+
}
|
|
799
|
+
if (!pkg.scripts['licenses:summary']) {
|
|
800
|
+
pkg.scripts['licenses:summary'] = 'license-checker --production --summary'
|
|
801
|
+
changed = true
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (changed) {
|
|
805
|
+
writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
|
|
806
|
+
console.log(' ✅ Added license check scripts to package.json')
|
|
807
|
+
console.log(' ℹ️ Blocked licenses: GPL-2.0, GPL-3.0, AGPL-1.0, AGPL-3.0')
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
console.log(' 📦 Run: npm install --save-dev license-checker')
|
|
812
|
+
}
|
|
813
|
+
|
|
261
814
|
program.parse()
|