laxy-verify 1.2.2 → 1.3.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 +204 -47
- package/dist/a11y-deep.d.ts +20 -0
- package/dist/a11y-deep.js +161 -0
- package/dist/audit/broken-links.d.ts +25 -21
- package/dist/audit/broken-links.js +97 -86
- package/dist/badge.d.ts +2 -1
- package/dist/badge.js +18 -14
- package/dist/bundle-size.d.ts +14 -0
- package/dist/bundle-size.js +209 -0
- package/dist/cli.js +1256 -865
- package/dist/config.d.ts +102 -65
- package/dist/config.js +360 -255
- package/dist/entitlement.d.ts +15 -11
- package/dist/entitlement.js +98 -90
- package/dist/init.js +153 -87
- package/dist/lighthouse.d.ts +37 -7
- package/dist/lighthouse.js +231 -158
- package/dist/outdated-check.d.ts +17 -0
- package/dist/outdated-check.js +123 -0
- package/dist/report-markdown.d.ts +53 -39
- package/dist/report-markdown.js +407 -386
- package/dist/secret-scan.d.ts +15 -0
- package/dist/secret-scan.js +218 -0
- package/dist/security-audit.d.ts +17 -9
- package/dist/security-audit.js +127 -64
- package/dist/seo-deep.d.ts +24 -0
- package/dist/seo-deep.js +147 -0
- package/dist/typecheck.d.ts +8 -0
- package/dist/typecheck.js +99 -0
- package/dist/verification-core/report.js +526 -409
- package/dist/verification-core/types.d.ts +164 -108
- package/dist/visual-diff.d.ts +33 -26
- package/dist/visual-diff.js +223 -178
- package/dist/vitals-budget.d.ts +23 -0
- package/dist/vitals-budget.js +168 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,311 +1,389 @@
|
|
|
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
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
console.log(`
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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 typecheck_js_1 = require("./typecheck.js");
|
|
62
|
+
const secret_scan_js_1 = require("./secret-scan.js");
|
|
63
|
+
const bundle_size_js_1 = require("./bundle-size.js");
|
|
64
|
+
const outdated_check_js_1 = require("./outdated-check.js");
|
|
65
|
+
const a11y_deep_js_1 = require("./a11y-deep.js");
|
|
66
|
+
const seo_deep_js_1 = require("./seo-deep.js");
|
|
67
|
+
const vitals_budget_js_1 = require("./vitals-budget.js");
|
|
68
|
+
const index_js_1 = require("./verification-core/index.js");
|
|
69
|
+
const trend_js_1 = require("./trend.js");
|
|
70
|
+
const ai_analysis_js_1 = require("./ai-analysis.js");
|
|
71
|
+
const compare_env_js_1 = require("./compare-env.js");
|
|
72
|
+
const route_discovery_js_1 = require("./route-discovery.js");
|
|
73
|
+
const package_json_1 = __importDefault(require("../package.json"));
|
|
74
|
+
let activeDevServerPid;
|
|
75
|
+
let activeDevServerCleanup = null;
|
|
76
|
+
async function cleanupActiveDevServer() {
|
|
77
|
+
if (activeDevServerPid === undefined)
|
|
78
|
+
return;
|
|
79
|
+
if (activeDevServerCleanup) {
|
|
80
|
+
await activeDevServerCleanup;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const pid = activeDevServerPid;
|
|
84
|
+
activeDevServerPid = undefined;
|
|
85
|
+
activeDevServerCleanup = (0, serve_js_1.stopDevServer)(pid).finally(() => {
|
|
86
|
+
activeDevServerCleanup = null;
|
|
87
|
+
});
|
|
88
|
+
await activeDevServerCleanup;
|
|
89
|
+
}
|
|
90
|
+
function installSignalCleanupHandlers() {
|
|
91
|
+
const handleSignal = (signal) => {
|
|
92
|
+
void cleanupActiveDevServer().finally(() => {
|
|
93
|
+
const exitCode = signal === "SIGINT" ? 130 : 143;
|
|
94
|
+
exitGracefully(exitCode);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
process.once("SIGINT", () => handleSignal("SIGINT"));
|
|
98
|
+
process.once("SIGTERM", () => handleSignal("SIGTERM"));
|
|
99
|
+
}
|
|
100
|
+
function shouldFailVerificationResult(report, failOn) {
|
|
101
|
+
if (failOn === "unverified")
|
|
102
|
+
return false;
|
|
103
|
+
if (report.verdict === "build-failed" || report.verdict === "hold")
|
|
104
|
+
return true;
|
|
105
|
+
if (report.verdict === "investigate")
|
|
106
|
+
return true;
|
|
107
|
+
return (0, grade_js_1.isWorseOrEqual)(report.grade, failOn);
|
|
108
|
+
}
|
|
109
|
+
function exitGracefully(code) {
|
|
110
|
+
if (process.platform === "win32") {
|
|
111
|
+
setTimeout(() => process.exit(code), 100);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
process.exit(code);
|
|
115
|
+
}
|
|
116
|
+
async function ensurePortAvailableForVerification(port) {
|
|
117
|
+
const status = await (0, serve_js_1.probeServerStatus)(port);
|
|
118
|
+
if (status === null)
|
|
119
|
+
return;
|
|
120
|
+
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.`);
|
|
121
|
+
}
|
|
122
|
+
function parseArgs() {
|
|
123
|
+
const raw = process.argv.slice(2);
|
|
124
|
+
let projectDir = ".";
|
|
125
|
+
const flags = {};
|
|
126
|
+
for (let i = 0; i < raw.length; i++) {
|
|
127
|
+
const arg = raw[i];
|
|
128
|
+
if (arg.startsWith("--")) {
|
|
129
|
+
const eqIndex = arg.indexOf("=");
|
|
130
|
+
if (eqIndex >= 0) {
|
|
131
|
+
const key = arg.slice(2, eqIndex);
|
|
132
|
+
flags[key] = arg.slice(eqIndex + 1);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const key = arg.slice(2);
|
|
136
|
+
if (i + 1 < raw.length && !raw[i + 1].startsWith("-")) {
|
|
137
|
+
flags[key] = raw[++i];
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
flags[key] = "true";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (projectDir === ".") {
|
|
145
|
+
projectDir = arg;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
let subcommand;
|
|
149
|
+
let subcommandArg;
|
|
150
|
+
if (projectDir === "login" || projectDir === "logout" || projectDir === "whoami") {
|
|
151
|
+
subcommand = projectDir;
|
|
152
|
+
projectDir = ".";
|
|
153
|
+
subcommandArg = flags.email;
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
projectDir: path.resolve(projectDir),
|
|
157
|
+
subcommand,
|
|
158
|
+
subcommandArg,
|
|
159
|
+
format: flags.format ?? "console",
|
|
160
|
+
ciMode: flags.ci !== undefined || process.env.CI === "true",
|
|
161
|
+
configPath: flags.config,
|
|
162
|
+
failOn: flags["fail-on"] ?? undefined,
|
|
163
|
+
skipLighthouse: flags["skip-lighthouse"] !== undefined,
|
|
164
|
+
badge: flags.badge !== undefined,
|
|
165
|
+
init: flags.init !== undefined,
|
|
166
|
+
initRun: flags.init !== undefined && flags.run !== undefined,
|
|
167
|
+
multiViewport: flags["multi-viewport"] !== undefined,
|
|
168
|
+
failureAnalysis: flags["failure-analysis"] !== undefined,
|
|
169
|
+
crawl: flags.crawl !== undefined,
|
|
170
|
+
planOverride: flags["plan-override"],
|
|
171
|
+
port: flags.port !== undefined ? Number(flags.port) : undefined,
|
|
172
|
+
share: flags.share !== undefined,
|
|
173
|
+
help: flags.help !== undefined || flags.h !== undefined,
|
|
174
|
+
compareUrl: flags.compare,
|
|
175
|
+
typecheck: flags.typecheck !== undefined,
|
|
176
|
+
secretScan: flags["secret-scan"] !== undefined,
|
|
177
|
+
bundleSize: flags["bundle-size"] !== undefined,
|
|
178
|
+
outdatedCheck: flags["outdated-check"] !== undefined,
|
|
179
|
+
a11yDeep: flags["a11y-deep"] !== undefined,
|
|
180
|
+
seoDeep: flags["seo-deep"] !== undefined,
|
|
181
|
+
vitalsBudget: flags["vitals-budget"] !== undefined,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function writeResultFile(projectDir, result) {
|
|
185
|
+
const filePath = path.join(projectDir, ".laxy-result.json");
|
|
186
|
+
fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
|
|
187
|
+
}
|
|
188
|
+
function uniqueRouteList(routes) {
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
const result = [];
|
|
191
|
+
for (const route of routes) {
|
|
192
|
+
if (!route)
|
|
193
|
+
continue;
|
|
194
|
+
const normalized = route.startsWith("/") ? route : `/${route}`;
|
|
195
|
+
const trimmed = normalized.split("?")[0]?.split("#")[0]?.replace(/\/+/g, "/") ?? "/";
|
|
196
|
+
const finalRoute = trimmed.endsWith("/") && trimmed !== "/" ? trimmed.slice(0, -1) : trimmed;
|
|
197
|
+
if (finalRoute === "/" || finalRoute.startsWith("/api/") || finalRoute.startsWith("/_next/"))
|
|
198
|
+
continue;
|
|
199
|
+
if (seen.has(finalRoute))
|
|
200
|
+
continue;
|
|
201
|
+
seen.add(finalRoute);
|
|
202
|
+
result.push(finalRoute);
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
function summarizeViewportIssues(scores, thresholds) {
|
|
207
|
+
if (!scores)
|
|
208
|
+
return { count: 0 };
|
|
209
|
+
const failed = [];
|
|
210
|
+
for (const [label, viewportScores] of Object.entries(scores)) {
|
|
211
|
+
if (!viewportScores) {
|
|
212
|
+
failed.push(`${label}: missing`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const passes = viewportScores.performance >= thresholds.performance &&
|
|
216
|
+
viewportScores.accessibility >= thresholds.accessibility &&
|
|
217
|
+
viewportScores.seo >= thresholds.seo &&
|
|
218
|
+
viewportScores.bestPractices >= thresholds.bestPractices;
|
|
219
|
+
if (!passes) {
|
|
220
|
+
failed.push(`${label}: P${viewportScores.performance} A${viewportScores.accessibility} SEO${viewportScores.seo} BP${viewportScores.bestPractices}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
count: failed.length,
|
|
225
|
+
summary: failed.length > 0 ? failed.join(" | ") : "All checked viewports passed.",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function consoleOutput(result) {
|
|
229
|
+
const gradeLabel = result.grade;
|
|
230
|
+
const checkEmoji = result.grade !== "Unverified" ? " OK" : "";
|
|
231
|
+
console.log(`\n Laxy Verify: blocker check ${gradeLabel}${checkEmoji}`);
|
|
232
|
+
console.log(` Build: ${result.build.success ? `OK (${result.build.durationMs}ms)` : "FAILED"}`);
|
|
233
|
+
if (result.build.errors.length > 0) {
|
|
234
|
+
console.log(" Errors:");
|
|
235
|
+
const firstError = result.build.errors.find((line) => /error/i.test(line));
|
|
236
|
+
const last5 = result.build.errors.slice(-5);
|
|
237
|
+
const toShow = firstError && !last5.includes(firstError)
|
|
238
|
+
? [firstError, " ...", ...last5]
|
|
239
|
+
: last5;
|
|
240
|
+
for (const line of toShow)
|
|
241
|
+
console.error(` ${line}`);
|
|
242
|
+
}
|
|
243
|
+
if (result.lighthouse !== null) {
|
|
244
|
+
const lh = result.lighthouse;
|
|
245
|
+
const t = result.thresholds;
|
|
246
|
+
const check = (passed) => (passed ? " OK" : " FAIL");
|
|
247
|
+
const routeLabel = result.lighthouseRoutes && result.lighthouseRoutes.length > 1
|
|
248
|
+
? ` (weighted avg of ${result.lighthouseRoutes.length} routes)`
|
|
249
|
+
: "";
|
|
250
|
+
console.log(` Lighthouse${routeLabel}:`);
|
|
251
|
+
console.log(` Performance: ${lh.performance} / ${t.performance}${check(lh.performance >= t.performance)}`);
|
|
252
|
+
console.log(` Accessibility: ${lh.accessibility} / ${t.accessibility}${check(lh.accessibility >= t.accessibility)}`);
|
|
253
|
+
console.log(` SEO: ${lh.seo} / ${t.seo}${check(lh.seo >= t.seo)}`);
|
|
254
|
+
console.log(` Best Practices: ${lh.bestPractices} / ${t.bestPractices}${check(lh.bestPractices >= t.bestPractices)}`);
|
|
255
|
+
console.log(` Runs: ${lh.runs}`);
|
|
256
|
+
if (result.lighthouseRoutes && result.lighthouseRoutes.length > 1) {
|
|
257
|
+
console.log(" Per-route:");
|
|
258
|
+
for (const r of result.lighthouseRoutes) {
|
|
259
|
+
if (r.scores) {
|
|
260
|
+
const pass = r.scores.performance >= t.performance &&
|
|
261
|
+
r.scores.accessibility >= t.accessibility &&
|
|
262
|
+
r.scores.seo >= t.seo &&
|
|
263
|
+
r.scores.bestPractices >= t.bestPractices;
|
|
264
|
+
console.log(` ${r.route.padEnd(20)} P=${r.scores.performance} A=${r.scores.accessibility} S=${r.scores.seo} BP=${r.scores.bestPractices}${pass ? "" : " FAIL"}`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
console.log(` ${r.route.padEnd(20)} (no data)`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
console.log(" Lighthouse: skipped");
|
|
274
|
+
}
|
|
275
|
+
if (result.e2e) {
|
|
276
|
+
console.log(` E2E: ${result.e2e.passed}/${result.e2e.total} passed`);
|
|
277
|
+
}
|
|
278
|
+
if (result.crossBrowser && result.crossBrowser.length > 0) {
|
|
279
|
+
console.log(" Cross-browser:");
|
|
280
|
+
for (const cbr of result.crossBrowser) {
|
|
281
|
+
const status = cbr.failed === 0 ? "OK" : "FAIL";
|
|
282
|
+
console.log(` ${cbr.browser}: ${cbr.passed}/${cbr.results.length} passed ${status}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (result.visualDiff) {
|
|
286
|
+
console.log(` Visual diff: ${(0, visual_diff_js_1.formatVisualDiffSummary)(result.visualDiff)}`);
|
|
287
|
+
}
|
|
288
|
+
if (result.security) {
|
|
289
|
+
console.log(` Security: ${result.security.summary}`);
|
|
290
|
+
}
|
|
291
|
+
if (result.mobileLighthouse) {
|
|
292
|
+
const ml = result.mobileLighthouse;
|
|
293
|
+
console.log(` Mobile LH: P=${ml.performance} A=${ml.accessibility} SEO=${ml.seo} BP=${ml.bestPractices}`);
|
|
294
|
+
}
|
|
295
|
+
if (result.typecheck && !result.typecheck.skipped) {
|
|
296
|
+
console.log(` TypeScript: ${result.typecheck.passed ? "OK" : `${result.typecheck.errorCount} error(s)`}`);
|
|
297
|
+
}
|
|
298
|
+
if (result.secretScan && !result.secretScan.skipped) {
|
|
299
|
+
console.log(` Secret scan: ${result.secretScan.passed ? "OK" : `${result.secretScan.findings.length} finding(s)`} (${result.secretScan.filesScanned} files)`);
|
|
300
|
+
}
|
|
301
|
+
if (result.bundleSize && !result.bundleSize.skipped) {
|
|
302
|
+
console.log(` Bundle size: ${result.bundleSize.advisory}`);
|
|
303
|
+
}
|
|
304
|
+
if (result.outdatedCheck && !result.outdatedCheck.skipped) {
|
|
305
|
+
console.log(` Outdated: ${result.outdatedCheck.advisory}`);
|
|
306
|
+
}
|
|
307
|
+
if (result.a11yDeep && !result.a11yDeep.skipped) {
|
|
308
|
+
console.log(` A11y deep: ${result.a11yDeep.summary}`);
|
|
309
|
+
}
|
|
310
|
+
if (result.seoDeep && !result.seoDeep.skipped) {
|
|
311
|
+
console.log(` SEO deep: ${result.seoDeep.summary}`);
|
|
312
|
+
}
|
|
313
|
+
if (result.vitalsBudget && !result.vitalsBudget.skipped) {
|
|
314
|
+
console.log(` Vitals budget: ${result.vitalsBudget.summary}`);
|
|
315
|
+
}
|
|
316
|
+
if (result.verification) {
|
|
317
|
+
const view = result.verification.view;
|
|
318
|
+
const verboseFailure = result._verbose_failure ?? true;
|
|
319
|
+
const failureAnalysis = result._failure_analysis ?? true;
|
|
320
|
+
console.log(` Verification depth: ${view.tier}`);
|
|
321
|
+
console.log(` Decision question: ${view.question}`);
|
|
322
|
+
console.log(` Decision: ${view.verdict} (${view.confidence})`);
|
|
323
|
+
console.log(` Why it stopped here: ${view.summary}`);
|
|
324
|
+
// Passed/Failed checks summary
|
|
325
|
+
if (view.passes.length > 0) {
|
|
326
|
+
const passedChecks = view.passes.filter((p) => p.passed).map((p) => p.label);
|
|
327
|
+
const failedChecks = view.passes.filter((p) => !p.passed).map((p) => p.label);
|
|
328
|
+
if (passedChecks.length > 0)
|
|
329
|
+
console.log(` Passed: ${passedChecks.join(", ")}`);
|
|
330
|
+
if (failedChecks.length > 0)
|
|
331
|
+
console.log(` Failed: ${failedChecks.join(", ")}`);
|
|
332
|
+
}
|
|
333
|
+
// Blockers: ?�목?� 모든 ?�어, Fix ?�션?� verbose_failure(Pro+) ?�상?�서 ?�시
|
|
334
|
+
if (view.blockers.length > 0) {
|
|
335
|
+
console.log(" Deployment blockers:");
|
|
336
|
+
for (const blocker of view.blockers) {
|
|
337
|
+
console.log(` - ${blocker.title}`);
|
|
338
|
+
if (verboseFailure)
|
|
339
|
+
console.log(` Fix: ${blocker.action}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Warnings: always show all
|
|
343
|
+
if (view.warnings.length > 0) {
|
|
344
|
+
console.log(" Risks to review:");
|
|
345
|
+
for (const warning of view.warnings) {
|
|
346
|
+
console.log(` - ${warning.title}`);
|
|
347
|
+
if (failureAnalysis)
|
|
348
|
+
console.log(` Review: ${warning.action}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (view.nextActions.length > 0) {
|
|
352
|
+
console.log(" Next actions:");
|
|
353
|
+
for (const action of view.nextActions)
|
|
354
|
+
console.log(` - ${action}`);
|
|
355
|
+
}
|
|
356
|
+
// Evidence: failure_analysis(Pro+)???�체, verbose_failure(Pro)??3�? Free??2�?
|
|
357
|
+
const evidenceLimit = failureAnalysis ? view.failureEvidence.length : verboseFailure ? 3 : 2;
|
|
358
|
+
const evidenceToShow = view.failureEvidence.slice(0, evidenceLimit);
|
|
359
|
+
if (evidenceToShow.length > 0) {
|
|
360
|
+
console.log(" Evidence collected:");
|
|
361
|
+
for (const item of evidenceToShow)
|
|
362
|
+
console.log(` - ${item}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (result.github) {
|
|
366
|
+
if (result.github.status === "comment_posted")
|
|
367
|
+
console.log(" PR comment: posted");
|
|
368
|
+
if (result.github.status === "status_set")
|
|
369
|
+
console.log(` Status check: ${result.github.grade}`);
|
|
370
|
+
}
|
|
371
|
+
if (result.compareEnv) {
|
|
372
|
+
(0, compare_env_js_1.printEnvComparison)(result.compareEnv);
|
|
373
|
+
}
|
|
374
|
+
console.log(" Result: .laxy-result.json");
|
|
375
|
+
if (result.markdownReportPath) {
|
|
376
|
+
console.log(` Report: ${path.basename(result.markdownReportPath)}`);
|
|
377
|
+
}
|
|
378
|
+
if (result.share?.url) {
|
|
379
|
+
console.log(` Share: ${result.share.url}`);
|
|
380
|
+
}
|
|
381
|
+
console.log(` Exit code: ${result.exitCode}`);
|
|
382
|
+
}
|
|
383
|
+
async function run() {
|
|
384
|
+
installSignalCleanupHandlers();
|
|
385
|
+
const args = parseArgs();
|
|
386
|
+
if (args.help) {
|
|
309
387
|
console.log(`
|
|
310
388
|
laxy-verify v${package_json_1.default.version}
|
|
311
389
|
Deployment blocker gate for frontend apps
|
|
@@ -329,10 +407,18 @@ async function run() {
|
|
|
329
407
|
--skip-lighthouse Skip Lighthouse but still run build and E2E
|
|
330
408
|
--port <port> Use an already-running dev server on this port (skip build & server start)
|
|
331
409
|
--share Create a public share link for this verification result (Pro)
|
|
410
|
+
--compare <url> Compare Lighthouse scores against a reference environment URL (Pro)
|
|
332
411
|
--plan-override free | pro | team (testing metadata only)
|
|
333
412
|
--multi-viewport Lighthouse on desktop/tablet/mobile
|
|
334
|
-
--crawl Crawl the app to discover routes before E2E
|
|
413
|
+
--crawl Crawl the app to discover routes before E2E (also enables multi-route Lighthouse)
|
|
335
414
|
--badge Print shields.io badge markdown
|
|
415
|
+
--typecheck Run TypeScript type check (tsc --noEmit)
|
|
416
|
+
--secret-scan Scan for hardcoded secrets and credentials
|
|
417
|
+
--bundle-size Analyze bundle size (Next.js/Vite)
|
|
418
|
+
--outdated-check Check for outdated dependencies
|
|
419
|
+
--a11y-deep Deep accessibility audit (axe-core)
|
|
420
|
+
--seo-deep Deep SEO audit (meta, OG, JSON-LD)
|
|
421
|
+
--vitals-budget Core Web Vitals budget check (LCP, CLS, INP)
|
|
336
422
|
--help Show this help
|
|
337
423
|
|
|
338
424
|
Exit codes:
|
|
@@ -347,561 +433,866 @@ async function run() {
|
|
|
347
433
|
npx laxy-verify . --fail-on silver # Block Bronze or worse
|
|
348
434
|
npx laxy-verify . --port 3001 # Use existing dev server on port 3001
|
|
349
435
|
npx laxy-verify . --share # Save and share a public verification link
|
|
436
|
+
npx laxy-verify . --compare https://staging.example.com # Compare vs reference env (Pro)
|
|
350
437
|
|
|
351
438
|
Docs: https://github.com/SUNgm24/Laxy/tree/main/laxy-verify
|
|
352
|
-
`);
|
|
353
|
-
exitGracefully(0);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
if (args.subcommand === "login") {
|
|
357
|
-
await (0, auth_js_1.login)(args.subcommandArg);
|
|
358
|
-
exitGracefully(0);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
if (args.subcommand === "logout") {
|
|
362
|
-
(0, auth_js_1.clearToken)();
|
|
363
|
-
exitGracefully(0);
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
if (args.subcommand === "whoami") {
|
|
367
|
-
(0, auth_js_1.whoami)();
|
|
368
|
-
exitGracefully(0);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
if (args.init) {
|
|
372
|
-
let initEntitlements = null;
|
|
373
|
-
try {
|
|
374
|
-
initEntitlements = await (0, entitlement_js_1.getEntitlements)();
|
|
375
|
-
}
|
|
376
|
-
catch {
|
|
377
|
-
// ignore fetch errors
|
|
378
|
-
}
|
|
379
|
-
const initPlan = initEntitlements?.plan ?? "free";
|
|
380
|
-
const hasProAccess = initPlan === "pro" || initPlan === "team";
|
|
381
|
-
if (!hasProAccess) {
|
|
382
|
-
console.log("\n GitHub Actions integration is a Pro feature.");
|
|
383
|
-
console.log(" Log in and upgrade to Pro to use --init.");
|
|
384
|
-
console.log(" → laxy-verify login");
|
|
385
|
-
console.log(" → https://laxy.dev/pricing\n");
|
|
386
|
-
exitGracefully(1);
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
(0, init_js_1.runInit)(args.projectDir);
|
|
390
|
-
if (!args.initRun) {
|
|
391
|
-
console.log("\n Next step: run npx laxy-verify . (or use --init --run to continue immediately)");
|
|
392
|
-
exitGracefully(0);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
console.log("\n Config created. Starting verification...\n");
|
|
396
|
-
}
|
|
397
|
-
if (args.badge) {
|
|
398
|
-
const resultPath = path.join(args.projectDir, ".laxy-result.json");
|
|
399
|
-
if (!fs.existsSync(resultPath)) {
|
|
400
|
-
console.error("Error: .laxy-result.json not found. Run `npx laxy-verify .` first to generate it.");
|
|
401
|
-
exitGracefully(2);
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
const content = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
405
|
-
// Pro: 실시간 배지 URL (verify_runs 기반)
|
|
406
|
-
let badgeEntitlements = null;
|
|
407
|
-
try {
|
|
408
|
-
badgeEntitlements = await (0, entitlement_js_1.getEntitlements)();
|
|
409
|
-
}
|
|
410
|
-
catch { /* ignore */ }
|
|
411
|
-
const badgePlan = badgeEntitlements?.plan ?? "free";
|
|
412
|
-
const badgeIsPro = badgePlan === "pro" || badgePlan === "team";
|
|
413
|
-
if (badgeIsPro) {
|
|
414
|
-
const { LAXY_API_URL } = await Promise.resolve().then(() => __importStar(require("./auth.js")));
|
|
415
|
-
const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
|
|
416
|
-
const dynamicUrl = `${LAXY_API_URL}/api/badge/${repoId}`;
|
|
417
|
-
console.log(``);
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
const badge = (0, badge_js_1.generateBadge)(content.grade);
|
|
421
|
-
console.log(badge);
|
|
422
|
-
}
|
|
423
|
-
exitGracefully(0);
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
// 팀 공통 임계값을 서버에서 로드 (토큰 없음/네트워크 오류 시 null)
|
|
427
|
-
const teamThresholds = await (0, config_js_1.fetchTeamThresholds)();
|
|
428
|
-
if (teamThresholds && args.format !== "json") {
|
|
429
|
-
console.log(" Team thresholds loaded from laxy.dev");
|
|
430
|
-
}
|
|
431
|
-
let config;
|
|
432
|
-
try {
|
|
433
|
-
config = (0, config_js_1.loadConfig)({
|
|
434
|
-
dir: args.projectDir,
|
|
435
|
-
configPath: args.configPath,
|
|
436
|
-
ciMode: args.ciMode,
|
|
437
|
-
cliFlags: {
|
|
438
|
-
failOn: args.failOn,
|
|
439
|
-
skipLighthouse: args.skipLighthouse,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
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
|
-
let
|
|
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
|
-
|
|
612
|
-
|
|
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
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
if (
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
439
|
+
`);
|
|
440
|
+
exitGracefully(0);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (args.subcommand === "login") {
|
|
444
|
+
await (0, auth_js_1.login)(args.subcommandArg);
|
|
445
|
+
exitGracefully(0);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (args.subcommand === "logout") {
|
|
449
|
+
(0, auth_js_1.clearToken)();
|
|
450
|
+
exitGracefully(0);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (args.subcommand === "whoami") {
|
|
454
|
+
(0, auth_js_1.whoami)();
|
|
455
|
+
exitGracefully(0);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (args.init) {
|
|
459
|
+
let initEntitlements = null;
|
|
460
|
+
try {
|
|
461
|
+
initEntitlements = await (0, entitlement_js_1.getEntitlements)();
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// ignore fetch errors
|
|
465
|
+
}
|
|
466
|
+
const initPlan = initEntitlements?.plan ?? "free";
|
|
467
|
+
const hasProAccess = initPlan === "pro" || initPlan === "team";
|
|
468
|
+
if (!hasProAccess) {
|
|
469
|
+
console.log("\n GitHub Actions integration is a Pro feature.");
|
|
470
|
+
console.log(" Log in and upgrade to Pro to use --init.");
|
|
471
|
+
console.log(" → laxy-verify login");
|
|
472
|
+
console.log(" → https://laxy.dev/pricing\n");
|
|
473
|
+
exitGracefully(1);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
(0, init_js_1.runInit)(args.projectDir);
|
|
477
|
+
if (!args.initRun) {
|
|
478
|
+
console.log("\n Next step: run npx laxy-verify . (or use --init --run to continue immediately)");
|
|
479
|
+
exitGracefully(0);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
console.log("\n Config created. Starting verification...\n");
|
|
483
|
+
}
|
|
484
|
+
if (args.badge) {
|
|
485
|
+
const resultPath = path.join(args.projectDir, ".laxy-result.json");
|
|
486
|
+
if (!fs.existsSync(resultPath)) {
|
|
487
|
+
console.error("Error: .laxy-result.json not found. Run `npx laxy-verify .` first to generate it.");
|
|
488
|
+
exitGracefully(2);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const content = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
492
|
+
// Pro: 실시간 배지 URL (verify_runs 기반)
|
|
493
|
+
let badgeEntitlements = null;
|
|
494
|
+
try {
|
|
495
|
+
badgeEntitlements = await (0, entitlement_js_1.getEntitlements)();
|
|
496
|
+
}
|
|
497
|
+
catch { /* ignore */ }
|
|
498
|
+
const badgePlan = badgeEntitlements?.plan ?? "free";
|
|
499
|
+
const badgeIsPro = badgePlan === "pro" || badgePlan === "team";
|
|
500
|
+
if (badgeIsPro) {
|
|
501
|
+
const { LAXY_API_URL } = await Promise.resolve().then(() => __importStar(require("./auth.js")));
|
|
502
|
+
const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
|
|
503
|
+
const dynamicUrl = `${LAXY_API_URL}/api/badge/${repoId}`;
|
|
504
|
+
console.log(``);
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
const badge = (0, badge_js_1.generateBadge)(content.grade);
|
|
508
|
+
console.log(badge);
|
|
509
|
+
}
|
|
510
|
+
exitGracefully(0);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// 팀 공통 임계값을 서버에서 로드 (토큰 없음/네트워크 오류 시 null)
|
|
514
|
+
const teamThresholds = await (0, config_js_1.fetchTeamThresholds)();
|
|
515
|
+
if (teamThresholds && args.format !== "json") {
|
|
516
|
+
console.log(" Team thresholds loaded from laxy.dev");
|
|
517
|
+
}
|
|
518
|
+
let config;
|
|
519
|
+
try {
|
|
520
|
+
config = (0, config_js_1.loadConfig)({
|
|
521
|
+
dir: args.projectDir,
|
|
522
|
+
configPath: args.configPath,
|
|
523
|
+
ciMode: args.ciMode,
|
|
524
|
+
cliFlags: {
|
|
525
|
+
failOn: args.failOn,
|
|
526
|
+
skipLighthouse: args.skipLighthouse,
|
|
527
|
+
typecheck: args.typecheck,
|
|
528
|
+
secretScan: args.secretScan,
|
|
529
|
+
bundleSize: args.bundleSize,
|
|
530
|
+
outdatedCheck: args.outdatedCheck,
|
|
531
|
+
a11yDeep: args.a11yDeep,
|
|
532
|
+
seoDeep: args.seoDeep,
|
|
533
|
+
vitalsBudget: args.vitalsBudget,
|
|
534
|
+
},
|
|
535
|
+
teamThresholds,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
console.error(`Config error: ${err instanceof Error ? err.message : String(err)}`);
|
|
540
|
+
exitGracefully(2);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (args.compareUrl) {
|
|
544
|
+
try {
|
|
545
|
+
const u = new URL(args.compareUrl);
|
|
546
|
+
if (!["http:", "https:"].includes(u.protocol)) {
|
|
547
|
+
console.error(`Config error: --compare URL must use http or https protocol: ${args.compareUrl}`);
|
|
548
|
+
exitGracefully(2);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
console.error(`Config error: --compare value is not a valid URL: ${args.compareUrl}`);
|
|
554
|
+
exitGracefully(2);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
let detected;
|
|
559
|
+
try {
|
|
560
|
+
detected = (0, detect_js_1.detect)(args.projectDir);
|
|
561
|
+
}
|
|
562
|
+
catch (err) {
|
|
563
|
+
console.error(`Detection error: ${err instanceof Error ? err.message : String(err)}`);
|
|
564
|
+
exitGracefully(2);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const buildCmd = config.build_command || detected.buildCmd;
|
|
568
|
+
const devCmd = config.dev_command || detected.devCmd;
|
|
569
|
+
const port = args.port ?? config.port;
|
|
570
|
+
const useExistingServer = args.port !== undefined;
|
|
571
|
+
if (!useExistingServer) {
|
|
572
|
+
try {
|
|
573
|
+
await ensurePortAvailableForVerification(port);
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
console.error(`Preflight error: ${err instanceof Error ? err.message : String(err)}`);
|
|
577
|
+
exitGracefully(2);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
// Verify the existing server is actually reachable
|
|
583
|
+
const status = await (0, serve_js_1.probeServerStatus)(port);
|
|
584
|
+
if (status === null) {
|
|
585
|
+
console.error(`Preflight error: No server responding on port ${port}. Make sure the dev server is running before using --port.`);
|
|
586
|
+
exitGracefully(2);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (args.format !== "json") {
|
|
590
|
+
console.log(`Using existing dev server on port ${port} (HTTP ${status})`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
let buildResult;
|
|
594
|
+
if (useExistingServer) {
|
|
595
|
+
// Skip build when using an external server
|
|
596
|
+
buildResult = { success: true, durationMs: 0, errors: [] };
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
try {
|
|
600
|
+
buildResult = await (0, build_js_1.runBuild)(buildCmd, config.build_timeout, args.projectDir);
|
|
601
|
+
}
|
|
602
|
+
catch (err) {
|
|
603
|
+
buildResult = {
|
|
604
|
+
success: false,
|
|
605
|
+
durationMs: 0,
|
|
606
|
+
errors: err instanceof Error ? [err.message] : [String(err)],
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
let scores;
|
|
611
|
+
let lighthouseResult = null;
|
|
612
|
+
let lighthouseErrorCount = 0;
|
|
613
|
+
const adjustedThresholds = {
|
|
614
|
+
performance: config.ciMode ? config.thresholds.performance - 10 : config.thresholds.performance,
|
|
615
|
+
accessibility: config.thresholds.accessibility,
|
|
616
|
+
seo: config.thresholds.seo,
|
|
617
|
+
bestPractices: config.thresholds.bestPractices,
|
|
618
|
+
};
|
|
619
|
+
let entitlements = null;
|
|
620
|
+
try {
|
|
621
|
+
entitlements = await (0, entitlement_js_1.getEntitlements)();
|
|
622
|
+
(0, entitlement_js_1.printPlanBanner)(entitlements);
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
// Ignore entitlement errors and keep the free feature set.
|
|
626
|
+
}
|
|
627
|
+
const features = entitlements ?? {
|
|
628
|
+
plan: "free",
|
|
629
|
+
github_actions: false,
|
|
630
|
+
queue_priority: false,
|
|
631
|
+
parallel_execution: false,
|
|
632
|
+
ai_failure_analysis: false,
|
|
633
|
+
compare_env: false,
|
|
634
|
+
share_result: false,
|
|
635
|
+
history_trend: false,
|
|
636
|
+
};
|
|
637
|
+
let effectiveFeatures = features;
|
|
638
|
+
if (args.planOverride) {
|
|
639
|
+
try {
|
|
640
|
+
effectiveFeatures = (0, entitlement_js_1.applyPlanOverride)(features, args.planOverride);
|
|
641
|
+
console.log(` Plan override: ${(0, entitlement_js_1.normalizePlan)(features.plan)} -> ${effectiveFeatures.plan} (verification behavior is unchanged)`);
|
|
642
|
+
}
|
|
643
|
+
catch (overrideErr) {
|
|
644
|
+
console.error(`Plan override error: ${overrideErr instanceof Error ? overrideErr.message : String(overrideErr)}`);
|
|
645
|
+
exitGracefully(2);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// Always run 3 Lighthouse passes for median score
|
|
650
|
+
if (config.lighthouse_runs < 3) {
|
|
651
|
+
config = { ...config, lighthouse_runs: 3 };
|
|
652
|
+
}
|
|
653
|
+
let multiRouteLhResult = null;
|
|
654
|
+
let multiViewportScores = null;
|
|
655
|
+
let allViewportsOk = false;
|
|
656
|
+
let e2eResult;
|
|
657
|
+
let crossBrowserResults;
|
|
658
|
+
let e2eCoverageGaps = [];
|
|
659
|
+
let e2eConsoleErrors = [];
|
|
660
|
+
let lastCrawlRoutes = [];
|
|
661
|
+
let runtimeDiscoveredRoutes = [];
|
|
662
|
+
let e2eStabilityPassed = true;
|
|
663
|
+
let visualDiffResult = null;
|
|
664
|
+
let securityAuditResult = null;
|
|
665
|
+
let mobileLighthouseScores = null;
|
|
666
|
+
let brokenLinksResult = null;
|
|
667
|
+
let compareEnvResult = null;
|
|
668
|
+
let typecheckResult = null;
|
|
669
|
+
let secretScanResult = null;
|
|
670
|
+
let bundleSizeResult = null;
|
|
671
|
+
let outdatedCheckResult = null;
|
|
672
|
+
let a11yDeepResult = null;
|
|
673
|
+
let seoDeepResult = null;
|
|
674
|
+
let vitalsBudgetResult = null;
|
|
675
|
+
const failureEvidence = [];
|
|
676
|
+
if (buildResult.success) {
|
|
677
|
+
let servePid;
|
|
678
|
+
try {
|
|
679
|
+
if (!useExistingServer) {
|
|
680
|
+
const serve = await (0, serve_js_1.startDevServer)(devCmd, port, config.dev_timeout, args.projectDir);
|
|
681
|
+
servePid = serve.pid;
|
|
682
|
+
activeDevServerPid = serve.pid;
|
|
683
|
+
}
|
|
684
|
+
const verifyUrl = `http://localhost:${port}/`;
|
|
685
|
+
const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
|
|
686
|
+
try {
|
|
687
|
+
const runtimeRouteDiscovery = await (0, route_discovery_js_1.discoverRuntimeRoutes)(verifyUrl);
|
|
688
|
+
runtimeDiscoveredRoutes = runtimeRouteDiscovery.routes;
|
|
689
|
+
if (runtimeDiscoveredRoutes.length > 0) {
|
|
690
|
+
console.log(` Runtime route discovery: found ${runtimeDiscoveredRoutes.length} route(s) from app bundles.`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
catch (routeErr) {
|
|
694
|
+
console.error(`Runtime route discovery warning: ${routeErr instanceof Error ? routeErr.message : String(routeErr)}`);
|
|
695
|
+
}
|
|
696
|
+
if (!args.skipLighthouse) {
|
|
697
|
+
try {
|
|
698
|
+
const explicitRoutes = config.lighthouse_routes?.length
|
|
699
|
+
? config.lighthouse_routes
|
|
700
|
+
: config.extra_routes?.length
|
|
701
|
+
? config.extra_routes
|
|
702
|
+
: undefined;
|
|
703
|
+
if (explicitRoutes && explicitRoutes.length > 0) {
|
|
704
|
+
// Multi-route mode: run on all explicitly configured routes
|
|
705
|
+
multiRouteLhResult = await (0, lighthouse_js_1.runLighthouseOnRoutes)(port, config.lighthouse_runs, explicitRoutes);
|
|
706
|
+
lighthouseErrorCount = multiRouteLhResult.perRoute.reduce((n, r) => n + r.errors.length, 0);
|
|
707
|
+
scores = multiRouteLhResult.aggregated ?? undefined;
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
// Single-route mode (default): root only
|
|
711
|
+
const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
|
|
712
|
+
lighthouseErrorCount = lhResult.errors.length;
|
|
713
|
+
scores = lhResult.scores ?? undefined;
|
|
714
|
+
}
|
|
715
|
+
if (scores) {
|
|
716
|
+
lighthouseResult = {
|
|
717
|
+
performance: scores.performance,
|
|
718
|
+
accessibility: scores.accessibility,
|
|
719
|
+
seo: scores.seo,
|
|
720
|
+
bestPractices: scores.bestPractices,
|
|
721
|
+
runs: config.lighthouse_runs,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
catch (lhErr) {
|
|
726
|
+
console.error(`Lighthouse error: ${lhErr instanceof Error ? lhErr.message : String(lhErr)}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (!args.skipLighthouse) {
|
|
730
|
+
try {
|
|
731
|
+
multiViewportScores = await (0, multi_viewport_js_1.runMultiViewportLighthouse)(port);
|
|
732
|
+
(0, multi_viewport_js_1.printMultiViewportResults)(multiViewportScores, adjustedThresholds);
|
|
733
|
+
allViewportsOk = (0, multi_viewport_js_1.allViewportsPass)(multiViewportScores, adjustedThresholds);
|
|
734
|
+
if (multiViewportScores.screenshotDiffs) {
|
|
735
|
+
for (const diff of multiViewportScores.screenshotDiffs) {
|
|
736
|
+
if (!diff.baselineCreated && diff.diffPercent > 10) {
|
|
737
|
+
failureEvidence.push(`Viewport screenshot: ${diff.viewport} diff ${diff.diffPercent}% exceeds 10% threshold`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
catch (mvErr) {
|
|
743
|
+
console.error(`Multi-viewport error: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// Security audit (npm audit)
|
|
747
|
+
try {
|
|
748
|
+
securityAuditResult = await (0, security_audit_js_1.runSecurityAudit)(args.projectDir, verifyUrl);
|
|
749
|
+
if (securityAuditResult.critical > 0 ||
|
|
750
|
+
securityAuditResult.high > 0 ||
|
|
751
|
+
securityAuditResult.missingHeaders.length > 0) {
|
|
752
|
+
failureEvidence.push(`Security: ${securityAuditResult.summary}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
catch (secErr) {
|
|
756
|
+
console.error(`Security audit error: ${secErr instanceof Error ? secErr.message : String(secErr)}`);
|
|
757
|
+
}
|
|
758
|
+
const crawlEnabled = args.crawl || config.crawl;
|
|
759
|
+
const crawlOpts = crawlEnabled
|
|
760
|
+
? { enabled: true, maxDepth: config.max_crawl_depth, maxPages: config.max_crawl_pages }
|
|
761
|
+
: undefined;
|
|
762
|
+
let lastE2EScenarios;
|
|
763
|
+
try {
|
|
764
|
+
const e2eRuns = await (0, e2e_js_1.runVerifyE2E)(verifyUrl, verificationTier, config.scenarios, crawlOpts);
|
|
765
|
+
lastE2EScenarios = e2eRuns.scenarios;
|
|
766
|
+
e2eResult = {
|
|
767
|
+
passed: e2eRuns.passed,
|
|
768
|
+
failed: e2eRuns.failed,
|
|
769
|
+
total: e2eRuns.results.length,
|
|
770
|
+
results: e2eRuns.results,
|
|
771
|
+
};
|
|
772
|
+
e2eCoverageGaps = e2eRuns.coverageGaps;
|
|
773
|
+
e2eConsoleErrors = e2eRuns.consoleErrors;
|
|
774
|
+
// Merge console errors captured during crawl phase
|
|
775
|
+
if (e2eRuns.crawlResult) {
|
|
776
|
+
const crawlConsoleErrors = e2eRuns.crawlResult.pages.flatMap((p) => p.consoleErrors);
|
|
777
|
+
if (crawlConsoleErrors.length > 0) {
|
|
778
|
+
const unique = crawlConsoleErrors.filter((e) => !e2eConsoleErrors.includes(e));
|
|
779
|
+
e2eConsoleErrors = [...e2eConsoleErrors, ...unique];
|
|
780
|
+
}
|
|
781
|
+
// Store crawl-discovered routes for post-E2E Lighthouse pass
|
|
782
|
+
lastCrawlRoutes = e2eRuns.crawlResult.pages
|
|
783
|
+
.map((p) => p.path)
|
|
784
|
+
.filter((p) => typeof p === "string" && p.startsWith("/"));
|
|
785
|
+
}
|
|
786
|
+
// Broken links audit — runs after E2E so we have the crawl result
|
|
787
|
+
const auditRoutes = uniqueRouteList([
|
|
788
|
+
...(config.extra_routes ?? []),
|
|
789
|
+
...runtimeDiscoveredRoutes,
|
|
790
|
+
...lastCrawlRoutes,
|
|
791
|
+
]);
|
|
792
|
+
if (e2eRuns.crawlResult || auditRoutes.length > 0) {
|
|
793
|
+
try {
|
|
794
|
+
brokenLinksResult = await (0, broken_links_js_1.auditBrokenLinks)(e2eRuns.crawlResult, verifyUrl, {
|
|
795
|
+
extraRoutes: auditRoutes,
|
|
796
|
+
});
|
|
797
|
+
if (brokenLinksResult.hasBrokenLinks) {
|
|
798
|
+
failureEvidence.push(`Broken links: ${brokenLinksResult.summary}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
catch (blErr) {
|
|
802
|
+
console.error(`Broken links audit error: ${blErr instanceof Error ? blErr.message : String(blErr)}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
// E2E stability: run a second time if first run passed all
|
|
806
|
+
if (e2eRuns.passed === e2eRuns.results.length && e2eRuns.results.length > 0) {
|
|
807
|
+
console.log(" Running stability pass (run 2/2)...");
|
|
808
|
+
const e2eRuns2 = await (0, e2e_js_1.runVerifyE2E)(verifyUrl, verificationTier, config.scenarios, crawlOpts);
|
|
809
|
+
if (e2eRuns2.passed < e2eRuns2.results.length) {
|
|
810
|
+
e2eStabilityPassed = false;
|
|
811
|
+
e2eCoverageGaps.push("Stability check failed on second run");
|
|
812
|
+
const failedNames = e2eRuns2.results
|
|
813
|
+
.filter((r) => !r.passed)
|
|
814
|
+
.map((r) => r.name)
|
|
815
|
+
.join(", ");
|
|
816
|
+
failureEvidence.push(`E2E stability: second run failed (${failedNames})`);
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
console.log(" Stability pass: OK (2/2 runs passed)");
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (e2eRuns.coverageGaps.length > 0) {
|
|
823
|
+
console.error(`E2E coverage warning: ${e2eRuns.coverageGaps.join(" ")}`);
|
|
824
|
+
}
|
|
825
|
+
if (e2eRuns.consoleErrors.length > 0) {
|
|
826
|
+
console.error(`E2E console errors: ${e2eRuns.consoleErrors.length} detected`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
catch (e2eErr) {
|
|
830
|
+
console.error(`E2E error: ${e2eErr instanceof Error ? e2eErr.message : String(e2eErr)}`);
|
|
831
|
+
}
|
|
832
|
+
// Crawl-based multi-route Lighthouse (only if no explicit lighthouse_routes configured)
|
|
833
|
+
if (!args.skipLighthouse &&
|
|
834
|
+
!config.lighthouse_routes?.length &&
|
|
835
|
+
uniqueRouteList([
|
|
836
|
+
...(config.extra_routes ?? []),
|
|
837
|
+
...runtimeDiscoveredRoutes,
|
|
838
|
+
...lastCrawlRoutes,
|
|
839
|
+
]).length > 1) {
|
|
840
|
+
try {
|
|
841
|
+
const crawlRoutes = uniqueRouteList([
|
|
842
|
+
...(config.extra_routes ?? []),
|
|
843
|
+
...runtimeDiscoveredRoutes,
|
|
844
|
+
...lastCrawlRoutes,
|
|
845
|
+
]).slice(0, config.max_lighthouse_routes);
|
|
846
|
+
console.log(`\n Running Lighthouse on ${crawlRoutes.length} discovered routes...`);
|
|
847
|
+
multiRouteLhResult = await (0, lighthouse_js_1.runLighthouseOnRoutes)(port, config.lighthouse_runs, crawlRoutes);
|
|
848
|
+
if (multiRouteLhResult.aggregated) {
|
|
849
|
+
scores = multiRouteLhResult.aggregated;
|
|
850
|
+
lighthouseResult = {
|
|
851
|
+
...multiRouteLhResult.aggregated,
|
|
852
|
+
runs: config.lighthouse_runs,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
catch (crawlLhErr) {
|
|
857
|
+
console.error(`Crawl-based Lighthouse error: ${crawlLhErr instanceof Error ? crawlLhErr.message : String(crawlLhErr)}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// Cross-browser E2E via Playwright (if non-chromium browsers configured)
|
|
861
|
+
const extraBrowsers = (config.browsers || []).filter((b) => b !== "chromium" && ["firefox", "webkit"].includes(b));
|
|
862
|
+
if (extraBrowsers.length > 0 && lastE2EScenarios && lastE2EScenarios.length > 0) {
|
|
863
|
+
const pwAvailable = await (0, playwright_runner_js_1.isPlaywrightAvailable)();
|
|
864
|
+
if (pwAvailable) {
|
|
865
|
+
try {
|
|
866
|
+
crossBrowserResults = await (0, playwright_runner_js_1.runPlaywrightE2E)(verifyUrl, lastE2EScenarios, extraBrowsers);
|
|
867
|
+
for (const cbr of crossBrowserResults) {
|
|
868
|
+
console.log(` Cross-browser ${cbr.browser}: ${cbr.passed}/${cbr.results.length} passed`);
|
|
869
|
+
if (cbr.failed > 0) {
|
|
870
|
+
const failedNames = cbr.results.filter(r => !r.passed).map(r => r.name).join(", ");
|
|
871
|
+
failureEvidence.push(`Cross-browser ${cbr.browser}: ${failedNames} failed`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
catch (cbErr) {
|
|
876
|
+
console.error(`Cross-browser error: ${cbErr instanceof Error ? cbErr.message : String(cbErr)}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
console.log(" Note: Cross-browser testing requires playwright. Run: npm install -D playwright && npx playwright install");
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
try {
|
|
884
|
+
visualDiffResult = await (0, visual_diff_js_1.runVisualDiff)(args.projectDir, verifyUrl, "verify", {
|
|
885
|
+
pixelmatchThreshold: config.visual_diff.pixelmatchThreshold,
|
|
886
|
+
warnThreshold: config.visual_diff.warnThreshold,
|
|
887
|
+
rollbackThreshold: config.visual_diff.rollbackThreshold,
|
|
888
|
+
ignoreSelectors: config.visual_diff.ignoreSelectors,
|
|
889
|
+
disableAnimations: config.visual_diff.disableAnimations,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
catch (visualErr) {
|
|
893
|
+
console.error(`Visual diff error: ${visualErr instanceof Error ? visualErr.message : String(visualErr)}`);
|
|
894
|
+
}
|
|
895
|
+
// Mobile Lighthouse (standalone — not part of multi-viewport)
|
|
896
|
+
if (!args.skipLighthouse && !args.multiViewport) {
|
|
897
|
+
try {
|
|
898
|
+
mobileLighthouseScores = await (0, multi_viewport_js_1.runMobileLighthouse)(port);
|
|
899
|
+
}
|
|
900
|
+
catch (mlErr) {
|
|
901
|
+
console.error(`Mobile Lighthouse error: ${mlErr instanceof Error ? mlErr.message : String(mlErr)}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
// TypeScript type check
|
|
905
|
+
if (config.typecheck) {
|
|
906
|
+
try {
|
|
907
|
+
typecheckResult = await (0, typecheck_js_1.runTypecheck)(args.projectDir);
|
|
908
|
+
if (!typecheckResult.skipped && !typecheckResult.passed) {
|
|
909
|
+
failureEvidence.push(`TypeScript: ${typecheckResult.errorCount} type error(s)`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
catch (tcErr) {
|
|
913
|
+
console.error(`Typecheck error: ${tcErr instanceof Error ? tcErr.message : String(tcErr)}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// Secret scan
|
|
917
|
+
if (config.secret_scan) {
|
|
918
|
+
try {
|
|
919
|
+
secretScanResult = await (0, secret_scan_js_1.runSecretScan)(args.projectDir, {
|
|
920
|
+
ignorePaths: config.secret_scan_ignore_paths,
|
|
921
|
+
});
|
|
922
|
+
if (!secretScanResult.skipped && !secretScanResult.passed) {
|
|
923
|
+
failureEvidence.push(`Secret scan: ${secretScanResult.findings.length} finding(s)`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
catch (ssErr) {
|
|
927
|
+
console.error(`Secret scan error: ${ssErr instanceof Error ? ssErr.message : String(ssErr)}`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
// Bundle size analysis (advisory)
|
|
931
|
+
if (config.bundle_size) {
|
|
932
|
+
try {
|
|
933
|
+
bundleSizeResult = await (0, bundle_size_js_1.runBundleSize)(args.projectDir);
|
|
934
|
+
}
|
|
935
|
+
catch (bsErr) {
|
|
936
|
+
console.error(`Bundle size error: ${bsErr instanceof Error ? bsErr.message : String(bsErr)}`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
// Outdated dependency check (advisory)
|
|
940
|
+
if (config.outdated_check) {
|
|
941
|
+
try {
|
|
942
|
+
outdatedCheckResult = await (0, outdated_check_js_1.runOutdatedCheck)(args.projectDir);
|
|
943
|
+
}
|
|
944
|
+
catch (ocErr) {
|
|
945
|
+
console.error(`Outdated check error: ${ocErr instanceof Error ? ocErr.message : String(ocErr)}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
// Deep accessibility audit (axe-core)
|
|
949
|
+
if (config.a11y_deep) {
|
|
950
|
+
try {
|
|
951
|
+
a11yDeepResult = await (0, a11y_deep_js_1.runA11yDeep)(verifyUrl);
|
|
952
|
+
if (!a11yDeepResult.skipped && !a11yDeepResult.passed) {
|
|
953
|
+
failureEvidence.push(`A11y deep: ${a11yDeepResult.summary}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
catch (a11yErr) {
|
|
957
|
+
console.error(`A11y deep error: ${a11yErr instanceof Error ? a11yErr.message : String(a11yErr)}`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
// Deep SEO audit
|
|
961
|
+
if (config.seo_deep) {
|
|
962
|
+
try {
|
|
963
|
+
seoDeepResult = await (0, seo_deep_js_1.runSeoDeep)(verifyUrl);
|
|
964
|
+
if (!seoDeepResult.skipped && !seoDeepResult.passed) {
|
|
965
|
+
failureEvidence.push(`SEO deep: ${seoDeepResult.summary}`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
catch (seoErr) {
|
|
969
|
+
console.error(`SEO deep error: ${seoErr instanceof Error ? seoErr.message : String(seoErr)}`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// Core Web Vitals budget check
|
|
973
|
+
if (config.vitals_budget) {
|
|
974
|
+
try {
|
|
975
|
+
vitalsBudgetResult = await (0, vitals_budget_js_1.runVitalsBudget)(verifyUrl);
|
|
976
|
+
if (!vitalsBudgetResult.skipped && !vitalsBudgetResult.passed) {
|
|
977
|
+
failureEvidence.push(`Vitals budget: ${vitalsBudgetResult.summary}`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
catch (vbErr) {
|
|
981
|
+
console.error(`Vitals budget error: ${vbErr instanceof Error ? vbErr.message : String(vbErr)}`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
// Pro: multi-environment comparison
|
|
985
|
+
if (effectiveFeatures.compare_env && args.compareUrl) {
|
|
986
|
+
try {
|
|
987
|
+
compareEnvResult = await (0, compare_env_js_1.runEnvComparison)(port, args.compareUrl, 1);
|
|
988
|
+
}
|
|
989
|
+
catch (cmpErr) {
|
|
990
|
+
console.error(`Env comparison error: ${cmpErr instanceof Error ? cmpErr.message : String(cmpErr)}`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
else if (args.compareUrl && !effectiveFeatures.compare_env) {
|
|
994
|
+
console.log(" [warn] --compare requires a logged-in Pro account. Skipping env comparison.");
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
catch (serveErr) {
|
|
998
|
+
console.error(`Dev server error: ${serveErr instanceof Error ? serveErr.message : String(serveErr)}`);
|
|
999
|
+
}
|
|
1000
|
+
finally {
|
|
1001
|
+
if (servePid) {
|
|
1002
|
+
await cleanupActiveDevServer();
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
|
|
1007
|
+
const viewportSummary = summarizeViewportIssues(multiViewportScores, adjustedThresholds);
|
|
1008
|
+
// Multi-route Lighthouse: add per-route failures to evidence
|
|
1009
|
+
const allRoutesPass = multiRouteLhResult
|
|
1010
|
+
? multiRouteLhResult.allRoutesPass(adjustedThresholds)
|
|
1011
|
+
: true;
|
|
1012
|
+
if (multiRouteLhResult && !allRoutesPass) {
|
|
1013
|
+
const failingRoutes = multiRouteLhResult.perRoute.filter((r) => r.scores !== null &&
|
|
1014
|
+
(r.scores.performance < adjustedThresholds.performance ||
|
|
1015
|
+
r.scores.accessibility < adjustedThresholds.accessibility ||
|
|
1016
|
+
r.scores.seo < adjustedThresholds.seo ||
|
|
1017
|
+
r.scores.bestPractices < adjustedThresholds.bestPractices));
|
|
1018
|
+
for (const fr of failingRoutes.slice(0, 3)) {
|
|
1019
|
+
const s = fr.scores;
|
|
1020
|
+
failureEvidence.push(`Lighthouse ${fr.route}: P=${s.performance} A=${s.accessibility} SEO=${s.seo} BP=${s.bestPractices}`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
failureEvidence.push(...buildResult.errors.slice(0, 3).map((error) => `Build: ${error}`), ...(lighthouseErrorCount > 0 ? [`Lighthouse: ${lighthouseErrorCount} run error(s) were recorded during collection.`] : []), ...(e2eResult
|
|
1024
|
+
? e2eResult.results
|
|
1025
|
+
.filter((scenario) => !scenario.passed)
|
|
1026
|
+
.slice(0, 2)
|
|
1027
|
+
.map((scenario) => `E2E: ${scenario.name}${scenario.error ? ` - ${scenario.error}` : ""}`)
|
|
1028
|
+
: []), ...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
|
|
1029
|
+
? [
|
|
1030
|
+
`Visual diff: ${(0, visual_diff_js_1.formatVisualDiffSummary)(visualDiffResult)}`,
|
|
1031
|
+
]
|
|
1032
|
+
: []));
|
|
1033
|
+
const verificationReport = (0, index_js_1.buildVerificationReport)({
|
|
1034
|
+
buildSuccess: buildResult.success,
|
|
1035
|
+
buildErrors: buildResult.errors,
|
|
1036
|
+
e2ePassed: e2eResult?.passed,
|
|
1037
|
+
e2eTotal: e2eResult?.total,
|
|
1038
|
+
e2eCoverageGaps,
|
|
1039
|
+
e2eConsoleErrorCount: e2eConsoleErrors.length,
|
|
1040
|
+
e2eStabilityPassed,
|
|
1041
|
+
lighthouseSkipped: args.skipLighthouse,
|
|
1042
|
+
lighthouseErrorCount,
|
|
1043
|
+
viewportIssues: multiViewportScores ? viewportSummary.count : undefined,
|
|
1044
|
+
multiViewportPassed: multiViewportScores ? allViewportsOk : undefined,
|
|
1045
|
+
multiViewportSummary: multiViewportScores ? viewportSummary.summary : undefined,
|
|
1046
|
+
visualDiffVerdict: visualDiffResult?.verdict,
|
|
1047
|
+
visualDiffPercentage: visualDiffResult?.diffPercentage,
|
|
1048
|
+
hasVisualBaseline: visualDiffResult?.hasBaseline,
|
|
1049
|
+
lighthouseScores: scores,
|
|
1050
|
+
mobileLighthouseScores: mobileLighthouseScores ?? undefined,
|
|
1051
|
+
securityAudit: securityAuditResult
|
|
1052
|
+
? {
|
|
1053
|
+
totalVulnerabilities: securityAuditResult.totalVulnerabilities,
|
|
1054
|
+
critical: securityAuditResult.critical,
|
|
1055
|
+
high: securityAuditResult.high,
|
|
1056
|
+
summary: securityAuditResult.summary,
|
|
1057
|
+
}
|
|
1058
|
+
: undefined,
|
|
1059
|
+
brokenLinksAudit: brokenLinksResult
|
|
1060
|
+
? {
|
|
1061
|
+
checkedCount: brokenLinksResult.checkedCount,
|
|
1062
|
+
brokenCount: brokenLinksResult.brokenLinks.length,
|
|
1063
|
+
summary: brokenLinksResult.summary,
|
|
1064
|
+
}
|
|
1065
|
+
: undefined,
|
|
1066
|
+
typecheck: typecheckResult
|
|
1067
|
+
? { passed: typecheckResult.passed, errorCount: typecheckResult.errorCount, skipped: typecheckResult.skipped }
|
|
1068
|
+
: undefined,
|
|
1069
|
+
secretScan: secretScanResult
|
|
1070
|
+
? { passed: secretScanResult.passed, findingsCount: secretScanResult.findings.length, filesScanned: secretScanResult.filesScanned, skipped: secretScanResult.skipped }
|
|
1071
|
+
: undefined,
|
|
1072
|
+
a11yDeep: a11yDeepResult
|
|
1073
|
+
? { passed: a11yDeepResult.passed, criticalCount: a11yDeepResult.criticalCount, seriousCount: a11yDeepResult.seriousCount, summary: a11yDeepResult.summary, skipped: a11yDeepResult.skipped }
|
|
1074
|
+
: undefined,
|
|
1075
|
+
seoDeep: seoDeepResult
|
|
1076
|
+
? { passed: seoDeepResult.passed, errorCount: seoDeepResult.errorCount, warningCount: seoDeepResult.warningCount, summary: seoDeepResult.summary, skipped: seoDeepResult.skipped }
|
|
1077
|
+
: undefined,
|
|
1078
|
+
vitalsBudget: vitalsBudgetResult
|
|
1079
|
+
? { passed: vitalsBudgetResult.passed, lcp: vitalsBudgetResult.lcp, cls: vitalsBudgetResult.cls, inp: vitalsBudgetResult.inp, summary: vitalsBudgetResult.summary, skipped: vitalsBudgetResult.skipped }
|
|
1080
|
+
: undefined,
|
|
1081
|
+
bundleSize: bundleSizeResult
|
|
1082
|
+
? { framework: bundleSizeResult.framework, firstLoadJsKb: bundleSizeResult.firstLoadJsKb, largestChunkKb: bundleSizeResult.largestChunkKb, advisory: bundleSizeResult.advisory, skipped: bundleSizeResult.skipped }
|
|
1083
|
+
: undefined,
|
|
1084
|
+
outdatedCheck: outdatedCheckResult
|
|
1085
|
+
? { outdatedCount: outdatedCheckResult.outdatedCount, majorOutdated: outdatedCheckResult.majorOutdated, advisory: outdatedCheckResult.advisory, skipped: outdatedCheckResult.skipped }
|
|
1086
|
+
: undefined,
|
|
1087
|
+
failureEvidence,
|
|
1088
|
+
}, {
|
|
1089
|
+
tier: verificationTier,
|
|
1090
|
+
thresholds: adjustedThresholds,
|
|
1091
|
+
});
|
|
1092
|
+
// Gold eligibility: every route must individually pass all thresholds.
|
|
1093
|
+
// If the weighted-average aggregate earned Gold but some routes individually failed,
|
|
1094
|
+
// downgrade to Silver to enforce the per-route requirement.
|
|
1095
|
+
if (multiRouteLhResult && !allRoutesPass && verificationReport.grade === "gold") {
|
|
1096
|
+
verificationReport.grade = "silver";
|
|
1097
|
+
}
|
|
1098
|
+
const verificationView = (0, index_js_1.getTierVerificationView)(verificationReport);
|
|
1099
|
+
const unifiedGrade = verificationReport.grade;
|
|
1100
|
+
const exitCode = shouldFailVerificationResult(verificationReport, config.fail_on) ? 1 : 0;
|
|
1101
|
+
const resultObj = {
|
|
1102
|
+
grade: unifiedGrade.charAt(0).toUpperCase() + unifiedGrade.slice(1),
|
|
1103
|
+
timestamp: new Date().toISOString(),
|
|
1104
|
+
build: {
|
|
1105
|
+
success: buildResult.success,
|
|
1106
|
+
durationMs: buildResult.durationMs,
|
|
1107
|
+
errors: buildResult.errors,
|
|
1108
|
+
},
|
|
1109
|
+
e2e: e2eResult,
|
|
1110
|
+
crossBrowser: crossBrowserResults,
|
|
1111
|
+
lighthouse: lighthouseResult,
|
|
1112
|
+
lighthouseRoutes: multiRouteLhResult
|
|
1113
|
+
? multiRouteLhResult.perRoute.map((r) => ({ route: r.route, scores: r.scores }))
|
|
1114
|
+
: undefined,
|
|
1115
|
+
mobileLighthouse: mobileLighthouseScores,
|
|
1116
|
+
security: securityAuditResult,
|
|
1117
|
+
visualDiff: visualDiffResult,
|
|
1118
|
+
typecheck: typecheckResult,
|
|
1119
|
+
secretScan: secretScanResult,
|
|
1120
|
+
bundleSize: bundleSizeResult,
|
|
1121
|
+
outdatedCheck: outdatedCheckResult,
|
|
1122
|
+
a11yDeep: a11yDeepResult,
|
|
1123
|
+
seoDeep: seoDeepResult,
|
|
1124
|
+
vitalsBudget: vitalsBudgetResult,
|
|
1125
|
+
thresholds: adjustedThresholds,
|
|
1126
|
+
ciMode: config.ciMode,
|
|
1127
|
+
framework: detected.framework,
|
|
1128
|
+
exitCode,
|
|
1129
|
+
config_fail_on: config.fail_on,
|
|
1130
|
+
_plan: effectiveFeatures.plan,
|
|
1131
|
+
_verbose_failure: true,
|
|
1132
|
+
_failure_analysis: true,
|
|
1133
|
+
verification: {
|
|
1134
|
+
tier: verificationTier,
|
|
1135
|
+
report: verificationReport,
|
|
1136
|
+
view: verificationView,
|
|
1137
|
+
},
|
|
1138
|
+
compareEnv: compareEnvResult ?? undefined,
|
|
1139
|
+
};
|
|
1140
|
+
const markdownReportPath = (0, report_markdown_js_1.getMarkdownReportPath)(args.projectDir);
|
|
1141
|
+
if ((0, report_markdown_js_1.shouldWriteMarkdownReport)(resultObj)) {
|
|
1142
|
+
const markdownReport = (0, report_markdown_js_1.buildMarkdownReport)(args.projectDir, resultObj);
|
|
1143
|
+
fs.writeFileSync(markdownReportPath, markdownReport, "utf-8");
|
|
1144
|
+
resultObj.markdownReportPath = markdownReportPath;
|
|
1145
|
+
}
|
|
1146
|
+
else if (fs.existsSync(markdownReportPath)) {
|
|
1147
|
+
fs.rmSync(markdownReportPath, { force: true });
|
|
1148
|
+
}
|
|
1149
|
+
const inGitHubActions = !!process.env.GITHUB_ACTIONS;
|
|
1150
|
+
if (inGitHubActions) {
|
|
1151
|
+
try {
|
|
1152
|
+
if (process.env.GITHUB_EVENT_NAME === "pull_request") {
|
|
1153
|
+
let trendDelta = null;
|
|
1154
|
+
const baseSnapshot = (0, trend_js_1.loadBaseSnapshot)((0, trend_js_1.getBaseResultPath)(args.projectDir));
|
|
1155
|
+
if (baseSnapshot) {
|
|
1156
|
+
const currentSnapshot = {
|
|
1157
|
+
grade: resultObj.grade,
|
|
1158
|
+
lighthouse: resultObj.lighthouse,
|
|
1159
|
+
e2e: resultObj.e2e ? { passed: resultObj.e2e.passed, total: resultObj.e2e.total } : null,
|
|
1160
|
+
timestamp: resultObj.timestamp,
|
|
1161
|
+
};
|
|
1162
|
+
trendDelta = (0, trend_js_1.computeTrendDelta)(currentSnapshot, baseSnapshot);
|
|
1163
|
+
}
|
|
1164
|
+
await (0, comment_js_1.postPRComment)(resultObj, trendDelta);
|
|
1165
|
+
resultObj.github = { status: "comment_posted", grade: resultObj.grade };
|
|
1166
|
+
}
|
|
1167
|
+
await (0, status_js_1.createStatusCheck)({ grade: resultObj.grade, exitCode: resultObj.exitCode });
|
|
1168
|
+
resultObj.github ??= { status: "status_set", grade: resultObj.grade };
|
|
1169
|
+
}
|
|
1170
|
+
catch (ghErr) {
|
|
1171
|
+
console.error(`GitHub API warning: ${ghErr instanceof Error ? ghErr.message : String(ghErr)}`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
writeResultFile(args.projectDir, resultObj);
|
|
1175
|
+
// Pro 이상: 결과를 서버에 저장 (배지, 공유 링크용)
|
|
1176
|
+
const token = (0, auth_js_1.loadToken)();
|
|
1177
|
+
const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
|
|
1178
|
+
if (args.share && (!token || !effectiveFeatures.share_result)) {
|
|
1179
|
+
console.warn(" [warn] --share requires a logged-in Pro or Team account.");
|
|
1180
|
+
}
|
|
1181
|
+
if (token && effectiveFeatures.share_result) {
|
|
1182
|
+
try {
|
|
1183
|
+
const { LAXY_API_URL } = await Promise.resolve().then(() => __importStar(require("./auth.js")));
|
|
1184
|
+
const saveRes = await fetch(`${LAXY_API_URL}/api/v1/cli-results`, {
|
|
1185
|
+
method: "POST",
|
|
1186
|
+
headers: {
|
|
1187
|
+
"Content-Type": "application/json",
|
|
1188
|
+
Authorization: `Bearer ${token}`,
|
|
1189
|
+
},
|
|
1190
|
+
body: JSON.stringify({
|
|
1191
|
+
repo_id: repoId,
|
|
1192
|
+
project_name: path.basename(path.resolve(args.projectDir)),
|
|
1193
|
+
grade: unifiedGrade,
|
|
1194
|
+
verdict: verificationReport.verdict === "client-ready" || verificationReport.verdict === "release-ready"
|
|
1195
|
+
? "client-ready"
|
|
1196
|
+
: "hold",
|
|
1197
|
+
share: args.share,
|
|
1198
|
+
blockers: verificationView.blockers.slice(0, 5).map((b) => b.title),
|
|
1199
|
+
scores: {
|
|
1200
|
+
performance: scores?.performance,
|
|
1201
|
+
accessibility: scores?.accessibility,
|
|
1202
|
+
seo: scores?.seo,
|
|
1203
|
+
best_practices: scores?.bestPractices,
|
|
1204
|
+
e2e_passed: e2eResult?.passed,
|
|
1205
|
+
e2e_total: e2eResult?.total,
|
|
1206
|
+
security_critical: securityAuditResult?.critical,
|
|
1207
|
+
console_error_count: e2eConsoleErrors.length,
|
|
1208
|
+
broken_link_count: brokenLinksResult?.brokenLinks.length,
|
|
1209
|
+
},
|
|
1210
|
+
full_result: {
|
|
1211
|
+
framework: detected.framework,
|
|
1212
|
+
build: { success: buildResult.success },
|
|
1213
|
+
e2e: e2eResult ? { passed: e2eResult.passed, total: e2eResult.total } : null,
|
|
1214
|
+
lighthouse: lighthouseResult,
|
|
1215
|
+
verification: {
|
|
1216
|
+
report: verificationReport,
|
|
1217
|
+
},
|
|
1218
|
+
},
|
|
1219
|
+
}),
|
|
1220
|
+
});
|
|
1221
|
+
if (!saveRes.ok) {
|
|
1222
|
+
const errBody = await saveRes.json().catch(() => ({}));
|
|
1223
|
+
console.warn(` [warn] Result save failed (${saveRes.status}): ${errBody.error ?? "unknown error"}`);
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
const saveData = await saveRes.json().catch(() => ({}));
|
|
1227
|
+
if (saveData.share_id && saveData.share_url) {
|
|
1228
|
+
resultObj.share = {
|
|
1229
|
+
id: saveData.share_id,
|
|
1230
|
+
url: saveData.share_url,
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
catch {
|
|
1236
|
+
// 네트워크 오류는 검증 결과에 영향 없음
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
writeResultFile(args.projectDir, resultObj);
|
|
1240
|
+
// Pro: AI failure analysis — runs after result is finalized, before output
|
|
1241
|
+
if (effectiveFeatures.ai_failure_analysis &&
|
|
1242
|
+
verificationReport.verdict !== "client-ready" &&
|
|
1243
|
+
verificationReport.verdict !== "release-ready" &&
|
|
1244
|
+
args.format !== "json") {
|
|
1245
|
+
try {
|
|
1246
|
+
console.log("\n Requesting AI failure analysis...");
|
|
1247
|
+
const aiAnalysis = await (0, ai_analysis_js_1.requestAiFailureAnalysis)({
|
|
1248
|
+
grade: unifiedGrade,
|
|
1249
|
+
blockers: verificationView.blockers.map((b) => ({ title: b.title, action: b.action })),
|
|
1250
|
+
lighthouseScores: scores ?? null,
|
|
1251
|
+
thresholds: adjustedThresholds,
|
|
1252
|
+
buildErrors: buildResult.errors.slice(0, 3),
|
|
1253
|
+
e2eFailed: e2eResult ? e2eResult.failed : 0,
|
|
1254
|
+
e2eTotal: e2eResult ? e2eResult.total : 0,
|
|
1255
|
+
securitySummary: securityAuditResult?.summary,
|
|
1256
|
+
});
|
|
1257
|
+
if (aiAnalysis) {
|
|
1258
|
+
console.log("\n AI Analysis (Pro):");
|
|
1259
|
+
console.log(` Root cause: ${aiAnalysis.rootCause}`);
|
|
1260
|
+
if (aiAnalysis.topFixes.length > 0) {
|
|
1261
|
+
console.log(" Top fixes:");
|
|
1262
|
+
for (const fix of aiAnalysis.topFixes) {
|
|
1263
|
+
console.log(` - ${fix}`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
catch {
|
|
1269
|
+
// AI analysis errors are non-critical
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
if (args.format === "json") {
|
|
1273
|
+
console.log(JSON.stringify(resultObj, null, 2));
|
|
1274
|
+
}
|
|
1275
|
+
else {
|
|
1276
|
+
consoleOutput(resultObj);
|
|
1277
|
+
// Pro: 동적 배지 URL 안내
|
|
1278
|
+
if (effectiveFeatures.share_result) {
|
|
1279
|
+
const { LAXY_API_URL } = await Promise.resolve().then(() => __importStar(require("./auth.js")));
|
|
1280
|
+
const { generateDynamicBadgeMarkdown } = await Promise.resolve().then(() => __importStar(require("./badge.js")));
|
|
1281
|
+
console.log(`\n Badge (auto-updates with each run):`);
|
|
1282
|
+
console.log(` ${generateDynamicBadgeMarkdown(repoId, LAXY_API_URL)}`);
|
|
1283
|
+
}
|
|
1284
|
+
// Free: 히스토리 트렌드 넛지
|
|
1285
|
+
if (!effectiveFeatures.history_trend) {
|
|
1286
|
+
console.log(`\n Tip: Pro tracks your last 30 runs so you can see if Performance or Grade is improving.`);
|
|
1287
|
+
console.log(` https://laxy.app/pricing`);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
if (inGitHubActions && process.env.GITHUB_OUTPUT) {
|
|
1291
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `grade=${resultObj.grade}\n`);
|
|
1292
|
+
}
|
|
1293
|
+
exitGracefully(exitCode);
|
|
1294
|
+
}
|
|
1295
|
+
run().catch((err) => {
|
|
1296
|
+
console.error(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1297
|
+
exitGracefully(1);
|
|
1298
|
+
});
|