laxy-verify 1.2.1 → 1.2.3
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/README.md +193 -64
- package/dist/a11y-deep.d.ts +20 -0
- package/dist/a11y-deep.js +161 -0
- package/dist/ai-analysis.d.ts +28 -0
- package/dist/ai-analysis.js +32 -0
- package/dist/audit/broken-links.d.ts +5 -1
- package/dist/audit/broken-links.js +23 -12
- package/dist/bundle-size.d.ts +14 -0
- package/dist/bundle-size.js +209 -0
- package/dist/cli.js +432 -16
- package/dist/compare-env.d.ts +23 -0
- package/dist/compare-env.js +55 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +149 -4
- package/dist/entitlement.d.ts +2 -0
- package/dist/entitlement.js +5 -1
- package/dist/init-analysis.d.ts +6 -0
- package/dist/init-analysis.js +302 -0
- package/dist/init.js +66 -0
- package/dist/lighthouse.d.ts +31 -1
- package/dist/lighthouse.js +76 -3
- package/dist/outdated-check.d.ts +17 -0
- package/dist/outdated-check.js +123 -0
- package/dist/report-markdown.d.ts +14 -0
- package/dist/report-markdown.js +21 -0
- package/dist/route-discovery.d.ts +7 -0
- package/dist/route-discovery.js +108 -0
- package/dist/secret-scan.d.ts +15 -0
- package/dist/secret-scan.js +218 -0
- package/dist/security-audit.d.ts +9 -1
- package/dist/security-audit.js +87 -24
- package/dist/seo-deep.d.ts +24 -0
- package/dist/seo-deep.js +147 -0
- package/dist/typecheck.d.ts +8 -0
- package/dist/typecheck.js +99 -0
- package/dist/verification-core/report.js +117 -0
- package/dist/verification-core/types.d.ts +58 -2
- package/dist/visual-diff.d.ts +8 -1
- package/dist/visual-diff.js +53 -8
- package/dist/vitals-budget.d.ts +23 -0
- package/dist/vitals-budget.js +168 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`laxy-verify` is a deployment blocker gate for frontend apps.
|
|
4
4
|
|
|
5
|
-
Every verification run includes
|
|
5
|
+
Every verification run includes build, Lighthouse, E2E, multi-viewport, security audit, visual diff, broken links, console error monitoring, and stability coverage regardless of plan. Optional opt-in checks add TypeScript type checking, secret scanning, bundle size analysis, outdated dependency detection, deep accessibility audit, deep SEO audit, and Core Web Vitals budget enforcement.
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
@@ -63,23 +63,28 @@ This is most useful if you ship frontend apps and want a practical gate before:
|
|
|
63
63
|
- QA handoff
|
|
64
64
|
- production release
|
|
65
65
|
|
|
66
|
-
##
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
|
71
|
-
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
## Why not just LHCI?
|
|
67
|
+
|
|
68
|
+
| | laxy-verify | LHCI | Checkly | Percy |
|
|
69
|
+
|--|--|--|--|--|
|
|
70
|
+
| Production build failure detection | Yes | No | No | No |
|
|
71
|
+
| User-flow E2E verification | Yes | No | Manual setup | No |
|
|
72
|
+
| Lighthouse scoring | Yes | Yes | No | No |
|
|
73
|
+
| Visual regression check | Yes | No | No | Yes |
|
|
74
|
+
| Broken link detection | Yes | No | No | No |
|
|
75
|
+
| Console error monitoring | Yes | No | No | No |
|
|
76
|
+
| Cross-browser (Firefox, WebKit) | Yes | No | Yes | No |
|
|
77
|
+
| Release decision (`hold` / `client-ready`) | Yes | No, score only | No | No |
|
|
78
|
+
| TypeScript type check | Yes | No | No | No |
|
|
79
|
+
| Hardcoded secret scanning | Yes | No | No | No |
|
|
80
|
+
| Bundle size analysis | Yes | No | No | No |
|
|
81
|
+
| Outdated dependency check | Yes | No | No | No |
|
|
82
|
+
| Deep WCAG audit (axe-core) | Yes | No | No | No |
|
|
83
|
+
| Deep SEO audit | Yes | No | No | No |
|
|
84
|
+
| Core Web Vitals budget | Yes | No | No | No |
|
|
85
|
+
| Zero-config local start | Yes | No | No | No |
|
|
86
|
+
|
|
87
|
+
LHCI measures Lighthouse. `laxy-verify` is for deciding whether this frontend is actually safe to ship.
|
|
83
88
|
|
|
84
89
|
## The failures it is meant to catch
|
|
85
90
|
|
|
@@ -90,22 +95,42 @@ Use `laxy-verify` when you want to catch things like:
|
|
|
90
95
|
- desktop looks fine, but a mobile CTA is pushed out of view
|
|
91
96
|
- Lighthouse looks acceptable, but the user-visible path is still not safe to ship
|
|
92
97
|
- a PR needs a clear hold reason instead of a vague "something failed"
|
|
98
|
+
- hardcoded API keys or secrets are about to be pushed to a public repo
|
|
99
|
+
- TypeScript type errors could cause runtime crashes
|
|
100
|
+
- bundle size has silently grown past acceptable limits
|
|
101
|
+
- critical WCAG violations block real users
|
|
102
|
+
- missing SEO meta tags hurt discoverability
|
|
93
103
|
|
|
94
104
|
## What it actually checks
|
|
95
105
|
|
|
96
|
-
A standard run includes:
|
|
97
|
-
|
|
98
|
-
- production build success
|
|
99
|
-
- Lighthouse thresholds (3 runs for stable evidence)
|
|
100
|
-
- E2E scenarios for user-visible flows
|
|
101
|
-
- multi-viewport checks (desktop, tablet, mobile)
|
|
102
|
-
- blocker-aware reporting and release decisions
|
|
103
|
-
|
|
104
106
|
Every run includes:
|
|
105
107
|
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
108
|
+
- **production build** — runs your actual build command, exit code determines pass/fail
|
|
109
|
+
- **Lighthouse** — 3 runs averaged for stable performance, accessibility, SEO, and best practices scores
|
|
110
|
+
- **E2E scenarios** — Puppeteer-based user flow testing (auto-detected or configured via `.laxy.yml`)
|
|
111
|
+
- **stability pass** — E2E runs a second time to catch flaky behavior
|
|
112
|
+
- **multi-viewport** — Lighthouse at desktop (1350px), tablet (1024px), and mobile (390px)
|
|
113
|
+
- **security audit** — `npm audit` dependency vulnerability scan
|
|
114
|
+
- **visual diff** — pixel-level screenshot comparison against baseline
|
|
115
|
+
- **broken links** — crawls all internal links and validates HTTP responses
|
|
116
|
+
- **console error monitoring** — captures browser JS errors during E2E execution
|
|
117
|
+
- **cross-browser** — Playwright on Firefox and WebKit if `browsers` is configured in `.laxy.yml`
|
|
118
|
+
|
|
119
|
+
### Opt-in checks
|
|
120
|
+
|
|
121
|
+
These checks are off by default. Enable them via CLI flags or `.laxy.yml`.
|
|
122
|
+
|
|
123
|
+
| Flag | What it checks | Blocker or Advisory |
|
|
124
|
+
|------|----------------|---------------------|
|
|
125
|
+
| `--typecheck` | TypeScript type errors via `tsc --noEmit` | **Blocker** (5+ errors → high severity) |
|
|
126
|
+
| `--secret-scan` | Hardcoded secrets: AWS keys, GitHub tokens, Stripe keys, private keys, Bearer tokens, JWTs, generic password/token assignments | **Blocker** (always high severity) |
|
|
127
|
+
| `--bundle-size` | Next.js or Vite bundle size analysis (first-load JS, largest chunk) | Advisory |
|
|
128
|
+
| `--outdated-check` | Outdated dependencies via `npm outdated --json` | Advisory (major behind → warning) |
|
|
129
|
+
| `--a11y-deep` | Deep WCAG audit via axe-core (critical/serious violations) | **Blocker** (critical → high severity) |
|
|
130
|
+
| `--seo-deep` | SEO meta tags, canonical, robots, Open Graph, Twitter Card, JSON-LD | Advisory |
|
|
131
|
+
| `--vitals-budget` | Core Web Vitals budget: LCP ≤ 2500ms, CLS ≤ 0.1, INP ≤ 200ms | Advisory |
|
|
132
|
+
|
|
133
|
+
Secret scan never exposes actual credential values — findings are masked with `***` in all output formats.
|
|
109
134
|
|
|
110
135
|
## What you get from one run
|
|
111
136
|
|
|
@@ -116,48 +141,26 @@ Every run includes:
|
|
|
116
141
|
|
|
117
142
|
Grades still exist, but they are not the main point. The main point is whether the run found blockers you should stop on.
|
|
118
143
|
|
|
119
|
-
##
|
|
120
|
-
|
|
121
|
-
Run it on a frontend app:
|
|
144
|
+
## Example workflow
|
|
122
145
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
146
|
+
1. Run `npx laxy-verify .` locally before opening or merging a PR.
|
|
147
|
+
2. Fix broken builds, broken flows, and visible regressions.
|
|
148
|
+
3. Commit `.laxy.yml`.
|
|
149
|
+
4. Run `npx laxy-verify --init`.
|
|
150
|
+
5. Let the GitHub Action apply the same gate on every PR.
|
|
127
151
|
|
|
128
|
-
|
|
152
|
+
Full verification with all opt-in checks:
|
|
129
153
|
|
|
130
154
|
```bash
|
|
131
|
-
npx laxy-verify --
|
|
155
|
+
npx laxy-verify . --typecheck --secret-scan --bundle-size --outdated-check --a11y-deep --seo-deep --vitals-budget
|
|
132
156
|
```
|
|
133
157
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
- `.laxy.yml`
|
|
137
|
-
- `.github/workflows/laxy-verify.yml`
|
|
138
|
-
|
|
139
|
-
Optional: log in to connect the CLI to your Laxy account:
|
|
158
|
+
Or enable them in `.laxy.yml` and just run:
|
|
140
159
|
|
|
141
160
|
```bash
|
|
142
|
-
npx laxy-verify
|
|
143
|
-
npx laxy-verify whoami
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
For CI, set `LAXY_TOKEN` instead of interactive login:
|
|
147
|
-
|
|
148
|
-
```yaml
|
|
149
|
-
env:
|
|
150
|
-
LAXY_TOKEN: ${{ secrets.LAXY_TOKEN }}
|
|
161
|
+
npx laxy-verify .
|
|
151
162
|
```
|
|
152
163
|
|
|
153
|
-
## Example workflow
|
|
154
|
-
|
|
155
|
-
1. Run `npx laxy-verify .` locally before opening or merging a PR.
|
|
156
|
-
2. Fix broken builds, broken flows, and visible regressions.
|
|
157
|
-
3. Commit `.laxy.yml`.
|
|
158
|
-
4. Run `npx laxy-verify --init`.
|
|
159
|
-
5. Let the GitHub Action apply the same gate on every PR.
|
|
160
|
-
|
|
161
164
|
## Example output
|
|
162
165
|
|
|
163
166
|
```text
|
|
@@ -166,9 +169,21 @@ Grade: Gold
|
|
|
166
169
|
|
|
167
170
|
Passed:
|
|
168
171
|
- production build
|
|
169
|
-
- Lighthouse thresholds
|
|
170
|
-
- core user flows
|
|
172
|
+
- Lighthouse thresholds (Performance 92, Accessibility 97, SEO 90, Best Practices 95)
|
|
173
|
+
- core user flows (5/5 scenarios passed)
|
|
174
|
+
- stability pass (second E2E run passed)
|
|
171
175
|
- desktop, tablet, and mobile viewport checks
|
|
176
|
+
- security audit (no known vulnerabilities)
|
|
177
|
+
- visual diff (no regressions)
|
|
178
|
+
- broken links (0 broken / 12 checked)
|
|
179
|
+
- console errors (0 detected)
|
|
180
|
+
- TypeScript (0 errors)
|
|
181
|
+
- Secret scan (0 findings, 42 files)
|
|
182
|
+
- Bundle size (vite, within thresholds)
|
|
183
|
+
- Outdated deps (0 outdated)
|
|
184
|
+
- WCAG deep (0 critical)
|
|
185
|
+
- SEO deep (0 errors)
|
|
186
|
+
- Core Web Vitals budget (LCP 1200ms, CLS 0.05, INP 80ms)
|
|
172
187
|
|
|
173
188
|
Artifacts:
|
|
174
189
|
- .laxy-result.json
|
|
@@ -213,7 +228,7 @@ package_manager: auto
|
|
|
213
228
|
port: 3000
|
|
214
229
|
build_timeout: 300
|
|
215
230
|
dev_timeout: 60
|
|
216
|
-
lighthouse_runs:
|
|
231
|
+
lighthouse_runs: 3
|
|
217
232
|
fail_on: bronze
|
|
218
233
|
|
|
219
234
|
thresholds:
|
|
@@ -227,6 +242,31 @@ max_crawl_depth: 3
|
|
|
227
242
|
max_crawl_pages: 10
|
|
228
243
|
browsers:
|
|
229
244
|
- chromium
|
|
245
|
+
|
|
246
|
+
# Explicit route list for Lighthouse (optional)
|
|
247
|
+
lighthouse_routes: []
|
|
248
|
+
# Extra routes not discoverable by the crawler (optional)
|
|
249
|
+
extra_routes: []
|
|
250
|
+
# Max crawl-discovered routes to run Lighthouse on (default: 5)
|
|
251
|
+
max_lighthouse_routes: 5
|
|
252
|
+
|
|
253
|
+
# Visual diff fine-tuning (optional)
|
|
254
|
+
visual_diff:
|
|
255
|
+
pixelmatch_threshold: 0.1
|
|
256
|
+
warn_threshold: 30
|
|
257
|
+
rollback_threshold: 60
|
|
258
|
+
ignore_selectors: []
|
|
259
|
+
disable_animations: true
|
|
260
|
+
|
|
261
|
+
# Opt-in checks (off by default)
|
|
262
|
+
typecheck: false
|
|
263
|
+
secret_scan: false
|
|
264
|
+
secret_scan_ignore_paths: []
|
|
265
|
+
bundle_size: false
|
|
266
|
+
outdated_check: false
|
|
267
|
+
a11y_deep: false
|
|
268
|
+
seo_deep: false
|
|
269
|
+
vitals_budget: false
|
|
230
270
|
```
|
|
231
271
|
|
|
232
272
|
Useful adjustments:
|
|
@@ -251,6 +291,16 @@ Options:
|
|
|
251
291
|
--badge
|
|
252
292
|
--init
|
|
253
293
|
--multi-viewport
|
|
294
|
+
--crawl Crawl the app to discover routes before E2E
|
|
295
|
+
--typecheck Run TypeScript type check (tsc --noEmit)
|
|
296
|
+
--secret-scan Scan for hardcoded secrets and credentials
|
|
297
|
+
--bundle-size Analyze bundle size (Next.js/Vite)
|
|
298
|
+
--outdated-check Check for outdated dependencies
|
|
299
|
+
--a11y-deep Deep accessibility audit (axe-core)
|
|
300
|
+
--seo-deep Deep SEO audit (meta, OG, JSON-LD)
|
|
301
|
+
--vitals-budget Core Web Vitals budget check (LCP, CLS, INP)
|
|
302
|
+
--share Save result and get a shareable URL (Pro)
|
|
303
|
+
--compare <url> Compare Lighthouse scores against a reference URL (Pro)
|
|
254
304
|
--help
|
|
255
305
|
|
|
256
306
|
Subcommands:
|
|
@@ -261,6 +311,56 @@ Subcommands:
|
|
|
261
311
|
|
|
262
312
|
`--plan-override` is only for plan-label and automation testing. Verification coverage stays the same on every plan.
|
|
263
313
|
|
|
314
|
+
## Pro features
|
|
315
|
+
|
|
316
|
+
Pro and Team accounts unlock additional capabilities on top of the same verification run:
|
|
317
|
+
|
|
318
|
+
- **Result saving and sharing** — `--share` saves the run to your dashboard and returns a shareable URL
|
|
319
|
+
- **Environment comparison** — `--compare <url>` runs Lighthouse against a reference URL (e.g. staging or production) and shows score deltas between your local build and the reference
|
|
320
|
+
- **AI failure analysis** — when a run ends in `hold`, Claude analyzes the failure context and returns a root cause summary with top fix suggestions
|
|
321
|
+
- **Team Slack / Discord alerts** — grade drops and `hold` verdicts fire webhook notifications with score deltas and blocker details
|
|
322
|
+
- **Weekly team report** — automated weekly summary of verification activity across your team's repos
|
|
323
|
+
|
|
324
|
+
## Secret scan patterns
|
|
325
|
+
|
|
326
|
+
When `--secret-scan` is enabled, the following patterns are detected:
|
|
327
|
+
|
|
328
|
+
| Pattern | Example |
|
|
329
|
+
|---------|--------|
|
|
330
|
+
| AWS Access Key | `AKIA...` (20 chars) |
|
|
331
|
+
| AWS Secret Key | `aws_secret_access_key = '...'` |
|
|
332
|
+
| GitHub Token | `ghp_`, `gho_`, `ghs_`, `ghu_` |
|
|
333
|
+
| Slack Token / Webhook | `xoxb-...`, `hooks.slack.com/...` |
|
|
334
|
+
| Private Key Block | `-----BEGIN RSA PRIVATE KEY-----` |
|
|
335
|
+
| Google API Key | `AIza...` |
|
|
336
|
+
| Stripe Key | `sk_live_...`, `pk_live_...` |
|
|
337
|
+
| Twilio / SendGrid / Mailgun | `SK...`, `SG...`, `key-...` |
|
|
338
|
+
| Hardcoded Bearer Token | `Bearer abc123...` |
|
|
339
|
+
| JWT-like Secret | `eyJ...` |
|
|
340
|
+
| Generic Secret Assignment | `password = '...'`, `api_key = '...'` |
|
|
341
|
+
|
|
342
|
+
False positive filtering:
|
|
343
|
+
|
|
344
|
+
- `process.env.*`, `import.meta.env.*`, `NEXT_PUBLIC_*`, `VITE_*` references are ignored
|
|
345
|
+
- GitHub Actions template variables (`${{ secrets.* }}`) are ignored
|
|
346
|
+
- `test/`, `tests/`, `__tests__/`, `spec/` directories are excluded
|
|
347
|
+
- Comment lines (`//`, `*`, `<!--`) are excluded — commented-out secrets are not flagged
|
|
348
|
+
- Placeholder/example values are ignored
|
|
349
|
+
- All secret values in findings are **masked with `***`** — never exposed in output
|
|
350
|
+
|
|
351
|
+
## Test fixture
|
|
352
|
+
|
|
353
|
+
A sample Vite + React + TypeScript app is included at `fixtures/sample-app/` for testing all verification checks:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
cd fixtures/sample-app
|
|
357
|
+
npm install
|
|
358
|
+
npm run build
|
|
359
|
+
npx laxy-verify . --typecheck --secret-scan --bundle-size --outdated-check --skip-lighthouse
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
See `fixtures/sample-app/README.md` for details on what each check finds in the fixture.
|
|
363
|
+
|
|
264
364
|
## Result files
|
|
265
365
|
|
|
266
366
|
Every run writes `.laxy-result.json`.
|
|
@@ -316,6 +416,35 @@ It is a pre-merge and pre-release verification layer, not your entire quality sy
|
|
|
316
416
|
- a frontend app with a runnable build flow
|
|
317
417
|
- optional: `playwright` if your project already uses it
|
|
318
418
|
|
|
419
|
+
## Changelog
|
|
420
|
+
|
|
421
|
+
### v1.2.3 — Bug fix
|
|
422
|
+
|
|
423
|
+
- Fix E2E and visual diff connection failure on Windows with Node.js 17+: `verifyUrl` now uses `localhost` instead of `127.0.0.1`. On Windows, Vite binds to `::1` (IPv6) which does not accept IPv4 connections.
|
|
424
|
+
|
|
425
|
+
### v1.2.2 — Opt-in verification checks
|
|
426
|
+
|
|
427
|
+
Added 7 opt-in checks (off by default, enable via CLI flags or `.laxy.yml`):
|
|
428
|
+
|
|
429
|
+
- `--typecheck` — TypeScript type error detection via `tsc --noEmit`
|
|
430
|
+
- `--secret-scan` — Hardcoded secret scanning with 11 regex patterns and `***` masking
|
|
431
|
+
- `--bundle-size` — Next.js/Vite bundle size analysis
|
|
432
|
+
- `--outdated-check` — Outdated dependency detection via `npm outdated`
|
|
433
|
+
- `--a11y-deep` — Deep WCAG audit via axe-core + Puppeteer
|
|
434
|
+
- `--seo-deep` — SEO meta/OG/JSON-LD audit
|
|
435
|
+
- `--vitals-budget` — Core Web Vitals budget enforcement (LCP, CLS, INP)
|
|
436
|
+
|
|
437
|
+
Other changes:
|
|
438
|
+
|
|
439
|
+
- Secret scan findings are **masked** — real credential values never appear in JSON, markdown, or console output
|
|
440
|
+
- Comment lines excluded from secret scanning to reduce false positives
|
|
441
|
+
- `test/`, `tests/`, `__tests__/`, `spec/` directories excluded from scanning
|
|
442
|
+
- `.laxy.yml` supports `secret_scan_ignore_paths` for custom exclusions
|
|
443
|
+
- Verification report includes all new checks in passes checklist and improvement recommendations
|
|
444
|
+
- Markdown report includes all new checks in the delivery evidence table
|
|
445
|
+
- Added `fixtures/sample-app/` test fixture (Vite + React + TypeScript)
|
|
446
|
+
- 17 test files, 88 unit tests
|
|
447
|
+
|
|
319
448
|
## Links
|
|
320
449
|
|
|
321
450
|
- GitHub: https://github.com/SUNgm24/Laxy/tree/main/laxy-verify
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface A11yViolation {
|
|
2
|
+
id: string;
|
|
3
|
+
impact: "critical" | "serious" | "moderate" | "minor";
|
|
4
|
+
description: string;
|
|
5
|
+
helpUrl: string;
|
|
6
|
+
nodeCount: number;
|
|
7
|
+
selector: string;
|
|
8
|
+
}
|
|
9
|
+
export interface A11yDeepResult {
|
|
10
|
+
passed: boolean;
|
|
11
|
+
criticalCount: number;
|
|
12
|
+
seriousCount: number;
|
|
13
|
+
moderateCount: number;
|
|
14
|
+
minorCount: number;
|
|
15
|
+
violations: A11yViolation[];
|
|
16
|
+
url: string;
|
|
17
|
+
skipped: boolean;
|
|
18
|
+
summary: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function runA11yDeep(url: string): Promise<A11yDeepResult>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runA11yDeep = runA11yDeep;
|
|
4
|
+
/**
|
|
5
|
+
* Deep accessibility audit using axe-core + Puppeteer.
|
|
6
|
+
*
|
|
7
|
+
* Runs axe-core against the running dev server to produce a detailed
|
|
8
|
+
* WCAG violation report beyond what Lighthouse provides.
|
|
9
|
+
* Blocker-capable: critical/serious violations can elevate to warnings.
|
|
10
|
+
*/
|
|
11
|
+
let puppeteerModule = null;
|
|
12
|
+
try {
|
|
13
|
+
puppeteerModule = require("puppeteer");
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Puppeteer not available — a11y deep will be skipped
|
|
17
|
+
}
|
|
18
|
+
const IMPACT_ORDER = {
|
|
19
|
+
critical: 0,
|
|
20
|
+
serious: 1,
|
|
21
|
+
moderate: 2,
|
|
22
|
+
minor: 3,
|
|
23
|
+
};
|
|
24
|
+
async function runA11yDeep(url) {
|
|
25
|
+
if (!puppeteerModule) {
|
|
26
|
+
return {
|
|
27
|
+
passed: true,
|
|
28
|
+
criticalCount: 0,
|
|
29
|
+
seriousCount: 0,
|
|
30
|
+
moderateCount: 0,
|
|
31
|
+
minorCount: 0,
|
|
32
|
+
violations: [],
|
|
33
|
+
url,
|
|
34
|
+
skipped: true,
|
|
35
|
+
summary: "Skipped (puppeteer not available)",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
console.log(" Running deep accessibility audit (axe-core)...");
|
|
39
|
+
const puppeteer = puppeteerModule.default || puppeteerModule;
|
|
40
|
+
let browser;
|
|
41
|
+
try {
|
|
42
|
+
browser = await puppeteer.launch({
|
|
43
|
+
headless: true,
|
|
44
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
45
|
+
});
|
|
46
|
+
const page = await browser.newPage();
|
|
47
|
+
await page.goto(url, { waitUntil: "networkidle2", timeout: 20000 });
|
|
48
|
+
// Inject and run axe-core
|
|
49
|
+
const results = await page.evaluate(async () => {
|
|
50
|
+
// @ts-expect-error — axe is injected at runtime
|
|
51
|
+
if (typeof window.axe === "undefined") {
|
|
52
|
+
try {
|
|
53
|
+
// Dynamically import axe-core from CDN as fallback
|
|
54
|
+
const script = document.createElement("script");
|
|
55
|
+
script.src = "https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.9.1/axe.min.js";
|
|
56
|
+
script.crossOrigin = "anonymous";
|
|
57
|
+
document.head.appendChild(script);
|
|
58
|
+
await new Promise((resolve, reject) => {
|
|
59
|
+
script.onload = () => resolve();
|
|
60
|
+
script.onerror = () => reject(new Error("axe-core CDN load failed"));
|
|
61
|
+
setTimeout(() => reject(new Error("axe-core load timed out")), 5000);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// @ts-expect-error — axe is now available
|
|
69
|
+
const result = await window.axe.run(document, {
|
|
70
|
+
runOnly: {
|
|
71
|
+
type: "tag",
|
|
72
|
+
values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"],
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
violations: result.violations.map((v) => ({
|
|
77
|
+
id: v.id,
|
|
78
|
+
impact: v.impact,
|
|
79
|
+
description: v.description,
|
|
80
|
+
helpUrl: v.helpUrl,
|
|
81
|
+
nodeCount: v.nodes?.length ?? 0,
|
|
82
|
+
selector: v.nodes?.[0]?.target?.[0] ?? "",
|
|
83
|
+
})),
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
if (!results) {
|
|
87
|
+
console.log(" A11y deep: skipped (axe-core could not be loaded)");
|
|
88
|
+
return {
|
|
89
|
+
passed: true,
|
|
90
|
+
criticalCount: 0,
|
|
91
|
+
seriousCount: 0,
|
|
92
|
+
moderateCount: 0,
|
|
93
|
+
minorCount: 0,
|
|
94
|
+
violations: [],
|
|
95
|
+
url,
|
|
96
|
+
skipped: true,
|
|
97
|
+
summary: "Skipped (axe-core could not be loaded)",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const violations = results.violations
|
|
101
|
+
.map((v) => ({
|
|
102
|
+
id: v.id,
|
|
103
|
+
impact: v.impact ?? "moderate",
|
|
104
|
+
description: v.description,
|
|
105
|
+
helpUrl: v.helpUrl,
|
|
106
|
+
nodeCount: v.nodeCount,
|
|
107
|
+
selector: v.selector,
|
|
108
|
+
}))
|
|
109
|
+
.filter((v) => ["critical", "serious", "moderate", "minor"].includes(v.impact))
|
|
110
|
+
.sort((a, b) => (IMPACT_ORDER[a.impact] ?? 3) - (IMPACT_ORDER[b.impact] ?? 3));
|
|
111
|
+
const criticalCount = violations.filter((v) => v.impact === "critical").length;
|
|
112
|
+
const seriousCount = violations.filter((v) => v.impact === "serious").length;
|
|
113
|
+
const moderateCount = violations.filter((v) => v.impact === "moderate").length;
|
|
114
|
+
const minorCount = violations.filter((v) => v.impact === "minor").length;
|
|
115
|
+
const passed = criticalCount === 0 && seriousCount === 0;
|
|
116
|
+
const parts = [];
|
|
117
|
+
if (criticalCount > 0)
|
|
118
|
+
parts.push(`${criticalCount} critical`);
|
|
119
|
+
if (seriousCount > 0)
|
|
120
|
+
parts.push(`${seriousCount} serious`);
|
|
121
|
+
if (moderateCount > 0)
|
|
122
|
+
parts.push(`${moderateCount} moderate`);
|
|
123
|
+
if (minorCount > 0)
|
|
124
|
+
parts.push(`${minorCount} minor`);
|
|
125
|
+
const summary = parts.length > 0
|
|
126
|
+
? `${parts.join(", ")} WCAG violation(s)`
|
|
127
|
+
: "No WCAG violations found";
|
|
128
|
+
console.log(` A11y deep: ${summary}`);
|
|
129
|
+
for (const v of violations.slice(0, 5)) {
|
|
130
|
+
console.error(` [${v.impact}] ${v.id}: ${v.description} (${v.nodeCount} nodes)`);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
passed,
|
|
134
|
+
criticalCount,
|
|
135
|
+
seriousCount,
|
|
136
|
+
moderateCount,
|
|
137
|
+
minorCount,
|
|
138
|
+
violations,
|
|
139
|
+
url,
|
|
140
|
+
skipped: false,
|
|
141
|
+
summary,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
console.error(` A11y deep error: ${err instanceof Error ? err.message : String(err)}`);
|
|
146
|
+
return {
|
|
147
|
+
passed: true,
|
|
148
|
+
criticalCount: 0,
|
|
149
|
+
seriousCount: 0,
|
|
150
|
+
moderateCount: 0,
|
|
151
|
+
minorCount: 0,
|
|
152
|
+
violations: [],
|
|
153
|
+
url,
|
|
154
|
+
skipped: true,
|
|
155
|
+
summary: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
await browser?.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface AiFailureContext {
|
|
2
|
+
grade: string;
|
|
3
|
+
blockers: Array<{
|
|
4
|
+
title: string;
|
|
5
|
+
action: string;
|
|
6
|
+
}>;
|
|
7
|
+
lighthouseScores: {
|
|
8
|
+
performance: number;
|
|
9
|
+
accessibility: number;
|
|
10
|
+
seo: number;
|
|
11
|
+
bestPractices: number;
|
|
12
|
+
} | null;
|
|
13
|
+
thresholds: {
|
|
14
|
+
performance: number;
|
|
15
|
+
accessibility: number;
|
|
16
|
+
seo: number;
|
|
17
|
+
bestPractices: number;
|
|
18
|
+
};
|
|
19
|
+
buildErrors: string[];
|
|
20
|
+
e2eFailed: number;
|
|
21
|
+
e2eTotal: number;
|
|
22
|
+
securitySummary?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface AiFailureAnalysis {
|
|
25
|
+
rootCause: string;
|
|
26
|
+
topFixes: string[];
|
|
27
|
+
}
|
|
28
|
+
export declare function requestAiFailureAnalysis(context: AiFailureContext): Promise<AiFailureAnalysis | null>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requestAiFailureAnalysis = requestAiFailureAnalysis;
|
|
4
|
+
/**
|
|
5
|
+
* AI failure analysis client (Pro feature).
|
|
6
|
+
*
|
|
7
|
+
* Sends verification failure context to the Laxy API, which returns
|
|
8
|
+
* a root-cause summary and top fixes generated by Claude.
|
|
9
|
+
*/
|
|
10
|
+
const auth_js_1 = require("./auth.js");
|
|
11
|
+
async function requestAiFailureAnalysis(context) {
|
|
12
|
+
const token = (0, auth_js_1.loadToken)();
|
|
13
|
+
if (!token)
|
|
14
|
+
return null;
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(`${auth_js_1.LAXY_API_URL}/api/v1/analyze-failure`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bearer ${token}`,
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
},
|
|
22
|
+
body: JSON.stringify(context),
|
|
23
|
+
signal: AbortSignal.timeout(30_000),
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok)
|
|
26
|
+
return null;
|
|
27
|
+
return (await res.json());
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -18,4 +18,8 @@ export interface BrokenLinksResult {
|
|
|
18
18
|
hasBrokenLinks: boolean;
|
|
19
19
|
summary: string;
|
|
20
20
|
}
|
|
21
|
-
export
|
|
21
|
+
export interface BrokenLinksAuditOptions {
|
|
22
|
+
extraRoutes?: string[];
|
|
23
|
+
abortSignal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
export declare function auditBrokenLinks(crawlResult: CrawlResult | undefined, baseUrl: string, options?: BrokenLinksAuditOptions): Promise<BrokenLinksResult>;
|
|
@@ -6,25 +6,36 @@ const VALID_OK_STATUS = [200, 201, 202, 203, 204, 301, 302, 303, 307, 308];
|
|
|
6
6
|
function isSuccessStatus(n) {
|
|
7
7
|
return VALID_OK_STATUS.includes(n);
|
|
8
8
|
}
|
|
9
|
-
async function auditBrokenLinks(crawlResult, baseUrl,
|
|
10
|
-
const origin = new URL(baseUrl).origin;
|
|
9
|
+
async function auditBrokenLinks(crawlResult, baseUrl, options = {}) {
|
|
11
10
|
const allUrls = [];
|
|
12
|
-
|
|
13
|
-
for (const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
allUrls.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
if (crawlResult) {
|
|
12
|
+
for (const page of crawlResult.pages) {
|
|
13
|
+
for (const href of page.internalLinks) {
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(href, baseUrl).href;
|
|
16
|
+
if (!allUrls.includes(url))
|
|
17
|
+
allUrls.push(url);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// skip malformed URLs
|
|
21
|
+
}
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
for (const route of options.extraRoutes ?? []) {
|
|
26
|
+
try {
|
|
27
|
+
const url = new URL(route, baseUrl).href;
|
|
28
|
+
if (!allUrls.includes(url))
|
|
29
|
+
allUrls.push(url);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// skip malformed routes
|
|
33
|
+
}
|
|
34
|
+
}
|
|
24
35
|
const uniqueUrls = allUrls;
|
|
25
36
|
const brokenLinks = [];
|
|
26
37
|
await Promise.all(uniqueUrls.map(async (url) => {
|
|
27
|
-
if (abortSignal?.aborted)
|
|
38
|
+
if (options.abortSignal?.aborted)
|
|
28
39
|
return;
|
|
29
40
|
try {
|
|
30
41
|
const controller = new AbortController();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface BundleSizeResult {
|
|
2
|
+
framework: "next" | "vite" | "unknown";
|
|
3
|
+
firstLoadJsKb: number | null;
|
|
4
|
+
largestChunkKb: number | null;
|
|
5
|
+
largestChunkName: string | null;
|
|
6
|
+
totalAssetsKb: number | null;
|
|
7
|
+
advisory: string;
|
|
8
|
+
chunks: Array<{
|
|
9
|
+
name: string;
|
|
10
|
+
sizeKb: number;
|
|
11
|
+
}>;
|
|
12
|
+
skipped: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function runBundleSize(projectDir: string): Promise<BundleSizeResult>;
|