create-qa-architect 5.0.7 → 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.
@@ -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
+ }