create-qa-architect 5.0.7 → 5.4.3
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/.github/workflows/auto-release.yml +49 -0
- package/.github/workflows/quality.yml +11 -11
- package/.github/workflows/shell-ci.yml.example +82 -0
- package/.github/workflows/shell-quality.yml.example +148 -0
- package/README.md +165 -12
- package/config/shell-ci.yml +82 -0
- package/config/shell-quality.yml +148 -0
- package/docs/ADOPTION-SUMMARY.md +41 -0
- package/docs/ARCHITECTURE-REVIEW.md +67 -0
- package/docs/ARCHITECTURE.md +29 -45
- package/docs/CI-COST-ANALYSIS.md +323 -0
- package/docs/CODE-REVIEW.md +100 -0
- package/docs/REQUIREMENTS.md +148 -0
- package/docs/SECURITY-AUDIT.md +68 -0
- package/docs/test-trace-matrix.md +28 -0
- package/eslint.config.cjs +2 -0
- package/lib/commands/analyze-ci.js +616 -0
- package/lib/commands/deps.js +293 -0
- package/lib/commands/index.js +29 -0
- package/lib/commands/validate.js +85 -0
- package/lib/config-validator.js +28 -45
- package/lib/error-reporter.js +14 -2
- package/lib/github-api.js +138 -13
- package/lib/license-signing.js +125 -0
- package/lib/license-validator.js +359 -71
- package/lib/licensing.js +434 -106
- package/lib/package-utils.js +9 -9
- package/lib/prelaunch-validator.js +828 -0
- package/lib/project-maturity.js +58 -6
- package/lib/quality-tools-generator.js +495 -0
- package/lib/result-types.js +112 -0
- package/lib/security-enhancements.js +1 -1
- package/lib/smart-strategy-generator.js +46 -10
- package/lib/telemetry.js +1 -1
- package/lib/template-loader.js +52 -19
- package/lib/ui-helpers.js +1 -1
- package/lib/validation/cache-manager.js +36 -6
- package/lib/validation/config-security.js +100 -33
- package/lib/validation/index.js +68 -97
- package/lib/validation/workflow-validation.js +28 -7
- package/package.json +4 -6
- package/scripts/check-test-coverage.sh +46 -0
- package/scripts/validate-claude-md.js +80 -0
- package/setup.js +923 -301
- package/create-saas-monetization.js +0 -1513
package/lib/project-maturity.js
CHANGED
|
@@ -157,6 +157,9 @@ class ProjectMaturityDetector {
|
|
|
157
157
|
hasTests: this.hasTests(),
|
|
158
158
|
hasDependencies: this.hasDependencies(),
|
|
159
159
|
hasCssFiles: this.hasCssFiles(),
|
|
160
|
+
hasShellScripts: this.hasShellScripts(),
|
|
161
|
+
shellScriptCount: this.countShellScripts(),
|
|
162
|
+
isShellProject: this.isShellProject(),
|
|
160
163
|
packageJsonExists: this.packageJsonExists(),
|
|
161
164
|
}
|
|
162
165
|
}
|
|
@@ -266,7 +269,7 @@ class ProjectMaturityDetector {
|
|
|
266
269
|
const deps = packageJson.dependencies || {}
|
|
267
270
|
const devDeps = packageJson.devDependencies || {}
|
|
268
271
|
return Object.keys(deps).length > 0 || Object.keys(devDeps).length > 0
|
|
269
|
-
} catch {
|
|
272
|
+
} catch (_error) {
|
|
270
273
|
return false
|
|
271
274
|
}
|
|
272
275
|
}
|
|
@@ -287,6 +290,36 @@ class ProjectMaturityDetector {
|
|
|
287
290
|
)
|
|
288
291
|
}
|
|
289
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Check if project has shell scripts
|
|
295
|
+
* @returns {boolean} True if shell scripts exist
|
|
296
|
+
*/
|
|
297
|
+
hasShellScripts() {
|
|
298
|
+
return this.countShellScripts() > 0
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Count shell script files
|
|
303
|
+
* @returns {number} Number of shell script files
|
|
304
|
+
*/
|
|
305
|
+
countShellScripts() {
|
|
306
|
+
const extensions = ['.sh', '.bash']
|
|
307
|
+
|
|
308
|
+
return this.countFilesRecursive(this.projectPath, {
|
|
309
|
+
extensions,
|
|
310
|
+
excludeDirs: EXCLUDE_DIRECTORIES.PROJECT_MATURITY,
|
|
311
|
+
maxDepth: 4,
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Check if this is primarily a shell script project
|
|
317
|
+
* @returns {boolean} True if shell project (has .sh files, no package.json)
|
|
318
|
+
*/
|
|
319
|
+
isShellProject() {
|
|
320
|
+
return this.hasShellScripts() && !this.packageJsonExists()
|
|
321
|
+
}
|
|
322
|
+
|
|
290
323
|
/**
|
|
291
324
|
* Check if package.json exists
|
|
292
325
|
* @returns {boolean} True if package.json exists
|
|
@@ -405,9 +438,17 @@ class ProjectMaturityDetector {
|
|
|
405
438
|
}
|
|
406
439
|
}
|
|
407
440
|
} catch (error) {
|
|
408
|
-
//
|
|
409
|
-
if (
|
|
410
|
-
console.warn(
|
|
441
|
+
// Always log permission errors (affects maturity detection accuracy)
|
|
442
|
+
if (error.code === 'EACCES') {
|
|
443
|
+
console.warn(`⚠️ Permission denied: ${dir} (excluded from analysis)`)
|
|
444
|
+
} else if (error.code !== 'ENOENT') {
|
|
445
|
+
console.warn(
|
|
446
|
+
`⚠️ Could not read ${dir}: ${error.message} (${error.code})`
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (this.verbose && error.stack) {
|
|
451
|
+
console.warn(` Stack: ${error.stack}`)
|
|
411
452
|
}
|
|
412
453
|
}
|
|
413
454
|
|
|
@@ -435,7 +476,7 @@ class ProjectMaturityDetector {
|
|
|
435
476
|
|
|
436
477
|
/**
|
|
437
478
|
* Generate GitHub Actions outputs for maturity detection
|
|
438
|
-
* @returns {{maturity: string, sourceCount: number, testCount: number, hasDeps: boolean, hasDocs: boolean, hasCss: boolean, requiredChecks: string, optionalChecks: string, disabledChecks: string}} GitHub Actions output format
|
|
479
|
+
* @returns {{maturity: string, sourceCount: number, testCount: number, hasDeps: boolean, hasDocs: boolean, hasCss: boolean, hasShell: boolean, shellCount: number, isShellProject: boolean, requiredChecks: string, optionalChecks: string, disabledChecks: string}} GitHub Actions output format
|
|
439
480
|
*/
|
|
440
481
|
generateGitHubActionsOutput() {
|
|
441
482
|
const maturity = this.detect()
|
|
@@ -449,6 +490,9 @@ class ProjectMaturityDetector {
|
|
|
449
490
|
hasDeps: stats.hasDependencies,
|
|
450
491
|
hasDocs: stats.hasDocumentation,
|
|
451
492
|
hasCss: stats.hasCssFiles,
|
|
493
|
+
hasShell: stats.hasShellScripts,
|
|
494
|
+
shellCount: stats.shellScriptCount,
|
|
495
|
+
isShellProject: stats.isShellProject,
|
|
452
496
|
requiredChecks: checks.required.join(','),
|
|
453
497
|
optionalChecks: checks.optional.join(','),
|
|
454
498
|
disabledChecks: checks.disabled.join(','),
|
|
@@ -472,7 +516,12 @@ class ProjectMaturityDetector {
|
|
|
472
516
|
console.log(` • Test files: ${stats.testFiles}`)
|
|
473
517
|
console.log(` • Documentation: ${stats.hasDocumentation ? 'Yes' : 'No'}`)
|
|
474
518
|
console.log(` • Dependencies: ${stats.hasDependencies ? 'Yes' : 'No'}`)
|
|
475
|
-
console.log(` • CSS files: ${stats.hasCssFiles ? 'Yes' : 'No'}
|
|
519
|
+
console.log(` • CSS files: ${stats.hasCssFiles ? 'Yes' : 'No'}`)
|
|
520
|
+
console.log(` • Shell scripts: ${stats.shellScriptCount}`)
|
|
521
|
+
if (stats.isShellProject) {
|
|
522
|
+
console.log(` • Project type: Shell script project`)
|
|
523
|
+
}
|
|
524
|
+
console.log()
|
|
476
525
|
|
|
477
526
|
console.log('Quality Checks:')
|
|
478
527
|
console.log(` ✅ Required: ${level.checks.required.join(', ')}`)
|
|
@@ -504,6 +553,9 @@ if (require.main === module) {
|
|
|
504
553
|
console.log(`has-deps=${output.hasDeps}`)
|
|
505
554
|
console.log(`has-docs=${output.hasDocs}`)
|
|
506
555
|
console.log(`has-css=${output.hasCss}`)
|
|
556
|
+
console.log(`has-shell=${output.hasShell}`)
|
|
557
|
+
console.log(`shell-count=${output.shellCount}`)
|
|
558
|
+
console.log(`is-shell-project=${output.isShellProject}`)
|
|
507
559
|
console.log(`required-checks=${output.requiredChecks}`)
|
|
508
560
|
console.log(`optional-checks=${output.optionalChecks}`)
|
|
509
561
|
console.log(`disabled-checks=${output.disabledChecks}`)
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Tools Generator
|
|
3
|
+
* Generates configuration for advanced quality tools:
|
|
4
|
+
* - Lighthouse CI (performance, accessibility, SEO, best practices)
|
|
5
|
+
* - Bundle size limits (size-limit)
|
|
6
|
+
* - axe-core accessibility testing
|
|
7
|
+
* - Conventional commits (commitlint)
|
|
8
|
+
* - Coverage thresholds
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs')
|
|
12
|
+
const path = require('path')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate Lighthouse CI configuration
|
|
16
|
+
* @param {Object} options - Configuration options
|
|
17
|
+
* @param {string} options.projectPath - Path to project
|
|
18
|
+
* @param {boolean} options.hasThresholds - Whether to include score thresholds (Pro)
|
|
19
|
+
* @param {string} options.collectUrl - URL to audit (defaults to http://localhost:3000)
|
|
20
|
+
* @returns {string} lighthouserc.js content
|
|
21
|
+
*/
|
|
22
|
+
function generateLighthouseConfig(options = {}) {
|
|
23
|
+
const {
|
|
24
|
+
hasThresholds = false,
|
|
25
|
+
collectUrl = 'http://localhost:3000',
|
|
26
|
+
staticDistDir = null,
|
|
27
|
+
} = options
|
|
28
|
+
|
|
29
|
+
const collectConfig = staticDistDir
|
|
30
|
+
? `staticDistDir: '${staticDistDir}',`
|
|
31
|
+
: `url: ['${collectUrl}'],
|
|
32
|
+
startServerCommand: 'npm run start',
|
|
33
|
+
startServerReadyPattern: 'ready|listening|started',
|
|
34
|
+
startServerReadyTimeout: 30000,`
|
|
35
|
+
|
|
36
|
+
const assertConfig = hasThresholds
|
|
37
|
+
? `
|
|
38
|
+
assert: {
|
|
39
|
+
preset: 'lighthouse:recommended',
|
|
40
|
+
assertions: {
|
|
41
|
+
// Performance
|
|
42
|
+
'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
|
|
43
|
+
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
|
|
44
|
+
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
|
|
45
|
+
'total-blocking-time': ['warn', { maxNumericValue: 300 }],
|
|
46
|
+
|
|
47
|
+
// Categories (0-1 scale)
|
|
48
|
+
'categories:performance': ['warn', { minScore: 0.8 }],
|
|
49
|
+
'categories:accessibility': ['error', { minScore: 0.9 }],
|
|
50
|
+
'categories:best-practices': ['warn', { minScore: 0.9 }],
|
|
51
|
+
'categories:seo': ['warn', { minScore: 0.9 }],
|
|
52
|
+
|
|
53
|
+
// Allow some common warnings
|
|
54
|
+
'unsized-images': 'off',
|
|
55
|
+
'uses-responsive-images': 'off',
|
|
56
|
+
},
|
|
57
|
+
},`
|
|
58
|
+
: `
|
|
59
|
+
assert: {
|
|
60
|
+
// Basic assertions for Free tier - just warnings, no failures
|
|
61
|
+
assertions: {
|
|
62
|
+
'categories:accessibility': ['warn', { minScore: 0.7 }],
|
|
63
|
+
'categories:best-practices': ['warn', { minScore: 0.7 }],
|
|
64
|
+
},
|
|
65
|
+
},`
|
|
66
|
+
|
|
67
|
+
return `module.exports = {
|
|
68
|
+
ci: {
|
|
69
|
+
collect: {
|
|
70
|
+
${collectConfig}
|
|
71
|
+
numberOfRuns: 3,
|
|
72
|
+
},
|
|
73
|
+
upload: {
|
|
74
|
+
target: 'temporary-public-storage',
|
|
75
|
+
},${assertConfig}
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generate size-limit configuration for bundle size limits
|
|
83
|
+
* @param {Object} options - Configuration options
|
|
84
|
+
* @param {string} options.projectPath - Path to project
|
|
85
|
+
* @returns {Array} size-limit config array
|
|
86
|
+
*/
|
|
87
|
+
function generateSizeLimitConfig(options = {}) {
|
|
88
|
+
const { projectPath = process.cwd() } = options
|
|
89
|
+
|
|
90
|
+
// Detect build output paths
|
|
91
|
+
const possibleDists = ['dist', 'build', '.next', 'out', 'public']
|
|
92
|
+
let distDir = 'dist'
|
|
93
|
+
|
|
94
|
+
for (const dir of possibleDists) {
|
|
95
|
+
if (fs.existsSync(path.join(projectPath, dir))) {
|
|
96
|
+
distDir = dir
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Detect if it's a Next.js app
|
|
102
|
+
const isNextJS =
|
|
103
|
+
fs.existsSync(path.join(projectPath, 'next.config.js')) ||
|
|
104
|
+
fs.existsSync(path.join(projectPath, 'next.config.mjs'))
|
|
105
|
+
|
|
106
|
+
if (isNextJS) {
|
|
107
|
+
return [
|
|
108
|
+
{
|
|
109
|
+
path: '.next/static/**/*.js',
|
|
110
|
+
limit: '300 kB',
|
|
111
|
+
webpack: false,
|
|
112
|
+
},
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
path: `${distDir}/**/*.js`,
|
|
119
|
+
limit: '250 kB',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
path: `${distDir}/**/*.css`,
|
|
123
|
+
limit: '50 kB',
|
|
124
|
+
},
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Generate commitlint configuration for conventional commits
|
|
130
|
+
* @returns {string} commitlint.config.js content
|
|
131
|
+
*/
|
|
132
|
+
function generateCommitlintConfig() {
|
|
133
|
+
return `module.exports = {
|
|
134
|
+
extends: ['@commitlint/config-conventional'],
|
|
135
|
+
rules: {
|
|
136
|
+
// Type must be one of the conventional types
|
|
137
|
+
'type-enum': [
|
|
138
|
+
2,
|
|
139
|
+
'always',
|
|
140
|
+
[
|
|
141
|
+
'feat', // New feature
|
|
142
|
+
'fix', // Bug fix
|
|
143
|
+
'docs', // Documentation only
|
|
144
|
+
'style', // Formatting, no code change
|
|
145
|
+
'refactor', // Code change that neither fixes a bug nor adds a feature
|
|
146
|
+
'perf', // Performance improvement
|
|
147
|
+
'test', // Adding tests
|
|
148
|
+
'build', // Build system or external dependencies
|
|
149
|
+
'ci', // CI configuration
|
|
150
|
+
'chore', // Maintenance tasks
|
|
151
|
+
'revert', // Revert a previous commit
|
|
152
|
+
],
|
|
153
|
+
],
|
|
154
|
+
// Subject line max length
|
|
155
|
+
'subject-max-length': [2, 'always', 100],
|
|
156
|
+
// Subject must not be empty
|
|
157
|
+
'subject-empty': [2, 'never'],
|
|
158
|
+
// Type must not be empty
|
|
159
|
+
'type-empty': [2, 'never'],
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Generate commit-msg hook content for commitlint
|
|
167
|
+
* @returns {string} commit-msg hook content
|
|
168
|
+
*/
|
|
169
|
+
function generateCommitMsgHook() {
|
|
170
|
+
return `#!/bin/sh
|
|
171
|
+
npx --no -- commitlint --edit "$1"
|
|
172
|
+
`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Generate vitest coverage thresholds configuration
|
|
177
|
+
* @param {Object} options - Configuration options
|
|
178
|
+
* @param {number} options.lines - Line coverage threshold (default: 70)
|
|
179
|
+
* @param {number} options.functions - Function coverage threshold (default: 70)
|
|
180
|
+
* @param {number} options.branches - Branch coverage threshold (default: 60)
|
|
181
|
+
* @param {number} options.statements - Statement coverage threshold (default: 70)
|
|
182
|
+
* @returns {Object} Coverage threshold config object
|
|
183
|
+
*/
|
|
184
|
+
function generateCoverageThresholds(options = {}) {
|
|
185
|
+
const { lines = 70, functions = 70, branches = 60, statements = 70 } = options
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
lines,
|
|
189
|
+
functions,
|
|
190
|
+
branches,
|
|
191
|
+
statements,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generate vitest.config.ts/js with coverage thresholds
|
|
197
|
+
* @param {Object} options - Configuration options
|
|
198
|
+
* @returns {string} vitest.config content
|
|
199
|
+
*/
|
|
200
|
+
function generateVitestConfigWithCoverage(options = {}) {
|
|
201
|
+
const { typescript = false, thresholds = {} } = options
|
|
202
|
+
const coverageThresholds = generateCoverageThresholds(thresholds)
|
|
203
|
+
|
|
204
|
+
const importStatement = typescript
|
|
205
|
+
? "import { defineConfig } from 'vitest/config'"
|
|
206
|
+
: "const { defineConfig } = require('vitest/config')"
|
|
207
|
+
const exportStatement = typescript ? 'export default' : 'module.exports ='
|
|
208
|
+
|
|
209
|
+
return `${importStatement}
|
|
210
|
+
|
|
211
|
+
${exportStatement} defineConfig({
|
|
212
|
+
test: {
|
|
213
|
+
globals: true,
|
|
214
|
+
environment: 'node',
|
|
215
|
+
include: ['tests/**/*.test.{js,ts}', 'src/**/*.test.{js,ts}'],
|
|
216
|
+
exclude: ['node_modules', 'dist', 'build'],
|
|
217
|
+
coverage: {
|
|
218
|
+
provider: 'v8',
|
|
219
|
+
reporter: ['text', 'json', 'html', 'lcov'],
|
|
220
|
+
exclude: [
|
|
221
|
+
'node_modules/**',
|
|
222
|
+
'tests/**',
|
|
223
|
+
'**/*.test.{js,ts}',
|
|
224
|
+
'**/*.config.{js,ts,mjs}',
|
|
225
|
+
'coverage/**',
|
|
226
|
+
'dist/**',
|
|
227
|
+
'build/**',
|
|
228
|
+
],
|
|
229
|
+
thresholds: {
|
|
230
|
+
lines: ${coverageThresholds.lines},
|
|
231
|
+
functions: ${coverageThresholds.functions},
|
|
232
|
+
branches: ${coverageThresholds.branches},
|
|
233
|
+
statements: ${coverageThresholds.statements},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generate axe-core accessibility test setup
|
|
243
|
+
* @returns {string} Accessibility test file content
|
|
244
|
+
*/
|
|
245
|
+
function generateAxeTestSetup() {
|
|
246
|
+
return `/**
|
|
247
|
+
* Accessibility Testing with axe-core
|
|
248
|
+
*
|
|
249
|
+
* This file sets up automated accessibility testing using axe-core.
|
|
250
|
+
* It can be run as part of your test suite or CI pipeline.
|
|
251
|
+
*
|
|
252
|
+
* Usage:
|
|
253
|
+
* npm run test:a11y
|
|
254
|
+
*
|
|
255
|
+
* For manual testing, use the browser extension:
|
|
256
|
+
* https://www.deque.com/axe/browser-extensions/
|
|
257
|
+
*/
|
|
258
|
+
|
|
259
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
260
|
+
|
|
261
|
+
// For component testing, use @axe-core/react or @axe-core/playwright
|
|
262
|
+
// This example uses puppeteer for full page testing
|
|
263
|
+
|
|
264
|
+
let browser
|
|
265
|
+
|
|
266
|
+
describe('Accessibility Tests', () => {
|
|
267
|
+
beforeAll(async () => {
|
|
268
|
+
// If using puppeteer for E2E accessibility testing:
|
|
269
|
+
// const puppeteer = require('puppeteer')
|
|
270
|
+
// browser = await puppeteer.launch()
|
|
271
|
+
// const page = await browser.newPage()
|
|
272
|
+
|
|
273
|
+
// For now, this is a placeholder that passes
|
|
274
|
+
// Replace with actual implementation based on your framework
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
afterAll(async () => {
|
|
278
|
+
if (browser) {
|
|
279
|
+
await browser.close()
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it.skip('should have no critical accessibility violations on homepage', async () => {
|
|
284
|
+
// Example with puppeteer + @axe-core/puppeteer:
|
|
285
|
+
// const { AxePuppeteer } = require('@axe-core/puppeteer')
|
|
286
|
+
// await page.goto('http://localhost:3000')
|
|
287
|
+
// const results = await new AxePuppeteer(page).analyze()
|
|
288
|
+
// expect(results.violations.filter(v => v.impact === 'critical')).toHaveLength(0)
|
|
289
|
+
expect(true).toBe(true)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it.skip('should have proper heading hierarchy', async () => {
|
|
293
|
+
// Implement heading hierarchy check
|
|
294
|
+
expect(true).toBe(true)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it.skip('should have sufficient color contrast', async () => {
|
|
298
|
+
// Implement color contrast check
|
|
299
|
+
expect(true).toBe(true)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it.skip('should have accessible form labels', async () => {
|
|
303
|
+
// Implement form label check
|
|
304
|
+
expect(true).toBe(true)
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Helper function to run axe-core on a page
|
|
310
|
+
* @param {Object} page - Puppeteer/Playwright page object
|
|
311
|
+
* @returns {Promise<Object>} axe results
|
|
312
|
+
*/
|
|
313
|
+
export async function runAxeOnPage(page) {
|
|
314
|
+
// Inject axe-core into the page
|
|
315
|
+
await page.addScriptTag({
|
|
316
|
+
path: require.resolve('axe-core'),
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// Run axe
|
|
320
|
+
const results = await page.evaluate(async () => {
|
|
321
|
+
// eslint-disable-next-line no-undef
|
|
322
|
+
return await axe.run()
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
return results
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Filter axe violations by impact level
|
|
330
|
+
* @param {Array} violations - axe violations array
|
|
331
|
+
* @param {Array} levels - Impact levels to include ['critical', 'serious', 'moderate', 'minor']
|
|
332
|
+
* @returns {Array} Filtered violations
|
|
333
|
+
*/
|
|
334
|
+
export function filterByImpact(violations, levels = ['critical', 'serious']) {
|
|
335
|
+
return violations.filter(v => levels.includes(v.impact))
|
|
336
|
+
}
|
|
337
|
+
`
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Write Lighthouse CI config to project
|
|
342
|
+
* @param {string} projectPath - Path to project
|
|
343
|
+
* @param {Object} options - Options for config generation
|
|
344
|
+
*/
|
|
345
|
+
function writeLighthouseConfig(projectPath, options = {}) {
|
|
346
|
+
const configPath = path.join(projectPath, 'lighthouserc.js')
|
|
347
|
+
const config = generateLighthouseConfig(options)
|
|
348
|
+
fs.writeFileSync(configPath, config)
|
|
349
|
+
return configPath
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Write size-limit config to package.json
|
|
354
|
+
* @param {string} projectPath - Path to project
|
|
355
|
+
* @param {Object} options - Options for config generation
|
|
356
|
+
*/
|
|
357
|
+
function writeSizeLimitConfig(projectPath, options = {}) {
|
|
358
|
+
const packageJsonPath = path.join(projectPath, 'package.json')
|
|
359
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
360
|
+
throw new Error('package.json not found')
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
364
|
+
packageJson['size-limit'] = generateSizeLimitConfig({
|
|
365
|
+
projectPath,
|
|
366
|
+
...options,
|
|
367
|
+
})
|
|
368
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n')
|
|
369
|
+
return packageJsonPath
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Write commitlint config to project
|
|
374
|
+
* @param {string} projectPath - Path to project
|
|
375
|
+
*/
|
|
376
|
+
function writeCommitlintConfig(projectPath) {
|
|
377
|
+
const configPath = path.join(projectPath, 'commitlint.config.js')
|
|
378
|
+
const config = generateCommitlintConfig()
|
|
379
|
+
fs.writeFileSync(configPath, config)
|
|
380
|
+
return configPath
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Write commit-msg hook for commitlint
|
|
385
|
+
* @param {string} projectPath - Path to project
|
|
386
|
+
*/
|
|
387
|
+
function writeCommitMsgHook(projectPath) {
|
|
388
|
+
const huskyDir = path.join(projectPath, '.husky')
|
|
389
|
+
if (!fs.existsSync(huskyDir)) {
|
|
390
|
+
fs.mkdirSync(huskyDir, { recursive: true })
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const hookPath = path.join(huskyDir, 'commit-msg')
|
|
394
|
+
const hook = generateCommitMsgHook()
|
|
395
|
+
fs.writeFileSync(hookPath, hook)
|
|
396
|
+
fs.chmodSync(hookPath, 0o755)
|
|
397
|
+
return hookPath
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Write axe test setup to project
|
|
402
|
+
* @param {string} projectPath - Path to project
|
|
403
|
+
* @param {Object} options - Options for test generation
|
|
404
|
+
*/
|
|
405
|
+
function writeAxeTestSetup(projectPath, options = {}) {
|
|
406
|
+
const testsDir = path.join(projectPath, 'tests')
|
|
407
|
+
if (!fs.existsSync(testsDir)) {
|
|
408
|
+
fs.mkdirSync(testsDir, { recursive: true })
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const testPath = path.join(testsDir, 'accessibility.test.js')
|
|
412
|
+
const content = generateAxeTestSetup(options)
|
|
413
|
+
fs.writeFileSync(testPath, content)
|
|
414
|
+
return testPath
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Get dev dependencies for quality tools
|
|
419
|
+
* @param {Object} features - Which features to include
|
|
420
|
+
* @returns {Object} Dev dependencies object
|
|
421
|
+
*/
|
|
422
|
+
function getQualityToolsDependencies(features = {}) {
|
|
423
|
+
const deps = {}
|
|
424
|
+
|
|
425
|
+
if (features.lighthouse) {
|
|
426
|
+
deps['@lhci/cli'] = '^0.14.0'
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (features.sizeLimit) {
|
|
430
|
+
deps['size-limit'] = '^11.0.0'
|
|
431
|
+
deps['@size-limit/file'] = '^11.0.0'
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (features.commitlint) {
|
|
435
|
+
deps['@commitlint/cli'] = '^19.0.0'
|
|
436
|
+
deps['@commitlint/config-conventional'] = '^19.0.0'
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (features.axeCore) {
|
|
440
|
+
deps['axe-core'] = '^4.10.0'
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return deps
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get npm scripts for quality tools
|
|
448
|
+
* @param {Object} features - Which features to include
|
|
449
|
+
* @returns {Object} Scripts object
|
|
450
|
+
*/
|
|
451
|
+
function getQualityToolsScripts(features = {}) {
|
|
452
|
+
const scripts = {}
|
|
453
|
+
|
|
454
|
+
if (features.lighthouse) {
|
|
455
|
+
scripts['lighthouse:ci'] = 'lhci autorun'
|
|
456
|
+
scripts['lighthouse:upload'] = 'lhci upload'
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (features.sizeLimit) {
|
|
460
|
+
scripts['size'] = 'size-limit'
|
|
461
|
+
scripts['size:why'] = 'size-limit --why'
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (features.axeCore) {
|
|
465
|
+
scripts['test:a11y'] = 'vitest run tests/accessibility.test.js'
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (features.coverage) {
|
|
469
|
+
scripts['test:coverage'] = 'vitest run --coverage'
|
|
470
|
+
scripts['test:coverage:check'] =
|
|
471
|
+
'vitest run --coverage --coverage.thresholds.lines=70 --coverage.thresholds.functions=70'
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return scripts
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
module.exports = {
|
|
478
|
+
// Config generators
|
|
479
|
+
generateLighthouseConfig,
|
|
480
|
+
generateSizeLimitConfig,
|
|
481
|
+
generateCommitlintConfig,
|
|
482
|
+
generateCommitMsgHook,
|
|
483
|
+
generateCoverageThresholds,
|
|
484
|
+
generateVitestConfigWithCoverage,
|
|
485
|
+
generateAxeTestSetup,
|
|
486
|
+
// Writers
|
|
487
|
+
writeLighthouseConfig,
|
|
488
|
+
writeSizeLimitConfig,
|
|
489
|
+
writeCommitlintConfig,
|
|
490
|
+
writeCommitMsgHook,
|
|
491
|
+
writeAxeTestSetup,
|
|
492
|
+
// Helpers
|
|
493
|
+
getQualityToolsDependencies,
|
|
494
|
+
getQualityToolsScripts,
|
|
495
|
+
}
|