laxy-verify 1.1.7 → 1.1.9
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 +14 -4
- package/dist/build.d.ts +1 -1
- package/dist/build.js +7 -3
- package/dist/cli.js +247 -37
- package/dist/e2e.d.ts +29 -0
- package/dist/e2e.js +304 -0
- package/dist/serve.d.ts +2 -1
- package/dist/serve.js +8 -3
- package/dist/verification-core/index.d.ts +3 -0
- package/dist/verification-core/index.js +19 -0
- package/dist/verification-core/report.d.ts +14 -0
- package/dist/verification-core/report.js +273 -0
- package/dist/verification-core/tier-policy.d.ts +13 -0
- package/dist/verification-core/tier-policy.js +71 -0
- package/dist/verification-core/types.d.ts +82 -0
- package/dist/verification-core/types.js +2 -0
- package/dist/visual-diff.d.ts +11 -0
- package/dist/visual-diff.js +126 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
Frontend quality gate for AI-generated code. Build + Lighthouse verification with grade output (Gold/Silver/Bronze/Unverified).
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npx laxy-verify --init
|
|
7
|
-
npx laxy-verify
|
|
8
|
-
npx laxy-verify
|
|
9
|
-
npx laxy-verify
|
|
6
|
+
npx laxy-verify --init --run # 설정 생성 + 즉시 검증 (원커맨드)
|
|
7
|
+
npx laxy-verify --init # Auto-detect framework, generate config
|
|
8
|
+
npx laxy-verify . # Run verification
|
|
9
|
+
npx laxy-verify login # Log in for Pro/Pro+ features
|
|
10
|
+
npx laxy-verify --badge # Show shields.io badge
|
|
11
|
+
npx laxy-verify --help # 도움말 보기
|
|
10
12
|
```
|
|
11
13
|
|
|
12
14
|
## Quick Start
|
|
@@ -82,6 +84,14 @@ env:
|
|
|
82
84
|
LAXY_TOKEN: ${{ secrets.LAXY_TOKEN }}
|
|
83
85
|
```
|
|
84
86
|
|
|
87
|
+
**LAXY_TOKEN을 GitHub Secrets에 등록하는 방법:**
|
|
88
|
+
|
|
89
|
+
1. 로컬에서 로그인: `npx laxy-verify login`
|
|
90
|
+
2. 토큰 복사: `cat ~/.laxy/credentials.json` → `"token"` 값을 복사
|
|
91
|
+
3. GitHub 리포 → Settings → Secrets and variables → Actions → **New repository secret**
|
|
92
|
+
4. Name: `LAXY_TOKEN`, Value: 복사한 토큰 붙여넣기
|
|
93
|
+
5. `--init` 으로 생성된 workflow 파일은 이미 `LAXY_TOKEN`을 포함합니다
|
|
94
|
+
|
|
85
95
|
### Multi-viewport (Pro+)
|
|
86
96
|
|
|
87
97
|
```bash
|
package/dist/build.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ export declare class BuildError extends Error {
|
|
|
8
8
|
timedOut: boolean;
|
|
9
9
|
constructor(message: string, errors: string[], timedOut?: boolean);
|
|
10
10
|
}
|
|
11
|
-
export declare function runBuild(command: string, timeoutSec: number): Promise<BuildResult>;
|
|
11
|
+
export declare function runBuild(command: string, timeoutSec: number, cwd?: string): Promise<BuildResult>;
|
package/dist/build.js
CHANGED
|
@@ -18,13 +18,17 @@ class BuildError extends Error {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
exports.BuildError = BuildError;
|
|
21
|
-
function runBuild(command, timeoutSec) {
|
|
21
|
+
function runBuild(command, timeoutSec, cwd) {
|
|
22
22
|
return new Promise((resolve, reject) => {
|
|
23
23
|
const startTime = Date.now();
|
|
24
24
|
const stderrLines = [];
|
|
25
25
|
const errorLines = [];
|
|
26
|
-
console.log(`\n Building: ${command}`);
|
|
27
|
-
const proc = (0, node_child_process_1.spawn)(command, {
|
|
26
|
+
console.log(`\n Building: ${command}${cwd ? ` (cwd: ${cwd})` : ""}`);
|
|
27
|
+
const proc = (0, node_child_process_1.spawn)(command, {
|
|
28
|
+
shell: true,
|
|
29
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
30
|
+
cwd,
|
|
31
|
+
});
|
|
28
32
|
let timedOut = false;
|
|
29
33
|
const timer = setTimeout(() => {
|
|
30
34
|
timedOut = true;
|
package/dist/cli.js
CHANGED
|
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
return result;
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
36
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
40
|
const fs = __importStar(require("node:fs"));
|
|
38
41
|
const path = __importStar(require("node:path"));
|
|
@@ -49,6 +52,23 @@ const status_js_1 = require("./status.js");
|
|
|
49
52
|
const auth_js_1 = require("./auth.js");
|
|
50
53
|
const entitlement_js_1 = require("./entitlement.js");
|
|
51
54
|
const multi_viewport_js_1 = require("./multi-viewport.js");
|
|
55
|
+
const e2e_js_1 = require("./e2e.js");
|
|
56
|
+
const visual_diff_js_1 = require("./visual-diff.js");
|
|
57
|
+
const index_js_1 = require("./verification-core/index.js");
|
|
58
|
+
const package_json_1 = __importDefault(require("../package.json"));
|
|
59
|
+
function exitGracefully(code) {
|
|
60
|
+
if (process.platform === "win32") {
|
|
61
|
+
setTimeout(() => process.exit(code), 100);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
process.exit(code);
|
|
65
|
+
}
|
|
66
|
+
async function ensurePortAvailableForVerification(port) {
|
|
67
|
+
const status = await (0, serve_js_1.probeServerStatus)(port);
|
|
68
|
+
if (status === null)
|
|
69
|
+
return;
|
|
70
|
+
throw new Error(`An existing local server is already responding on port ${port} (HTTP ${status}). Stop the running app before using laxy-verify, because the verification build can invalidate an active dev session.`);
|
|
71
|
+
}
|
|
52
72
|
function parseArgs() {
|
|
53
73
|
const raw = process.argv.slice(2);
|
|
54
74
|
// Single-pass: collect flags and first positional
|
|
@@ -80,20 +100,20 @@ function parseArgs() {
|
|
|
80
100
|
}
|
|
81
101
|
}
|
|
82
102
|
}
|
|
83
|
-
//
|
|
103
|
+
// ?쒕툕而ㅻ㎤??泥섎━: login [email], logout, whoami
|
|
84
104
|
let subcommand;
|
|
85
105
|
let subcommandArg;
|
|
86
106
|
if (projectDir === "login" || projectDir === "logout" || projectDir === "whoami") {
|
|
87
107
|
subcommand = projectDir;
|
|
88
108
|
projectDir = ".";
|
|
89
|
-
// login
|
|
109
|
+
// login ?ㅼ뿉 ?대찓?쇱씠 ?????덉쓬
|
|
90
110
|
if (subcommand === "login" && raw.length > 0 && !raw[0].startsWith("-")) {
|
|
91
111
|
// already consumed via loop above if it was first; re-check flags
|
|
92
112
|
}
|
|
93
|
-
//
|
|
113
|
+
// 泥?踰덉㎏ non-flag瑜??대찓?쇰줈 痍④툒
|
|
94
114
|
subcommandArg = flags["email"];
|
|
95
115
|
if (!subcommandArg) {
|
|
96
|
-
// login PSM@example.com
|
|
116
|
+
// login PSM@example.com ?뺥깭 ??positional already captured in projectDir before reset
|
|
97
117
|
}
|
|
98
118
|
}
|
|
99
119
|
return {
|
|
@@ -107,29 +127,59 @@ function parseArgs() {
|
|
|
107
127
|
skipLighthouse: flags["skip-lighthouse"] !== undefined,
|
|
108
128
|
badge: flags["badge"] !== undefined,
|
|
109
129
|
init: flags["init"] !== undefined,
|
|
130
|
+
initRun: flags["init"] !== undefined && flags["run"] !== undefined,
|
|
110
131
|
multiViewport: flags["multi-viewport"] !== undefined,
|
|
111
132
|
failureAnalysis: flags["failure-analysis"] !== undefined,
|
|
133
|
+
help: flags["help"] !== undefined || flags["h"] !== undefined,
|
|
112
134
|
};
|
|
113
135
|
}
|
|
114
136
|
function writeResultFile(projectDir, result) {
|
|
115
137
|
const filePath = path.join(projectDir, ".laxy-result.json");
|
|
116
138
|
fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
|
|
117
139
|
}
|
|
140
|
+
function summarizeViewportIssues(scores, thresholds) {
|
|
141
|
+
if (!scores)
|
|
142
|
+
return { count: 0 };
|
|
143
|
+
const failed = [];
|
|
144
|
+
for (const [label, viewportScores] of Object.entries(scores)) {
|
|
145
|
+
if (!viewportScores) {
|
|
146
|
+
failed.push(`${label}: missing`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const passes = viewportScores.performance >= thresholds.performance &&
|
|
150
|
+
viewportScores.accessibility >= thresholds.accessibility &&
|
|
151
|
+
viewportScores.seo >= thresholds.seo &&
|
|
152
|
+
viewportScores.bestPractices >= thresholds.bestPractices;
|
|
153
|
+
if (!passes) {
|
|
154
|
+
failed.push(`${label}: P${viewportScores.performance} A${viewportScores.accessibility} SEO${viewportScores.seo} BP${viewportScores.bestPractices}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
count: failed.length,
|
|
159
|
+
summary: failed.length > 0 ? failed.join(" | ") : "All checked viewports passed.",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
118
162
|
function consoleOutput(result) {
|
|
119
163
|
const gradeLabel = result.grade;
|
|
120
|
-
const checkEmoji = result.grade !== "Unverified" ? "
|
|
121
|
-
console.log(`\n Laxy Verify
|
|
164
|
+
const checkEmoji = result.grade !== "Unverified" ? " OK" : "";
|
|
165
|
+
console.log(`\n Laxy Verify ${gradeLabel}${checkEmoji}`);
|
|
122
166
|
console.log(` Build: ${result.build.success ? `OK (${result.build.durationMs}ms)` : "FAILED"}`);
|
|
123
167
|
if (result.build.errors.length > 0) {
|
|
124
|
-
const last5 = result.build.errors.slice(-5);
|
|
125
168
|
console.log(` Errors:`);
|
|
126
|
-
|
|
169
|
+
// 泥?踰덉㎏ 'error' ?ㅼ썙?쒓? ?ы븿??以?(?먯씤 ?뚯븙??
|
|
170
|
+
const firstError = result.build.errors.find(e => /error/i.test(e));
|
|
171
|
+
// 留덉?留?5以?(鍮뚮뱶 醫낅즺 而⑦뀓?ㅽ듃)
|
|
172
|
+
const last5 = result.build.errors.slice(-5);
|
|
173
|
+
const toShow = firstError && !last5.includes(firstError)
|
|
174
|
+
? [firstError, " ...", ...last5]
|
|
175
|
+
: last5;
|
|
176
|
+
for (const e of toShow)
|
|
127
177
|
console.error(` ${e}`);
|
|
128
178
|
}
|
|
129
179
|
if (result.lighthouse !== null) {
|
|
130
180
|
const lh = result.lighthouse;
|
|
131
181
|
const t = result.thresholds;
|
|
132
|
-
const check = (passed) => passed ? "
|
|
182
|
+
const check = (passed) => (passed ? " OK" : " FAIL");
|
|
133
183
|
console.log(` Lighthouse:`);
|
|
134
184
|
console.log(` Performance: ${lh.performance} / ${t.performance}${check(lh.performance >= t.performance)}`);
|
|
135
185
|
console.log(` Accessibility: ${lh.accessibility} / ${t.accessibility}${check(lh.accessibility >= t.accessibility)}`);
|
|
@@ -140,6 +190,34 @@ function consoleOutput(result) {
|
|
|
140
190
|
else {
|
|
141
191
|
console.log(` Lighthouse: skipped`);
|
|
142
192
|
}
|
|
193
|
+
if (result.e2e) {
|
|
194
|
+
console.log(` E2E: ${result.e2e.passed}/${result.e2e.total} passed`);
|
|
195
|
+
}
|
|
196
|
+
if (result.visualDiff) {
|
|
197
|
+
console.log(` Visual diff: ${result.visualDiff.diffPercentage}% (${result.visualDiff.verdict})`);
|
|
198
|
+
}
|
|
199
|
+
if (result.verification) {
|
|
200
|
+
const view = result.verification.view;
|
|
201
|
+
console.log(` Verification tier: ${view.tier}`);
|
|
202
|
+
console.log(` Question: ${view.question}`);
|
|
203
|
+
console.log(` Verdict: ${view.verdict} (${view.confidence})`);
|
|
204
|
+
console.log(` Summary: ${view.summary}`);
|
|
205
|
+
if (view.blockers.length > 0) {
|
|
206
|
+
console.log(` Blockers:`);
|
|
207
|
+
for (const blocker of view.blockers)
|
|
208
|
+
console.log(` - ${blocker.title}`);
|
|
209
|
+
}
|
|
210
|
+
if (view.nextActions.length > 0) {
|
|
211
|
+
console.log(` Next actions:`);
|
|
212
|
+
for (const action of view.nextActions)
|
|
213
|
+
console.log(` - ${action}`);
|
|
214
|
+
}
|
|
215
|
+
if (view.failureEvidence.length > 0) {
|
|
216
|
+
console.log(` Evidence:`);
|
|
217
|
+
for (const item of view.failureEvidence)
|
|
218
|
+
console.log(` - ${item}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
143
221
|
if (result.github) {
|
|
144
222
|
if (result.github.status === "comment_posted")
|
|
145
223
|
console.log(` PR comment: posted`);
|
|
@@ -148,45 +226,100 @@ function consoleOutput(result) {
|
|
|
148
226
|
}
|
|
149
227
|
console.log(` Result: .laxy-result.json`);
|
|
150
228
|
console.log(` Exit code: ${result.exitCode}`);
|
|
229
|
+
// Upsell CTA: Gold ?ъ꽦?????먭퀬 Free ?뚮옖????
|
|
230
|
+
if (result.grade === "silver" || result.grade === "bronze") {
|
|
231
|
+
if (!result._plan || result._plan === "free") {
|
|
232
|
+
console.log(`\n Gold ?깃툒???먰븯?좊떎硫?Pro/Pro+濡??낃렇?덉씠?쒗븯?몄슂:`);
|
|
233
|
+
console.log(` ??https://laxy-blue.vercel.app/pricing`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
151
236
|
}
|
|
152
237
|
async function run() {
|
|
153
238
|
const args = parseArgs();
|
|
154
|
-
//
|
|
239
|
+
// ?? --help ????????????????????????????????????????????????????????????????
|
|
240
|
+
if (args.help) {
|
|
241
|
+
console.log(`
|
|
242
|
+
laxy-verify v${package_json_1.default.version}
|
|
243
|
+
Frontend quality gate: build + Lighthouse verification
|
|
244
|
+
|
|
245
|
+
Usage:
|
|
246
|
+
npx laxy-verify [project-dir] [options]
|
|
247
|
+
npx laxy-verify <subcommand>
|
|
248
|
+
|
|
249
|
+
Subcommands:
|
|
250
|
+
login [email] Log in to unlock Pro/Pro+ features
|
|
251
|
+
logout Remove saved credentials
|
|
252
|
+
whoami Show current login status
|
|
253
|
+
|
|
254
|
+
Options:
|
|
255
|
+
--init Generate .laxy.yml + GitHub workflow file
|
|
256
|
+
--init --run Generate config and immediately run verification
|
|
257
|
+
--format console | json (default: console)
|
|
258
|
+
--ci CI mode: -10 Performance threshold, runs=3
|
|
259
|
+
--config <path> Path to .laxy.yml
|
|
260
|
+
--fail-on unverified | bronze | silver | gold
|
|
261
|
+
--skip-lighthouse Build-only (max Bronze grade)
|
|
262
|
+
--multi-viewport Pro+: Lighthouse on desktop/tablet/mobile
|
|
263
|
+
--badge Print shields.io badge markdown
|
|
264
|
+
--help Show this help
|
|
265
|
+
|
|
266
|
+
Exit codes:
|
|
267
|
+
0 Grade meets or exceeds fail_on threshold
|
|
268
|
+
1 Grade worse than fail_on, or build failed
|
|
269
|
+
2 Configuration error
|
|
270
|
+
|
|
271
|
+
Examples:
|
|
272
|
+
npx laxy-verify --init --run # Setup + first verification
|
|
273
|
+
npx laxy-verify . # Run in current directory
|
|
274
|
+
npx laxy-verify . --ci # CI mode
|
|
275
|
+
npx laxy-verify . --fail-on silver # Require Silver or better
|
|
276
|
+
|
|
277
|
+
Docs: https://github.com/psungmin24/laxy-verify
|
|
278
|
+
`);
|
|
279
|
+
exitGracefully(0);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// ?? ?쒕툕而ㅻ㎤??泥섎━ ??????????????????????????????????????????????????????
|
|
155
283
|
if (args.subcommand === "login") {
|
|
156
284
|
await (0, auth_js_1.login)(args.subcommandArg);
|
|
157
|
-
// Windows: fetch()
|
|
158
|
-
// UV_HANDLE_CLOSING Assertion
|
|
159
|
-
|
|
285
|
+
// Windows: fetch()??TCP ?뚯폆??closing 以묒뿉 process.exit()??利됱떆 ?몄텧?섎㈃
|
|
286
|
+
// UV_HANDLE_CLOSING Assertion 諛쒖깮. setTimeout?쇰줈 UV 猷⑦봽???뺣━ ?쒓컙??以??
|
|
287
|
+
exitGracefully(0);
|
|
160
288
|
return;
|
|
161
289
|
}
|
|
162
290
|
if (args.subcommand === "logout") {
|
|
163
291
|
(0, auth_js_1.clearToken)();
|
|
164
|
-
|
|
292
|
+
exitGracefully(0);
|
|
165
293
|
return;
|
|
166
294
|
}
|
|
167
295
|
if (args.subcommand === "whoami") {
|
|
168
296
|
(0, auth_js_1.whoami)();
|
|
169
|
-
|
|
297
|
+
exitGracefully(0);
|
|
170
298
|
return;
|
|
171
299
|
}
|
|
172
300
|
// --init
|
|
173
301
|
if (args.init) {
|
|
174
302
|
(0, init_js_1.runInit)(args.projectDir);
|
|
175
|
-
|
|
176
|
-
|
|
303
|
+
if (!args.initRun) {
|
|
304
|
+
console.log(`\n ?ㅼ쓬 ?④퀎: npx laxy-verify . (?먮뒗 --init --run ?쇰줈 諛붾줈 ?ㅽ뻾)`);
|
|
305
|
+
exitGracefully(0);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
console.log("\n 泥?踰덉㎏ 寃利앹쓣 ?쒖옉?⑸땲??..\n");
|
|
309
|
+
// initRun: fall through to run verification below
|
|
177
310
|
}
|
|
178
311
|
// --badge
|
|
179
312
|
if (args.badge) {
|
|
180
313
|
const resultPath = path.join(args.projectDir, ".laxy-result.json");
|
|
181
314
|
if (!fs.existsSync(resultPath)) {
|
|
182
315
|
console.error("Error: .laxy-result.json not found. Run `npx laxy-verify .` first to generate it.");
|
|
183
|
-
|
|
316
|
+
exitGracefully(2);
|
|
184
317
|
return;
|
|
185
318
|
}
|
|
186
319
|
const content = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
187
320
|
const badge = (0, badge_js_1.generateBadge)(content.grade);
|
|
188
321
|
console.log(badge);
|
|
189
|
-
|
|
322
|
+
exitGracefully(0);
|
|
190
323
|
return;
|
|
191
324
|
}
|
|
192
325
|
// Load config
|
|
@@ -204,7 +337,7 @@ async function run() {
|
|
|
204
337
|
}
|
|
205
338
|
catch (err) {
|
|
206
339
|
console.error(`Config error: ${err instanceof Error ? err.message : String(err)}`);
|
|
207
|
-
|
|
340
|
+
exitGracefully(2);
|
|
208
341
|
return;
|
|
209
342
|
}
|
|
210
343
|
// Auto-detect framework + package manager
|
|
@@ -214,17 +347,25 @@ async function run() {
|
|
|
214
347
|
}
|
|
215
348
|
catch (err) {
|
|
216
349
|
console.error(`Detection error: ${err instanceof Error ? err.message : String(err)}`);
|
|
217
|
-
|
|
350
|
+
exitGracefully(2);
|
|
218
351
|
return;
|
|
219
352
|
}
|
|
220
353
|
// Merge config overrides
|
|
221
354
|
const buildCmd = config.build_command || detected.buildCmd;
|
|
222
355
|
const devCmd = config.dev_command || detected.devCmd;
|
|
223
356
|
const port = config.port;
|
|
357
|
+
try {
|
|
358
|
+
await ensurePortAvailableForVerification(port);
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
console.error(`Preflight error: ${err instanceof Error ? err.message : String(err)}`);
|
|
362
|
+
exitGracefully(2);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
224
365
|
// Phase 1: Build
|
|
225
366
|
let buildResult;
|
|
226
367
|
try {
|
|
227
|
-
buildResult = await (0, build_js_1.runBuild)(buildCmd, config.build_timeout);
|
|
368
|
+
buildResult = await (0, build_js_1.runBuild)(buildCmd, config.build_timeout, args.projectDir);
|
|
228
369
|
}
|
|
229
370
|
catch (err) {
|
|
230
371
|
buildResult = {
|
|
@@ -243,14 +384,14 @@ async function run() {
|
|
|
243
384
|
seo: config.thresholds.seo,
|
|
244
385
|
bestPractices: config.thresholds.bestPractices,
|
|
245
386
|
};
|
|
246
|
-
//
|
|
387
|
+
// ?? ?뚮옖 湲곕뒫 議고쉶 (?좏겙 ?놁쑝硫?Free ?대갚) ??????????????????????????????
|
|
247
388
|
let entitlements = null;
|
|
248
389
|
try {
|
|
249
390
|
entitlements = await (0, entitlement_js_1.getEntitlements)();
|
|
250
391
|
(0, entitlement_js_1.printPlanBanner)(entitlements);
|
|
251
392
|
}
|
|
252
393
|
catch {
|
|
253
|
-
//
|
|
394
|
+
// ?ㅽ듃?뚰겕 ?ㅻ쪟 ??Free濡?吏꾪뻾
|
|
254
395
|
}
|
|
255
396
|
const features = entitlements ?? {
|
|
256
397
|
plan: "free",
|
|
@@ -261,18 +402,22 @@ async function run() {
|
|
|
261
402
|
failure_analysis: false,
|
|
262
403
|
fast_lane: false,
|
|
263
404
|
};
|
|
264
|
-
// Pro
|
|
405
|
+
// Pro ?댁긽?대㈃ Lighthouse 3???ㅽ뻾 (???뺣?)
|
|
265
406
|
if (features.lighthouse_runs_3 && config.lighthouse_runs < 3) {
|
|
266
407
|
config = { ...config, lighthouse_runs: 3 };
|
|
267
408
|
}
|
|
268
409
|
// Phase 2: Dev server + Lighthouse (only if build succeeded and not skipped)
|
|
269
410
|
let multiViewportScores = null;
|
|
270
411
|
let allViewportsOk = false;
|
|
412
|
+
let e2eResult;
|
|
413
|
+
let visualDiffResult = null;
|
|
271
414
|
if (buildResult.success && !args.skipLighthouse) {
|
|
272
415
|
let servePid;
|
|
273
416
|
try {
|
|
274
|
-
const serve = await (0, serve_js_1.startDevServer)(devCmd, port, config.dev_timeout);
|
|
417
|
+
const serve = await (0, serve_js_1.startDevServer)(devCmd, port, config.dev_timeout, args.projectDir);
|
|
275
418
|
servePid = serve.pid;
|
|
419
|
+
const verifyUrl = `http://127.0.0.1:${port}/`;
|
|
420
|
+
const verificationTier = (0, index_js_1.planToVerificationTier)(features.plan);
|
|
276
421
|
try {
|
|
277
422
|
const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
|
|
278
423
|
scores = lhResult.scores ?? undefined;
|
|
@@ -289,9 +434,9 @@ async function run() {
|
|
|
289
434
|
catch (lhErr) {
|
|
290
435
|
console.error(`Lighthouse error: ${lhErr instanceof Error ? lhErr.message : String(lhErr)}`);
|
|
291
436
|
}
|
|
292
|
-
//
|
|
437
|
+
// ?? Pro+: 硫??酉고룷??(dev server媛 ?뚮━??以? ?????????????????????
|
|
293
438
|
if (args.multiViewport && !features.multi_viewport) {
|
|
294
|
-
console.log("\n
|
|
439
|
+
console.log("\n ?좑툘 --multi-viewport??Pro+ ?뚮옖 ?꾩슜?낅땲?? laxy-verify login ?쇰줈 濡쒓렇?명븯?몄슂.");
|
|
295
440
|
}
|
|
296
441
|
else if (features.multi_viewport) {
|
|
297
442
|
try {
|
|
@@ -303,6 +448,26 @@ async function run() {
|
|
|
303
448
|
console.error(`Multi-viewport error: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
|
|
304
449
|
}
|
|
305
450
|
}
|
|
451
|
+
try {
|
|
452
|
+
const e2eRuns = await (0, e2e_js_1.runVerifyE2E)(verifyUrl, verificationTier);
|
|
453
|
+
e2eResult = {
|
|
454
|
+
passed: e2eRuns.passed,
|
|
455
|
+
failed: e2eRuns.failed,
|
|
456
|
+
total: e2eRuns.results.length,
|
|
457
|
+
results: e2eRuns.results,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
catch (e2eErr) {
|
|
461
|
+
console.error(`E2E error: ${e2eErr instanceof Error ? e2eErr.message : String(e2eErr)}`);
|
|
462
|
+
}
|
|
463
|
+
if (verificationTier === "pro_plus") {
|
|
464
|
+
try {
|
|
465
|
+
visualDiffResult = await (0, visual_diff_js_1.runVisualDiff)(args.projectDir, verifyUrl, "verify");
|
|
466
|
+
}
|
|
467
|
+
catch (visualErr) {
|
|
468
|
+
console.error(`Visual diff error: ${visualErr instanceof Error ? visualErr.message : String(visualErr)}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
306
471
|
}
|
|
307
472
|
catch (serveErr) {
|
|
308
473
|
console.error(`Dev server error: ${serveErr instanceof Error ? serveErr.message : String(serveErr)}`);
|
|
@@ -313,29 +478,74 @@ async function run() {
|
|
|
313
478
|
}
|
|
314
479
|
}
|
|
315
480
|
}
|
|
316
|
-
|
|
317
|
-
const
|
|
481
|
+
const verificationTier = (0, index_js_1.planToVerificationTier)(features.plan);
|
|
482
|
+
const viewportSummary = summarizeViewportIssues(multiViewportScores, adjustedThresholds);
|
|
483
|
+
const failureEvidence = [
|
|
484
|
+
...buildResult.errors.slice(0, 3).map((error) => `Build: ${error}`),
|
|
485
|
+
...(e2eResult
|
|
486
|
+
? e2eResult.results
|
|
487
|
+
.filter((scenario) => !scenario.passed)
|
|
488
|
+
.slice(0, 2)
|
|
489
|
+
.map((scenario) => `E2E: ${scenario.name}${scenario.error ? ` - ${scenario.error}` : ""}`)
|
|
490
|
+
: []),
|
|
491
|
+
...(viewportSummary.count > 0 && viewportSummary.summary
|
|
492
|
+
? [`Viewport: ${viewportSummary.summary}`]
|
|
493
|
+
: []),
|
|
494
|
+
...(visualDiffResult
|
|
495
|
+
? [
|
|
496
|
+
visualDiffResult.hasBaseline
|
|
497
|
+
? `Visual diff: ${visualDiffResult.diffPercentage}% (${visualDiffResult.verdict})`
|
|
498
|
+
: "Visual diff: baseline seeded",
|
|
499
|
+
]
|
|
500
|
+
: []),
|
|
501
|
+
];
|
|
502
|
+
const verificationReport = (0, index_js_1.buildVerificationReport)({
|
|
318
503
|
buildSuccess: buildResult.success,
|
|
319
|
-
|
|
504
|
+
buildErrors: buildResult.errors,
|
|
505
|
+
e2ePassed: e2eResult?.passed,
|
|
506
|
+
e2eTotal: e2eResult?.total,
|
|
507
|
+
viewportIssues: multiViewportScores ? viewportSummary.count : undefined,
|
|
508
|
+
multiViewportPassed: multiViewportScores ? allViewportsOk : undefined,
|
|
509
|
+
multiViewportSummary: multiViewportScores ? viewportSummary.summary : undefined,
|
|
510
|
+
visualDiffVerdict: visualDiffResult?.verdict,
|
|
511
|
+
visualDiffPercentage: visualDiffResult?.diffPercentage,
|
|
512
|
+
hasVisualBaseline: visualDiffResult?.hasBaseline,
|
|
513
|
+
lighthouseScores: scores,
|
|
514
|
+
failureEvidence,
|
|
515
|
+
}, {
|
|
516
|
+
tier: verificationTier,
|
|
320
517
|
thresholds: adjustedThresholds,
|
|
321
|
-
failOn: config.fail_on,
|
|
322
|
-
goldEligible: features.gold_grade && allViewportsOk,
|
|
323
518
|
});
|
|
519
|
+
const verificationView = (0, index_js_1.getTierVerificationView)(verificationReport);
|
|
520
|
+
const unifiedGrade = verificationReport.grade;
|
|
521
|
+
const exitCode = config.fail_on === "unverified"
|
|
522
|
+
? 0
|
|
523
|
+
: (0, grade_js_1.isWorseOrEqual)(unifiedGrade, config.fail_on)
|
|
524
|
+
? 1
|
|
525
|
+
: 0;
|
|
324
526
|
// Build result object
|
|
325
527
|
const resultObj = {
|
|
326
|
-
grade:
|
|
528
|
+
grade: unifiedGrade.charAt(0).toUpperCase() + unifiedGrade.slice(1),
|
|
327
529
|
timestamp: new Date().toISOString(),
|
|
328
530
|
build: {
|
|
329
531
|
success: buildResult.success,
|
|
330
532
|
durationMs: buildResult.durationMs,
|
|
331
533
|
errors: buildResult.errors,
|
|
332
534
|
},
|
|
535
|
+
e2e: e2eResult,
|
|
333
536
|
lighthouse: lighthouseResult,
|
|
537
|
+
visualDiff: visualDiffResult,
|
|
334
538
|
thresholds: adjustedThresholds,
|
|
335
539
|
ciMode: config.ciMode,
|
|
336
540
|
framework: detected.framework,
|
|
337
|
-
exitCode
|
|
541
|
+
exitCode,
|
|
338
542
|
config_fail_on: config.fail_on,
|
|
543
|
+
_plan: features.plan,
|
|
544
|
+
verification: {
|
|
545
|
+
tier: verificationTier,
|
|
546
|
+
report: verificationReport,
|
|
547
|
+
view: verificationView,
|
|
548
|
+
},
|
|
339
549
|
};
|
|
340
550
|
// GitHub integration (only in Actions)
|
|
341
551
|
const inGitHubActions = !!process.env.GITHUB_ACTIONS;
|
|
@@ -364,9 +574,9 @@ async function run() {
|
|
|
364
574
|
if (inGitHubActions && process.env.GITHUB_OUTPUT) {
|
|
365
575
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `grade=${resultObj.grade}\n`);
|
|
366
576
|
}
|
|
367
|
-
|
|
577
|
+
exitGracefully(exitCode);
|
|
368
578
|
}
|
|
369
579
|
run().catch((err) => {
|
|
370
580
|
console.error(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
|
|
371
|
-
|
|
581
|
+
exitGracefully(1);
|
|
372
582
|
});
|
package/dist/e2e.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { VerificationTier } from "./verification-core/types.js";
|
|
2
|
+
export interface E2EStep {
|
|
3
|
+
type: "click" | "fill" | "check_visible" | "wait" | "scroll" | "clear_fill";
|
|
4
|
+
selector?: string;
|
|
5
|
+
value?: string;
|
|
6
|
+
duration?: number;
|
|
7
|
+
description: string;
|
|
8
|
+
}
|
|
9
|
+
export interface E2EScenario {
|
|
10
|
+
name: string;
|
|
11
|
+
steps: E2EStep[];
|
|
12
|
+
}
|
|
13
|
+
export interface E2EStepResult {
|
|
14
|
+
description: string;
|
|
15
|
+
passed: boolean;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface E2EScenarioResult {
|
|
19
|
+
name: string;
|
|
20
|
+
passed: boolean;
|
|
21
|
+
steps: E2EStepResult[];
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function runVerifyE2E(url: string, tier: VerificationTier): Promise<{
|
|
25
|
+
scenarios: E2EScenario[];
|
|
26
|
+
results: E2EScenarioResult[];
|
|
27
|
+
passed: number;
|
|
28
|
+
failed: number;
|
|
29
|
+
}>;
|