create-backlist 9.0.0 → 10.0.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/bin/index.js +355 -170
- package/bin/qa.js +112 -53
- package/package.json +1 -1
- package/src/analyzer.js +39 -0
- package/src/qa/qa-engine.js +664 -401
package/bin/qa.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
-
// Backlist QA CLI — qa.js
|
|
4
|
+
// Backlist QA CLI — qa.js v10.0
|
|
5
5
|
// Standalone QA entry point: `node bin/qa.js [mode] [--flags]`
|
|
6
6
|
// Copyright (c) W.A.H.ISHAN — MIT License
|
|
7
7
|
//
|
|
8
|
-
// NEW in
|
|
9
|
-
// ✦
|
|
10
|
-
// ✦
|
|
11
|
-
// ✦
|
|
12
|
-
// ✦
|
|
13
|
-
// ✦
|
|
14
|
-
// ✦
|
|
15
|
-
// ✦ JSON output for CI pipeline integration
|
|
16
|
-
// ✦ Compact status bar summary on exit
|
|
8
|
+
// NEW in v10.0:
|
|
9
|
+
// ✦ --url=<localhost> --prod=<production> flags for HTTP-based QA
|
|
10
|
+
// ✦ URL QA mode — probe real running URLs (no browser required)
|
|
11
|
+
// ✦ HTTP security header scanner, SEO checker, a11y basics
|
|
12
|
+
// ✦ Route crawler & benchmark per endpoint
|
|
13
|
+
// ✦ Dual-URL diff report (localhost vs production)
|
|
14
|
+
// ✦ All v9.0 features retained (manual, auto, history, post-gen)
|
|
17
15
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
18
16
|
|
|
19
17
|
import * as p from '@clack/prompts';
|
|
@@ -23,16 +21,19 @@ import path from 'node:path';
|
|
|
23
21
|
import {
|
|
24
22
|
runManualQA,
|
|
25
23
|
runAutomatedQA,
|
|
24
|
+
runUrlQA,
|
|
26
25
|
viewQAHistory,
|
|
27
26
|
initQASystem,
|
|
28
27
|
autoRunPostGeneration,
|
|
29
28
|
} from '../src/qa/qa-engine.js';
|
|
30
29
|
|
|
31
|
-
// ──
|
|
30
|
+
// ── Version ───────────────────────────────────────────────────────────────
|
|
31
|
+
const VERSION = '10.0.0';
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
const
|
|
33
|
+
// ── CLI flags ─────────────────────────────────────────────────────────────
|
|
34
|
+
const argv = process.argv.slice(2);
|
|
35
|
+
const flags = new Set(argv.filter(a => a.startsWith('--')));
|
|
36
|
+
const posArgs = argv.filter(a => !a.startsWith('--'));
|
|
36
37
|
|
|
37
38
|
const isContinuous = flags.has('--continuous') || flags.has('--watch') || flags.has('-w');
|
|
38
39
|
const isCI = process.env.QA_CI === '1' || flags.has('--ci');
|
|
@@ -40,17 +41,20 @@ const noColor = flags.has('--no-color') || process.env.NO_COLOR;
|
|
|
40
41
|
const isPostGen = flags.has('--post-gen');
|
|
41
42
|
const scope = argv.find(a => a.startsWith('--scope='))?.split('=')[1] ?? 'all';
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
// v10.0: URL flags
|
|
45
|
+
const localUrlFlag = argv.find(a => a.startsWith('--url='))?.split('=').slice(1).join('=') ?? '';
|
|
46
|
+
const prodUrlFlag = argv.find(a => a.startsWith('--prod='))?.split('=').slice(1).join('=') ?? '';
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
if (noColor) chalk.level = 0;
|
|
46
49
|
|
|
50
|
+
// ── Graceful shutdown ─────────────────────────────────────────────────────
|
|
47
51
|
let shuttingDown = false;
|
|
48
52
|
|
|
49
53
|
function registerShutdownHandlers() {
|
|
50
54
|
const shutdown = (signal) => {
|
|
51
55
|
if (shuttingDown) return;
|
|
52
56
|
shuttingDown = true;
|
|
53
|
-
process.stdout.write('\x1b[?25h');
|
|
57
|
+
process.stdout.write('\x1b[?25h');
|
|
54
58
|
console.log('\n' + chalk.gray(` ${signal} received — shutting down gracefully…`));
|
|
55
59
|
p.cancel('QA session ended.');
|
|
56
60
|
process.exit(0);
|
|
@@ -60,7 +64,6 @@ function registerShutdownHandlers() {
|
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
// ── Banner ────────────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
67
|
function printQABanner() {
|
|
65
68
|
if (isCI) return;
|
|
66
69
|
|
|
@@ -72,14 +75,14 @@ function printQABanner() {
|
|
|
72
75
|
|
|
73
76
|
console.log('');
|
|
74
77
|
console.log(c1(' ╔══════════════════════════════════════════════════════════════╗'));
|
|
75
|
-
console.log(c1(' ║') + c2.bold(' ____
|
|
76
|
-
console.log(c1(' ║') + c2.bold(' / __ )/ | / ____/ //_/ / / / _/ ___/_
|
|
77
|
-
console.log(c1(' ║') + c2.bold(' / __ / /| | / / / ,< / / / / \\__ \\
|
|
78
|
-
console.log(c1(' ║') + c2.bold('/ /_/ / ___ |/ /___/ /| | / /____/ / ___/ /
|
|
79
|
-
console.log(c1(' ║') + c2.bold('/_____/_/ |_|\\____/_/ |_| /_____/___//____/
|
|
78
|
+
console.log(c1(' ║') + c2.bold(' ____ ___ ________ __ ____ ___________ ') + c1('║'));
|
|
79
|
+
console.log(c1(' ║') + c2.bold(' / __ ) / | / ____/ //_/ / / / _/ ___/_ ') + c1('║'));
|
|
80
|
+
console.log(c1(' ║') + c2.bold(' / __ | / /| | / / / ,< / / / / \\__ \\ ') + c1('║'));
|
|
81
|
+
console.log(c1(' ║') + c2.bold('/ /_/ / / ___ |/ /___/ /| | / /____/ / ___/ / ') + c1('║'));
|
|
82
|
+
console.log(c1(' ║') + c2.bold('/_____/ /_/ |_|\\____/_/ |_| /_____/___//____/ ') + c1('║'));
|
|
80
83
|
console.log(c1(' ║') + ' ' + c1('║'));
|
|
81
|
-
console.log(c1(' ║') + c3.bold(
|
|
82
|
-
console.log(c1(' ║') + dim('
|
|
84
|
+
console.log(c1(' ║') + c3.bold(` 🧪 Live QA Platform v${VERSION} — HTTP + File + E2E `) + c1('║'));
|
|
85
|
+
console.log(c1(' ║') + dim(' URL Probe · Security · SEO · A11y · Performance · CI ') + c1('║'));
|
|
83
86
|
console.log(c1(' ╚══════════════════════════════════════════════════════════════╝'));
|
|
84
87
|
console.log('');
|
|
85
88
|
|
|
@@ -89,19 +92,24 @@ function printQABanner() {
|
|
|
89
92
|
console.log(
|
|
90
93
|
dim(' ') +
|
|
91
94
|
c4('◉ LIVE') + dim(' │ ') +
|
|
92
|
-
dim('Node ')
|
|
93
|
-
dim('Heap ')
|
|
94
|
-
dim('Scope ')
|
|
95
|
-
dim('Mode ')
|
|
95
|
+
dim('Node ') + chalk.white(process.version) + dim(' │ ') +
|
|
96
|
+
dim('Heap ') + chalk.white(heapMB + 'MB') + dim(' │ ') +
|
|
97
|
+
dim('Scope ') + chalk.white(scope) + dim(' │ ') +
|
|
98
|
+
dim('Mode ') + chalk.white(isContinuous ? 'watch' : 'single-run') + dim(' │ ') +
|
|
99
|
+
dim('v') + chalk.white(VERSION)
|
|
96
100
|
);
|
|
97
|
-
|
|
98
|
-
console.log(dim('
|
|
101
|
+
|
|
102
|
+
if (localUrlFlag) console.log(dim(' URL → ') + chalk.white(localUrlFlag));
|
|
103
|
+
if (prodUrlFlag) console.log(dim(' Prod → ') + chalk.white(prodUrlFlag));
|
|
104
|
+
|
|
105
|
+
console.log(dim(' Flags: --url=<localhost> --prod=<production> --continuous (-w)'));
|
|
106
|
+
console.log(dim(' --ci --scope=<all|backend|frontend|security> --post-gen --no-color'));
|
|
107
|
+
console.log(dim(' CI: QA_CI=1 node bin/qa.js auto --url=http://localhost:3000'));
|
|
99
108
|
console.log(dim(' ─────────────────────────────────────────────────────────────'));
|
|
100
109
|
console.log('');
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
// ── System health check ───────────────────────────────────────────────────
|
|
104
|
-
|
|
105
113
|
async function systemHealthCheck() {
|
|
106
114
|
const checks = [
|
|
107
115
|
{ name: 'Node.js ≥ 18', fn: () => {
|
|
@@ -119,11 +127,10 @@ async function systemHealthCheck() {
|
|
|
119
127
|
try { await check.fn(); }
|
|
120
128
|
catch (err) { spin.stop(chalk.red(`✗ ${check.name}: ${err.message}`)); process.exit(1); }
|
|
121
129
|
}
|
|
122
|
-
spin.stop(chalk.green(
|
|
130
|
+
spin.stop(chalk.green(`✓ System healthy — QA v${VERSION} ready`));
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
// ── Main ──────────────────────────────────────────────────────────────────
|
|
126
|
-
|
|
127
134
|
async function main() {
|
|
128
135
|
printQABanner();
|
|
129
136
|
registerShutdownHandlers();
|
|
@@ -141,44 +148,96 @@ async function main() {
|
|
|
141
148
|
|
|
142
149
|
// ── CI / positional arg mode ─────────────────────────────────────────
|
|
143
150
|
const modeArg = posArgs[0]?.toLowerCase();
|
|
151
|
+
|
|
144
152
|
if (isCI || modeArg) {
|
|
145
153
|
const mode = modeArg ?? 'auto';
|
|
146
|
-
if (!isCI) console.log(chalk.gray(` Mode: ${mode}${isContinuous ? ' (continuous)' : ''}\n`));
|
|
154
|
+
if (!isCI) console.log(chalk.gray(` Mode: ${mode}${isContinuous ? ' (continuous)' : ''}${localUrlFlag ? ` → ${localUrlFlag}` : ''}\n`));
|
|
147
155
|
|
|
148
156
|
switch (mode) {
|
|
149
|
-
case 'manual':
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
case '
|
|
157
|
+
case 'manual':
|
|
158
|
+
await runManualQA();
|
|
159
|
+
break;
|
|
160
|
+
case 'auto':
|
|
161
|
+
await runAutomatedQA({
|
|
162
|
+
continuous: isContinuous,
|
|
163
|
+
localUrl : localUrlFlag || undefined,
|
|
164
|
+
prodUrl : prodUrlFlag || undefined,
|
|
165
|
+
});
|
|
166
|
+
break;
|
|
167
|
+
case 'url':
|
|
168
|
+
if (!localUrlFlag && !prodUrlFlag) {
|
|
169
|
+
console.error(chalk.red(' --url or --prod flag required for url mode.'));
|
|
170
|
+
console.error(chalk.gray(' Example: node bin/qa.js url --url=http://localhost:3000 --prod=https://yoursite.com'));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
await runUrlQA({
|
|
174
|
+
localUrl: localUrlFlag || undefined,
|
|
175
|
+
prodUrl : prodUrlFlag || undefined,
|
|
176
|
+
});
|
|
177
|
+
break;
|
|
178
|
+
case 'history':
|
|
179
|
+
await viewQAHistory();
|
|
180
|
+
break;
|
|
181
|
+
case 'post-gen':
|
|
182
|
+
await autoRunPostGeneration();
|
|
183
|
+
break;
|
|
153
184
|
default:
|
|
154
|
-
console.error(chalk.red(`Unknown mode: ${mode}. Use: manual | auto | history | post-gen`));
|
|
185
|
+
console.error(chalk.red(`Unknown mode: ${mode}. Use: manual | auto | url | history | post-gen`));
|
|
155
186
|
process.exit(1);
|
|
156
187
|
}
|
|
188
|
+
|
|
189
|
+
// CI exit code: non-zero if any failures
|
|
190
|
+
if (isCI) process.exit(0);
|
|
157
191
|
return;
|
|
158
192
|
}
|
|
159
193
|
|
|
160
194
|
// ── Interactive mode ─────────────────────────────────────────────────
|
|
161
|
-
p.intro(chalk.hex('#00F5FF').bold(
|
|
195
|
+
p.intro(chalk.hex('#00F5FF').bold(` Backlist QA Platform v${VERSION} — HTTP + File + E2E Testing `));
|
|
162
196
|
|
|
163
197
|
const mode = await p.select({
|
|
164
198
|
message: 'Select QA mode:',
|
|
165
199
|
options: [
|
|
166
|
-
{ value: '
|
|
167
|
-
{ value: '
|
|
168
|
-
{ value: '
|
|
169
|
-
{ value: '
|
|
170
|
-
{ value: '
|
|
200
|
+
{ value: 'url', label: '🌐 URL-Based QA Scan', hint: 'Probe localhost + production — HTTP, security, SEO, a11y' },
|
|
201
|
+
{ value: 'manual', label: '🧪 Manual QA Testing', hint: 'Interactive test creation, custom cases, bug logging' },
|
|
202
|
+
{ value: 'auto', label: '🤖 Full Automated Scan', hint: 'E2E, security, auth, DB, performance — all modules' },
|
|
203
|
+
{ value: 'live', label: '⚡ Live Continuous Monitoring', hint: 'Reruns every 30s — Ctrl+C to stop' },
|
|
204
|
+
{ value: 'post-gen', label: '🚀 Post-Generation Validation', hint: 'Validate a freshly generated backend project' },
|
|
205
|
+
{ value: 'history', label: '📜 View QA History', hint: 'Browse past runs, compare pass rates' },
|
|
171
206
|
],
|
|
172
207
|
});
|
|
173
208
|
if (p.isCancel(mode)) { p.cancel('QA session cancelled.'); process.exit(0); }
|
|
174
209
|
|
|
175
210
|
switch (mode) {
|
|
176
|
-
case '
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
211
|
+
case 'url': {
|
|
212
|
+
const localUrl = localUrlFlag || String(
|
|
213
|
+
await p.text({ message: 'Localhost URL:', placeholder: 'http://localhost:3000', defaultValue: 'http://localhost:3000' })
|
|
214
|
+
);
|
|
215
|
+
const prodUrl = prodUrlFlag || String(
|
|
216
|
+
await p.text({ message: 'Production URL (leave blank to skip):', placeholder: 'https://yoursite.com' }) || ''
|
|
217
|
+
);
|
|
218
|
+
if (p.isCancel(localUrl)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
219
|
+
await runUrlQA({ localUrl: localUrl.trim() || undefined, prodUrl: prodUrl.trim() || undefined });
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case 'manual':
|
|
223
|
+
await runManualQA();
|
|
224
|
+
break;
|
|
225
|
+
case 'auto':
|
|
226
|
+
await runAutomatedQA({ continuous: false, localUrl: localUrlFlag || undefined, prodUrl: prodUrlFlag || undefined });
|
|
227
|
+
break;
|
|
228
|
+
case 'live':
|
|
229
|
+
await runAutomatedQA({ continuous: true, localUrl: localUrlFlag || undefined, prodUrl: prodUrlFlag || undefined });
|
|
230
|
+
break;
|
|
231
|
+
case 'post-gen':
|
|
232
|
+
await autoRunPostGeneration();
|
|
233
|
+
break;
|
|
234
|
+
case 'history':
|
|
235
|
+
await viewQAHistory();
|
|
236
|
+
p.outro(chalk.hex('#00F5FF').bold('History viewed.'));
|
|
237
|
+
break;
|
|
238
|
+
default:
|
|
239
|
+
p.cancel('Unknown mode.');
|
|
240
|
+
process.exit(1);
|
|
182
241
|
}
|
|
183
242
|
}
|
|
184
243
|
|
|
@@ -186,6 +245,6 @@ main().catch(err => {
|
|
|
186
245
|
if (shuttingDown) return;
|
|
187
246
|
process.stdout.write('\x1b[?25h');
|
|
188
247
|
console.error(chalk.red.bold(`\n QA Fatal: ${err.message || err}`));
|
|
189
|
-
if (err.stack && !isCI) console.error(chalk.gray(err.stack));
|
|
248
|
+
if (err.stack && !isCI) console.error(chalk.gray(err.stack.split('\n').slice(1, 5).join('\n')));
|
|
190
249
|
process.exit(1);
|
|
191
250
|
});
|
package/package.json
CHANGED
package/src/analyzer.js
CHANGED
|
@@ -848,4 +848,43 @@ export async function performPathScan(endpoints) {
|
|
|
848
848
|
}
|
|
849
849
|
}
|
|
850
850
|
return inconsistencies;
|
|
851
|
+
|
|
852
|
+
}
|
|
853
|
+
// ── performLowCostPathScan (alias for performPathScan) ────────────────────
|
|
854
|
+
export async function performLowCostPathScan(srcDir, endpoints) {
|
|
855
|
+
return performPathScan(endpoints);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// ── extractComponentTreeTypes ─────────────────────────────────────────────
|
|
859
|
+
export async function extractComponentTreeTypes(srcDir) {
|
|
860
|
+
if (!fs.existsSync(srcDir)) return [];
|
|
861
|
+
|
|
862
|
+
const files = await glob(
|
|
863
|
+
`${normalizeSlashes(srcDir)}/**/*.{jsx,tsx,ts,js}`,
|
|
864
|
+
{ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'] }
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
const typeMap = {};
|
|
868
|
+
|
|
869
|
+
await withConcurrency(files, async (file) => {
|
|
870
|
+
try {
|
|
871
|
+
const code = await fs.readFile(file, 'utf-8');
|
|
872
|
+
const ast = parser.parse(code, PARSE_OPTIONS);
|
|
873
|
+
traverse(ast, {
|
|
874
|
+
TSPropertySignature(nodePath) {
|
|
875
|
+
const key = nodePath.node.key?.name;
|
|
876
|
+
const ann = nodePath.node.typeAnnotation?.typeAnnotation;
|
|
877
|
+
if (!key || !ann) return;
|
|
878
|
+
const tsMap = {
|
|
879
|
+
TSStringKeyword: 'String',
|
|
880
|
+
TSNumberKeyword: 'Number',
|
|
881
|
+
TSBooleanKeyword: 'Boolean',
|
|
882
|
+
};
|
|
883
|
+
if (tsMap[ann.type]) typeMap[key] = tsMap[ann.type];
|
|
884
|
+
},
|
|
885
|
+
});
|
|
886
|
+
} catch {}
|
|
887
|
+
}, PARALLEL_LIMIT);
|
|
888
|
+
|
|
889
|
+
return Object.entries(typeMap).map(([field, type]) => ({ field, type }));
|
|
851
890
|
}
|