create-backlist 10.0.7 → 10.0.8

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 CHANGED
@@ -272,7 +272,7 @@ async function printAnimatedBanner() {
272
272
  ' ║ / /_/ / / ___ |/ /___/ /| | / /____/ / ___/ / ║',
273
273
  ' ║/_____/ /_/ |_|\\____/_/ |_| /_____/___//____/ ║',
274
274
  ' ║ ║',
275
- ' ║ ⚡ v9.0-ULTRA — Animated Enterprise CLI ⚡ ║',
275
+ ' ║ ⚡ v10.0-ULTRA — BACKLIST NEXTOG ENTERPRICE CLI ⚡ ║',
276
276
  ' ║ Real Testing · AI Powered · Zero Fake Data · Playwright ║',
277
277
  ' ╚══════════════════════════════════════════════════════════════╝',
278
278
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-backlist",
3
- "version": "10.0.7",
3
+ "version": "10.0.8",
4
4
  "description": "An advanced, multi-language backend generator based on frontend analysis. Smart Freemium SaaS CLI with Live QA.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,5 @@
1
1
  // Smart crawler with HTTP fallback when browser unavailable
2
+ import chalk from 'chalk';
2
3
  import { URL } from 'node:url';
3
4
  import { shortId } from '../qa-engine.js';
4
5
  import { getBrowserLaunchOptions } from './installer.js';
@@ -19,9 +20,9 @@ export class HTTPCrawler {
19
20
  while (queue.length > 0 && routes.length < maxPages) {
20
21
  const { url, depth } = queue.shift();
21
22
  const norm = this.#norm(url);
22
- if (!norm || this.#visited.has(norm)) continue;
23
- if (!this.#sameOrigin(norm, baseUrl)) continue;
24
- if (depth > 3) continue;
23
+ if (!norm || this.#visited.has(norm)) continue;
24
+ if (!this.#sameOrigin(norm, baseUrl)) continue;
25
+ if (depth > 3) continue;
25
26
 
26
27
  this.#visited.add(norm);
27
28
 
@@ -29,18 +30,15 @@ export class HTTPCrawler {
29
30
  routes.push(route);
30
31
  if (onRoute) onRoute(route);
31
32
 
32
- // Extract links from HTML response
33
- if (route.links) {
34
- for (const link of route.links) {
35
- const ln = this.#norm(link);
36
- if (ln && !this.#visited.has(ln) && this.#sameOrigin(ln, baseUrl)) {
37
- queue.push({ url: ln, depth: depth + 1 });
38
- }
33
+ for (const link of (route.links || [])) {
34
+ const ln = this.#norm(link);
35
+ if (ln && !this.#visited.has(ln) && this.#sameOrigin(ln, baseUrl)) {
36
+ queue.push({ url: ln, depth: depth + 1 });
39
37
  }
40
38
  }
41
39
  }
42
40
 
43
- // Also probe common API paths
41
+ // Probe common API paths
44
42
  const apiPaths = [
45
43
  '/api/health', '/api/status', '/api/v1/health',
46
44
  '/api/v1/users', '/api/v1/products', '/health',
@@ -48,15 +46,17 @@ export class HTTPCrawler {
48
46
  ];
49
47
 
50
48
  for (const p of apiPaths) {
51
- const url = new URL(p, baseUrl).toString();
52
- const norm = this.#norm(url);
53
- if (this.#visited.has(norm)) continue;
54
- this.#visited.add(norm);
55
- const route = await this.#probeURL(url, 0);
56
- if (route.status > 0 && route.status < 500) {
57
- routes.push(route);
58
- if (onRoute) onRoute(route);
59
- }
49
+ try {
50
+ const url = new URL(p, baseUrl).toString();
51
+ const norm = this.#norm(url);
52
+ if (this.#visited.has(norm)) continue;
53
+ this.#visited.add(norm);
54
+ const route = await this.#probeURL(url, 0);
55
+ if (route.status > 0 && route.status < 500) {
56
+ routes.push(route);
57
+ if (onRoute) onRoute(route);
58
+ }
59
+ } catch {}
60
60
  }
61
61
 
62
62
  return routes;
@@ -75,7 +75,7 @@ export class HTTPCrawler {
75
75
  });
76
76
  clearTimeout(timer);
77
77
 
78
- const ct = res.headers.get('content-type') || '';
78
+ const ct = res.headers.get('content-type') || '';
79
79
  const duration = Date.now() - t0;
80
80
  const headers = {};
81
81
  res.headers.forEach((v, k) => { headers[k] = v; });
@@ -151,25 +151,24 @@ export class HTTPCrawler {
151
151
  }
152
152
 
153
153
  #detectType(url, ct, status) {
154
- if (status >= 400) return 'error-page';
154
+ if (status >= 400) return 'error-page';
155
155
  if (ct.includes('json') || url.includes('/api/')) return 'api';
156
156
  if (url.endsWith('.xml') || url.endsWith('.txt')) return 'resource';
157
- if (/\/(login|signin|auth)/i.test(url)) return 'auth';
158
- if (/\/(admin)/i.test(url)) return 'admin';
159
- if (/\/(dashboard)/i.test(url)) return 'dashboard';
157
+ if (/\/(login|signin|auth)/i.test(url)) return 'auth';
158
+ if (/\/(admin)/i.test(url)) return 'admin';
159
+ if (/\/(dashboard)/i.test(url)) return 'dashboard';
160
160
  return 'page';
161
161
  }
162
162
  }
163
163
 
164
164
  // ── Browser-powered crawler (Playwright) ──────────────────────────────────
165
165
  export class SmartCrawler {
166
- #visited = new Set();
166
+ #visited = new Set();
167
167
  #launchOpts = null;
168
168
 
169
- constructor(_playwright) {} // playwright passed but we resolve it ourselves
169
+ constructor(_playwright) {}
170
170
 
171
171
  async crawl(baseUrl, { maxPages = 60, maxDepth = 4, onRoute } = {}) {
172
- // Resolve launch options including auto-install
173
172
  if (!this.#launchOpts) {
174
173
  this.#launchOpts = await getBrowserLaunchOptions();
175
174
  }
@@ -213,9 +212,9 @@ export class SmartCrawler {
213
212
  while (queue.length > 0 && routes.length < maxPages) {
214
213
  const { url, depth } = queue.shift();
215
214
  const norm = this.#norm(url);
216
- if (!norm || this.#visited.has(norm)) continue;
217
- if (!this.#sameOrigin(norm, baseUrl)) continue;
218
- if (depth > maxDepth) continue;
215
+ if (!norm || this.#visited.has(norm)) continue;
216
+ if (!this.#sameOrigin(norm, baseUrl)) continue;
217
+ if (depth > maxDepth) continue;
219
218
  this.#visited.add(norm);
220
219
 
221
220
  const route = await this.#probePage(context, norm, depth, baseUrl);
@@ -240,9 +239,9 @@ export class SmartCrawler {
240
239
  }
241
240
 
242
241
  async #probePage(context, url, depth, baseUrl) {
243
- const page = await context.newPage();
244
- const networkRequests = [];
245
- const links = new Set();
242
+ const page = await context.newPage();
243
+ const networkRequests = [];
244
+ const links = new Set();
246
245
 
247
246
  page.on('request', req => {
248
247
  const u = req.url();
@@ -257,27 +256,44 @@ export class SmartCrawler {
257
256
  const ct = response?.headers()['content-type'] || '';
258
257
 
259
258
  if (!ct.includes('text/html') && !ct.includes('application/xhtml')) {
260
- return { id: shortId(), url, type: this.#detectType(url, ct, status), status, depth, links: [], forms: [] };
259
+ return {
260
+ id: shortId(), url,
261
+ type : this.#detectType(url, ct, status),
262
+ status, depth, links: [], forms: [],
263
+ };
261
264
  }
262
265
 
263
- const hrefs = await page.$$eval('a[href]', els => els.map(e => e.href).filter(Boolean)).catch(() => []);
266
+ const hrefs = await page.$$eval('a[href]', els => els.map(e => e.href).filter(Boolean))
267
+ .catch(() => []);
264
268
  hrefs.forEach(h => links.add(h));
265
269
 
266
270
  const forms = await page.$$eval('form', els => els.map(f => ({
267
- action: f.action, method: f.method || 'GET',
271
+ action: f.action,
272
+ method: f.method || 'GET',
268
273
  fields: Array.from(f.elements).map(el => ({
269
- name: el.name, type: el.type, required: el.required,
274
+ name : el.name,
275
+ type : el.type,
276
+ required: el.required,
270
277
  })).filter(f => f.name),
271
278
  }))).catch(() => []);
272
279
 
273
- networkRequests.forEach(r => { if (this.#sameOrigin(r.url, baseUrl)) links.add(r.url); });
280
+ networkRequests.forEach(r => {
281
+ if (this.#sameOrigin(r.url, baseUrl)) links.add(r.url);
282
+ });
274
283
 
275
284
  return {
276
- id: shortId(), url, type: this.#detectType(url, ct, status),
277
- status, depth, links: [...links], forms, contentType: ct,
285
+ id: shortId(), url,
286
+ type : this.#detectType(url, ct, status),
287
+ status, depth,
288
+ links : [...links],
289
+ forms,
290
+ contentType: ct,
278
291
  };
279
292
  } catch (err) {
280
- return { id: shortId(), url, type: 'error', status: 0, depth, links: [], forms: [], error: err.message };
293
+ return {
294
+ id: shortId(), url, type: 'error', status: 0,
295
+ depth, links: [], forms: [], error: err.message,
296
+ };
281
297
  } finally {
282
298
  await page.close().catch(() => {});
283
299
  }
@@ -294,12 +310,12 @@ export class SmartCrawler {
294
310
  }
295
311
 
296
312
  #detectType(url, ct, status) {
297
- if (status >= 400) return 'error-page';
313
+ if (status >= 400) return 'error-page';
298
314
  if (ct.includes('json') || url.includes('/api/')) return 'api';
299
315
  if (url.endsWith('.xml') || url.endsWith('.txt')) return 'resource';
300
- if (/\/(login|signin|auth)/i.test(url)) return 'auth';
301
- if (/\/(admin)/i.test(url)) return 'admin';
302
- if (/\/(dashboard)/i.test(url)) return 'dashboard';
316
+ if (/\/(login|signin|auth)/i.test(url)) return 'auth';
317
+ if (/\/(admin)/i.test(url)) return 'admin';
318
+ if (/\/(dashboard)/i.test(url)) return 'dashboard';
303
319
  return 'page';
304
320
  }
305
- }
321
+ }
@@ -1,5 +1,6 @@
1
1
  // Real browser interactions with HTTP fallback
2
- import { shortId, sleep } from '../qa-engine.js';
2
+ import chalk from 'chalk';
3
+ import { shortId, sleep } from '../qa-engine.js';
3
4
  import { getBrowserLaunchOptions } from './installer.js';
4
5
 
5
6
  // ── HTTP-only page tester (fallback) ──────────────────────────────────────
@@ -27,30 +28,28 @@ export class HTTPPageTester {
27
28
  try { text = await res.text(); } catch {}
28
29
  }
29
30
 
30
- const pass = status >= 200 && status < 400;
31
-
32
- // Extract forms from HTML
31
+ const pass = status >= 200 && status < 400;
33
32
  const forms = this.#extractForms(text);
34
33
 
35
34
  onNetworkEvent?.({ type: 'response', url, status, headers });
36
35
 
37
36
  return {
38
37
  pass,
39
- failReason : pass ? null : `HTTP ${status}`,
40
- page : null, // no real browser page
41
- loadTime : duration,
42
- consoleErrors : [], // HTTP mode — no JS execution
43
- networkErrors : [],
44
- jsErrors : [],
45
- resourcesFailed : [],
38
+ failReason : pass ? null : `HTTP ${status}`,
39
+ page : null,
40
+ loadTime : duration,
41
+ consoleErrors : [],
42
+ networkErrors : [],
43
+ jsErrors : [],
44
+ resourcesFailed : [],
46
45
  interactedElements: [],
47
- renderTime : null,
48
- domContentLoaded: null,
46
+ renderTime : null,
47
+ domContentLoaded : null,
49
48
  forms,
50
49
  status,
51
50
  headers,
52
51
  message: pass ? `HTTP ${status} in ${duration}ms` : `HTTP ${status}`,
53
- mode : 'http-fallback',
52
+ mode : 'http-fallback',
54
53
  };
55
54
  } catch (err) {
56
55
  return {
@@ -63,13 +62,22 @@ export class HTTPPageTester {
63
62
  }
64
63
 
65
64
  async testForm(page, form) {
66
- return { pass: true, validationOk: true, submissionOk: true, errors: [], duration: 0, message: 'HTTP mode — form structure detected' };
65
+ return {
66
+ pass : true,
67
+ validationOk: true,
68
+ submissionOk: true,
69
+ errors : [],
70
+ duration : 0,
71
+ message : 'HTTP mode — form structure detected',
72
+ };
67
73
  }
68
74
 
69
75
  async testAuthFlow(page, url, opts) {
70
- // HTTP-mode auth check: verify login page exists + returns 200
71
76
  try {
72
- const res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(8000) });
77
+ const res = await fetch(url, {
78
+ redirect: 'follow',
79
+ signal : AbortSignal.timeout(8000),
80
+ });
73
81
  return {
74
82
  pass : res.status === 200,
75
83
  details: [{ url, status: res.status, note: 'Login page reachable (HTTP mode)' }],
@@ -93,7 +101,7 @@ export class HTTPPageTester {
93
101
  const method = (attrs.match(/method=["']([^"']+)["']/) || [])[1] || 'GET';
94
102
  const fields = [];
95
103
  const inpRe = /<input([^>]*)>/gi;
96
- let inp;
104
+ let inp;
97
105
  while ((inp = inpRe.exec(body)) !== null) {
98
106
  const name = (inp[1].match(/name=["']([^"']+)["']/) || [])[1];
99
107
  const type = (inp[1].match(/type=["']([^"']+)["']/) || [])[1] || 'text';
@@ -110,9 +118,9 @@ export class HTTPPageTester {
110
118
  export class BrowserInteractor {
111
119
  #playwright;
112
120
  #session;
113
- #browser = null;
114
- #context = null;
115
- #launchOpts = null;
121
+ #browser = null;
122
+ #context = null;
123
+ #launchOpts = null;
116
124
  #httpFallback;
117
125
  #useFallback = false;
118
126
 
@@ -133,9 +141,20 @@ export class BrowserInteractor {
133
141
  return;
134
142
  }
135
143
 
144
+ // If playwright module wasn't loaded, fall back
145
+ if (!this.#playwright) {
146
+ this.#useFallback = true;
147
+ console.log(chalk.yellow('\n ⚠ Playwright module not available — using HTTP-only mode\n'));
148
+ return;
149
+ }
150
+
136
151
  try {
137
152
  const { executablePath, headless, args } = this.#launchOpts;
138
- this.#browser = await this.#playwright.chromium.launch({ executablePath, headless, args });
153
+ this.#browser = await this.#playwright.chromium.launch({
154
+ executablePath,
155
+ headless,
156
+ args,
157
+ });
139
158
  this.#context = await this.#browser.newContext({
140
159
  viewport : { width: 1280, height: 800 },
141
160
  ignoreHTTPSErrors: true,
@@ -160,11 +179,11 @@ export class BrowserInteractor {
160
179
  return this.#httpFallback.testPage(url, opts);
161
180
  }
162
181
 
163
- const page = await this.#context.newPage();
164
- const consoleErrors = [];
165
- const networkErrors = [];
166
- const jsErrors = [];
167
- const resourcesFailed = [];
182
+ const page = await this.#context.newPage();
183
+ const consoleErrors = [];
184
+ const networkErrors = [];
185
+ const jsErrors = [];
186
+ const resourcesFailed = [];
168
187
  const interactedElements = [];
169
188
 
170
189
  page.on('console', msg => {
@@ -182,7 +201,12 @@ export class BrowserInteractor {
182
201
  });
183
202
 
184
203
  page.on('requestfailed', req => {
185
- const entry = { url: req.url(), method: req.method(), failure: req.failure()?.errorText || 'unknown', timestamp: Date.now() };
204
+ const entry = {
205
+ url : req.url(),
206
+ method : req.method(),
207
+ failure : req.failure()?.errorText || 'unknown',
208
+ timestamp: Date.now(),
209
+ };
186
210
  networkErrors.push(entry);
187
211
  resourcesFailed.push({ url: req.url(), type: req.resourceType(), failure: entry.failure });
188
212
  opts.onNetworkEvent?.({ type: 'failed', ...entry });
@@ -199,27 +223,33 @@ export class BrowserInteractor {
199
223
  const response = await page.goto(url, { waitUntil: 'networkidle', timeout: 20_000 });
200
224
  const status = response?.status() || 0;
201
225
 
202
- if (status >= 500) { pass = false; failReason = `Server error: HTTP ${status}`; }
226
+ if (status >= 500) { pass = false; failReason = `Server error: HTTP ${status}`; }
203
227
  else if (status >= 400) { pass = false; failReason = `Client error: HTTP ${status}`; }
204
228
 
205
229
  const timing = await page.evaluate(() => {
206
230
  const t = window.performance?.timing;
207
231
  if (!t) return null;
208
- return { renderTime: t.domComplete - t.navigationStart, domContentLoaded: t.domContentLoadedEventEnd - t.navigationStart };
232
+ return {
233
+ renderTime : t.domComplete - t.navigationStart,
234
+ domContentLoaded: t.domContentLoadedEventEnd - t.navigationStart,
235
+ };
209
236
  }).catch(() => null);
210
237
 
211
238
  renderTime = timing?.renderTime;
212
239
  domContentLoaded = timing?.domContentLoaded;
213
240
 
214
241
  const forms = await page.$$eval('form', els => els.map(f => ({
215
- action: f.action, method: f.method || 'GET',
242
+ action: f.action,
243
+ method: f.method || 'GET',
216
244
  fields: Array.from(f.elements).map(el => ({
217
- name: el.name, type: el.type, required: el.required,
245
+ name : el.name,
246
+ type : el.type,
247
+ required: el.required,
218
248
  tagName: el.tagName?.toLowerCase(),
219
249
  })).filter(f => f.name),
220
250
  }))).catch(() => []);
221
251
 
222
- // Real interactions
252
+ // Real hover interactions on nav links
223
253
  const navLinks = await page.$$('nav a, header a').catch(() => []);
224
254
  for (const link of navLinks.slice(0, 5)) {
225
255
  try {
@@ -243,8 +273,10 @@ export class BrowserInteractor {
243
273
 
244
274
  } catch (err) {
245
275
  return {
246
- pass: false, failReason: err.message, page,
247
- loadTime: Date.now() - t0,
276
+ pass : false,
277
+ failReason: err.message,
278
+ page,
279
+ loadTime : Date.now() - t0,
248
280
  consoleErrors, networkErrors, jsErrors,
249
281
  resourcesFailed, interactedElements,
250
282
  forms: [], message: err.message, mode: 'browser',
@@ -270,10 +302,24 @@ export class BrowserInteractor {
270
302
  els => els.map(e => e.textContent?.trim()).filter(Boolean)
271
303
  ).catch(() => []);
272
304
 
273
- return { pass: true, validationOk: msgs.length > 0, submissionOk: true, errors, duration: Date.now() - t0, message: 'Form tested' };
305
+ return {
306
+ pass : true,
307
+ validationOk: msgs.length > 0,
308
+ submissionOk: true,
309
+ errors,
310
+ duration : Date.now() - t0,
311
+ message : 'Form tested',
312
+ };
274
313
  } catch (err) {
275
314
  errors.push(err.message);
276
- return { pass: false, validationOk: false, submissionOk: false, errors, duration: Date.now() - t0, message: err.message };
315
+ return {
316
+ pass : false,
317
+ validationOk: false,
318
+ submissionOk: false,
319
+ errors,
320
+ duration : Date.now() - t0,
321
+ message : err.message,
322
+ };
277
323
  }
278
324
  }
279
325
 
@@ -286,35 +332,51 @@ export class BrowserInteractor {
286
332
  for (const cred of (opts.testCredentials || [])) {
287
333
  try {
288
334
  await page.goto(url, { waitUntil: 'networkidle', timeout: 15_000 });
335
+
289
336
  const uf = await page.$('input[type="email"],input[name="email"],input[name="username"]');
290
337
  const pf = await page.$('input[type="password"]');
291
338
  if (!uf || !pf) { details.push({ note: 'Login fields not found' }); continue; }
339
+
292
340
  await uf.fill(cred.username);
293
341
  await pf.fill(cred.password);
342
+
294
343
  const btn = await page.$('[type="submit"]');
295
344
  if (btn) { await btn.click(); await page.waitForTimeout(2000); }
296
345
 
297
346
  const body = await page.textContent('body').catch(() => '');
298
347
  const curUrl = page.url();
299
- const rejected = /invalid|incorrect|error|wrong|fail/i.test(body) || curUrl.includes('login');
348
+ const rejected = /invalid|incorrect|error|wrong|fail/i.test(body)
349
+ || curUrl.includes('login');
350
+
351
+ details.push({
352
+ credentials: cred.username.slice(0, 8) + '...',
353
+ expectFail : cred.expectFail,
354
+ wasRejected: rejected,
355
+ currentUrl : curUrl,
356
+ });
300
357
 
301
- details.push({ credentials: cred.username.slice(0, 8) + '...', expectFail: cred.expectFail, wasRejected: rejected, currentUrl: curUrl });
302
358
  if (cred.expectFail && !rejected) { pass = false; }
303
359
  } catch (err) {
304
360
  details.push({ error: err.message });
305
361
  }
306
362
  }
307
- return { pass, details, duration: Date.now() - t0, message: pass ? 'Auth flow validated' : 'Auth issues detected' };
363
+
364
+ return {
365
+ pass,
366
+ details,
367
+ duration: Date.now() - t0,
368
+ message : pass ? 'Auth flow validated' : 'Auth issues detected',
369
+ };
308
370
  }
309
371
 
310
372
  #testValue(field) {
311
373
  const t = (field.type || 'text').toLowerCase();
312
374
  const n = (field.name || '').toLowerCase();
313
- if (t === 'email' || n.includes('email')) return 'test@backlist-qa.dev';
314
- if (t === 'password' || n.includes('pass')) return 'TestPass123!';
315
- if (t === 'tel' || n.includes('phone')) return '+1-555-000-0000';
316
- if (t === 'number') return '42';
317
- if (n.includes('name')) return 'QA Test User';
375
+ if (t === 'email' || n.includes('email')) return 'test@backlist-qa.dev';
376
+ if (t === 'password' || n.includes('pass')) return 'TestPass123!';
377
+ if (t === 'tel' || n.includes('phone')) return '+1-555-000-0000';
378
+ if (t === 'number') return '42';
379
+ if (n.includes('name')) return 'QA Test User';
318
380
  return 'backlist-qa-test';
319
381
  }
320
- }
382
+ }