create-qa-architect 5.0.6 → 5.3.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.
- package/.github/workflows/auto-release.yml +49 -0
- package/.github/workflows/dependabot-auto-merge.yml +32 -0
- package/LICENSE +3 -3
- package/README.md +54 -15
- package/docs/ADOPTION-SUMMARY.md +41 -0
- package/docs/ARCHITECTURE-REVIEW.md +67 -0
- package/docs/ARCHITECTURE.md +29 -41
- package/docs/CODE-REVIEW.md +100 -0
- package/docs/PREFLIGHT_REPORT.md +32 -40
- package/docs/REQUIREMENTS.md +148 -0
- package/docs/SECURITY-AUDIT.md +68 -0
- package/docs/TESTING.md +3 -4
- package/docs/test-trace-matrix.md +28 -0
- package/lib/billing-dashboard.html +6 -12
- package/lib/commands/deps.js +245 -0
- package/lib/commands/index.js +25 -0
- package/lib/commands/validate.js +85 -0
- package/lib/error-reporter.js +13 -1
- package/lib/github-api.js +108 -13
- package/lib/license-signing.js +110 -0
- package/lib/license-validator.js +359 -71
- package/lib/licensing.js +343 -111
- package/lib/prelaunch-validator.js +828 -0
- 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 +28 -9
- package/lib/template-loader.js +52 -19
- package/lib/validation/cache-manager.js +36 -6
- package/lib/validation/config-security.js +82 -15
- package/lib/validation/workflow-validation.js +49 -23
- package/package.json +8 -10
- package/scripts/check-test-coverage.sh +46 -0
- package/setup.js +356 -285
- package/templates/QUALITY_TROUBLESHOOTING.md +32 -33
- package/templates/scripts/smart-test-strategy.sh +1 -1
- package/create-saas-monetization.js +0 -1513
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-Launch Validation Module
|
|
3
|
+
* Automated SOTA checks for web applications before launch
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const path = require('path')
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// SEO VALIDATION
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate sitemap validation script
|
|
15
|
+
*/
|
|
16
|
+
function generateSitemapValidator() {
|
|
17
|
+
return `#!/usr/bin/env node
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const sitemapPaths = [
|
|
22
|
+
'public/sitemap.xml',
|
|
23
|
+
'dist/sitemap.xml',
|
|
24
|
+
'out/sitemap.xml',
|
|
25
|
+
'build/sitemap.xml',
|
|
26
|
+
'.next/sitemap.xml'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
let found = false;
|
|
30
|
+
let sitemapPath = null;
|
|
31
|
+
|
|
32
|
+
for (const p of sitemapPaths) {
|
|
33
|
+
if (fs.existsSync(p)) {
|
|
34
|
+
found = true;
|
|
35
|
+
sitemapPath = p;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!found) {
|
|
41
|
+
console.error('❌ No sitemap.xml found');
|
|
42
|
+
console.error(' Expected locations: ' + sitemapPaths.join(', '));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const content = fs.readFileSync(sitemapPath, 'utf8');
|
|
47
|
+
|
|
48
|
+
// Basic XML validation
|
|
49
|
+
if (!content.includes('<?xml')) {
|
|
50
|
+
console.error('❌ sitemap.xml missing XML declaration');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!content.includes('<urlset')) {
|
|
55
|
+
console.error('❌ sitemap.xml missing <urlset> element');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!content.includes('<url>')) {
|
|
60
|
+
console.error('❌ sitemap.xml has no <url> entries');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Count URLs
|
|
65
|
+
const urlCount = (content.match(/<url>/g) || []).length;
|
|
66
|
+
console.log('✅ sitemap.xml valid (' + urlCount + ' URLs)');
|
|
67
|
+
`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate robots.txt validation script
|
|
72
|
+
*/
|
|
73
|
+
function generateRobotsValidator() {
|
|
74
|
+
return `#!/usr/bin/env node
|
|
75
|
+
const fs = require('fs');
|
|
76
|
+
|
|
77
|
+
const robotsPaths = [
|
|
78
|
+
'public/robots.txt',
|
|
79
|
+
'dist/robots.txt',
|
|
80
|
+
'out/robots.txt',
|
|
81
|
+
'build/robots.txt'
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
let found = false;
|
|
85
|
+
let robotsPath = null;
|
|
86
|
+
|
|
87
|
+
for (const p of robotsPaths) {
|
|
88
|
+
if (fs.existsSync(p)) {
|
|
89
|
+
found = true;
|
|
90
|
+
robotsPath = p;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!found) {
|
|
96
|
+
console.error('❌ No robots.txt found');
|
|
97
|
+
console.error(' Expected locations: ' + robotsPaths.join(', '));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const content = fs.readFileSync(robotsPath, 'utf8');
|
|
102
|
+
|
|
103
|
+
// Check for User-agent directive
|
|
104
|
+
if (!content.toLowerCase().includes('user-agent')) {
|
|
105
|
+
console.error('❌ robots.txt missing User-agent directive');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check for sitemap reference
|
|
110
|
+
if (!content.toLowerCase().includes('sitemap')) {
|
|
111
|
+
console.warn('⚠️ robots.txt should reference sitemap.xml');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log('✅ robots.txt valid');
|
|
115
|
+
`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Generate meta tags validation script
|
|
120
|
+
*/
|
|
121
|
+
function generateMetaTagsValidator() {
|
|
122
|
+
return `#!/usr/bin/env node
|
|
123
|
+
const fs = require('fs');
|
|
124
|
+
const path = require('path');
|
|
125
|
+
|
|
126
|
+
const htmlPaths = [
|
|
127
|
+
'public/index.html',
|
|
128
|
+
'dist/index.html',
|
|
129
|
+
'out/index.html',
|
|
130
|
+
'build/index.html',
|
|
131
|
+
'index.html'
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
// For Next.js/React apps, check for meta component patterns
|
|
135
|
+
const metaPatterns = [
|
|
136
|
+
'src/app/layout.tsx',
|
|
137
|
+
'src/app/layout.js',
|
|
138
|
+
'pages/_app.tsx',
|
|
139
|
+
'pages/_app.js',
|
|
140
|
+
'pages/_document.tsx',
|
|
141
|
+
'pages/_document.js'
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
let content = '';
|
|
145
|
+
let source = '';
|
|
146
|
+
|
|
147
|
+
// Try HTML files first
|
|
148
|
+
for (const p of htmlPaths) {
|
|
149
|
+
if (fs.existsSync(p)) {
|
|
150
|
+
content = fs.readFileSync(p, 'utf8');
|
|
151
|
+
source = p;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Try framework files if no HTML
|
|
157
|
+
if (!content) {
|
|
158
|
+
for (const p of metaPatterns) {
|
|
159
|
+
if (fs.existsSync(p)) {
|
|
160
|
+
content = fs.readFileSync(p, 'utf8');
|
|
161
|
+
source = p;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!content) {
|
|
168
|
+
console.warn('⚠️ No HTML or layout file found to check meta tags');
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const errors = [];
|
|
173
|
+
const warnings = [];
|
|
174
|
+
|
|
175
|
+
// Required meta tags
|
|
176
|
+
if (!content.includes('viewport')) {
|
|
177
|
+
errors.push('Missing viewport meta tag');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!content.includes('<title') && !content.includes('title:') && !content.includes('Title')) {
|
|
181
|
+
errors.push('Missing title tag');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!content.includes('description')) {
|
|
185
|
+
errors.push('Missing meta description');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// OG tags (warnings)
|
|
189
|
+
if (!content.includes('og:title')) {
|
|
190
|
+
warnings.push('Missing og:title');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!content.includes('og:description')) {
|
|
194
|
+
warnings.push('Missing og:description');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!content.includes('og:image')) {
|
|
198
|
+
warnings.push('Missing og:image');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!content.includes('og:url')) {
|
|
202
|
+
warnings.push('Missing og:url');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Twitter cards
|
|
206
|
+
if (!content.includes('twitter:card')) {
|
|
207
|
+
warnings.push('Missing twitter:card');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Canonical
|
|
211
|
+
if (!content.includes('canonical')) {
|
|
212
|
+
warnings.push('Missing canonical URL');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log('Checking: ' + source);
|
|
216
|
+
|
|
217
|
+
if (errors.length > 0) {
|
|
218
|
+
console.error('❌ Meta tag errors:');
|
|
219
|
+
errors.forEach(e => console.error(' - ' + e));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (warnings.length > 0) {
|
|
223
|
+
console.warn('⚠️ Meta tag warnings:');
|
|
224
|
+
warnings.forEach(w => console.warn(' - ' + w));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (errors.length === 0) {
|
|
228
|
+
console.log('✅ Required meta tags present');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
process.exit(errors.length > 0 ? 1 : 0);
|
|
232
|
+
`
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// LINK VALIDATION
|
|
237
|
+
// ============================================================================
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Generate linkinator config
|
|
241
|
+
*/
|
|
242
|
+
function generateLinkinatorConfig() {
|
|
243
|
+
return {
|
|
244
|
+
recurse: true,
|
|
245
|
+
skip: ['localhost', '127.0.0.1', 'example.com', 'placeholder.com'],
|
|
246
|
+
timeout: 10000,
|
|
247
|
+
concurrency: 10,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Generate link validation script
|
|
253
|
+
*/
|
|
254
|
+
function generateLinkValidator() {
|
|
255
|
+
return `#!/usr/bin/env node
|
|
256
|
+
const { spawnSync } = require('child_process');
|
|
257
|
+
const fs = require('fs');
|
|
258
|
+
|
|
259
|
+
// Check for dist/build directories (allowlist only - prevents injection)
|
|
260
|
+
const ALLOWED_DIST_PATHS = ['dist', 'build', 'out', '.next', 'public'];
|
|
261
|
+
let servePath = null;
|
|
262
|
+
|
|
263
|
+
for (const p of ALLOWED_DIST_PATHS) {
|
|
264
|
+
if (fs.existsSync(p)) {
|
|
265
|
+
servePath = p;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!servePath) {
|
|
271
|
+
console.log('⚠️ No build output found, skipping link validation');
|
|
272
|
+
console.log(' Run after: npm run build');
|
|
273
|
+
process.exit(0);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Security: validate servePath is in allowlist (defense in depth)
|
|
277
|
+
if (!ALLOWED_DIST_PATHS.includes(servePath)) {
|
|
278
|
+
console.error('❌ Invalid build path');
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
// Check if linkinator is available
|
|
284
|
+
const versionCheck = spawnSync('npx', ['linkinator', '--version'], { stdio: 'pipe' });
|
|
285
|
+
if (versionCheck.error) throw versionCheck.error;
|
|
286
|
+
|
|
287
|
+
console.log('Checking links in: ' + servePath);
|
|
288
|
+
// Use spawnSync with args array to prevent command injection
|
|
289
|
+
const result = spawnSync('npx', [
|
|
290
|
+
'linkinator',
|
|
291
|
+
servePath,
|
|
292
|
+
'--recurse',
|
|
293
|
+
'--skip', 'localhost|127.0.0.1'
|
|
294
|
+
], {
|
|
295
|
+
stdio: 'inherit',
|
|
296
|
+
timeout: 120000
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (result.status === 0) {
|
|
300
|
+
console.log('✅ All links valid');
|
|
301
|
+
} else if (result.status) {
|
|
302
|
+
console.error('❌ Broken links found');
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
// linkinator not available, skip
|
|
307
|
+
console.log('⚠️ linkinator not available, skipping');
|
|
308
|
+
}
|
|
309
|
+
`
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ============================================================================
|
|
313
|
+
// ACCESSIBILITY VALIDATION
|
|
314
|
+
// ============================================================================
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Generate pa11y-ci config
|
|
318
|
+
*/
|
|
319
|
+
function generatePa11yConfig(urls = []) {
|
|
320
|
+
const defaultUrls = urls.length > 0 ? urls : ['http://localhost:3000']
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
defaults: {
|
|
324
|
+
timeout: 30000,
|
|
325
|
+
wait: 1000,
|
|
326
|
+
standard: 'WCAG2AA',
|
|
327
|
+
runners: ['axe', 'htmlcs'],
|
|
328
|
+
chromeLaunchConfig: {
|
|
329
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
urls: defaultUrls,
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Generate accessibility validation script
|
|
338
|
+
* Security: Uses spawnSync with args array to avoid shell injection risks
|
|
339
|
+
*/
|
|
340
|
+
function generateA11yValidator() {
|
|
341
|
+
return `#!/usr/bin/env node
|
|
342
|
+
const { spawnSync } = require('child_process');
|
|
343
|
+
const fs = require('fs');
|
|
344
|
+
|
|
345
|
+
// Check for pa11yci config
|
|
346
|
+
const configPath = '.pa11yci';
|
|
347
|
+
if (!fs.existsSync(configPath)) {
|
|
348
|
+
console.log('⚠️ No .pa11yci config found');
|
|
349
|
+
console.log(' Creating default config...');
|
|
350
|
+
|
|
351
|
+
const defaultConfig = {
|
|
352
|
+
defaults: {
|
|
353
|
+
timeout: 30000,
|
|
354
|
+
standard: 'WCAG2AA'
|
|
355
|
+
},
|
|
356
|
+
urls: ['http://localhost:3000']
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
console.log('Running accessibility audit (WCAG 2.1 AA)...');
|
|
363
|
+
console.log('⚠️ Ensure dev server is running on configured URL');
|
|
364
|
+
|
|
365
|
+
// Security: Use spawnSync with args array instead of execSync with shell
|
|
366
|
+
const result = spawnSync('npx', ['pa11y-ci'], {
|
|
367
|
+
stdio: 'inherit',
|
|
368
|
+
timeout: 120000,
|
|
369
|
+
shell: false
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
if (result.status === 0) {
|
|
373
|
+
console.log('✅ Accessibility checks passed');
|
|
374
|
+
} else if (result.status !== null) {
|
|
375
|
+
console.error('❌ Accessibility issues found');
|
|
376
|
+
process.exit(1);
|
|
377
|
+
} else if (result.error) {
|
|
378
|
+
console.error('⚠️ pa11y-ci failed to run:', result.error.message);
|
|
379
|
+
}
|
|
380
|
+
`
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ============================================================================
|
|
384
|
+
// DOCS COMPLETENESS VALIDATION
|
|
385
|
+
// ============================================================================
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Generate docs completeness validator
|
|
389
|
+
*/
|
|
390
|
+
function generateDocsValidator() {
|
|
391
|
+
return `#!/usr/bin/env node
|
|
392
|
+
const fs = require('fs');
|
|
393
|
+
const path = require('path');
|
|
394
|
+
|
|
395
|
+
const errors = [];
|
|
396
|
+
const warnings = [];
|
|
397
|
+
|
|
398
|
+
// Required files
|
|
399
|
+
const requiredFiles = [
|
|
400
|
+
{ file: 'README.md', message: 'README.md is required' },
|
|
401
|
+
{ file: 'LICENSE', message: 'LICENSE file recommended', warn: true }
|
|
402
|
+
];
|
|
403
|
+
|
|
404
|
+
// Required README sections
|
|
405
|
+
const requiredSections = [
|
|
406
|
+
'install',
|
|
407
|
+
'usage',
|
|
408
|
+
'getting started'
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
// Check required files
|
|
412
|
+
for (const { file, message, warn } of requiredFiles) {
|
|
413
|
+
if (!fs.existsSync(file)) {
|
|
414
|
+
if (warn) {
|
|
415
|
+
warnings.push(message);
|
|
416
|
+
} else {
|
|
417
|
+
errors.push(message);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Check README content
|
|
423
|
+
if (fs.existsSync('README.md')) {
|
|
424
|
+
const readme = fs.readFileSync('README.md', 'utf8').toLowerCase();
|
|
425
|
+
|
|
426
|
+
for (const section of requiredSections) {
|
|
427
|
+
if (!readme.includes(section)) {
|
|
428
|
+
warnings.push('README.md missing "' + section + '" section');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Check for placeholder content
|
|
433
|
+
if (readme.includes('todo') || readme.includes('coming soon') || readme.includes('tbd')) {
|
|
434
|
+
warnings.push('README.md contains placeholder content (TODO/TBD)');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Check for API docs if src/api exists
|
|
439
|
+
if (fs.existsSync('src/api') || fs.existsSync('api') || fs.existsSync('pages/api')) {
|
|
440
|
+
const apiDocPaths = ['docs/api.md', 'API.md', 'docs/API.md'];
|
|
441
|
+
const hasApiDocs = apiDocPaths.some(p => fs.existsSync(p));
|
|
442
|
+
if (!hasApiDocs) {
|
|
443
|
+
warnings.push('API directory found but no API documentation');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Check for deployment docs
|
|
448
|
+
const deployDocPaths = ['DEPLOYMENT.md', 'docs/deployment.md', 'docs/DEPLOYMENT.md'];
|
|
449
|
+
const hasDeployDocs = deployDocPaths.some(p => fs.existsSync(p));
|
|
450
|
+
if (!hasDeployDocs) {
|
|
451
|
+
warnings.push('No deployment documentation found');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Report
|
|
455
|
+
if (errors.length > 0) {
|
|
456
|
+
console.error('❌ Documentation errors:');
|
|
457
|
+
errors.forEach(e => console.error(' - ' + e));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (warnings.length > 0) {
|
|
461
|
+
console.warn('⚠️ Documentation warnings:');
|
|
462
|
+
warnings.forEach(w => console.warn(' - ' + w));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
466
|
+
console.log('✅ Documentation complete');
|
|
467
|
+
} else if (errors.length === 0) {
|
|
468
|
+
console.log('✅ Required documentation present');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
process.exit(errors.length > 0 ? 1 : 0);
|
|
472
|
+
`
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ============================================================================
|
|
476
|
+
// ENV VARS VALIDATION (Pro)
|
|
477
|
+
// ============================================================================
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Generate env vars validator (Pro feature)
|
|
481
|
+
*/
|
|
482
|
+
function generateEnvValidator() {
|
|
483
|
+
return `#!/usr/bin/env node
|
|
484
|
+
const fs = require('fs');
|
|
485
|
+
const path = require('path');
|
|
486
|
+
|
|
487
|
+
const errors = [];
|
|
488
|
+
const warnings = [];
|
|
489
|
+
|
|
490
|
+
// Find .env.example or .env.template
|
|
491
|
+
const envTemplatePaths = ['.env.example', '.env.template', '.env.sample'];
|
|
492
|
+
let templatePath = null;
|
|
493
|
+
let templateVars = [];
|
|
494
|
+
|
|
495
|
+
for (const p of envTemplatePaths) {
|
|
496
|
+
if (fs.existsSync(p)) {
|
|
497
|
+
templatePath = p;
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (!templatePath) {
|
|
503
|
+
console.warn('⚠️ No .env.example found - skipping env var validation');
|
|
504
|
+
process.exit(0);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Parse template
|
|
508
|
+
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
509
|
+
const envRegex = /^([A-Z][A-Z0-9_]*)\\s*=/gm;
|
|
510
|
+
let match;
|
|
511
|
+
|
|
512
|
+
while ((match = envRegex.exec(templateContent)) !== null) {
|
|
513
|
+
templateVars.push(match[1]);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
console.log('Found ' + templateVars.length + ' env vars in ' + templatePath);
|
|
517
|
+
|
|
518
|
+
// Search for usage in code
|
|
519
|
+
const codeExtensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
|
|
520
|
+
const searchDirs = ['src', 'app', 'pages', 'lib', 'api'];
|
|
521
|
+
|
|
522
|
+
function findEnvUsage(dir) {
|
|
523
|
+
const usedVars = new Set();
|
|
524
|
+
|
|
525
|
+
function walkDir(currentPath) {
|
|
526
|
+
if (!fs.existsSync(currentPath)) return;
|
|
527
|
+
|
|
528
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
529
|
+
|
|
530
|
+
for (const entry of entries) {
|
|
531
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
532
|
+
|
|
533
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
534
|
+
walkDir(fullPath);
|
|
535
|
+
} else if (entry.isFile() && codeExtensions.some(ext => entry.name.endsWith(ext))) {
|
|
536
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
537
|
+
|
|
538
|
+
// Match process.env.VAR_NAME and import.meta.env.VAR_NAME
|
|
539
|
+
const envMatches = content.matchAll(/(?:process\\.env|import\\.meta\\.env)\\.([A-Z][A-Z0-9_]*)/g);
|
|
540
|
+
for (const m of envMatches) {
|
|
541
|
+
usedVars.add(m[1]);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Match env('VAR_NAME') or getenv('VAR_NAME')
|
|
545
|
+
const funcMatches = content.matchAll(/(?:env|getenv)\\(['\`"]([A-Z][A-Z0-9_]*)['\`"]\\)/g);
|
|
546
|
+
for (const m of funcMatches) {
|
|
547
|
+
usedVars.add(m[1]);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
walkDir(dir);
|
|
554
|
+
return usedVars;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const usedInCode = new Set();
|
|
558
|
+
for (const dir of searchDirs) {
|
|
559
|
+
const found = findEnvUsage(dir);
|
|
560
|
+
found.forEach(v => usedInCode.add(v));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Also check root files
|
|
564
|
+
const rootFiles = fs.readdirSync('.').filter(f => codeExtensions.some(ext => f.endsWith(ext)));
|
|
565
|
+
for (const file of rootFiles) {
|
|
566
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
567
|
+
const matches = content.matchAll(/(?:process\\.env|import\\.meta\\.env)\\.([A-Z][A-Z0-9_]*)/g);
|
|
568
|
+
for (const m of matches) {
|
|
569
|
+
usedInCode.add(m[1]);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Check for undocumented vars
|
|
574
|
+
const undocumented = [...usedInCode].filter(v => !templateVars.includes(v) && !v.startsWith('NODE_'));
|
|
575
|
+
if (undocumented.length > 0) {
|
|
576
|
+
warnings.push('Env vars used in code but not in ' + templatePath + ': ' + undocumented.join(', '));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Check for unused documented vars
|
|
580
|
+
const unused = templateVars.filter(v => !usedInCode.has(v));
|
|
581
|
+
if (unused.length > 0) {
|
|
582
|
+
warnings.push('Env vars in ' + templatePath + ' but not used in code: ' + unused.join(', '));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Report
|
|
586
|
+
if (errors.length > 0) {
|
|
587
|
+
console.error('❌ Env var errors:');
|
|
588
|
+
errors.forEach(e => console.error(' - ' + e));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (warnings.length > 0) {
|
|
592
|
+
console.warn('⚠️ Env var warnings:');
|
|
593
|
+
warnings.forEach(w => console.warn(' - ' + w));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
597
|
+
console.log('✅ Env vars properly documented');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
process.exit(errors.length > 0 ? 1 : 0);
|
|
601
|
+
`
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ============================================================================
|
|
605
|
+
// COMBINED PRELAUNCH VALIDATION
|
|
606
|
+
// ============================================================================
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Generate combined prelaunch validation script
|
|
610
|
+
*/
|
|
611
|
+
function generatePrelaunchValidator() {
|
|
612
|
+
return `#!/usr/bin/env node
|
|
613
|
+
const { execSync } = require('child_process');
|
|
614
|
+
|
|
615
|
+
console.log('');
|
|
616
|
+
console.log('╔════════════════════════════════════════╗');
|
|
617
|
+
console.log('║ PRE-LAUNCH VALIDATION SUITE ║');
|
|
618
|
+
console.log('╚════════════════════════════════════════╝');
|
|
619
|
+
console.log('');
|
|
620
|
+
|
|
621
|
+
const checks = [
|
|
622
|
+
{ name: 'SEO: Sitemap', cmd: 'npm run validate:sitemap --silent' },
|
|
623
|
+
{ name: 'SEO: robots.txt', cmd: 'npm run validate:robots --silent' },
|
|
624
|
+
{ name: 'SEO: Meta Tags', cmd: 'npm run validate:meta --silent' },
|
|
625
|
+
{ name: 'Links', cmd: 'npm run validate:links --silent', optional: true },
|
|
626
|
+
{ name: 'Accessibility', cmd: 'npm run validate:a11y --silent', optional: true },
|
|
627
|
+
{ name: 'Documentation', cmd: 'npm run validate:docs --silent' }
|
|
628
|
+
];
|
|
629
|
+
|
|
630
|
+
let passed = 0;
|
|
631
|
+
let failed = 0;
|
|
632
|
+
let skipped = 0;
|
|
633
|
+
|
|
634
|
+
for (const check of checks) {
|
|
635
|
+
process.stdout.write(check.name.padEnd(25) + ' ');
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
execSync(check.cmd, { stdio: 'pipe', timeout: 120000 });
|
|
639
|
+
console.log('✅ PASS');
|
|
640
|
+
passed++;
|
|
641
|
+
} catch (error) {
|
|
642
|
+
if (check.optional) {
|
|
643
|
+
console.log('⚠️ SKIP');
|
|
644
|
+
skipped++;
|
|
645
|
+
} else {
|
|
646
|
+
console.log('❌ FAIL');
|
|
647
|
+
failed++;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
console.log('');
|
|
653
|
+
console.log('────────────────────────────────────────');
|
|
654
|
+
console.log('Results: ' + passed + ' passed, ' + failed + ' failed, ' + skipped + ' skipped');
|
|
655
|
+
console.log('');
|
|
656
|
+
|
|
657
|
+
if (failed > 0) {
|
|
658
|
+
console.log('❌ Pre-launch validation FAILED');
|
|
659
|
+
console.log(' Fix issues above before launching');
|
|
660
|
+
process.exit(1);
|
|
661
|
+
} else {
|
|
662
|
+
console.log('✅ Pre-launch validation PASSED');
|
|
663
|
+
}
|
|
664
|
+
`
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ============================================================================
|
|
668
|
+
// FILE WRITERS
|
|
669
|
+
// ============================================================================
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Write validation scripts to project
|
|
673
|
+
*/
|
|
674
|
+
function writeValidationScripts(targetDir) {
|
|
675
|
+
const scriptsDir = path.join(targetDir, 'scripts', 'validate')
|
|
676
|
+
|
|
677
|
+
// Create scripts directory
|
|
678
|
+
if (!fs.existsSync(scriptsDir)) {
|
|
679
|
+
fs.mkdirSync(scriptsDir, { recursive: true })
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const scripts = {
|
|
683
|
+
'sitemap.js': generateSitemapValidator(),
|
|
684
|
+
'robots.js': generateRobotsValidator(),
|
|
685
|
+
'meta-tags.js': generateMetaTagsValidator(),
|
|
686
|
+
'links.js': generateLinkValidator(),
|
|
687
|
+
'a11y.js': generateA11yValidator(),
|
|
688
|
+
'docs.js': generateDocsValidator(),
|
|
689
|
+
'prelaunch.js': generatePrelaunchValidator(),
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
for (const [filename, content] of Object.entries(scripts)) {
|
|
693
|
+
fs.writeFileSync(path.join(scriptsDir, filename), content)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return Object.keys(scripts).map(f => path.join('scripts', 'validate', f))
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Write env validator (Pro only)
|
|
701
|
+
*/
|
|
702
|
+
function writeEnvValidator(targetDir) {
|
|
703
|
+
const scriptsDir = path.join(targetDir, 'scripts', 'validate')
|
|
704
|
+
|
|
705
|
+
if (!fs.existsSync(scriptsDir)) {
|
|
706
|
+
fs.mkdirSync(scriptsDir, { recursive: true })
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
fs.writeFileSync(path.join(scriptsDir, 'env.js'), generateEnvValidator())
|
|
710
|
+
|
|
711
|
+
return path.join('scripts', 'validate', 'env.js')
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Write pa11y-ci config
|
|
716
|
+
*/
|
|
717
|
+
function writePa11yConfig(targetDir, urls = []) {
|
|
718
|
+
const configPath = path.join(targetDir, '.pa11yci')
|
|
719
|
+
const config = generatePa11yConfig(urls)
|
|
720
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
|
|
721
|
+
return '.pa11yci'
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ============================================================================
|
|
725
|
+
// PACKAGE.JSON SCRIPTS
|
|
726
|
+
// ============================================================================
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Get prelaunch validation scripts for package.json
|
|
730
|
+
*/
|
|
731
|
+
function getPrelaunchScripts(isPro = false) {
|
|
732
|
+
const scripts = {
|
|
733
|
+
'validate:sitemap': 'node scripts/validate/sitemap.js',
|
|
734
|
+
'validate:robots': 'node scripts/validate/robots.js',
|
|
735
|
+
'validate:meta': 'node scripts/validate/meta-tags.js',
|
|
736
|
+
'validate:links': 'node scripts/validate/links.js',
|
|
737
|
+
'validate:a11y': 'node scripts/validate/a11y.js',
|
|
738
|
+
'validate:docs': 'node scripts/validate/docs.js',
|
|
739
|
+
'validate:prelaunch': 'node scripts/validate/prelaunch.js',
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (isPro) {
|
|
743
|
+
scripts['validate:env'] = 'node scripts/validate/env.js'
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return scripts
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Get prelaunch dependencies
|
|
751
|
+
*/
|
|
752
|
+
function getPrelaunchDependencies() {
|
|
753
|
+
return {
|
|
754
|
+
linkinator: '^6.0.0',
|
|
755
|
+
'pa11y-ci': '^3.1.0',
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ============================================================================
|
|
760
|
+
// GITHUB ACTIONS
|
|
761
|
+
// ============================================================================
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Generate prelaunch validation workflow job
|
|
765
|
+
*/
|
|
766
|
+
function generatePrelaunchWorkflowJob() {
|
|
767
|
+
return `
|
|
768
|
+
prelaunch:
|
|
769
|
+
name: Pre-Launch Validation
|
|
770
|
+
runs-on: ubuntu-latest
|
|
771
|
+
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
|
|
772
|
+
steps:
|
|
773
|
+
- uses: actions/checkout@v4
|
|
774
|
+
|
|
775
|
+
- name: Setup Node.js
|
|
776
|
+
uses: actions/setup-node@v4
|
|
777
|
+
with:
|
|
778
|
+
node-version: '20'
|
|
779
|
+
cache: 'npm'
|
|
780
|
+
|
|
781
|
+
- name: Install dependencies
|
|
782
|
+
run: npm ci
|
|
783
|
+
|
|
784
|
+
- name: Build
|
|
785
|
+
run: npm run build --if-present
|
|
786
|
+
|
|
787
|
+
- name: Validate SEO
|
|
788
|
+
run: |
|
|
789
|
+
npm run validate:sitemap --if-present || echo "Sitemap check skipped"
|
|
790
|
+
npm run validate:robots --if-present || echo "Robots check skipped"
|
|
791
|
+
npm run validate:meta --if-present || echo "Meta check skipped"
|
|
792
|
+
|
|
793
|
+
- name: Validate Documentation
|
|
794
|
+
run: npm run validate:docs --if-present || echo "Docs check skipped"
|
|
795
|
+
|
|
796
|
+
- name: Check Links
|
|
797
|
+
run: npm run validate:links --if-present || echo "Link check skipped"
|
|
798
|
+
continue-on-error: true
|
|
799
|
+
`
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ============================================================================
|
|
803
|
+
// EXPORTS
|
|
804
|
+
// ============================================================================
|
|
805
|
+
|
|
806
|
+
module.exports = {
|
|
807
|
+
// Generators
|
|
808
|
+
generateSitemapValidator,
|
|
809
|
+
generateRobotsValidator,
|
|
810
|
+
generateMetaTagsValidator,
|
|
811
|
+
generateLinkinatorConfig,
|
|
812
|
+
generateLinkValidator,
|
|
813
|
+
generatePa11yConfig,
|
|
814
|
+
generateA11yValidator,
|
|
815
|
+
generateDocsValidator,
|
|
816
|
+
generateEnvValidator,
|
|
817
|
+
generatePrelaunchValidator,
|
|
818
|
+
generatePrelaunchWorkflowJob,
|
|
819
|
+
|
|
820
|
+
// Writers
|
|
821
|
+
writeValidationScripts,
|
|
822
|
+
writeEnvValidator,
|
|
823
|
+
writePa11yConfig,
|
|
824
|
+
|
|
825
|
+
// Package.json helpers
|
|
826
|
+
getPrelaunchScripts,
|
|
827
|
+
getPrelaunchDependencies,
|
|
828
|
+
}
|