laxy-verify 1.2.0 → 1.2.1
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/dist/audit/broken-links.d.ts +21 -21
- package/dist/audit/broken-links.js +86 -86
- package/dist/auth.d.ts +11 -11
- package/dist/auth.js +222 -222
- package/dist/cli.js +830 -806
- package/dist/comment.d.ts +21 -21
- package/dist/comment.js +125 -125
- package/dist/crawler.d.ts +36 -36
- package/dist/crawler.js +357 -357
- package/dist/e2e.d.ts +49 -49
- package/dist/e2e.js +565 -565
- package/dist/entitlement.d.ts +11 -11
- package/dist/entitlement.js +90 -90
- package/dist/init.js +87 -87
- package/dist/multi-viewport.d.ts +31 -31
- package/dist/multi-viewport.js +298 -298
- package/dist/playwright-runner.d.ts +16 -16
- package/dist/playwright-runner.js +208 -208
- package/dist/report-markdown.d.ts +39 -39
- package/dist/report-markdown.js +386 -386
- package/dist/security-audit.d.ts +9 -9
- package/dist/security-audit.js +64 -64
- package/dist/serve.d.ts +13 -13
- package/dist/serve.js +196 -196
- package/dist/trend.d.ts +50 -50
- package/dist/trend.js +148 -148
- package/dist/verification-core/index.d.ts +3 -3
- package/dist/verification-core/index.js +19 -19
- package/dist/verification-core/report.d.ts +14 -14
- package/dist/verification-core/report.js +409 -409
- package/dist/verification-core/tier-policy.d.ts +13 -13
- package/dist/verification-core/tier-policy.js +60 -60
- package/dist/verification-core/types.d.ts +108 -108
- package/dist/verification-core/types.js +2 -2
- package/dist/visual-diff.d.ts +26 -26
- package/dist/visual-diff.js +178 -178
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,306 +1,307 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
-
if (k2 === undefined) k2 = k;
|
|
5
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
-
}
|
|
9
|
-
Object.defineProperty(o, k2, desc);
|
|
10
|
-
}) : (function(o, m, k, k2) {
|
|
11
|
-
if (k2 === undefined) k2 = k;
|
|
12
|
-
o[k2] = m[k];
|
|
13
|
-
}));
|
|
14
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
-
}) : function(o, v) {
|
|
17
|
-
o["default"] = v;
|
|
18
|
-
});
|
|
19
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
-
var ownKeys = function(o) {
|
|
21
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
-
var ar = [];
|
|
23
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
-
return ar;
|
|
25
|
-
};
|
|
26
|
-
return ownKeys(o);
|
|
27
|
-
};
|
|
28
|
-
return function (mod) {
|
|
29
|
-
if (mod && mod.__esModule) return mod;
|
|
30
|
-
var result = {};
|
|
31
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
-
__setModuleDefault(result, mod);
|
|
33
|
-
return result;
|
|
34
|
-
};
|
|
35
|
-
})();
|
|
36
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
-
};
|
|
39
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
const fs = __importStar(require("node:fs"));
|
|
41
|
-
const path = __importStar(require("node:path"));
|
|
42
|
-
const config_js_1 = require("./config.js");
|
|
43
|
-
const detect_js_1 = require("./detect.js");
|
|
44
|
-
const build_js_1 = require("./build.js");
|
|
45
|
-
const serve_js_1 = require("./serve.js");
|
|
46
|
-
const lighthouse_js_1 = require("./lighthouse.js");
|
|
47
|
-
const grade_js_1 = require("./grade.js");
|
|
48
|
-
const init_js_1 = require("./init.js");
|
|
49
|
-
const badge_js_1 = require("./badge.js");
|
|
50
|
-
const comment_js_1 = require("./comment.js");
|
|
51
|
-
const status_js_1 = require("./status.js");
|
|
52
|
-
const auth_js_1 = require("./auth.js");
|
|
53
|
-
const entitlement_js_1 = require("./entitlement.js");
|
|
54
|
-
const multi_viewport_js_1 = require("./multi-viewport.js");
|
|
55
|
-
const e2e_js_1 = require("./e2e.js");
|
|
56
|
-
const playwright_runner_js_1 = require("./playwright-runner.js");
|
|
57
|
-
const report_markdown_js_1 = require("./report-markdown.js");
|
|
58
|
-
const visual_diff_js_1 = require("./visual-diff.js");
|
|
59
|
-
const security_audit_js_1 = require("./security-audit.js");
|
|
60
|
-
const broken_links_js_1 = require("./audit/broken-links.js");
|
|
61
|
-
const index_js_1 = require("./verification-core/index.js");
|
|
62
|
-
const trend_js_1 = require("./trend.js");
|
|
63
|
-
const package_json_1 = __importDefault(require("../package.json"));
|
|
64
|
-
let activeDevServerPid;
|
|
65
|
-
let activeDevServerCleanup = null;
|
|
66
|
-
async function cleanupActiveDevServer() {
|
|
67
|
-
if (activeDevServerPid === undefined)
|
|
68
|
-
return;
|
|
69
|
-
if (activeDevServerCleanup) {
|
|
70
|
-
await activeDevServerCleanup;
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const pid = activeDevServerPid;
|
|
74
|
-
activeDevServerPid = undefined;
|
|
75
|
-
activeDevServerCleanup = (0, serve_js_1.stopDevServer)(pid).finally(() => {
|
|
76
|
-
activeDevServerCleanup = null;
|
|
77
|
-
});
|
|
78
|
-
await activeDevServerCleanup;
|
|
79
|
-
}
|
|
80
|
-
function installSignalCleanupHandlers() {
|
|
81
|
-
const handleSignal = (signal) => {
|
|
82
|
-
void cleanupActiveDevServer().finally(() => {
|
|
83
|
-
const exitCode = signal === "SIGINT" ? 130 : 143;
|
|
84
|
-
exitGracefully(exitCode);
|
|
85
|
-
});
|
|
86
|
-
};
|
|
87
|
-
process.once("SIGINT", () => handleSignal("SIGINT"));
|
|
88
|
-
process.once("SIGTERM", () => handleSignal("SIGTERM"));
|
|
89
|
-
}
|
|
90
|
-
function shouldFailVerificationResult(report, failOn) {
|
|
91
|
-
if (failOn === "unverified")
|
|
92
|
-
return false;
|
|
93
|
-
if (report.verdict === "build-failed" || report.verdict === "hold")
|
|
94
|
-
return true;
|
|
95
|
-
if (report.verdict === "investigate")
|
|
96
|
-
return true;
|
|
97
|
-
return (0, grade_js_1.isWorseOrEqual)(report.grade, failOn);
|
|
98
|
-
}
|
|
99
|
-
function exitGracefully(code) {
|
|
100
|
-
if (process.platform === "win32") {
|
|
101
|
-
setTimeout(() => process.exit(code), 100);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
process.exit(code);
|
|
105
|
-
}
|
|
106
|
-
async function ensurePortAvailableForVerification(port) {
|
|
107
|
-
const status = await (0, serve_js_1.probeServerStatus)(port);
|
|
108
|
-
if (status === null)
|
|
109
|
-
return;
|
|
110
|
-
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.`);
|
|
111
|
-
}
|
|
112
|
-
function parseArgs() {
|
|
113
|
-
const raw = process.argv.slice(2);
|
|
114
|
-
let projectDir = ".";
|
|
115
|
-
const flags = {};
|
|
116
|
-
for (let i = 0; i < raw.length; i++) {
|
|
117
|
-
const arg = raw[i];
|
|
118
|
-
if (arg.startsWith("--")) {
|
|
119
|
-
const eqIndex = arg.indexOf("=");
|
|
120
|
-
if (eqIndex >= 0) {
|
|
121
|
-
const key = arg.slice(2, eqIndex);
|
|
122
|
-
flags[key] = arg.slice(eqIndex + 1);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
const key = arg.slice(2);
|
|
126
|
-
if (i + 1 < raw.length && !raw[i + 1].startsWith("-")) {
|
|
127
|
-
flags[key] = raw[++i];
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
flags[key] = "true";
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
else if (projectDir === ".") {
|
|
135
|
-
projectDir = arg;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
let subcommand;
|
|
139
|
-
let subcommandArg;
|
|
140
|
-
if (projectDir === "login" || projectDir === "logout" || projectDir === "whoami") {
|
|
141
|
-
subcommand = projectDir;
|
|
142
|
-
projectDir = ".";
|
|
143
|
-
subcommandArg = flags.email;
|
|
144
|
-
}
|
|
145
|
-
return {
|
|
146
|
-
projectDir: path.resolve(projectDir),
|
|
147
|
-
subcommand,
|
|
148
|
-
subcommandArg,
|
|
149
|
-
format: flags.format ?? "console",
|
|
150
|
-
ciMode: flags.ci !== undefined || process.env.CI === "true",
|
|
151
|
-
configPath: flags.config,
|
|
152
|
-
failOn: flags["fail-on"] ?? undefined,
|
|
153
|
-
skipLighthouse: flags["skip-lighthouse"] !== undefined,
|
|
154
|
-
badge: flags.badge !== undefined,
|
|
155
|
-
init: flags.init !== undefined,
|
|
156
|
-
initRun: flags.init !== undefined && flags.run !== undefined,
|
|
157
|
-
multiViewport: flags["multi-viewport"] !== undefined,
|
|
158
|
-
failureAnalysis: flags["failure-analysis"] !== undefined,
|
|
159
|
-
crawl: flags.crawl !== undefined,
|
|
160
|
-
planOverride: flags["plan-override"],
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
viewportScores.
|
|
180
|
-
viewportScores.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
console.log(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
console.log(
|
|
211
|
-
console.log(`
|
|
212
|
-
console.log(`
|
|
213
|
-
console.log(`
|
|
214
|
-
console.log(`
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
console.log(`
|
|
245
|
-
console.log(` Decision: ${view.
|
|
246
|
-
console.log(`
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
const config_js_1 = require("./config.js");
|
|
43
|
+
const detect_js_1 = require("./detect.js");
|
|
44
|
+
const build_js_1 = require("./build.js");
|
|
45
|
+
const serve_js_1 = require("./serve.js");
|
|
46
|
+
const lighthouse_js_1 = require("./lighthouse.js");
|
|
47
|
+
const grade_js_1 = require("./grade.js");
|
|
48
|
+
const init_js_1 = require("./init.js");
|
|
49
|
+
const badge_js_1 = require("./badge.js");
|
|
50
|
+
const comment_js_1 = require("./comment.js");
|
|
51
|
+
const status_js_1 = require("./status.js");
|
|
52
|
+
const auth_js_1 = require("./auth.js");
|
|
53
|
+
const entitlement_js_1 = require("./entitlement.js");
|
|
54
|
+
const multi_viewport_js_1 = require("./multi-viewport.js");
|
|
55
|
+
const e2e_js_1 = require("./e2e.js");
|
|
56
|
+
const playwright_runner_js_1 = require("./playwright-runner.js");
|
|
57
|
+
const report_markdown_js_1 = require("./report-markdown.js");
|
|
58
|
+
const visual_diff_js_1 = require("./visual-diff.js");
|
|
59
|
+
const security_audit_js_1 = require("./security-audit.js");
|
|
60
|
+
const broken_links_js_1 = require("./audit/broken-links.js");
|
|
61
|
+
const index_js_1 = require("./verification-core/index.js");
|
|
62
|
+
const trend_js_1 = require("./trend.js");
|
|
63
|
+
const package_json_1 = __importDefault(require("../package.json"));
|
|
64
|
+
let activeDevServerPid;
|
|
65
|
+
let activeDevServerCleanup = null;
|
|
66
|
+
async function cleanupActiveDevServer() {
|
|
67
|
+
if (activeDevServerPid === undefined)
|
|
68
|
+
return;
|
|
69
|
+
if (activeDevServerCleanup) {
|
|
70
|
+
await activeDevServerCleanup;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const pid = activeDevServerPid;
|
|
74
|
+
activeDevServerPid = undefined;
|
|
75
|
+
activeDevServerCleanup = (0, serve_js_1.stopDevServer)(pid).finally(() => {
|
|
76
|
+
activeDevServerCleanup = null;
|
|
77
|
+
});
|
|
78
|
+
await activeDevServerCleanup;
|
|
79
|
+
}
|
|
80
|
+
function installSignalCleanupHandlers() {
|
|
81
|
+
const handleSignal = (signal) => {
|
|
82
|
+
void cleanupActiveDevServer().finally(() => {
|
|
83
|
+
const exitCode = signal === "SIGINT" ? 130 : 143;
|
|
84
|
+
exitGracefully(exitCode);
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
process.once("SIGINT", () => handleSignal("SIGINT"));
|
|
88
|
+
process.once("SIGTERM", () => handleSignal("SIGTERM"));
|
|
89
|
+
}
|
|
90
|
+
function shouldFailVerificationResult(report, failOn) {
|
|
91
|
+
if (failOn === "unverified")
|
|
92
|
+
return false;
|
|
93
|
+
if (report.verdict === "build-failed" || report.verdict === "hold")
|
|
94
|
+
return true;
|
|
95
|
+
if (report.verdict === "investigate")
|
|
96
|
+
return true;
|
|
97
|
+
return (0, grade_js_1.isWorseOrEqual)(report.grade, failOn);
|
|
98
|
+
}
|
|
99
|
+
function exitGracefully(code) {
|
|
100
|
+
if (process.platform === "win32") {
|
|
101
|
+
setTimeout(() => process.exit(code), 100);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
process.exit(code);
|
|
105
|
+
}
|
|
106
|
+
async function ensurePortAvailableForVerification(port) {
|
|
107
|
+
const status = await (0, serve_js_1.probeServerStatus)(port);
|
|
108
|
+
if (status === null)
|
|
109
|
+
return;
|
|
110
|
+
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.`);
|
|
111
|
+
}
|
|
112
|
+
function parseArgs() {
|
|
113
|
+
const raw = process.argv.slice(2);
|
|
114
|
+
let projectDir = ".";
|
|
115
|
+
const flags = {};
|
|
116
|
+
for (let i = 0; i < raw.length; i++) {
|
|
117
|
+
const arg = raw[i];
|
|
118
|
+
if (arg.startsWith("--")) {
|
|
119
|
+
const eqIndex = arg.indexOf("=");
|
|
120
|
+
if (eqIndex >= 0) {
|
|
121
|
+
const key = arg.slice(2, eqIndex);
|
|
122
|
+
flags[key] = arg.slice(eqIndex + 1);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const key = arg.slice(2);
|
|
126
|
+
if (i + 1 < raw.length && !raw[i + 1].startsWith("-")) {
|
|
127
|
+
flags[key] = raw[++i];
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
flags[key] = "true";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (projectDir === ".") {
|
|
135
|
+
projectDir = arg;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
let subcommand;
|
|
139
|
+
let subcommandArg;
|
|
140
|
+
if (projectDir === "login" || projectDir === "logout" || projectDir === "whoami") {
|
|
141
|
+
subcommand = projectDir;
|
|
142
|
+
projectDir = ".";
|
|
143
|
+
subcommandArg = flags.email;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
projectDir: path.resolve(projectDir),
|
|
147
|
+
subcommand,
|
|
148
|
+
subcommandArg,
|
|
149
|
+
format: flags.format ?? "console",
|
|
150
|
+
ciMode: flags.ci !== undefined || process.env.CI === "true",
|
|
151
|
+
configPath: flags.config,
|
|
152
|
+
failOn: flags["fail-on"] ?? undefined,
|
|
153
|
+
skipLighthouse: flags["skip-lighthouse"] !== undefined,
|
|
154
|
+
badge: flags.badge !== undefined,
|
|
155
|
+
init: flags.init !== undefined,
|
|
156
|
+
initRun: flags.init !== undefined && flags.run !== undefined,
|
|
157
|
+
multiViewport: flags["multi-viewport"] !== undefined,
|
|
158
|
+
failureAnalysis: flags["failure-analysis"] !== undefined,
|
|
159
|
+
crawl: flags.crawl !== undefined,
|
|
160
|
+
planOverride: flags["plan-override"],
|
|
161
|
+
port: flags.port !== undefined ? Number(flags.port) : undefined,
|
|
162
|
+
help: flags.help !== undefined || flags.h !== undefined,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function writeResultFile(projectDir, result) {
|
|
166
|
+
const filePath = path.join(projectDir, ".laxy-result.json");
|
|
167
|
+
fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
|
|
168
|
+
}
|
|
169
|
+
function summarizeViewportIssues(scores, thresholds) {
|
|
170
|
+
if (!scores)
|
|
171
|
+
return { count: 0 };
|
|
172
|
+
const failed = [];
|
|
173
|
+
for (const [label, viewportScores] of Object.entries(scores)) {
|
|
174
|
+
if (!viewportScores) {
|
|
175
|
+
failed.push(`${label}: missing`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const passes = viewportScores.performance >= thresholds.performance &&
|
|
179
|
+
viewportScores.accessibility >= thresholds.accessibility &&
|
|
180
|
+
viewportScores.seo >= thresholds.seo &&
|
|
181
|
+
viewportScores.bestPractices >= thresholds.bestPractices;
|
|
182
|
+
if (!passes) {
|
|
183
|
+
failed.push(`${label}: P${viewportScores.performance} A${viewportScores.accessibility} SEO${viewportScores.seo} BP${viewportScores.bestPractices}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
count: failed.length,
|
|
188
|
+
summary: failed.length > 0 ? failed.join(" | ") : "All checked viewports passed.",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function consoleOutput(result) {
|
|
192
|
+
const gradeLabel = result.grade;
|
|
193
|
+
const checkEmoji = result.grade !== "Unverified" ? " OK" : "";
|
|
194
|
+
console.log(`\n Laxy Verify: blocker check ${gradeLabel}${checkEmoji}`);
|
|
195
|
+
console.log(` Build: ${result.build.success ? `OK (${result.build.durationMs}ms)` : "FAILED"}`);
|
|
196
|
+
if (result.build.errors.length > 0) {
|
|
197
|
+
console.log(" Errors:");
|
|
198
|
+
const firstError = result.build.errors.find((line) => /error/i.test(line));
|
|
199
|
+
const last5 = result.build.errors.slice(-5);
|
|
200
|
+
const toShow = firstError && !last5.includes(firstError)
|
|
201
|
+
? [firstError, " ...", ...last5]
|
|
202
|
+
: last5;
|
|
203
|
+
for (const line of toShow)
|
|
204
|
+
console.error(` ${line}`);
|
|
205
|
+
}
|
|
206
|
+
if (result.lighthouse !== null) {
|
|
207
|
+
const lh = result.lighthouse;
|
|
208
|
+
const t = result.thresholds;
|
|
209
|
+
const check = (passed) => (passed ? " OK" : " FAIL");
|
|
210
|
+
console.log(" Lighthouse:");
|
|
211
|
+
console.log(` Performance: ${lh.performance} / ${t.performance}${check(lh.performance >= t.performance)}`);
|
|
212
|
+
console.log(` Accessibility: ${lh.accessibility} / ${t.accessibility}${check(lh.accessibility >= t.accessibility)}`);
|
|
213
|
+
console.log(` SEO: ${lh.seo} / ${t.seo}${check(lh.seo >= t.seo)}`);
|
|
214
|
+
console.log(` Best Practices: ${lh.bestPractices} / ${t.bestPractices}${check(lh.bestPractices >= t.bestPractices)}`);
|
|
215
|
+
console.log(` Runs: ${lh.runs}`);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
console.log(" Lighthouse: skipped");
|
|
219
|
+
}
|
|
220
|
+
if (result.e2e) {
|
|
221
|
+
console.log(` E2E: ${result.e2e.passed}/${result.e2e.total} passed`);
|
|
222
|
+
}
|
|
223
|
+
if (result.crossBrowser && result.crossBrowser.length > 0) {
|
|
224
|
+
console.log(" Cross-browser:");
|
|
225
|
+
for (const cbr of result.crossBrowser) {
|
|
226
|
+
const status = cbr.failed === 0 ? "OK" : "FAIL";
|
|
227
|
+
console.log(` ${cbr.browser}: ${cbr.passed}/${cbr.results.length} passed ${status}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (result.visualDiff) {
|
|
231
|
+
console.log(` Visual diff: ${(0, visual_diff_js_1.formatVisualDiffSummary)(result.visualDiff)}`);
|
|
232
|
+
}
|
|
233
|
+
if (result.security) {
|
|
234
|
+
console.log(` Security: ${result.security.summary}`);
|
|
235
|
+
}
|
|
236
|
+
if (result.mobileLighthouse) {
|
|
237
|
+
const ml = result.mobileLighthouse;
|
|
238
|
+
console.log(` Mobile LH: P=${ml.performance} A=${ml.accessibility} SEO=${ml.seo} BP=${ml.bestPractices}`);
|
|
239
|
+
}
|
|
240
|
+
if (result.verification) {
|
|
241
|
+
const view = result.verification.view;
|
|
242
|
+
const verboseFailure = result._verbose_failure ?? true;
|
|
243
|
+
const failureAnalysis = result._failure_analysis ?? true;
|
|
244
|
+
console.log(` Verification depth: ${view.tier}`);
|
|
245
|
+
console.log(` Decision question: ${view.question}`);
|
|
246
|
+
console.log(` Decision: ${view.verdict} (${view.confidence})`);
|
|
247
|
+
console.log(` Why it stopped here: ${view.summary}`);
|
|
248
|
+
// Passed/Failed checks summary
|
|
249
|
+
if (view.passes.length > 0) {
|
|
250
|
+
const passedChecks = view.passes.filter((p) => p.passed).map((p) => p.label);
|
|
251
|
+
const failedChecks = view.passes.filter((p) => !p.passed).map((p) => p.label);
|
|
252
|
+
if (passedChecks.length > 0)
|
|
253
|
+
console.log(` Passed: ${passedChecks.join(", ")}`);
|
|
254
|
+
if (failedChecks.length > 0)
|
|
255
|
+
console.log(` Failed: ${failedChecks.join(", ")}`);
|
|
256
|
+
}
|
|
257
|
+
// Blockers: ?�목?� 모든 ?�어, Fix ?�션?� verbose_failure(Pro+) ?�상?�서 ?�시
|
|
258
|
+
if (view.blockers.length > 0) {
|
|
259
|
+
console.log(" Deployment blockers:");
|
|
260
|
+
for (const blocker of view.blockers) {
|
|
261
|
+
console.log(` - ${blocker.title}`);
|
|
262
|
+
if (verboseFailure)
|
|
263
|
+
console.log(` Fix: ${blocker.action}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Warnings: always show all
|
|
267
|
+
if (view.warnings.length > 0) {
|
|
268
|
+
console.log(" Risks to review:");
|
|
269
|
+
for (const warning of view.warnings) {
|
|
270
|
+
console.log(` - ${warning.title}`);
|
|
271
|
+
if (failureAnalysis)
|
|
272
|
+
console.log(` Review: ${warning.action}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (view.nextActions.length > 0) {
|
|
276
|
+
console.log(" Next actions:");
|
|
277
|
+
for (const action of view.nextActions)
|
|
278
|
+
console.log(` - ${action}`);
|
|
279
|
+
}
|
|
280
|
+
// Evidence: failure_analysis(Pro+)???�체, verbose_failure(Pro)??3�? Free??2�?
|
|
281
|
+
const evidenceLimit = failureAnalysis ? view.failureEvidence.length : verboseFailure ? 3 : 2;
|
|
282
|
+
const evidenceToShow = view.failureEvidence.slice(0, evidenceLimit);
|
|
283
|
+
if (evidenceToShow.length > 0) {
|
|
284
|
+
console.log(" Evidence collected:");
|
|
285
|
+
for (const item of evidenceToShow)
|
|
286
|
+
console.log(` - ${item}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (result.github) {
|
|
290
|
+
if (result.github.status === "comment_posted")
|
|
291
|
+
console.log(" PR comment: posted");
|
|
292
|
+
if (result.github.status === "status_set")
|
|
293
|
+
console.log(` Status check: ${result.github.grade}`);
|
|
294
|
+
}
|
|
295
|
+
console.log(" Result: .laxy-result.json");
|
|
296
|
+
if (result.markdownReportPath) {
|
|
297
|
+
console.log(` Report: ${path.basename(result.markdownReportPath)}`);
|
|
298
|
+
}
|
|
299
|
+
console.log(` Exit code: ${result.exitCode}`);
|
|
300
|
+
}
|
|
301
|
+
async function run() {
|
|
302
|
+
installSignalCleanupHandlers();
|
|
303
|
+
const args = parseArgs();
|
|
304
|
+
if (args.help) {
|
|
304
305
|
console.log(`
|
|
305
306
|
laxy-verify v${package_json_1.default.version}
|
|
306
307
|
Deployment blocker gate for frontend apps
|
|
@@ -322,6 +323,7 @@ async function run() {
|
|
|
322
323
|
--config <path> Path to .laxy.yml
|
|
323
324
|
--fail-on unverified | bronze | silver | gold
|
|
324
325
|
--skip-lighthouse Skip Lighthouse but still run build and E2E
|
|
326
|
+
--port <port> Use an already-running dev server on this port (skip build & server start)
|
|
325
327
|
--plan-override free | pro | team (testing metadata only)
|
|
326
328
|
--multi-viewport Lighthouse on desktop/tablet/mobile
|
|
327
329
|
--crawl Crawl the app to discover routes before E2E
|
|
@@ -338,508 +340,530 @@ async function run() {
|
|
|
338
340
|
npx laxy-verify . # Run in current directory
|
|
339
341
|
npx laxy-verify . --ci # CI mode
|
|
340
342
|
npx laxy-verify . --fail-on silver # Block Bronze or worse
|
|
343
|
+
npx laxy-verify . --port 3001 # Use existing dev server on port 3001
|
|
341
344
|
|
|
342
345
|
Docs: https://github.com/SUNgm24/Laxy/tree/main/laxy-verify
|
|
343
|
-
`);
|
|
344
|
-
exitGracefully(0);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
if (args.subcommand === "login") {
|
|
348
|
-
await (0, auth_js_1.login)(args.subcommandArg);
|
|
349
|
-
exitGracefully(0);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (args.subcommand === "logout") {
|
|
353
|
-
(0, auth_js_1.clearToken)();
|
|
354
|
-
exitGracefully(0);
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
if (args.subcommand === "whoami") {
|
|
358
|
-
(0, auth_js_1.whoami)();
|
|
359
|
-
exitGracefully(0);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
if (args.init) {
|
|
363
|
-
let initEntitlements = null;
|
|
364
|
-
try {
|
|
365
|
-
initEntitlements = await (0, entitlement_js_1.getEntitlements)();
|
|
366
|
-
}
|
|
367
|
-
catch {
|
|
368
|
-
// ignore fetch errors
|
|
369
|
-
}
|
|
370
|
-
const initPlan = initEntitlements?.plan ?? "free";
|
|
371
|
-
const hasProAccess = initPlan === "pro" || initPlan === "team";
|
|
372
|
-
if (!hasProAccess) {
|
|
373
|
-
console.log("\n GitHub Actions integration is a Pro feature.");
|
|
374
|
-
console.log(" Log in and upgrade to Pro to use --init.");
|
|
375
|
-
console.log(" → laxy-verify login");
|
|
376
|
-
console.log(" → https://laxy.dev/pricing\n");
|
|
377
|
-
exitGracefully(1);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
(0, init_js_1.runInit)(args.projectDir);
|
|
381
|
-
if (!args.initRun) {
|
|
382
|
-
console.log("\n Next step: run npx laxy-verify . (or use --init --run to continue immediately)");
|
|
383
|
-
exitGracefully(0);
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
console.log("\n Config created. Starting verification...\n");
|
|
387
|
-
}
|
|
388
|
-
if (args.badge) {
|
|
389
|
-
const resultPath = path.join(args.projectDir, ".laxy-result.json");
|
|
390
|
-
if (!fs.existsSync(resultPath)) {
|
|
391
|
-
console.error("Error: .laxy-result.json not found. Run `npx laxy-verify .` first to generate it.");
|
|
392
|
-
exitGracefully(2);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
const content = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
396
|
-
// Pro: 실시간 배지 URL (verify_runs 기반)
|
|
397
|
-
let badgeEntitlements = null;
|
|
398
|
-
try {
|
|
399
|
-
badgeEntitlements = await (0, entitlement_js_1.getEntitlements)();
|
|
400
|
-
}
|
|
401
|
-
catch { /* ignore */ }
|
|
402
|
-
const badgePlan = badgeEntitlements?.plan ?? "free";
|
|
403
|
-
const badgeIsPro = badgePlan === "pro" || badgePlan === "team";
|
|
404
|
-
if (badgeIsPro) {
|
|
405
|
-
const { LAXY_API_URL } = await Promise.resolve().then(() => __importStar(require("./auth.js")));
|
|
406
|
-
const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
|
|
407
|
-
const dynamicUrl = `${LAXY_API_URL}/api/badge/${repoId}`;
|
|
408
|
-
console.log(``);
|
|
409
|
-
}
|
|
410
|
-
else {
|
|
411
|
-
const badge = (0, badge_js_1.generateBadge)(content.grade);
|
|
412
|
-
console.log(badge);
|
|
413
|
-
}
|
|
414
|
-
exitGracefully(0);
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
let config;
|
|
418
|
-
try {
|
|
419
|
-
config = (0, config_js_1.loadConfig)({
|
|
420
|
-
dir: args.projectDir,
|
|
421
|
-
configPath: args.configPath,
|
|
422
|
-
ciMode: args.ciMode,
|
|
423
|
-
cliFlags: {
|
|
424
|
-
failOn: args.failOn,
|
|
425
|
-
skipLighthouse: args.skipLighthouse,
|
|
426
|
-
},
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
catch (err) {
|
|
430
|
-
console.error(`Config error: ${err instanceof Error ? err.message : String(err)}`);
|
|
431
|
-
exitGracefully(2);
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
let detected;
|
|
435
|
-
try {
|
|
436
|
-
detected = (0, detect_js_1.detect)(args.projectDir);
|
|
437
|
-
}
|
|
438
|
-
catch (err) {
|
|
439
|
-
console.error(`Detection error: ${err instanceof Error ? err.message : String(err)}`);
|
|
440
|
-
exitGracefully(2);
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
const buildCmd = config.build_command || detected.buildCmd;
|
|
444
|
-
const devCmd = config.dev_command || detected.devCmd;
|
|
445
|
-
const port = config.port;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
let
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
let
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
const
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
});
|
|
346
|
+
`);
|
|
347
|
+
exitGracefully(0);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (args.subcommand === "login") {
|
|
351
|
+
await (0, auth_js_1.login)(args.subcommandArg);
|
|
352
|
+
exitGracefully(0);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (args.subcommand === "logout") {
|
|
356
|
+
(0, auth_js_1.clearToken)();
|
|
357
|
+
exitGracefully(0);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (args.subcommand === "whoami") {
|
|
361
|
+
(0, auth_js_1.whoami)();
|
|
362
|
+
exitGracefully(0);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (args.init) {
|
|
366
|
+
let initEntitlements = null;
|
|
367
|
+
try {
|
|
368
|
+
initEntitlements = await (0, entitlement_js_1.getEntitlements)();
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// ignore fetch errors
|
|
372
|
+
}
|
|
373
|
+
const initPlan = initEntitlements?.plan ?? "free";
|
|
374
|
+
const hasProAccess = initPlan === "pro" || initPlan === "team";
|
|
375
|
+
if (!hasProAccess) {
|
|
376
|
+
console.log("\n GitHub Actions integration is a Pro feature.");
|
|
377
|
+
console.log(" Log in and upgrade to Pro to use --init.");
|
|
378
|
+
console.log(" → laxy-verify login");
|
|
379
|
+
console.log(" → https://laxy.dev/pricing\n");
|
|
380
|
+
exitGracefully(1);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
(0, init_js_1.runInit)(args.projectDir);
|
|
384
|
+
if (!args.initRun) {
|
|
385
|
+
console.log("\n Next step: run npx laxy-verify . (or use --init --run to continue immediately)");
|
|
386
|
+
exitGracefully(0);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
console.log("\n Config created. Starting verification...\n");
|
|
390
|
+
}
|
|
391
|
+
if (args.badge) {
|
|
392
|
+
const resultPath = path.join(args.projectDir, ".laxy-result.json");
|
|
393
|
+
if (!fs.existsSync(resultPath)) {
|
|
394
|
+
console.error("Error: .laxy-result.json not found. Run `npx laxy-verify .` first to generate it.");
|
|
395
|
+
exitGracefully(2);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const content = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
399
|
+
// Pro: 실시간 배지 URL (verify_runs 기반)
|
|
400
|
+
let badgeEntitlements = null;
|
|
401
|
+
try {
|
|
402
|
+
badgeEntitlements = await (0, entitlement_js_1.getEntitlements)();
|
|
403
|
+
}
|
|
404
|
+
catch { /* ignore */ }
|
|
405
|
+
const badgePlan = badgeEntitlements?.plan ?? "free";
|
|
406
|
+
const badgeIsPro = badgePlan === "pro" || badgePlan === "team";
|
|
407
|
+
if (badgeIsPro) {
|
|
408
|
+
const { LAXY_API_URL } = await Promise.resolve().then(() => __importStar(require("./auth.js")));
|
|
409
|
+
const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
|
|
410
|
+
const dynamicUrl = `${LAXY_API_URL}/api/badge/${repoId}`;
|
|
411
|
+
console.log(``);
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
const badge = (0, badge_js_1.generateBadge)(content.grade);
|
|
415
|
+
console.log(badge);
|
|
416
|
+
}
|
|
417
|
+
exitGracefully(0);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
let config;
|
|
421
|
+
try {
|
|
422
|
+
config = (0, config_js_1.loadConfig)({
|
|
423
|
+
dir: args.projectDir,
|
|
424
|
+
configPath: args.configPath,
|
|
425
|
+
ciMode: args.ciMode,
|
|
426
|
+
cliFlags: {
|
|
427
|
+
failOn: args.failOn,
|
|
428
|
+
skipLighthouse: args.skipLighthouse,
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
console.error(`Config error: ${err instanceof Error ? err.message : String(err)}`);
|
|
434
|
+
exitGracefully(2);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
let detected;
|
|
438
|
+
try {
|
|
439
|
+
detected = (0, detect_js_1.detect)(args.projectDir);
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
console.error(`Detection error: ${err instanceof Error ? err.message : String(err)}`);
|
|
443
|
+
exitGracefully(2);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const buildCmd = config.build_command || detected.buildCmd;
|
|
447
|
+
const devCmd = config.dev_command || detected.devCmd;
|
|
448
|
+
const port = args.port ?? config.port;
|
|
449
|
+
const useExistingServer = args.port !== undefined;
|
|
450
|
+
if (!useExistingServer) {
|
|
451
|
+
try {
|
|
452
|
+
await ensurePortAvailableForVerification(port);
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
console.error(`Preflight error: ${err instanceof Error ? err.message : String(err)}`);
|
|
456
|
+
exitGracefully(2);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
// Verify the existing server is actually reachable
|
|
462
|
+
const status = await (0, serve_js_1.probeServerStatus)(port);
|
|
463
|
+
if (status === null) {
|
|
464
|
+
console.error(`Preflight error: No server responding on port ${port}. Make sure the dev server is running before using --port.`);
|
|
465
|
+
exitGracefully(2);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
console.log(`Using existing dev server on port ${port} (HTTP ${status})`);
|
|
469
|
+
}
|
|
470
|
+
let buildResult;
|
|
471
|
+
if (useExistingServer) {
|
|
472
|
+
// Skip build when using an external server
|
|
473
|
+
buildResult = { success: true, durationMs: 0, errors: [] };
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
try {
|
|
477
|
+
buildResult = await (0, build_js_1.runBuild)(buildCmd, config.build_timeout, args.projectDir);
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
buildResult = {
|
|
481
|
+
success: false,
|
|
482
|
+
durationMs: 0,
|
|
483
|
+
errors: err instanceof Error ? [err.message] : [String(err)],
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
let scores;
|
|
488
|
+
let lighthouseResult = null;
|
|
489
|
+
let lighthouseErrorCount = 0;
|
|
490
|
+
const adjustedThresholds = {
|
|
491
|
+
performance: config.ciMode ? config.thresholds.performance - 10 : config.thresholds.performance,
|
|
492
|
+
accessibility: config.thresholds.accessibility,
|
|
493
|
+
seo: config.thresholds.seo,
|
|
494
|
+
bestPractices: config.thresholds.bestPractices,
|
|
495
|
+
};
|
|
496
|
+
let entitlements = null;
|
|
497
|
+
try {
|
|
498
|
+
entitlements = await (0, entitlement_js_1.getEntitlements)();
|
|
499
|
+
(0, entitlement_js_1.printPlanBanner)(entitlements);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
// Ignore entitlement errors and keep the free feature set.
|
|
503
|
+
}
|
|
504
|
+
const features = entitlements ?? {
|
|
505
|
+
plan: "free",
|
|
506
|
+
// All verification features run on every plan
|
|
507
|
+
// Automation features ??not available on Free
|
|
508
|
+
github_actions: false,
|
|
509
|
+
queue_priority: false,
|
|
510
|
+
parallel_execution: false,
|
|
511
|
+
};
|
|
512
|
+
let effectiveFeatures = features;
|
|
513
|
+
if (args.planOverride) {
|
|
514
|
+
try {
|
|
515
|
+
effectiveFeatures = (0, entitlement_js_1.applyPlanOverride)(features, args.planOverride);
|
|
516
|
+
console.log(` Plan override: ${(0, entitlement_js_1.normalizePlan)(features.plan)} -> ${effectiveFeatures.plan} (verification behavior is unchanged)`);
|
|
517
|
+
}
|
|
518
|
+
catch (overrideErr) {
|
|
519
|
+
console.error(`Plan override error: ${overrideErr instanceof Error ? overrideErr.message : String(overrideErr)}`);
|
|
520
|
+
exitGracefully(2);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// Always run 3 Lighthouse passes for median score
|
|
525
|
+
if (config.lighthouse_runs < 3) {
|
|
526
|
+
config = { ...config, lighthouse_runs: 3 };
|
|
527
|
+
}
|
|
528
|
+
let multiViewportScores = null;
|
|
529
|
+
let allViewportsOk = false;
|
|
530
|
+
let e2eResult;
|
|
531
|
+
let crossBrowserResults;
|
|
532
|
+
let e2eCoverageGaps = [];
|
|
533
|
+
let e2eConsoleErrors = [];
|
|
534
|
+
let e2eStabilityPassed = true;
|
|
535
|
+
let visualDiffResult = null;
|
|
536
|
+
let securityAuditResult = null;
|
|
537
|
+
let mobileLighthouseScores = null;
|
|
538
|
+
let brokenLinksResult = null;
|
|
539
|
+
const failureEvidence = [];
|
|
540
|
+
if (buildResult.success) {
|
|
541
|
+
let servePid;
|
|
542
|
+
try {
|
|
543
|
+
if (!useExistingServer) {
|
|
544
|
+
const serve = await (0, serve_js_1.startDevServer)(devCmd, port, config.dev_timeout, args.projectDir);
|
|
545
|
+
servePid = serve.pid;
|
|
546
|
+
activeDevServerPid = serve.pid;
|
|
547
|
+
}
|
|
548
|
+
const verifyUrl = `http://127.0.0.1:${port}/`;
|
|
549
|
+
const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
|
|
550
|
+
if (!args.skipLighthouse) {
|
|
551
|
+
try {
|
|
552
|
+
const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
|
|
553
|
+
lighthouseErrorCount = lhResult.errors.length;
|
|
554
|
+
scores = lhResult.scores ?? undefined;
|
|
555
|
+
if (scores) {
|
|
556
|
+
lighthouseResult = {
|
|
557
|
+
performance: scores.performance,
|
|
558
|
+
accessibility: scores.accessibility,
|
|
559
|
+
seo: scores.seo,
|
|
560
|
+
bestPractices: scores.bestPractices,
|
|
561
|
+
runs: config.lighthouse_runs,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
catch (lhErr) {
|
|
566
|
+
console.error(`Lighthouse error: ${lhErr instanceof Error ? lhErr.message : String(lhErr)}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (!args.skipLighthouse) {
|
|
570
|
+
try {
|
|
571
|
+
multiViewportScores = await (0, multi_viewport_js_1.runMultiViewportLighthouse)(port);
|
|
572
|
+
(0, multi_viewport_js_1.printMultiViewportResults)(multiViewportScores, adjustedThresholds);
|
|
573
|
+
allViewportsOk = (0, multi_viewport_js_1.allViewportsPass)(multiViewportScores, adjustedThresholds);
|
|
574
|
+
if (multiViewportScores.screenshotDiffs) {
|
|
575
|
+
for (const diff of multiViewportScores.screenshotDiffs) {
|
|
576
|
+
if (!diff.baselineCreated && diff.diffPercent > 10) {
|
|
577
|
+
failureEvidence.push(`Viewport screenshot: ${diff.viewport} diff ${diff.diffPercent}% exceeds 10% threshold`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch (mvErr) {
|
|
583
|
+
console.error(`Multi-viewport error: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Security audit (npm audit)
|
|
587
|
+
try {
|
|
588
|
+
securityAuditResult = await (0, security_audit_js_1.runSecurityAudit)(args.projectDir);
|
|
589
|
+
if (securityAuditResult.critical > 0 || securityAuditResult.high > 0) {
|
|
590
|
+
failureEvidence.push(`Security: ${securityAuditResult.summary}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
catch (secErr) {
|
|
594
|
+
console.error(`Security audit error: ${secErr instanceof Error ? secErr.message : String(secErr)}`);
|
|
595
|
+
}
|
|
596
|
+
const crawlEnabled = args.crawl || config.crawl;
|
|
597
|
+
const crawlOpts = crawlEnabled
|
|
598
|
+
? { enabled: true, maxDepth: config.max_crawl_depth, maxPages: config.max_crawl_pages }
|
|
599
|
+
: undefined;
|
|
600
|
+
let lastE2EScenarios;
|
|
601
|
+
try {
|
|
602
|
+
const e2eRuns = await (0, e2e_js_1.runVerifyE2E)(verifyUrl, verificationTier, config.scenarios, crawlOpts);
|
|
603
|
+
lastE2EScenarios = e2eRuns.scenarios;
|
|
604
|
+
e2eResult = {
|
|
605
|
+
passed: e2eRuns.passed,
|
|
606
|
+
failed: e2eRuns.failed,
|
|
607
|
+
total: e2eRuns.results.length,
|
|
608
|
+
results: e2eRuns.results,
|
|
609
|
+
};
|
|
610
|
+
e2eCoverageGaps = e2eRuns.coverageGaps;
|
|
611
|
+
e2eConsoleErrors = e2eRuns.consoleErrors;
|
|
612
|
+
// Merge console errors captured during crawl phase
|
|
613
|
+
if (e2eRuns.crawlResult) {
|
|
614
|
+
const crawlConsoleErrors = e2eRuns.crawlResult.pages.flatMap((p) => p.consoleErrors);
|
|
615
|
+
if (crawlConsoleErrors.length > 0) {
|
|
616
|
+
const unique = crawlConsoleErrors.filter((e) => !e2eConsoleErrors.includes(e));
|
|
617
|
+
e2eConsoleErrors = [...e2eConsoleErrors, ...unique];
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// Broken links audit — runs after E2E so we have the crawl result
|
|
621
|
+
if (e2eRuns.crawlResult && e2eRuns.crawlResult.totalLinks > 0) {
|
|
622
|
+
try {
|
|
623
|
+
brokenLinksResult = await (0, broken_links_js_1.auditBrokenLinks)(e2eRuns.crawlResult, verifyUrl);
|
|
624
|
+
if (brokenLinksResult.hasBrokenLinks) {
|
|
625
|
+
failureEvidence.push(`Broken links: ${brokenLinksResult.summary}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
catch (blErr) {
|
|
629
|
+
console.error(`Broken links audit error: ${blErr instanceof Error ? blErr.message : String(blErr)}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// E2E stability: run a second time if first run passed all
|
|
633
|
+
if (e2eRuns.passed === e2eRuns.results.length && e2eRuns.results.length > 0) {
|
|
634
|
+
console.log(" Running stability pass (run 2/2)...");
|
|
635
|
+
const e2eRuns2 = await (0, e2e_js_1.runVerifyE2E)(verifyUrl, verificationTier, config.scenarios, crawlOpts);
|
|
636
|
+
if (e2eRuns2.passed < e2eRuns2.results.length) {
|
|
637
|
+
e2eStabilityPassed = false;
|
|
638
|
+
e2eCoverageGaps.push("Stability check failed on second run");
|
|
639
|
+
const failedNames = e2eRuns2.results
|
|
640
|
+
.filter((r) => !r.passed)
|
|
641
|
+
.map((r) => r.name)
|
|
642
|
+
.join(", ");
|
|
643
|
+
failureEvidence.push(`E2E stability: second run failed (${failedNames})`);
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
console.log(" Stability pass: OK (2/2 runs passed)");
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (e2eRuns.coverageGaps.length > 0) {
|
|
650
|
+
console.error(`E2E coverage warning: ${e2eRuns.coverageGaps.join(" ")}`);
|
|
651
|
+
}
|
|
652
|
+
if (e2eRuns.consoleErrors.length > 0) {
|
|
653
|
+
console.error(`E2E console errors: ${e2eRuns.consoleErrors.length} detected`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
catch (e2eErr) {
|
|
657
|
+
console.error(`E2E error: ${e2eErr instanceof Error ? e2eErr.message : String(e2eErr)}`);
|
|
658
|
+
}
|
|
659
|
+
// Cross-browser E2E via Playwright (if non-chromium browsers configured)
|
|
660
|
+
const extraBrowsers = (config.browsers || []).filter((b) => b !== "chromium" && ["firefox", "webkit"].includes(b));
|
|
661
|
+
if (extraBrowsers.length > 0 && lastE2EScenarios && lastE2EScenarios.length > 0) {
|
|
662
|
+
const pwAvailable = await (0, playwright_runner_js_1.isPlaywrightAvailable)();
|
|
663
|
+
if (pwAvailable) {
|
|
664
|
+
try {
|
|
665
|
+
crossBrowserResults = await (0, playwright_runner_js_1.runPlaywrightE2E)(verifyUrl, lastE2EScenarios, extraBrowsers);
|
|
666
|
+
for (const cbr of crossBrowserResults) {
|
|
667
|
+
console.log(` Cross-browser ${cbr.browser}: ${cbr.passed}/${cbr.results.length} passed`);
|
|
668
|
+
if (cbr.failed > 0) {
|
|
669
|
+
const failedNames = cbr.results.filter(r => !r.passed).map(r => r.name).join(", ");
|
|
670
|
+
failureEvidence.push(`Cross-browser ${cbr.browser}: ${failedNames} failed`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
catch (cbErr) {
|
|
675
|
+
console.error(`Cross-browser error: ${cbErr instanceof Error ? cbErr.message : String(cbErr)}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
console.log(" Note: Cross-browser testing requires playwright. Run: npm install -D playwright && npx playwright install");
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
visualDiffResult = await (0, visual_diff_js_1.runVisualDiff)(args.projectDir, verifyUrl, "verify");
|
|
684
|
+
}
|
|
685
|
+
catch (visualErr) {
|
|
686
|
+
console.error(`Visual diff error: ${visualErr instanceof Error ? visualErr.message : String(visualErr)}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
catch (serveErr) {
|
|
690
|
+
console.error(`Dev server error: ${serveErr instanceof Error ? serveErr.message : String(serveErr)}`);
|
|
691
|
+
}
|
|
692
|
+
finally {
|
|
693
|
+
if (servePid) {
|
|
694
|
+
await cleanupActiveDevServer();
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
|
|
699
|
+
const viewportSummary = summarizeViewportIssues(multiViewportScores, adjustedThresholds);
|
|
700
|
+
failureEvidence.push(...buildResult.errors.slice(0, 3).map((error) => `Build: ${error}`), ...(lighthouseErrorCount > 0 ? [`Lighthouse: ${lighthouseErrorCount} run error(s) were recorded during collection.`] : []), ...(e2eResult
|
|
701
|
+
? e2eResult.results
|
|
702
|
+
.filter((scenario) => !scenario.passed)
|
|
703
|
+
.slice(0, 2)
|
|
704
|
+
.map((scenario) => `E2E: ${scenario.name}${scenario.error ? ` - ${scenario.error}` : ""}`)
|
|
705
|
+
: []), ...e2eConsoleErrors.slice(0, 2).map((e) => `Console: ${e}`), ...e2eCoverageGaps.slice(0, 2).map((gap) => `E2E coverage: ${gap}`), ...(viewportSummary.count > 0 && viewportSummary.summary ? [`Viewport: ${viewportSummary.summary}`] : []), ...(visualDiffResult
|
|
706
|
+
? [
|
|
707
|
+
`Visual diff: ${(0, visual_diff_js_1.formatVisualDiffSummary)(visualDiffResult)}`,
|
|
708
|
+
]
|
|
709
|
+
: []));
|
|
710
|
+
const verificationReport = (0, index_js_1.buildVerificationReport)({
|
|
711
|
+
buildSuccess: buildResult.success,
|
|
712
|
+
buildErrors: buildResult.errors,
|
|
713
|
+
e2ePassed: e2eResult?.passed,
|
|
714
|
+
e2eTotal: e2eResult?.total,
|
|
715
|
+
e2eCoverageGaps,
|
|
716
|
+
e2eConsoleErrorCount: e2eConsoleErrors.length,
|
|
717
|
+
e2eStabilityPassed,
|
|
718
|
+
lighthouseSkipped: args.skipLighthouse,
|
|
719
|
+
lighthouseErrorCount,
|
|
720
|
+
viewportIssues: multiViewportScores ? viewportSummary.count : undefined,
|
|
721
|
+
multiViewportPassed: multiViewportScores ? allViewportsOk : undefined,
|
|
722
|
+
multiViewportSummary: multiViewportScores ? viewportSummary.summary : undefined,
|
|
723
|
+
visualDiffVerdict: visualDiffResult?.verdict,
|
|
724
|
+
visualDiffPercentage: visualDiffResult?.diffPercentage,
|
|
725
|
+
hasVisualBaseline: visualDiffResult?.hasBaseline,
|
|
726
|
+
lighthouseScores: scores,
|
|
727
|
+
mobileLighthouseScores: mobileLighthouseScores ?? undefined,
|
|
728
|
+
securityAudit: securityAuditResult
|
|
729
|
+
? {
|
|
730
|
+
totalVulnerabilities: securityAuditResult.totalVulnerabilities,
|
|
731
|
+
critical: securityAuditResult.critical,
|
|
732
|
+
high: securityAuditResult.high,
|
|
733
|
+
summary: securityAuditResult.summary,
|
|
734
|
+
}
|
|
735
|
+
: undefined,
|
|
736
|
+
brokenLinksAudit: brokenLinksResult
|
|
737
|
+
? {
|
|
738
|
+
checkedCount: brokenLinksResult.checkedCount,
|
|
739
|
+
brokenCount: brokenLinksResult.brokenLinks.length,
|
|
740
|
+
summary: brokenLinksResult.summary,
|
|
741
|
+
}
|
|
742
|
+
: undefined,
|
|
743
|
+
failureEvidence,
|
|
744
|
+
}, {
|
|
745
|
+
tier: verificationTier,
|
|
746
|
+
thresholds: adjustedThresholds,
|
|
747
|
+
});
|
|
748
|
+
const verificationView = (0, index_js_1.getTierVerificationView)(verificationReport);
|
|
749
|
+
const unifiedGrade = verificationReport.grade;
|
|
750
|
+
const exitCode = shouldFailVerificationResult(verificationReport, config.fail_on) ? 1 : 0;
|
|
751
|
+
const resultObj = {
|
|
752
|
+
grade: unifiedGrade.charAt(0).toUpperCase() + unifiedGrade.slice(1),
|
|
753
|
+
timestamp: new Date().toISOString(),
|
|
754
|
+
build: {
|
|
755
|
+
success: buildResult.success,
|
|
756
|
+
durationMs: buildResult.durationMs,
|
|
757
|
+
errors: buildResult.errors,
|
|
758
|
+
},
|
|
759
|
+
e2e: e2eResult,
|
|
760
|
+
crossBrowser: crossBrowserResults,
|
|
761
|
+
lighthouse: lighthouseResult,
|
|
762
|
+
mobileLighthouse: mobileLighthouseScores,
|
|
763
|
+
security: securityAuditResult,
|
|
764
|
+
visualDiff: visualDiffResult,
|
|
765
|
+
thresholds: adjustedThresholds,
|
|
766
|
+
ciMode: config.ciMode,
|
|
767
|
+
framework: detected.framework,
|
|
768
|
+
exitCode,
|
|
769
|
+
config_fail_on: config.fail_on,
|
|
770
|
+
_plan: effectiveFeatures.plan,
|
|
771
|
+
_verbose_failure: true,
|
|
772
|
+
_failure_analysis: true,
|
|
773
|
+
verification: {
|
|
774
|
+
tier: verificationTier,
|
|
775
|
+
report: verificationReport,
|
|
776
|
+
view: verificationView,
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
const markdownReportPath = (0, report_markdown_js_1.getMarkdownReportPath)(args.projectDir);
|
|
780
|
+
if ((0, report_markdown_js_1.shouldWriteMarkdownReport)(resultObj)) {
|
|
781
|
+
const markdownReport = (0, report_markdown_js_1.buildMarkdownReport)(args.projectDir, resultObj);
|
|
782
|
+
fs.writeFileSync(markdownReportPath, markdownReport, "utf-8");
|
|
783
|
+
resultObj.markdownReportPath = markdownReportPath;
|
|
784
|
+
}
|
|
785
|
+
else if (fs.existsSync(markdownReportPath)) {
|
|
786
|
+
fs.rmSync(markdownReportPath, { force: true });
|
|
787
|
+
}
|
|
788
|
+
const inGitHubActions = !!process.env.GITHUB_ACTIONS;
|
|
789
|
+
if (inGitHubActions) {
|
|
790
|
+
try {
|
|
791
|
+
if (process.env.GITHUB_EVENT_NAME === "pull_request") {
|
|
792
|
+
let trendDelta = null;
|
|
793
|
+
const baseSnapshot = (0, trend_js_1.loadBaseSnapshot)((0, trend_js_1.getBaseResultPath)(args.projectDir));
|
|
794
|
+
if (baseSnapshot) {
|
|
795
|
+
const currentSnapshot = {
|
|
796
|
+
grade: resultObj.grade,
|
|
797
|
+
lighthouse: resultObj.lighthouse,
|
|
798
|
+
e2e: resultObj.e2e ? { passed: resultObj.e2e.passed, total: resultObj.e2e.total } : null,
|
|
799
|
+
timestamp: resultObj.timestamp,
|
|
800
|
+
};
|
|
801
|
+
trendDelta = (0, trend_js_1.computeTrendDelta)(currentSnapshot, baseSnapshot);
|
|
802
|
+
}
|
|
803
|
+
await (0, comment_js_1.postPRComment)(resultObj, trendDelta);
|
|
804
|
+
resultObj.github = { status: "comment_posted", grade: resultObj.grade };
|
|
805
|
+
}
|
|
806
|
+
await (0, status_js_1.createStatusCheck)({ grade: resultObj.grade, exitCode: resultObj.exitCode });
|
|
807
|
+
resultObj.github ??= { status: "status_set", grade: resultObj.grade };
|
|
808
|
+
}
|
|
809
|
+
catch (ghErr) {
|
|
810
|
+
console.error(`GitHub API warning: ${ghErr instanceof Error ? ghErr.message : String(ghErr)}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
writeResultFile(args.projectDir, resultObj);
|
|
814
|
+
// Pro 이상: 결과를 서버에 저장 (배지, 공유 링크용)
|
|
815
|
+
const token = (0, auth_js_1.loadToken)();
|
|
816
|
+
const isPro = effectiveFeatures.plan === "pro" || effectiveFeatures.plan === "team";
|
|
817
|
+
if (token && isPro) {
|
|
818
|
+
try {
|
|
819
|
+
const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
|
|
820
|
+
const { LAXY_API_URL } = await Promise.resolve().then(() => __importStar(require("./auth.js")));
|
|
821
|
+
const saveRes = await fetch(`${LAXY_API_URL}/api/v1/cli-results`, {
|
|
822
|
+
method: "POST",
|
|
823
|
+
headers: {
|
|
824
|
+
"Content-Type": "application/json",
|
|
825
|
+
Authorization: `Bearer ${token}`,
|
|
826
|
+
},
|
|
827
|
+
body: JSON.stringify({
|
|
828
|
+
repo_id: repoId,
|
|
829
|
+
project_name: path.basename(path.resolve(args.projectDir)),
|
|
830
|
+
grade: unifiedGrade,
|
|
831
|
+
verdict: verificationReport.verdict,
|
|
832
|
+
scores: {
|
|
833
|
+
performance: scores?.performance,
|
|
834
|
+
accessibility: scores?.accessibility,
|
|
835
|
+
seo: scores?.seo,
|
|
836
|
+
best_practices: scores?.bestPractices,
|
|
837
|
+
e2e_passed: e2eResult?.passed,
|
|
838
|
+
e2e_total: e2eResult?.total,
|
|
839
|
+
security_critical: securityAuditResult?.critical,
|
|
840
|
+
console_error_count: e2eConsoleErrors.length,
|
|
841
|
+
broken_link_count: brokenLinksResult?.brokenLinks.length,
|
|
842
|
+
},
|
|
843
|
+
full_result: { framework: detected.framework },
|
|
844
|
+
}),
|
|
845
|
+
});
|
|
846
|
+
if (!saveRes.ok) {
|
|
847
|
+
const errBody = await saveRes.json().catch(() => ({}));
|
|
848
|
+
console.warn(` [warn] Result save failed (${saveRes.status}): ${errBody.error ?? "unknown error"}`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
catch {
|
|
852
|
+
// 네트워크 오류는 검증 결과에 영향 없음
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (args.format === "json") {
|
|
856
|
+
console.log(JSON.stringify(resultObj, null, 2));
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
consoleOutput(resultObj);
|
|
860
|
+
}
|
|
861
|
+
if (inGitHubActions && process.env.GITHUB_OUTPUT) {
|
|
862
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `grade=${resultObj.grade}\n`);
|
|
863
|
+
}
|
|
864
|
+
exitGracefully(exitCode);
|
|
865
|
+
}
|
|
866
|
+
run().catch((err) => {
|
|
867
|
+
console.error(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
|
|
868
|
+
exitGracefully(1);
|
|
869
|
+
});
|