create-backlist 10.0.4 → 10.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-backlist",
3
- "version": "10.0.4",
3
+ "version": "10.0.5",
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,6 @@
1
- // Real accessibility checker using axe-core via Playwright
1
+ // Accessibility checker with HTTP fallback
2
+ import { getBrowserLaunchOptions } from '../browser/installer.js';
3
+
2
4
  export class AccessibilityChecker {
3
5
  #playwright;
4
6
  #session;
@@ -9,73 +11,189 @@ export class AccessibilityChecker {
9
11
  }
10
12
 
11
13
  async check(url) {
12
- const browser = await this.#playwright.chromium.launch({ headless: true });
14
+ const launchOpts = await getBrowserLaunchOptions();
15
+
16
+ if (!launchOpts.available) {
17
+ return this.#httpFallback(url);
18
+ }
19
+
20
+ let playwright;
21
+ try { playwright = await import('playwright'); }
22
+ catch { return this.#httpFallback(url); }
23
+
24
+ const { executablePath, headless, args } = launchOpts;
25
+ let browser;
26
+
27
+ try {
28
+ browser = await playwright.chromium.launch({ executablePath, headless, args });
29
+ } catch {
30
+ return this.#httpFallback(url);
31
+ }
32
+
13
33
  const context = await browser.newContext({ ignoreHTTPSErrors: true });
14
34
  const page = await context.newPage();
15
35
 
16
36
  try {
17
37
  await page.goto(url, { waitUntil: 'networkidle', timeout: 20_000 });
18
38
 
19
- // Inject axe-core from CDN for real WCAG testing
20
39
  await page.addScriptTag({
21
40
  url: 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.9.1/axe.min.js',
22
41
  });
23
42
 
24
- // Run real axe analysis
25
43
  const axeResults = await page.evaluate(async () => {
26
44
  return await window.axe.run(document, {
27
- runOnly: {
28
- type: 'tag',
29
- values: ['wcag2a', 'wcag2aa', 'wcag21aa', 'best-practice'],
30
- },
45
+ runOnly: { type: 'tag', values: ['wcag2a','wcag2aa','wcag21aa','best-practice'] },
31
46
  });
32
47
  });
33
48
 
34
49
  const violations = axeResults.violations.map(v => ({
35
- id : v.id,
36
- description : v.description,
37
- help : v.help,
38
- helpUrl : v.helpUrl,
39
- impact : v.impact,
40
- tags : v.tags,
41
- category : v.tags.find(t => t.startsWith('wcag')) || 'best-practice',
42
- nodes : v.nodes.length,
43
- affectedNodes: v.nodes.slice(0, 3).map(n => ({
44
- html : n.html,
45
- target : n.target,
46
- message: n.failureSummary,
47
- })),
50
+ id : v.id,
51
+ description: v.description,
52
+ help : v.help,
53
+ helpUrl : v.helpUrl,
54
+ impact : v.impact,
55
+ tags : v.tags,
56
+ category : v.tags.find(t => t.startsWith('wcag')) || 'best-practice',
57
+ nodes : v.nodes.length,
58
+ affectedNodes: v.nodes.slice(0, 3).map(n => ({ html: n.html, target: n.target, message: n.failureSummary })),
48
59
  }));
49
60
 
50
61
  const passes = axeResults.passes.map(p => ({
51
- id : p.id,
52
- description: p.description,
53
- nodes : p.nodes.length,
54
- }));
55
-
56
- const incomplete = axeResults.incomplete.map(i => ({
57
- id : i.id,
58
- description: i.description,
59
- nodes : i.nodes.length,
62
+ id: p.id, description: p.description, nodes: p.nodes.length,
60
63
  }));
61
64
 
62
65
  return {
63
- pass : violations.length === 0,
64
- violations,
65
- passes,
66
- incomplete,
67
- score : passes.length > 0
68
- ? Math.round((passes.length / (passes.length + violations.length)) * 100)
69
- : 0,
70
- url,
66
+ pass : violations.length === 0,
67
+ violations, passes,
68
+ incomplete: axeResults.incomplete.map(i => ({ id: i.id, description: i.description, nodes: i.nodes.length })),
69
+ score : passes.length > 0 ? Math.round((passes.length / (passes.length + violations.length)) * 100) : 0,
70
+ url, mode : 'browser-axe',
71
71
  };
72
72
 
73
73
  } catch (err) {
74
- return { pass: false, violations: [], passes: [], incomplete: [], error: err.message, url };
74
+ return { pass: false, violations: [], passes: [], incomplete: [], error: err.message, url, mode: 'browser-error' };
75
75
  } finally {
76
76
  await page.close().catch(() => {});
77
77
  await context.close().catch(() => {});
78
78
  await browser.close().catch(() => {});
79
79
  }
80
80
  }
81
+
82
+ // HTTP fallback — parses raw HTML for common a11y issues
83
+ async #httpFallback(url) {
84
+ try {
85
+ const controller = new AbortController();
86
+ const timer = setTimeout(() => controller.abort(), 12_000);
87
+ const res = await fetch(url, { signal: controller.signal, redirect: 'follow' });
88
+ clearTimeout(timer);
89
+
90
+ if (!res.ok) {
91
+ return { pass: false, violations: [], passes: [], url, mode: 'http-fallback', error: `HTTP ${res.status}` };
92
+ }
93
+
94
+ const html = await res.text();
95
+ const violations = [];
96
+ const passes = [];
97
+
98
+ // Real HTML analysis checks
99
+ const checks = [
100
+ {
101
+ id : 'html-lang',
102
+ desc : 'HTML element must have a lang attribute',
103
+ help : 'Ensures every HTML document has a lang attribute',
104
+ impact : 'serious',
105
+ test : () => !/<html[^>]+lang=["'][^"']+["']/i.test(html),
106
+ passMsg : 'HTML lang attribute present',
107
+ },
108
+ {
109
+ id : 'img-alt',
110
+ desc : 'Images must have alternate text',
111
+ help : 'Ensures <img> elements have alternate text or a role of none/presentation',
112
+ impact : 'critical',
113
+ test : () => /<img(?![^>]*\balt=)[^>]*>/i.test(html),
114
+ passMsg : 'All images have alt attributes',
115
+ },
116
+ {
117
+ id : 'document-title',
118
+ desc : 'Documents must have <title>',
119
+ help : 'Ensures every HTML document has a non-empty title element',
120
+ impact : 'serious',
121
+ test : () => !/<title[^>]*>[^<]+<\/title>/i.test(html),
122
+ passMsg : 'Document has a title',
123
+ },
124
+ {
125
+ id : 'viewport',
126
+ desc : 'Zoom and scaling must not be disabled',
127
+ help : 'Ensures the viewport meta does not disable text scaling',
128
+ impact : 'critical',
129
+ test : () => /user-scalable=no|maximum-scale=1/i.test(html),
130
+ passMsg : 'Viewport allows scaling',
131
+ },
132
+ {
133
+ id : 'region',
134
+ desc : 'Page should have a main landmark',
135
+ help : 'Ensures the page has a <main> element',
136
+ impact : 'moderate',
137
+ test : () => !/<main[^>]*>/i.test(html),
138
+ passMsg : 'Main landmark found',
139
+ },
140
+ {
141
+ id : 'heading-order',
142
+ desc : 'Heading levels should not be skipped',
143
+ help : 'Ensures the order of headings is semantically correct',
144
+ impact : 'moderate',
145
+ test : () => !/<h1[^>]*>/i.test(html),
146
+ passMsg : 'H1 heading present',
147
+ },
148
+ {
149
+ id : 'label',
150
+ desc : 'Form elements must have labels',
151
+ help : 'Ensures every form element has a label',
152
+ impact : 'critical',
153
+ test : () => /<input(?![^>]*(?:aria-label|aria-labelledby|id=))[^>]*type=(?!"hidden")[^>]*>/i.test(html),
154
+ passMsg : 'Form inputs appear to have labels',
155
+ },
156
+ {
157
+ id : 'link-name',
158
+ desc : 'Links must have discernible text',
159
+ help : 'Ensures links have discernible text',
160
+ impact : 'serious',
161
+ test : () => /<a[^>]*>\s*<\/a>/i.test(html),
162
+ passMsg : 'Links appear to have text content',
163
+ },
164
+ ];
165
+
166
+ for (const check of checks) {
167
+ if (check.test()) {
168
+ violations.push({
169
+ id : check.id,
170
+ description: check.desc,
171
+ help : check.help,
172
+ impact : check.impact,
173
+ tags : ['wcag2a'],
174
+ category : 'wcag2a',
175
+ nodes : 1,
176
+ affectedNodes: [],
177
+ helpUrl : `https://dequeuniversity.com/rules/axe/4.9/${check.id}`,
178
+ });
179
+ } else {
180
+ passes.push({ id: check.id, description: check.passMsg, nodes: 1 });
181
+ }
182
+ }
183
+
184
+ const score = passes.length > 0
185
+ ? Math.round((passes.length / (passes.length + violations.length)) * 100)
186
+ : 0;
187
+
188
+ return {
189
+ pass: violations.length === 0,
190
+ violations, passes, incomplete: [],
191
+ score, url, mode: 'http-html-analysis',
192
+ note: 'Full axe-core WCAG scan requires Playwright browser — run: npx playwright install chromium',
193
+ };
194
+
195
+ } catch (err) {
196
+ return { pass: false, violations: [], passes: [], url, mode: 'http-fallback', error: err.message };
197
+ }
198
+ }
81
199
  }
@@ -1,125 +1,82 @@
1
- // Real performance profiler Playwright + Performance API
1
+ // Performance profiler with HTTP fallback
2
+ import { getBrowserLaunchOptions } from '../browser/installer.js';
3
+ import { formatBytes } from '../qa-engine.js';
4
+
2
5
  export class PerformanceProfiler {
3
6
  #session;
4
7
 
5
8
  constructor(session) { this.#session = session; }
6
9
 
7
10
  async profile(url) {
11
+ const launchOpts = await getBrowserLaunchOptions();
12
+
13
+ if (!launchOpts.available) {
14
+ return this.#httpFallback(url);
15
+ }
16
+
8
17
  let playwright;
9
18
  try { playwright = await import('playwright'); }
10
- catch { return this.#empty(); }
19
+ catch { return this.#httpFallback(url); }
11
20
 
12
- const browser = await playwright.chromium.launch({
13
- headless: true,
14
- args : ['--no-sandbox'],
15
- });
21
+ const { executablePath, headless, args } = launchOpts;
22
+ let browser;
16
23
 
17
- const context = await browser.newContext({
18
- ignoreHTTPSErrors: true,
19
- });
20
- const page = await context.newPage();
24
+ try {
25
+ browser = await playwright.chromium.launch({ executablePath, headless, args });
26
+ } catch {
27
+ return this.#httpFallback(url);
28
+ }
21
29
 
30
+ const context = await browser.newContext({ ignoreHTTPSErrors: true });
31
+ const page = await context.newPage();
22
32
  const slowResources = [];
23
33
  const resourceTimings = [];
24
34
 
25
35
  page.on('response', async res => {
26
- const timing = res.timing();
27
- if (timing && timing.responseEnd > 0) {
28
- const duration = timing.responseEnd - timing.requestStart;
29
- const entry = {
30
- url : res.url(),
31
- type : res.request().resourceType(),
32
- status : res.status(),
33
- duration: Math.round(duration),
34
- size : 0,
35
- };
36
- try {
37
- const body = await res.body().catch(() => null);
38
- entry.size = body?.length || 0;
39
- } catch {}
36
+ try {
37
+ const timing = res.timing();
38
+ if (!timing || timing.responseEnd <= 0) return;
39
+ const duration = Math.round(timing.responseEnd - timing.requestStart);
40
+ const entry = { url: res.url(), type: res.request().resourceType(), status: res.status(), duration, size: 0 };
41
+ try { const body = await res.body(); entry.size = body?.length || 0; } catch {}
40
42
  resourceTimings.push(entry);
41
43
  if (duration > 2000) slowResources.push(entry);
42
- }
44
+ } catch {}
43
45
  });
44
46
 
45
47
  try {
46
48
  await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 });
47
49
 
48
- // Real Web Vitals via Performance API
49
- const metrics = await page.evaluate(() => {
50
- return new Promise(resolve => {
51
- const result = {
52
- lcp : null, fcp : null, cls : null,
53
- fid : null, ttfb: null, tti : null, tbt: null,
54
- memoryUsed: null, domNodes: document.querySelectorAll('*').length,
55
- };
56
-
57
- // FCP + LCP
58
- const po = new PerformanceObserver(list => {
59
- for (const entry of list.getEntries()) {
60
- if (entry.entryType === 'paint') {
61
- if (entry.name === 'first-contentful-paint') result.fcp = Math.round(entry.startTime);
62
- }
63
- if (entry.entryType === 'largest-contentful-paint') {
64
- result.lcp = Math.round(entry.startTime);
65
- }
66
- }
67
- });
68
- try { po.observe({ entryTypes: ['paint','largest-contentful-paint'] }); } catch {}
69
-
70
- // CLS
71
- let clsValue = 0;
72
- const clsPo = new PerformanceObserver(list => {
73
- for (const entry of list.getEntries()) {
74
- if (!entry.hadRecentInput) clsValue += entry.value;
50
+ const metrics = await page.evaluate(() => new Promise(resolve => {
51
+ const result = { lcp: null, fcp: null, cls: null, fid: null, ttfb: null, tbt: null, memoryUsed: null, domNodes: document.querySelectorAll('*').length };
52
+
53
+ try {
54
+ new PerformanceObserver(list => {
55
+ for (const e of list.getEntries()) {
56
+ if (e.entryType === 'paint' && e.name === 'first-contentful-paint') result.fcp = Math.round(e.startTime);
57
+ if (e.entryType === 'largest-contentful-paint') result.lcp = Math.round(e.startTime);
75
58
  }
76
- });
77
- try { clsPo.observe({ entryTypes: ['layout-shift'] }); } catch {}
78
-
79
- // TTFB
80
- const nav = performance.getEntriesByType('navigation')[0];
81
- if (nav) result.ttfb = Math.round(nav.responseStart);
82
-
83
- // Memory
84
- if (performance.memory) {
85
- result.memoryUsed = performance.memory.usedJSHeapSize;
86
- }
87
-
88
- setTimeout(() => {
89
- result.cls = parseFloat(clsValue.toFixed(4));
90
- po.disconnect();
91
- clsPo.disconnect();
92
- resolve(result);
93
- }, 5000);
94
- });
95
- });
96
-
97
- // CPU timing via long task detection
98
- const longTasks = await page.evaluate(() => {
99
- const tasks = [];
100
- const po = new PerformanceObserver(list => {
101
- tasks.push(...list.getEntries().map(e => ({
102
- duration : Math.round(e.duration),
103
- startTime: Math.round(e.startTime),
104
- })));
105
- });
106
- try { po.observe({ entryTypes: ['longtask'] }); } catch {}
107
- return new Promise(r => setTimeout(() => { po.disconnect(); r(tasks); }, 3000));
108
- }).catch(() => []);
109
-
110
- const tbt = longTasks.reduce((sum, t) => sum + Math.max(t.duration - 50, 0), 0);
59
+ }).observe({ entryTypes: ['paint','largest-contentful-paint'] });
60
+ } catch {}
111
61
 
112
- return {
113
- ...metrics,
114
- tbt : Math.round(tbt),
115
- slowResources,
116
- resourceTimings: resourceTimings.slice(0, 50),
117
- longTasks,
118
- url,
119
- };
62
+ let cls = 0;
63
+ try {
64
+ new PerformanceObserver(list => {
65
+ for (const e of list.getEntries()) if (!e.hadRecentInput) cls += e.value;
66
+ }).observe({ entryTypes: ['layout-shift'] });
67
+ } catch {}
68
+
69
+ const nav = performance.getEntriesByType('navigation')[0];
70
+ if (nav) result.ttfb = Math.round(nav.responseStart);
71
+ if (performance.memory) result.memoryUsed = performance.memory.usedJSHeapSize;
72
+
73
+ setTimeout(() => { result.cls = parseFloat(cls.toFixed(4)); resolve(result); }, 5000);
74
+ }));
75
+
76
+ return { ...metrics, slowResources, resourceTimings: resourceTimings.slice(0, 50), url, mode: 'browser' };
120
77
 
121
78
  } catch (err) {
122
- return { ...this.#empty(), error: err.message, url };
79
+ return { ...this.#emptyMetrics(), error: err.message, url, mode: 'browser-error' };
123
80
  } finally {
124
81
  await page.close().catch(() => {});
125
82
  await context.close().catch(() => {});
@@ -127,11 +84,41 @@ export class PerformanceProfiler {
127
84
  }
128
85
  }
129
86
 
130
- #empty() {
131
- return {
132
- lcp: null, fcp: null, cls: null, fid: null,
133
- ttfb: null, tti: null, tbt: null,
134
- slowResources: [], resourceTimings: [], longTasks: [],
135
- };
87
+ // HTTP-only fallback: measures real TTFB + response time
88
+ async #httpFallback(url) {
89
+ const t0 = Date.now();
90
+ try {
91
+ const controller = new AbortController();
92
+ const timer = setTimeout(() => controller.abort(), 15_000);
93
+ const res = await fetch(url, { signal: controller.signal, redirect: 'follow' });
94
+ clearTimeout(timer);
95
+ const ttfb = Date.now() - t0;
96
+ const body = await res.text().catch(() => '');
97
+ const totalTime = Date.now() - t0;
98
+ const bodySize = new TextEncoder().encode(body).length;
99
+
100
+ return {
101
+ lcp : null, // requires browser
102
+ fcp : null,
103
+ cls : null,
104
+ fid : null,
105
+ tbt : null,
106
+ ttfb, // real TTFB from HTTP
107
+ totalTime, // real total response time
108
+ bodySize,
109
+ statusCode : res.status,
110
+ slowResources: totalTime > 3000 ? [{ url, duration: totalTime, size: bodySize, type: 'document' }] : [],
111
+ resourceTimings: [],
112
+ url,
113
+ mode : 'http-fallback',
114
+ note : 'LCP/FCP/CLS require Playwright browser — run: npx playwright install chromium',
115
+ };
116
+ } catch (err) {
117
+ return { ...this.#emptyMetrics(), ttfb: null, error: err.message, url, mode: 'http-fallback' };
118
+ }
119
+ }
120
+
121
+ #emptyMetrics() {
122
+ return { lcp: null, fcp: null, cls: null, fid: null, ttfb: null, tbt: null, slowResources: [], resourceTimings: [] };
136
123
  }
137
124
  }