laxy-verify 0.1.0
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 +163 -0
- package/action.yml +97 -0
- package/dist/build-runner.d.ts +22 -0
- package/dist/build-runner.js +156 -0
- package/dist/build-runner.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +102 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +55 -0
- package/dist/config.js.map +1 -0
- package/dist/dev-server.d.ts +8 -0
- package/dist/dev-server.js +81 -0
- package/dist/dev-server.js.map +1 -0
- package/dist/lighthouse-runner.d.ts +10 -0
- package/dist/lighthouse-runner.js +99 -0
- package/dist/lighthouse-runner.js.map +1 -0
- package/dist/project-runtime.d.ts +32 -0
- package/dist/project-runtime.js +99 -0
- package/dist/project-runtime.js.map +1 -0
- package/dist/reporter.d.ts +21 -0
- package/dist/reporter.js +100 -0
- package/dist/reporter.js.map +1 -0
- package/dist/verification.d.ts +42 -0
- package/dist/verification.js +105 -0
- package/dist/verification.js.map +1 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# laxy-verify
|
|
2
|
+
|
|
3
|
+
Frontend quality gate for any project. Checks your build and runs a Lighthouse audit — outputs a Silver/Bronze/Unverified grade.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx laxy-verify .
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Laxy Verify — ✅ Silver
|
|
11
|
+
========================================
|
|
12
|
+
|
|
13
|
+
Build: PASS (12.3s)
|
|
14
|
+
|
|
15
|
+
Lighthouse Scores:
|
|
16
|
+
Performance: 87 / 70 ✓
|
|
17
|
+
Accessibility: 92 / 85 ✓
|
|
18
|
+
SEO: 88 / 80 ✓
|
|
19
|
+
Best Practices: 90 / 80 ✓
|
|
20
|
+
|
|
21
|
+
Grade: Silver
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> Want Gold? Gold requires E2E tests on top of build + Lighthouse. Run full verification at [laxy.dev](https://laxy.dev).
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Run once (no install needed)
|
|
32
|
+
npx laxy-verify .
|
|
33
|
+
|
|
34
|
+
# Or install globally
|
|
35
|
+
npm install -g laxy-verify
|
|
36
|
+
laxy-verify .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
laxy-verify [dir] [options]
|
|
45
|
+
|
|
46
|
+
Arguments:
|
|
47
|
+
dir Project directory (default: current directory)
|
|
48
|
+
|
|
49
|
+
Options:
|
|
50
|
+
--format <type> Output format: console, json, md (default: console)
|
|
51
|
+
--ci CI mode: relaxed thresholds, 3 Lighthouse runs
|
|
52
|
+
--skip-lighthouse Build check only, skip Lighthouse
|
|
53
|
+
--runs <number> Number of Lighthouse runs
|
|
54
|
+
--port <number> Dev server port (default: 3000)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Examples
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Check current directory
|
|
61
|
+
npx laxy-verify .
|
|
62
|
+
|
|
63
|
+
# Build check only (fast)
|
|
64
|
+
npx laxy-verify . --skip-lighthouse
|
|
65
|
+
|
|
66
|
+
# CI mode with JSON output
|
|
67
|
+
npx laxy-verify . --ci --format json
|
|
68
|
+
|
|
69
|
+
# Markdown output (for PR comments)
|
|
70
|
+
npx laxy-verify . --format md
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Exit Codes
|
|
76
|
+
|
|
77
|
+
| Code | Meaning |
|
|
78
|
+
|------|---------|
|
|
79
|
+
| `0` | Silver or Gold — all checks passed |
|
|
80
|
+
| `1` | Bronze — build passed, Lighthouse failed (soft fail) |
|
|
81
|
+
| `2` | Unverified — build failed |
|
|
82
|
+
| `3` | Config error — no package.json, no build script |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Grades
|
|
87
|
+
|
|
88
|
+
| Grade | Condition |
|
|
89
|
+
|-------|-----------|
|
|
90
|
+
| ✅ Silver | Build passed + all Lighthouse thresholds met |
|
|
91
|
+
| 🔨 Bronze | Build passed, Lighthouse failed or skipped |
|
|
92
|
+
| ⚠️ Unverified | Build failed |
|
|
93
|
+
|
|
94
|
+
> 🏆 **Gold** requires E2E tests. Use [Laxy](https://laxy.dev) for full Gold verification.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## GitHub Action
|
|
99
|
+
|
|
100
|
+
Add this to `.github/workflows/verify.yml`:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
name: Laxy Verify
|
|
104
|
+
|
|
105
|
+
on:
|
|
106
|
+
pull_request:
|
|
107
|
+
|
|
108
|
+
jobs:
|
|
109
|
+
verify:
|
|
110
|
+
runs-on: ubuntu-latest
|
|
111
|
+
steps:
|
|
112
|
+
- uses: actions/checkout@v4
|
|
113
|
+
- uses: psungmin24/laxy-verify@v1
|
|
114
|
+
with:
|
|
115
|
+
format: md
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
On every PR, laxy-verify will:
|
|
119
|
+
1. Run `npm run build`
|
|
120
|
+
2. Start your dev server and run Lighthouse
|
|
121
|
+
3. Post a comment with the grade and scores
|
|
122
|
+
|
|
123
|
+
### Action Inputs
|
|
124
|
+
|
|
125
|
+
| Input | Default | Description |
|
|
126
|
+
|-------|---------|-------------|
|
|
127
|
+
| `node-version` | `20` | Node.js version |
|
|
128
|
+
| `skip-lighthouse` | `false` | Build check only |
|
|
129
|
+
| `format` | `md` | Output format |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Configuration
|
|
134
|
+
|
|
135
|
+
Create `.laxy.yml` in your project root to customize thresholds:
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
thresholds:
|
|
139
|
+
performance: 70
|
|
140
|
+
accessibility: 85
|
|
141
|
+
seo: 80
|
|
142
|
+
bestPractices: 80
|
|
143
|
+
|
|
144
|
+
port: 3000
|
|
145
|
+
runs: 1
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Supported Frameworks
|
|
151
|
+
|
|
152
|
+
Automatically detects and runs the right build command:
|
|
153
|
+
|
|
154
|
+
- **Next.js** — `next build`
|
|
155
|
+
- **Vite** — `vite build`
|
|
156
|
+
- **Create React App** — `react-scripts build`
|
|
157
|
+
- **Custom** — reads `scripts.build` from `package.json`
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT
|
package/action.yml
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
name: 'Laxy Verify'
|
|
2
|
+
description: 'Frontend quality gate: build check + Lighthouse audit + verification grade'
|
|
3
|
+
inputs:
|
|
4
|
+
threshold-config:
|
|
5
|
+
description: 'Path to .laxy.yml threshold config'
|
|
6
|
+
required: false
|
|
7
|
+
default: '.laxy.yml'
|
|
8
|
+
node-version:
|
|
9
|
+
description: 'Node.js version'
|
|
10
|
+
required: false
|
|
11
|
+
default: '20'
|
|
12
|
+
skip-lighthouse:
|
|
13
|
+
description: 'Skip Lighthouse, run build check only'
|
|
14
|
+
required: false
|
|
15
|
+
default: 'false'
|
|
16
|
+
outputs:
|
|
17
|
+
grade:
|
|
18
|
+
description: 'Verification grade (Gold/Silver/Bronze/Unverified)'
|
|
19
|
+
value: ${{ steps.verify.outputs.grade }}
|
|
20
|
+
report:
|
|
21
|
+
description: 'JSON report string'
|
|
22
|
+
value: ${{ steps.verify.outputs.report }}
|
|
23
|
+
runs:
|
|
24
|
+
using: 'composite'
|
|
25
|
+
steps:
|
|
26
|
+
- name: Setup Node.js
|
|
27
|
+
uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: ${{ inputs.node-version }}
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
shell: bash
|
|
33
|
+
run: npm ci
|
|
34
|
+
|
|
35
|
+
- name: Run Laxy Verify
|
|
36
|
+
id: verify
|
|
37
|
+
shell: bash
|
|
38
|
+
run: |
|
|
39
|
+
SKIP_FLAG=""
|
|
40
|
+
if [ "${{ inputs.skip-lighthouse }}" = "true" ]; then
|
|
41
|
+
SKIP_FLAG="--skip-lighthouse"
|
|
42
|
+
fi
|
|
43
|
+
npx laxy-verify . --format json --ci $SKIP_FLAG > laxy-report.json 2>&1 || true
|
|
44
|
+
GRADE=$(node -e "const r=JSON.parse(require('fs').readFileSync('laxy-report.json','utf8'));console.log(r.grade)")
|
|
45
|
+
echo "grade=$GRADE" >> $GITHUB_OUTPUT
|
|
46
|
+
echo "report=$(cat laxy-report.json)" >> $GITHUB_OUTPUT
|
|
47
|
+
|
|
48
|
+
- name: Post PR Comment
|
|
49
|
+
if: github.event_name == 'pull_request'
|
|
50
|
+
uses: actions/github-script@v7
|
|
51
|
+
with:
|
|
52
|
+
script: |
|
|
53
|
+
const fs = require('fs');
|
|
54
|
+
let report;
|
|
55
|
+
try {
|
|
56
|
+
report = JSON.parse(fs.readFileSync('laxy-report.json', 'utf8'));
|
|
57
|
+
} catch {
|
|
58
|
+
console.log('No report file found');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Generate markdown from report
|
|
63
|
+
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
64
|
+
const { execSync } = require('child_process');
|
|
65
|
+
let md;
|
|
66
|
+
try {
|
|
67
|
+
md = execSync(`${npx} laxy-verify . --format md --ci --skip-lighthouse 2>/dev/null || true`).toString();
|
|
68
|
+
} catch {
|
|
69
|
+
md = `## Laxy Verify — ${report.grade}\n\nSee JSON report for details.`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Find existing comment
|
|
73
|
+
const marker = '<!-- laxy-verify -->';
|
|
74
|
+
const { data: comments } = await github.rest.issues.listComments({
|
|
75
|
+
owner: context.repo.owner,
|
|
76
|
+
repo: context.repo.repo,
|
|
77
|
+
issue_number: context.issue.number,
|
|
78
|
+
});
|
|
79
|
+
const existing = comments.find(c => c.body?.includes(marker));
|
|
80
|
+
|
|
81
|
+
const body = md.includes(marker) ? md : `${marker}\n${md}`;
|
|
82
|
+
|
|
83
|
+
if (existing) {
|
|
84
|
+
await github.rest.issues.updateComment({
|
|
85
|
+
owner: context.repo.owner,
|
|
86
|
+
repo: context.repo.repo,
|
|
87
|
+
comment_id: existing.id,
|
|
88
|
+
body,
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
await github.rest.issues.createComment({
|
|
92
|
+
owner: context.repo.owner,
|
|
93
|
+
repo: context.repo.repo,
|
|
94
|
+
issue_number: context.issue.number,
|
|
95
|
+
body,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface BuildError {
|
|
2
|
+
file?: string;
|
|
3
|
+
line?: number;
|
|
4
|
+
col?: number;
|
|
5
|
+
code?: string;
|
|
6
|
+
message: string;
|
|
7
|
+
raw: string;
|
|
8
|
+
}
|
|
9
|
+
export interface BuildCheckResult {
|
|
10
|
+
success: boolean;
|
|
11
|
+
errors: string[];
|
|
12
|
+
structuredErrors: BuildError[];
|
|
13
|
+
warnings: string[];
|
|
14
|
+
duration: number;
|
|
15
|
+
output: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function parseBuildOutput(output: string): {
|
|
18
|
+
errors: string[];
|
|
19
|
+
structuredErrors: BuildError[];
|
|
20
|
+
warnings: string[];
|
|
21
|
+
};
|
|
22
|
+
export declare function runBuild(projectPath: string): Promise<BuildCheckResult>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { detectRuntimeFromFiles, getBuildCommandCandidates, parseProjectPackageJson, } from "./project-runtime.js";
|
|
5
|
+
function parseErrorLine(raw) {
|
|
6
|
+
const clean = raw.trim();
|
|
7
|
+
if (!clean)
|
|
8
|
+
return null;
|
|
9
|
+
const tsMatch = clean.match(/^(.+?)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/);
|
|
10
|
+
if (tsMatch) {
|
|
11
|
+
return {
|
|
12
|
+
file: tsMatch[1].trim(),
|
|
13
|
+
line: parseInt(tsMatch[2], 10),
|
|
14
|
+
col: parseInt(tsMatch[3], 10),
|
|
15
|
+
code: tsMatch[4],
|
|
16
|
+
message: tsMatch[5].slice(0, 200),
|
|
17
|
+
raw: clean.slice(0, 250),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (/error TS\d+:|Module not found|Cannot find module|Failed to resolve|SyntaxError:|Unexpected token|Build error occurred|Failed to compile/i.test(clean)) {
|
|
21
|
+
return { message: clean.slice(0, 200), raw: clean.slice(0, 250) };
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
export function parseBuildOutput(output) {
|
|
26
|
+
const errors = [];
|
|
27
|
+
const structuredErrors = [];
|
|
28
|
+
const warnings = [];
|
|
29
|
+
for (const line of output.split("\n")) {
|
|
30
|
+
const clean = line.trim();
|
|
31
|
+
if (!clean)
|
|
32
|
+
continue;
|
|
33
|
+
const structured = parseErrorLine(clean);
|
|
34
|
+
if (structured) {
|
|
35
|
+
errors.push(structured.raw);
|
|
36
|
+
structuredErrors.push(structured);
|
|
37
|
+
}
|
|
38
|
+
else if (/^warn|warning:/i.test(clean)) {
|
|
39
|
+
warnings.push(clean.slice(0, 150));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
errors: errors.slice(0, 20),
|
|
44
|
+
structuredErrors: structuredErrors.slice(0, 20),
|
|
45
|
+
warnings: warnings.slice(0, 10),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function extractFallbackBuildErrors(output, exitCode) {
|
|
49
|
+
const meaningfulLines = output
|
|
50
|
+
.split("\n")
|
|
51
|
+
.map((l) => l.trim())
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.filter((l) => !/^warn|warning:|^> |^npm notice/i.test(l));
|
|
54
|
+
const likelyErrorLines = meaningfulLines.filter((l) => /error|failed|unable|cannot|missing|unexpected|resolve|exception|ELIFECYCLE|ERR!/i.test(l));
|
|
55
|
+
const selected = (likelyErrorLines.length > 0 ? likelyErrorLines : meaningfulLines)
|
|
56
|
+
.slice(-5)
|
|
57
|
+
.map((l) => l.slice(0, 200));
|
|
58
|
+
if (selected.length > 0)
|
|
59
|
+
return Array.from(new Set(selected));
|
|
60
|
+
return [`Build failed with exit code ${exitCode ?? "unknown"} but produced no parsed diagnostics.`];
|
|
61
|
+
}
|
|
62
|
+
function runBuildCommand(command, projectPath) {
|
|
63
|
+
const startTime = Date.now();
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
const cmd = process.platform === "win32" ? `chcp 65001 > nul && ${command.cmd}` : command.cmd;
|
|
66
|
+
const child = spawn(cmd, command.args, {
|
|
67
|
+
cwd: projectPath,
|
|
68
|
+
shell: true,
|
|
69
|
+
env: { ...process.env, CI: "true", NODE_ENV: "production" },
|
|
70
|
+
});
|
|
71
|
+
let output = "";
|
|
72
|
+
child.stdout?.on("data", (d) => { output += d.toString(); });
|
|
73
|
+
child.stderr?.on("data", (d) => { output += d.toString(); });
|
|
74
|
+
const timeout = setTimeout(() => {
|
|
75
|
+
child.kill();
|
|
76
|
+
resolve({
|
|
77
|
+
success: false,
|
|
78
|
+
errors: ["Build timed out after 90s"],
|
|
79
|
+
structuredErrors: [{ message: "Build timed out after 90s", raw: "Build timed out after 90s" }],
|
|
80
|
+
warnings: [],
|
|
81
|
+
duration: 90000,
|
|
82
|
+
output: output.slice(-1000),
|
|
83
|
+
});
|
|
84
|
+
}, 90000);
|
|
85
|
+
child.on("close", (code) => {
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
const duration = Date.now() - startTime;
|
|
88
|
+
let { errors, structuredErrors, warnings } = parseBuildOutput(output);
|
|
89
|
+
if (code !== 0 && errors.length === 0) {
|
|
90
|
+
const fallback = extractFallbackBuildErrors(output, code);
|
|
91
|
+
errors = fallback;
|
|
92
|
+
structuredErrors = fallback.map((raw) => ({ message: raw.slice(0, 200), raw }));
|
|
93
|
+
}
|
|
94
|
+
resolve({ success: code === 0, errors, structuredErrors, warnings, duration, output: output.slice(-2000) });
|
|
95
|
+
});
|
|
96
|
+
child.on("error", (err) => {
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
resolve({
|
|
99
|
+
success: false,
|
|
100
|
+
errors: [`Build process error: ${err.message}`],
|
|
101
|
+
structuredErrors: [{ message: `Build process error: ${err.message}`, raw: err.message }],
|
|
102
|
+
warnings: [],
|
|
103
|
+
duration: Date.now() - startTime,
|
|
104
|
+
output: "",
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export async function runBuild(projectPath) {
|
|
110
|
+
const packageJsonPath = join(projectPath, "package.json");
|
|
111
|
+
if (!existsSync(packageJsonPath)) {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
errors: ["Not a Node.js project: package.json not found"],
|
|
115
|
+
structuredErrors: [{ message: "Not a Node.js project", raw: "package.json not found" }],
|
|
116
|
+
warnings: [],
|
|
117
|
+
duration: 0,
|
|
118
|
+
output: "",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const pkgContent = readFileSync(packageJsonPath, "utf-8");
|
|
122
|
+
const packageJson = parseProjectPackageJson(pkgContent);
|
|
123
|
+
const candidateFiles = readdirSync(projectPath).filter((f) => !f.startsWith("."));
|
|
124
|
+
const runtime = detectRuntimeFromFiles(candidateFiles, packageJson);
|
|
125
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
126
|
+
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
127
|
+
const buildCommands = getBuildCommandCandidates(runtime, { npmCommand: npm, npxCommand: npx });
|
|
128
|
+
if (buildCommands.length === 0) {
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
errors: ["No build command found in package.json"],
|
|
132
|
+
structuredErrors: [{ message: "No build command found", raw: "No build script in package.json" }],
|
|
133
|
+
warnings: [],
|
|
134
|
+
duration: 0,
|
|
135
|
+
output: "",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
for (const command of buildCommands) {
|
|
139
|
+
const result = await runBuildCommand(command, projectPath);
|
|
140
|
+
if (result.success)
|
|
141
|
+
return result;
|
|
142
|
+
// If this wasn't a "command not found" error, stop trying
|
|
143
|
+
if (!/missing script|command not found|is not recognized/i.test(result.output)) {
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
errors: ["All build commands failed"],
|
|
150
|
+
structuredErrors: [{ message: "All build commands failed", raw: "No build command succeeded" }],
|
|
151
|
+
warnings: [],
|
|
152
|
+
duration: 0,
|
|
153
|
+
output: "",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=build-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-runner.js","sourceRoot":"","sources":["../src/build-runner.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import { runBuild } from "./build-runner.js";
|
|
7
|
+
import { runLighthouse } from "./lighthouse-runner.js";
|
|
8
|
+
import { getVerificationGrade, getImprovementRecommendations } from "./verification.js";
|
|
9
|
+
import { formatReport } from "./reporter.js";
|
|
10
|
+
const EXIT_PASS = 0; // Silver or Gold
|
|
11
|
+
const EXIT_SOFT_FAIL = 1; // Bronze (build OK, LH failed)
|
|
12
|
+
const EXIT_FAIL = 2; // Unverified (build failed)
|
|
13
|
+
const EXIT_CONFIG = 3; // Config error
|
|
14
|
+
const program = new Command();
|
|
15
|
+
program
|
|
16
|
+
.name("laxy-verify")
|
|
17
|
+
.description("Frontend quality gate: build check + Lighthouse audit + verification grade")
|
|
18
|
+
.version("0.1.0")
|
|
19
|
+
.argument("[dir]", "Project directory", ".")
|
|
20
|
+
.option("--format <type>", "Output format: console, json, md", "console")
|
|
21
|
+
.option("--ci", "CI mode: relaxed thresholds, 3 Lighthouse runs")
|
|
22
|
+
.option("--skip-lighthouse", "Skip Lighthouse, build-only verification")
|
|
23
|
+
.option("--runs <number>", "Number of Lighthouse runs", parseInt)
|
|
24
|
+
.option("--port <number>", "Dev server port", parseInt)
|
|
25
|
+
.action(async (dir, opts) => {
|
|
26
|
+
const projectPath = resolve(dir);
|
|
27
|
+
if (!existsSync(projectPath)) {
|
|
28
|
+
console.error(`Directory not found: ${projectPath}`);
|
|
29
|
+
process.exit(EXIT_CONFIG);
|
|
30
|
+
}
|
|
31
|
+
if (!existsSync(resolve(projectPath, "package.json"))) {
|
|
32
|
+
console.error("Not a Node.js project: package.json not found");
|
|
33
|
+
process.exit(EXIT_CONFIG);
|
|
34
|
+
}
|
|
35
|
+
// Load config
|
|
36
|
+
const { config, warnings } = loadConfig(projectPath, opts.ci);
|
|
37
|
+
for (const w of warnings)
|
|
38
|
+
console.warn(`[warn] ${w}`);
|
|
39
|
+
// Override config with CLI flags
|
|
40
|
+
if (opts.runs)
|
|
41
|
+
config.runs = opts.runs;
|
|
42
|
+
if (opts.port)
|
|
43
|
+
config.port = opts.port;
|
|
44
|
+
// Step 1: Build check
|
|
45
|
+
const buildResult = await runBuild(projectPath);
|
|
46
|
+
if (!buildResult.success) {
|
|
47
|
+
const report = {
|
|
48
|
+
grade: "unverified",
|
|
49
|
+
build: { success: false, errors: buildResult.errors, duration: buildResult.duration },
|
|
50
|
+
lighthouse: { scores: null },
|
|
51
|
+
thresholds: config.thresholds,
|
|
52
|
+
recommendations: getImprovementRecommendations({
|
|
53
|
+
buildSuccess: false,
|
|
54
|
+
buildErrors: buildResult.errors,
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
console.log(formatReport(report, opts.format));
|
|
58
|
+
process.exit(EXIT_FAIL);
|
|
59
|
+
}
|
|
60
|
+
// Step 2: Lighthouse (optional)
|
|
61
|
+
let lighthouseScores = null;
|
|
62
|
+
let lighthouseError;
|
|
63
|
+
if (!opts.skipLighthouse) {
|
|
64
|
+
const lhResult = await runLighthouse(projectPath, {
|
|
65
|
+
port: config.port,
|
|
66
|
+
runs: config.runs,
|
|
67
|
+
ciMode: opts.ci,
|
|
68
|
+
});
|
|
69
|
+
lighthouseScores = lhResult.scores;
|
|
70
|
+
lighthouseError = lhResult.error;
|
|
71
|
+
}
|
|
72
|
+
// Step 3: Grade
|
|
73
|
+
const grade = getVerificationGrade({
|
|
74
|
+
buildSuccess: true,
|
|
75
|
+
lighthouseScores: lighthouseScores ?? undefined,
|
|
76
|
+
});
|
|
77
|
+
const report = {
|
|
78
|
+
grade,
|
|
79
|
+
build: { success: true, errors: [], duration: buildResult.duration },
|
|
80
|
+
lighthouse: { scores: lighthouseScores, error: lighthouseError },
|
|
81
|
+
thresholds: config.thresholds,
|
|
82
|
+
recommendations: getImprovementRecommendations({
|
|
83
|
+
buildSuccess: true,
|
|
84
|
+
lighthouseScores: lighthouseScores ?? undefined,
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
console.log(formatReport(report, opts.format));
|
|
88
|
+
// Exit codes
|
|
89
|
+
switch (grade) {
|
|
90
|
+
case "gold":
|
|
91
|
+
case "silver":
|
|
92
|
+
process.exit(EXIT_PASS);
|
|
93
|
+
break;
|
|
94
|
+
case "bronze":
|
|
95
|
+
process.exit(EXIT_SOFT_FAIL);
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
process.exit(EXIT_FAIL);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
program.parse();
|
|
102
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAqB,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AAC3G,OAAO,EAAE,YAAY,EAAqB,MAAM,eAAe,CAAC;AAEhE,MAAM,SAAS,GAAG,CAAC,CAAC,CAAK,iBAAiB;AAC1C,MAAM,cAAc,GAAG,CAAC,CAAC,CAAC,+BAA+B;AACzD,MAAM,SAAS,GAAG,CAAC,CAAC,CAAM,4BAA4B;AACtD,MAAM,WAAW,GAAG,CAAC,CAAC,CAAI,eAAe;AAEzC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,aAAa,CAAC;KACnB,WAAW,CAAC,4EAA4E,CAAC;KACzF,OAAO,CAAC,OAAO,CAAC;KAChB,QAAQ,CAAC,OAAO,EAAE,mBAAmB,EAAE,GAAG,CAAC;KAC3C,MAAM,CAAC,iBAAiB,EAAE,kCAAkC,EAAE,SAAS,CAAC;KACxE,MAAM,CAAC,MAAM,EAAE,gDAAgD,CAAC;KAChE,MAAM,CAAC,mBAAmB,EAAE,0CAA0C,CAAC;KACvE,MAAM,CAAC,iBAAiB,EAAE,2BAA2B,EAAE,QAAQ,CAAC;KAChE,MAAM,CAAC,iBAAiB,EAAE,iBAAiB,EAAE,QAAQ,CAAC;KACtD,MAAM,CAAC,KAAK,EAAE,GAAW,EAAE,IAAI,EAAE,EAAE;IAClC,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAEjC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,KAAK,CAAC,wBAAwB,WAAW,EAAE,CAAC,CAAC;QACrD,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC5B,CAAC;IAED,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC;QACtD,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;QAC/D,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC5B,CAAC;IAED,cAAc;IACd,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,UAAU,CAAC,WAAW,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAC9D,KAAK,MAAM,CAAC,IAAI,QAAQ;QAAE,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;IAEtD,iCAAiC;IACjC,IAAI,IAAI,CAAC,IAAI;QAAE,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACvC,IAAI,IAAI,CAAC,IAAI;QAAE,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IAEvC,sBAAsB;IACtB,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,CAAC;IAEhD,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;QACzB,MAAM,MAAM,GAAiB;YAC3B,KAAK,EAAE,YAAY;YACnB,KAAK,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,CAAC,QAAQ,EAAE;YACrF,UAAU,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;YAC5B,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,eAAe,EAAE,6BAA6B,CAAC;gBAC7C,YAAY,EAAE,KAAK;gBACnB,WAAW,EAAE,WAAW,CAAC,MAAM;aAChC,CAAC;SACH,CAAC;QACF,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC1B,CAAC;IAED,gCAAgC;IAChC,IAAI,gBAAgB,GAAG,IAAI,CAAC;IAC5B,IAAI,eAAmC,CAAC;IAExC,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE;YAChD,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,MAAM,EAAE,IAAI,CAAC,EAAE;SAChB,CAAC,CAAC;QACH,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC;QACnC,eAAe,GAAG,QAAQ,CAAC,KAAK,CAAC;IACnC,CAAC;IAED,gBAAgB;IAChB,MAAM,KAAK,GAAG,oBAAoB,CAAC;QACjC,YAAY,EAAE,IAAI;QAClB,gBAAgB,EAAE,gBAAgB,IAAI,SAAS;KAChD,CAAC,CAAC;IAEH,MAAM,MAAM,GAAiB;QAC3B,KAAK;QACL,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,WAAW,CAAC,QAAQ,EAAE;QACpE,UAAU,EAAE,EAAE,MAAM,EAAE,gBAAgB,EAAE,KAAK,EAAE,eAAe,EAAE;QAChE,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,eAAe,EAAE,6BAA6B,CAAC;YAC7C,YAAY,EAAE,IAAI;YAClB,gBAAgB,EAAE,gBAAgB,IAAI,SAAS;SAChD,CAAC;KACH,CAAC;IAEF,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAE/C,aAAa;IACb,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,MAAM,CAAC;QACZ,KAAK,QAAQ;YACX,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACxB,MAAM;QACR,KAAK,QAAQ;YACX,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YAC7B,MAAM;QACR;YACE,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface LaxyConfig {
|
|
2
|
+
thresholds: {
|
|
3
|
+
performance: number;
|
|
4
|
+
accessibility: number;
|
|
5
|
+
seo: number;
|
|
6
|
+
bestPractices: number;
|
|
7
|
+
};
|
|
8
|
+
buildCommand?: string;
|
|
9
|
+
devCommand?: string;
|
|
10
|
+
port: number;
|
|
11
|
+
runs: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function loadConfig(projectPath: string, ciMode?: boolean): {
|
|
14
|
+
config: LaxyConfig;
|
|
15
|
+
warnings: string[];
|
|
16
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import { LH_THRESHOLDS, LH_CI_THRESHOLDS } from "./verification.js";
|
|
5
|
+
const DEFAULT_CONFIG = {
|
|
6
|
+
thresholds: { ...LH_THRESHOLDS },
|
|
7
|
+
port: 3000,
|
|
8
|
+
runs: 1,
|
|
9
|
+
};
|
|
10
|
+
export function loadConfig(projectPath, ciMode = false) {
|
|
11
|
+
const warnings = [];
|
|
12
|
+
const configPath = join(projectPath, ".laxy.yml");
|
|
13
|
+
const defaults = {
|
|
14
|
+
thresholds: ciMode ? { ...LH_CI_THRESHOLDS } : { ...LH_THRESHOLDS },
|
|
15
|
+
port: DEFAULT_CONFIG.port,
|
|
16
|
+
runs: ciMode ? 3 : 1,
|
|
17
|
+
};
|
|
18
|
+
if (!existsSync(configPath)) {
|
|
19
|
+
return { config: defaults, warnings };
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
23
|
+
const parsed = parseYaml(raw);
|
|
24
|
+
if (!parsed || typeof parsed !== "object") {
|
|
25
|
+
warnings.push("Invalid .laxy.yml: not an object. Using defaults.");
|
|
26
|
+
return { config: defaults, warnings };
|
|
27
|
+
}
|
|
28
|
+
const config = { ...defaults };
|
|
29
|
+
if (parsed.thresholds && typeof parsed.thresholds === "object") {
|
|
30
|
+
const t = parsed.thresholds;
|
|
31
|
+
if (typeof t.performance === "number")
|
|
32
|
+
config.thresholds.performance = t.performance;
|
|
33
|
+
if (typeof t.accessibility === "number")
|
|
34
|
+
config.thresholds.accessibility = t.accessibility;
|
|
35
|
+
if (typeof t.seo === "number")
|
|
36
|
+
config.thresholds.seo = t.seo;
|
|
37
|
+
if (typeof t.bestPractices === "number")
|
|
38
|
+
config.thresholds.bestPractices = t.bestPractices;
|
|
39
|
+
}
|
|
40
|
+
if (typeof parsed.buildCommand === "string")
|
|
41
|
+
config.buildCommand = parsed.buildCommand;
|
|
42
|
+
if (typeof parsed.devCommand === "string")
|
|
43
|
+
config.devCommand = parsed.devCommand;
|
|
44
|
+
if (typeof parsed.port === "number")
|
|
45
|
+
config.port = parsed.port;
|
|
46
|
+
if (typeof parsed.runs === "number")
|
|
47
|
+
config.runs = Math.max(1, Math.min(parsed.runs, 5));
|
|
48
|
+
return { config, warnings };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
warnings.push("Failed to parse .laxy.yml. Using defaults.");
|
|
52
|
+
return { config: defaults, warnings };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ChildProcess } from "child_process";
|
|
2
|
+
export interface DevServerHandle {
|
|
3
|
+
process: ChildProcess;
|
|
4
|
+
port: number;
|
|
5
|
+
url: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function startDevServer(projectPath: string, port: number): Promise<DevServerHandle>;
|
|
8
|
+
export declare function stopDevServer(handle: DevServerHandle): void;
|