laxy-verify 1.2.3 → 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 +23 -0
- package/dist/audit/broken-links.d.ts +25 -25
- package/dist/audit/broken-links.js +97 -97
- package/dist/badge.d.ts +2 -1
- package/dist/badge.js +18 -14
- package/dist/cli.js +1246 -1233
- package/dist/config.d.ts +102 -102
- package/dist/config.js +360 -360
- package/dist/entitlement.d.ts +15 -13
- package/dist/entitlement.js +98 -94
- package/dist/init.js +132 -132
- package/dist/lighthouse.d.ts +37 -37
- package/dist/lighthouse.js +231 -231
- package/dist/report-markdown.d.ts +53 -53
- package/dist/report-markdown.js +407 -407
- package/dist/security-audit.d.ts +17 -17
- package/dist/security-audit.js +127 -127
- package/dist/verification-core/report.js +526 -526
- package/dist/verification-core/types.d.ts +164 -164
- package/dist/visual-diff.d.ts +33 -33
- package/dist/visual-diff.js +213 -213
- package/package.json +1 -1
- package/dist/ai-analysis.d.ts +0 -28
- package/dist/ai-analysis.js +0 -32
- package/dist/compare-env.d.ts +0 -23
- package/dist/compare-env.js +0 -55
- package/dist/init-analysis.d.ts +0 -6
- package/dist/init-analysis.js +0 -302
- package/dist/route-discovery.d.ts +0 -7
- package/dist/route-discovery.js +0 -108
package/dist/config.js
CHANGED
|
@@ -1,360 +1,360 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.ConfigParseError = void 0;
|
|
37
|
-
exports.fetchTeamThresholds = fetchTeamThresholds;
|
|
38
|
-
exports.loadConfig = loadConfig;
|
|
39
|
-
const fs = __importStar(require("node:fs"));
|
|
40
|
-
const path = __importStar(require("node:path"));
|
|
41
|
-
const yaml = __importStar(require("js-yaml"));
|
|
42
|
-
const auth_js_1 = require("./auth.js");
|
|
43
|
-
const DEFAULT_CONFIG = {
|
|
44
|
-
framework: "auto",
|
|
45
|
-
build_command: "",
|
|
46
|
-
dev_command: "",
|
|
47
|
-
package_manager: "auto",
|
|
48
|
-
port: 3000,
|
|
49
|
-
build_timeout: 300,
|
|
50
|
-
dev_timeout: 60,
|
|
51
|
-
lighthouse_runs: 3,
|
|
52
|
-
thresholds: {
|
|
53
|
-
performance: 70,
|
|
54
|
-
accessibility: 85,
|
|
55
|
-
seo: 80,
|
|
56
|
-
bestPractices: 80,
|
|
57
|
-
},
|
|
58
|
-
fail_on: "bronze",
|
|
59
|
-
crawl: false,
|
|
60
|
-
max_crawl_depth: 3,
|
|
61
|
-
max_crawl_pages: 10,
|
|
62
|
-
browsers: ["chromium"],
|
|
63
|
-
max_lighthouse_routes: 5,
|
|
64
|
-
visual_diff: {
|
|
65
|
-
pixelmatchThreshold: 0.1,
|
|
66
|
-
warnThreshold: 30,
|
|
67
|
-
rollbackThreshold: 60,
|
|
68
|
-
ignoreSelectors: [],
|
|
69
|
-
disableAnimations: true,
|
|
70
|
-
},
|
|
71
|
-
typecheck: false,
|
|
72
|
-
secret_scan: false,
|
|
73
|
-
secret_scan_ignore_paths: [],
|
|
74
|
-
bundle_size: false,
|
|
75
|
-
outdated_check: false,
|
|
76
|
-
a11y_deep: false,
|
|
77
|
-
seo_deep: false,
|
|
78
|
-
vitals_budget: false,
|
|
79
|
-
};
|
|
80
|
-
const VALID_FAIL_ON = ["unverified", "bronze", "silver", "gold"];
|
|
81
|
-
class ConfigParseError extends Error {
|
|
82
|
-
constructor(msg) {
|
|
83
|
-
super(msg);
|
|
84
|
-
this.name = "ConfigParseError";
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
exports.ConfigParseError = ConfigParseError;
|
|
88
|
-
function parseYaml(filePath) {
|
|
89
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
90
|
-
const raw = yaml.load(content);
|
|
91
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
92
|
-
throw new ConfigParseError("Invalid YAML structure in .laxy.yml");
|
|
93
|
-
}
|
|
94
|
-
const result = {};
|
|
95
|
-
if (typeof raw.framework === "string")
|
|
96
|
-
result.framework = raw.framework;
|
|
97
|
-
if (typeof raw.build_command === "string")
|
|
98
|
-
result.build_command = raw.build_command;
|
|
99
|
-
if (typeof raw.dev_command === "string")
|
|
100
|
-
result.dev_command = raw.dev_command;
|
|
101
|
-
if (typeof raw.package_manager === "string")
|
|
102
|
-
result.package_manager = raw.package_manager;
|
|
103
|
-
if (typeof raw.port === "number")
|
|
104
|
-
result.port = raw.port;
|
|
105
|
-
if (typeof raw.build_timeout === "number")
|
|
106
|
-
result.build_timeout = raw.build_timeout;
|
|
107
|
-
if (typeof raw.dev_timeout === "number")
|
|
108
|
-
result.dev_timeout = raw.dev_timeout;
|
|
109
|
-
if (typeof raw.lighthouse_runs === "number")
|
|
110
|
-
result.lighthouse_runs = raw.lighthouse_runs;
|
|
111
|
-
if (typeof raw.crawl === "boolean")
|
|
112
|
-
result.crawl = raw.crawl;
|
|
113
|
-
if (typeof raw.max_crawl_depth === "number")
|
|
114
|
-
result.max_crawl_depth = raw.max_crawl_depth;
|
|
115
|
-
if (typeof raw.max_crawl_pages === "number")
|
|
116
|
-
result.max_crawl_pages = raw.max_crawl_pages;
|
|
117
|
-
if (Array.isArray(raw.browsers)) {
|
|
118
|
-
const validBrowsers = ["chromium", "firefox", "webkit"];
|
|
119
|
-
const browsers = raw.browsers
|
|
120
|
-
.filter((b) => typeof b === "string" && validBrowsers.includes(b));
|
|
121
|
-
if (browsers.length > 0)
|
|
122
|
-
result.browsers = browsers;
|
|
123
|
-
}
|
|
124
|
-
if (Array.isArray(raw.lighthouse_routes)) {
|
|
125
|
-
const routes = raw.lighthouse_routes
|
|
126
|
-
.filter((r) => typeof r === "string" && r.startsWith("/"))
|
|
127
|
-
.slice(0, 20);
|
|
128
|
-
if (routes.length > 0)
|
|
129
|
-
result.lighthouse_routes = routes;
|
|
130
|
-
}
|
|
131
|
-
if (Array.isArray(raw.extra_routes)) {
|
|
132
|
-
const routes = raw.extra_routes
|
|
133
|
-
.filter((r) => typeof r === "string" && r.startsWith("/"))
|
|
134
|
-
.slice(0, 20);
|
|
135
|
-
if (routes.length > 0)
|
|
136
|
-
result.extra_routes = routes;
|
|
137
|
-
}
|
|
138
|
-
if (typeof raw.max_lighthouse_routes === "number") {
|
|
139
|
-
result.max_lighthouse_routes = Math.max(1, Math.min(20, raw.max_lighthouse_routes));
|
|
140
|
-
}
|
|
141
|
-
if (typeof raw.visual_diff === "object" &&
|
|
142
|
-
raw.visual_diff !== null &&
|
|
143
|
-
!Array.isArray(raw.visual_diff)) {
|
|
144
|
-
const vd = raw.visual_diff;
|
|
145
|
-
const visualDiff = {};
|
|
146
|
-
if (typeof vd.pixelmatch_threshold === "number") {
|
|
147
|
-
visualDiff.pixelmatchThreshold = Math.max(0, Math.min(1, vd.pixelmatch_threshold));
|
|
148
|
-
}
|
|
149
|
-
if (typeof vd.warn_threshold === "number") {
|
|
150
|
-
visualDiff.warnThreshold = Math.max(0, Math.min(100, vd.warn_threshold));
|
|
151
|
-
}
|
|
152
|
-
if (typeof vd.rollback_threshold === "number") {
|
|
153
|
-
visualDiff.rollbackThreshold = Math.max(0, Math.min(100, vd.rollback_threshold));
|
|
154
|
-
}
|
|
155
|
-
if (Array.isArray(vd.ignore_selectors)) {
|
|
156
|
-
visualDiff.ignoreSelectors = vd.ignore_selectors
|
|
157
|
-
.filter((selector) => typeof selector === "string" && selector.trim().length > 0)
|
|
158
|
-
.slice(0, 30);
|
|
159
|
-
}
|
|
160
|
-
if (typeof vd.disable_animations === "boolean") {
|
|
161
|
-
visualDiff.disableAnimations = vd.disable_animations;
|
|
162
|
-
}
|
|
163
|
-
result.visual_diff = visualDiff;
|
|
164
|
-
}
|
|
165
|
-
if (typeof raw.fail_on === "string") {
|
|
166
|
-
const f = raw.fail_on;
|
|
167
|
-
if (!VALID_FAIL_ON.includes(f)) {
|
|
168
|
-
throw new ConfigParseError(`Invalid fail_on value: "${f}". Must be one of: ${VALID_FAIL_ON.join(", ")}`);
|
|
169
|
-
}
|
|
170
|
-
result.fail_on = f;
|
|
171
|
-
}
|
|
172
|
-
if (typeof raw.thresholds === "object" &&
|
|
173
|
-
raw.thresholds !== null &&
|
|
174
|
-
!Array.isArray(raw.thresholds)) {
|
|
175
|
-
const t = raw.thresholds;
|
|
176
|
-
const thr = {};
|
|
177
|
-
if (typeof t.performance === "number")
|
|
178
|
-
thr.performance = t.performance;
|
|
179
|
-
if (typeof t.accessibility === "number")
|
|
180
|
-
thr.accessibility = t.accessibility;
|
|
181
|
-
if (typeof t.seo === "number")
|
|
182
|
-
thr.seo = t.seo;
|
|
183
|
-
if (typeof t.best_practices === "number")
|
|
184
|
-
thr.bestPractices = t.best_practices;
|
|
185
|
-
result.thresholds = thr;
|
|
186
|
-
}
|
|
187
|
-
if (Array.isArray(raw.scenarios)) {
|
|
188
|
-
const scenarios = [];
|
|
189
|
-
for (const item of raw.scenarios) {
|
|
190
|
-
if (typeof item === "object" &&
|
|
191
|
-
item !== null &&
|
|
192
|
-
typeof item.name === "string" &&
|
|
193
|
-
Array.isArray(item.steps)) {
|
|
194
|
-
const rawScenario = item;
|
|
195
|
-
const steps = [];
|
|
196
|
-
for (const rawStep of rawScenario.steps) {
|
|
197
|
-
if (typeof rawStep === "object" && rawStep !== null) {
|
|
198
|
-
const s = rawStep;
|
|
199
|
-
const step = {};
|
|
200
|
-
if (typeof s.goto === "string")
|
|
201
|
-
step.goto = s.goto;
|
|
202
|
-
if (typeof s.fill === "string")
|
|
203
|
-
step.fill = s.fill;
|
|
204
|
-
if (typeof s.with === "string")
|
|
205
|
-
step.with = s.with;
|
|
206
|
-
if (typeof s.click === "string")
|
|
207
|
-
step.click = s.click;
|
|
208
|
-
if (typeof s.expect_visible === "string")
|
|
209
|
-
step.expect_visible = s.expect_visible;
|
|
210
|
-
if (typeof s.expect_text === "string")
|
|
211
|
-
step.expect_text = s.expect_text;
|
|
212
|
-
if (typeof s.wait === "number")
|
|
213
|
-
step.wait = s.wait;
|
|
214
|
-
steps.push(step);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
scenarios.push({ name: String(rawScenario.name), steps });
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (scenarios.length > 0) {
|
|
221
|
-
result.scenarios = scenarios;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if (typeof raw.typecheck === "boolean")
|
|
225
|
-
result.typecheck = raw.typecheck;
|
|
226
|
-
if (typeof raw.secret_scan === "boolean")
|
|
227
|
-
result.secret_scan = raw.secret_scan;
|
|
228
|
-
if (Array.isArray(raw.secret_scan_ignore_paths)) {
|
|
229
|
-
result.secret_scan_ignore_paths = raw.secret_scan_ignore_paths
|
|
230
|
-
.filter((p) => typeof p === "string");
|
|
231
|
-
}
|
|
232
|
-
if (typeof raw.bundle_size === "boolean")
|
|
233
|
-
result.bundle_size = raw.bundle_size;
|
|
234
|
-
if (typeof raw.outdated_check === "boolean")
|
|
235
|
-
result.outdated_check = raw.outdated_check;
|
|
236
|
-
if (typeof raw.a11y_deep === "boolean")
|
|
237
|
-
result.a11y_deep = raw.a11y_deep;
|
|
238
|
-
if (typeof raw.seo_deep === "boolean")
|
|
239
|
-
result.seo_deep = raw.seo_deep;
|
|
240
|
-
if (typeof raw.vitals_budget === "boolean")
|
|
241
|
-
result.vitals_budget = raw.vitals_budget;
|
|
242
|
-
return result;
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* 로그인된 CLI 토큰으로 팀 공통 임계값을 서버에서 가져온다.
|
|
246
|
-
* 토큰 없음 / 팀 없음 / 네트워크 오류 시 null 반환 (graceful degradation).
|
|
247
|
-
*/
|
|
248
|
-
async function fetchTeamThresholds() {
|
|
249
|
-
const token = (0, auth_js_1.loadToken)();
|
|
250
|
-
if (!token)
|
|
251
|
-
return null;
|
|
252
|
-
try {
|
|
253
|
-
const res = await fetch(`${auth_js_1.LAXY_API_URL}/api/v1/team-thresholds`, {
|
|
254
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
255
|
-
signal: AbortSignal.timeout(5000),
|
|
256
|
-
});
|
|
257
|
-
if (!res.ok)
|
|
258
|
-
return null;
|
|
259
|
-
const data = (await res.json());
|
|
260
|
-
return data.thresholds ?? null;
|
|
261
|
-
}
|
|
262
|
-
catch {
|
|
263
|
-
return null;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
function loadConfig(options) {
|
|
267
|
-
const configPath = options.configPath ?? path.join(options.dir, ".laxy.yml");
|
|
268
|
-
let base = {};
|
|
269
|
-
const hasLocalConfig = fs.existsSync(configPath);
|
|
270
|
-
if (hasLocalConfig) {
|
|
271
|
-
base = parseYaml(configPath);
|
|
272
|
-
}
|
|
273
|
-
// 팀 임계값: 로컬 .laxy.yml에 thresholds가 없을 때만 적용
|
|
274
|
-
const team = options.teamThresholds ?? null;
|
|
275
|
-
const teamThresholdFallback = (!hasLocalConfig || !base.thresholds) && team
|
|
276
|
-
? {
|
|
277
|
-
performance: team.performance,
|
|
278
|
-
accessibility: team.accessibility,
|
|
279
|
-
seo: team.seo,
|
|
280
|
-
bestPractices: team.best_practices,
|
|
281
|
-
}
|
|
282
|
-
: {};
|
|
283
|
-
const teamFailOnFallback = (!hasLocalConfig || !base.fail_on) && team ? team.fail_on : undefined;
|
|
284
|
-
const config = {
|
|
285
|
-
...DEFAULT_CONFIG,
|
|
286
|
-
framework: base.framework ?? DEFAULT_CONFIG.framework,
|
|
287
|
-
build_command: base.build_command ?? DEFAULT_CONFIG.build_command,
|
|
288
|
-
dev_command: base.dev_command ?? DEFAULT_CONFIG.dev_command,
|
|
289
|
-
package_manager: base.package_manager ?? DEFAULT_CONFIG.package_manager,
|
|
290
|
-
port: base.port ?? DEFAULT_CONFIG.port,
|
|
291
|
-
build_timeout: base.build_timeout ?? DEFAULT_CONFIG.build_timeout,
|
|
292
|
-
dev_timeout: base.dev_timeout ?? DEFAULT_CONFIG.dev_timeout,
|
|
293
|
-
lighthouse_runs: base.lighthouse_runs ?? DEFAULT_CONFIG.lighthouse_runs,
|
|
294
|
-
fail_on: base.fail_on ?? teamFailOnFallback ?? DEFAULT_CONFIG.fail_on,
|
|
295
|
-
scenarios: base.scenarios,
|
|
296
|
-
crawl: base.crawl ?? DEFAULT_CONFIG.crawl,
|
|
297
|
-
max_crawl_depth: base.max_crawl_depth ?? DEFAULT_CONFIG.max_crawl_depth,
|
|
298
|
-
max_crawl_pages: base.max_crawl_pages ?? DEFAULT_CONFIG.max_crawl_pages,
|
|
299
|
-
browsers: base.browsers ?? DEFAULT_CONFIG.browsers,
|
|
300
|
-
lighthouse_routes: base.lighthouse_routes,
|
|
301
|
-
extra_routes: base.extra_routes,
|
|
302
|
-
max_lighthouse_routes: base.max_lighthouse_routes ?? DEFAULT_CONFIG.max_lighthouse_routes,
|
|
303
|
-
visual_diff: {
|
|
304
|
-
...DEFAULT_CONFIG.visual_diff,
|
|
305
|
-
...(base.visual_diff ?? {}),
|
|
306
|
-
},
|
|
307
|
-
typecheck: base.typecheck ?? DEFAULT_CONFIG.typecheck,
|
|
308
|
-
secret_scan: base.secret_scan ?? DEFAULT_CONFIG.secret_scan,
|
|
309
|
-
secret_scan_ignore_paths: base.secret_scan_ignore_paths ?? DEFAULT_CONFIG.secret_scan_ignore_paths,
|
|
310
|
-
bundle_size: base.bundle_size ?? DEFAULT_CONFIG.bundle_size,
|
|
311
|
-
outdated_check: base.outdated_check ?? DEFAULT_CONFIG.outdated_check,
|
|
312
|
-
a11y_deep: base.a11y_deep ?? DEFAULT_CONFIG.a11y_deep,
|
|
313
|
-
seo_deep: base.seo_deep ?? DEFAULT_CONFIG.seo_deep,
|
|
314
|
-
vitals_budget: base.vitals_budget ?? DEFAULT_CONFIG.vitals_budget,
|
|
315
|
-
};
|
|
316
|
-
config.thresholds = {
|
|
317
|
-
...DEFAULT_CONFIG.thresholds,
|
|
318
|
-
...teamThresholdFallback,
|
|
319
|
-
...(base.thresholds ?? {}),
|
|
320
|
-
};
|
|
321
|
-
// CLI flag overrides
|
|
322
|
-
if (options.cliFlags?.failOn !== undefined) {
|
|
323
|
-
if (!VALID_FAIL_ON.includes(options.cliFlags.failOn)) {
|
|
324
|
-
throw new ConfigParseError(`Invalid --fail-on value: "${options.cliFlags.failOn}". Must be one of: ${VALID_FAIL_ON.join(", ")}`);
|
|
325
|
-
}
|
|
326
|
-
config.fail_on = options.cliFlags.failOn;
|
|
327
|
-
}
|
|
328
|
-
// CI mode: apply CI defaults
|
|
329
|
-
const ciMode = options.ciMode;
|
|
330
|
-
if (ciMode) {
|
|
331
|
-
// dev_timeout: 90s in CI
|
|
332
|
-
if (!base.dev_timeout) {
|
|
333
|
-
config.dev_timeout = 90;
|
|
334
|
-
}
|
|
335
|
-
// lighthouse_runs: default to 3 in CI, but explicit config file value wins
|
|
336
|
-
if (!base.lighthouse_runs) {
|
|
337
|
-
config.lighthouse_runs = 3;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
// Skip lighthouse: max grade is Bronze
|
|
341
|
-
if (options.cliFlags?.skipLighthouse) {
|
|
342
|
-
// Effectively disables Lighthouse grading
|
|
343
|
-
}
|
|
344
|
-
// CLI flag overrides for new checks
|
|
345
|
-
if (options.cliFlags?.typecheck !== undefined)
|
|
346
|
-
config.typecheck = options.cliFlags.typecheck;
|
|
347
|
-
if (options.cliFlags?.secretScan !== undefined)
|
|
348
|
-
config.secret_scan = options.cliFlags.secretScan;
|
|
349
|
-
if (options.cliFlags?.bundleSize !== undefined)
|
|
350
|
-
config.bundle_size = options.cliFlags.bundleSize;
|
|
351
|
-
if (options.cliFlags?.outdatedCheck !== undefined)
|
|
352
|
-
config.outdated_check = options.cliFlags.outdatedCheck;
|
|
353
|
-
if (options.cliFlags?.a11yDeep !== undefined)
|
|
354
|
-
config.a11y_deep = options.cliFlags.a11yDeep;
|
|
355
|
-
if (options.cliFlags?.seoDeep !== undefined)
|
|
356
|
-
config.seo_deep = options.cliFlags.seoDeep;
|
|
357
|
-
if (options.cliFlags?.vitalsBudget !== undefined)
|
|
358
|
-
config.vitals_budget = options.cliFlags.vitalsBudget;
|
|
359
|
-
return { ...config, ciMode };
|
|
360
|
-
}
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ConfigParseError = void 0;
|
|
37
|
+
exports.fetchTeamThresholds = fetchTeamThresholds;
|
|
38
|
+
exports.loadConfig = loadConfig;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const yaml = __importStar(require("js-yaml"));
|
|
42
|
+
const auth_js_1 = require("./auth.js");
|
|
43
|
+
const DEFAULT_CONFIG = {
|
|
44
|
+
framework: "auto",
|
|
45
|
+
build_command: "",
|
|
46
|
+
dev_command: "",
|
|
47
|
+
package_manager: "auto",
|
|
48
|
+
port: 3000,
|
|
49
|
+
build_timeout: 300,
|
|
50
|
+
dev_timeout: 60,
|
|
51
|
+
lighthouse_runs: 3,
|
|
52
|
+
thresholds: {
|
|
53
|
+
performance: 70,
|
|
54
|
+
accessibility: 85,
|
|
55
|
+
seo: 80,
|
|
56
|
+
bestPractices: 80,
|
|
57
|
+
},
|
|
58
|
+
fail_on: "bronze",
|
|
59
|
+
crawl: false,
|
|
60
|
+
max_crawl_depth: 3,
|
|
61
|
+
max_crawl_pages: 10,
|
|
62
|
+
browsers: ["chromium"],
|
|
63
|
+
max_lighthouse_routes: 5,
|
|
64
|
+
visual_diff: {
|
|
65
|
+
pixelmatchThreshold: 0.1,
|
|
66
|
+
warnThreshold: 30,
|
|
67
|
+
rollbackThreshold: 60,
|
|
68
|
+
ignoreSelectors: [],
|
|
69
|
+
disableAnimations: true,
|
|
70
|
+
},
|
|
71
|
+
typecheck: false,
|
|
72
|
+
secret_scan: false,
|
|
73
|
+
secret_scan_ignore_paths: [],
|
|
74
|
+
bundle_size: false,
|
|
75
|
+
outdated_check: false,
|
|
76
|
+
a11y_deep: false,
|
|
77
|
+
seo_deep: false,
|
|
78
|
+
vitals_budget: false,
|
|
79
|
+
};
|
|
80
|
+
const VALID_FAIL_ON = ["unverified", "bronze", "silver", "gold"];
|
|
81
|
+
class ConfigParseError extends Error {
|
|
82
|
+
constructor(msg) {
|
|
83
|
+
super(msg);
|
|
84
|
+
this.name = "ConfigParseError";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.ConfigParseError = ConfigParseError;
|
|
88
|
+
function parseYaml(filePath) {
|
|
89
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
90
|
+
const raw = yaml.load(content);
|
|
91
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
92
|
+
throw new ConfigParseError("Invalid YAML structure in .laxy.yml");
|
|
93
|
+
}
|
|
94
|
+
const result = {};
|
|
95
|
+
if (typeof raw.framework === "string")
|
|
96
|
+
result.framework = raw.framework;
|
|
97
|
+
if (typeof raw.build_command === "string")
|
|
98
|
+
result.build_command = raw.build_command;
|
|
99
|
+
if (typeof raw.dev_command === "string")
|
|
100
|
+
result.dev_command = raw.dev_command;
|
|
101
|
+
if (typeof raw.package_manager === "string")
|
|
102
|
+
result.package_manager = raw.package_manager;
|
|
103
|
+
if (typeof raw.port === "number")
|
|
104
|
+
result.port = raw.port;
|
|
105
|
+
if (typeof raw.build_timeout === "number")
|
|
106
|
+
result.build_timeout = raw.build_timeout;
|
|
107
|
+
if (typeof raw.dev_timeout === "number")
|
|
108
|
+
result.dev_timeout = raw.dev_timeout;
|
|
109
|
+
if (typeof raw.lighthouse_runs === "number")
|
|
110
|
+
result.lighthouse_runs = raw.lighthouse_runs;
|
|
111
|
+
if (typeof raw.crawl === "boolean")
|
|
112
|
+
result.crawl = raw.crawl;
|
|
113
|
+
if (typeof raw.max_crawl_depth === "number")
|
|
114
|
+
result.max_crawl_depth = raw.max_crawl_depth;
|
|
115
|
+
if (typeof raw.max_crawl_pages === "number")
|
|
116
|
+
result.max_crawl_pages = raw.max_crawl_pages;
|
|
117
|
+
if (Array.isArray(raw.browsers)) {
|
|
118
|
+
const validBrowsers = ["chromium", "firefox", "webkit"];
|
|
119
|
+
const browsers = raw.browsers
|
|
120
|
+
.filter((b) => typeof b === "string" && validBrowsers.includes(b));
|
|
121
|
+
if (browsers.length > 0)
|
|
122
|
+
result.browsers = browsers;
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(raw.lighthouse_routes)) {
|
|
125
|
+
const routes = raw.lighthouse_routes
|
|
126
|
+
.filter((r) => typeof r === "string" && r.startsWith("/"))
|
|
127
|
+
.slice(0, 20);
|
|
128
|
+
if (routes.length > 0)
|
|
129
|
+
result.lighthouse_routes = routes;
|
|
130
|
+
}
|
|
131
|
+
if (Array.isArray(raw.extra_routes)) {
|
|
132
|
+
const routes = raw.extra_routes
|
|
133
|
+
.filter((r) => typeof r === "string" && r.startsWith("/"))
|
|
134
|
+
.slice(0, 20);
|
|
135
|
+
if (routes.length > 0)
|
|
136
|
+
result.extra_routes = routes;
|
|
137
|
+
}
|
|
138
|
+
if (typeof raw.max_lighthouse_routes === "number") {
|
|
139
|
+
result.max_lighthouse_routes = Math.max(1, Math.min(20, raw.max_lighthouse_routes));
|
|
140
|
+
}
|
|
141
|
+
if (typeof raw.visual_diff === "object" &&
|
|
142
|
+
raw.visual_diff !== null &&
|
|
143
|
+
!Array.isArray(raw.visual_diff)) {
|
|
144
|
+
const vd = raw.visual_diff;
|
|
145
|
+
const visualDiff = {};
|
|
146
|
+
if (typeof vd.pixelmatch_threshold === "number") {
|
|
147
|
+
visualDiff.pixelmatchThreshold = Math.max(0, Math.min(1, vd.pixelmatch_threshold));
|
|
148
|
+
}
|
|
149
|
+
if (typeof vd.warn_threshold === "number") {
|
|
150
|
+
visualDiff.warnThreshold = Math.max(0, Math.min(100, vd.warn_threshold));
|
|
151
|
+
}
|
|
152
|
+
if (typeof vd.rollback_threshold === "number") {
|
|
153
|
+
visualDiff.rollbackThreshold = Math.max(0, Math.min(100, vd.rollback_threshold));
|
|
154
|
+
}
|
|
155
|
+
if (Array.isArray(vd.ignore_selectors)) {
|
|
156
|
+
visualDiff.ignoreSelectors = vd.ignore_selectors
|
|
157
|
+
.filter((selector) => typeof selector === "string" && selector.trim().length > 0)
|
|
158
|
+
.slice(0, 30);
|
|
159
|
+
}
|
|
160
|
+
if (typeof vd.disable_animations === "boolean") {
|
|
161
|
+
visualDiff.disableAnimations = vd.disable_animations;
|
|
162
|
+
}
|
|
163
|
+
result.visual_diff = visualDiff;
|
|
164
|
+
}
|
|
165
|
+
if (typeof raw.fail_on === "string") {
|
|
166
|
+
const f = raw.fail_on;
|
|
167
|
+
if (!VALID_FAIL_ON.includes(f)) {
|
|
168
|
+
throw new ConfigParseError(`Invalid fail_on value: "${f}". Must be one of: ${VALID_FAIL_ON.join(", ")}`);
|
|
169
|
+
}
|
|
170
|
+
result.fail_on = f;
|
|
171
|
+
}
|
|
172
|
+
if (typeof raw.thresholds === "object" &&
|
|
173
|
+
raw.thresholds !== null &&
|
|
174
|
+
!Array.isArray(raw.thresholds)) {
|
|
175
|
+
const t = raw.thresholds;
|
|
176
|
+
const thr = {};
|
|
177
|
+
if (typeof t.performance === "number")
|
|
178
|
+
thr.performance = t.performance;
|
|
179
|
+
if (typeof t.accessibility === "number")
|
|
180
|
+
thr.accessibility = t.accessibility;
|
|
181
|
+
if (typeof t.seo === "number")
|
|
182
|
+
thr.seo = t.seo;
|
|
183
|
+
if (typeof t.best_practices === "number")
|
|
184
|
+
thr.bestPractices = t.best_practices;
|
|
185
|
+
result.thresholds = thr;
|
|
186
|
+
}
|
|
187
|
+
if (Array.isArray(raw.scenarios)) {
|
|
188
|
+
const scenarios = [];
|
|
189
|
+
for (const item of raw.scenarios) {
|
|
190
|
+
if (typeof item === "object" &&
|
|
191
|
+
item !== null &&
|
|
192
|
+
typeof item.name === "string" &&
|
|
193
|
+
Array.isArray(item.steps)) {
|
|
194
|
+
const rawScenario = item;
|
|
195
|
+
const steps = [];
|
|
196
|
+
for (const rawStep of rawScenario.steps) {
|
|
197
|
+
if (typeof rawStep === "object" && rawStep !== null) {
|
|
198
|
+
const s = rawStep;
|
|
199
|
+
const step = {};
|
|
200
|
+
if (typeof s.goto === "string")
|
|
201
|
+
step.goto = s.goto;
|
|
202
|
+
if (typeof s.fill === "string")
|
|
203
|
+
step.fill = s.fill;
|
|
204
|
+
if (typeof s.with === "string")
|
|
205
|
+
step.with = s.with;
|
|
206
|
+
if (typeof s.click === "string")
|
|
207
|
+
step.click = s.click;
|
|
208
|
+
if (typeof s.expect_visible === "string")
|
|
209
|
+
step.expect_visible = s.expect_visible;
|
|
210
|
+
if (typeof s.expect_text === "string")
|
|
211
|
+
step.expect_text = s.expect_text;
|
|
212
|
+
if (typeof s.wait === "number")
|
|
213
|
+
step.wait = s.wait;
|
|
214
|
+
steps.push(step);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
scenarios.push({ name: String(rawScenario.name), steps });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (scenarios.length > 0) {
|
|
221
|
+
result.scenarios = scenarios;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (typeof raw.typecheck === "boolean")
|
|
225
|
+
result.typecheck = raw.typecheck;
|
|
226
|
+
if (typeof raw.secret_scan === "boolean")
|
|
227
|
+
result.secret_scan = raw.secret_scan;
|
|
228
|
+
if (Array.isArray(raw.secret_scan_ignore_paths)) {
|
|
229
|
+
result.secret_scan_ignore_paths = raw.secret_scan_ignore_paths
|
|
230
|
+
.filter((p) => typeof p === "string");
|
|
231
|
+
}
|
|
232
|
+
if (typeof raw.bundle_size === "boolean")
|
|
233
|
+
result.bundle_size = raw.bundle_size;
|
|
234
|
+
if (typeof raw.outdated_check === "boolean")
|
|
235
|
+
result.outdated_check = raw.outdated_check;
|
|
236
|
+
if (typeof raw.a11y_deep === "boolean")
|
|
237
|
+
result.a11y_deep = raw.a11y_deep;
|
|
238
|
+
if (typeof raw.seo_deep === "boolean")
|
|
239
|
+
result.seo_deep = raw.seo_deep;
|
|
240
|
+
if (typeof raw.vitals_budget === "boolean")
|
|
241
|
+
result.vitals_budget = raw.vitals_budget;
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 로그인된 CLI 토큰으로 팀 공통 임계값을 서버에서 가져온다.
|
|
246
|
+
* 토큰 없음 / 팀 없음 / 네트워크 오류 시 null 반환 (graceful degradation).
|
|
247
|
+
*/
|
|
248
|
+
async function fetchTeamThresholds() {
|
|
249
|
+
const token = (0, auth_js_1.loadToken)();
|
|
250
|
+
if (!token)
|
|
251
|
+
return null;
|
|
252
|
+
try {
|
|
253
|
+
const res = await fetch(`${auth_js_1.LAXY_API_URL}/api/v1/team-thresholds`, {
|
|
254
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
255
|
+
signal: AbortSignal.timeout(5000),
|
|
256
|
+
});
|
|
257
|
+
if (!res.ok)
|
|
258
|
+
return null;
|
|
259
|
+
const data = (await res.json());
|
|
260
|
+
return data.thresholds ?? null;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function loadConfig(options) {
|
|
267
|
+
const configPath = options.configPath ?? path.join(options.dir, ".laxy.yml");
|
|
268
|
+
let base = {};
|
|
269
|
+
const hasLocalConfig = fs.existsSync(configPath);
|
|
270
|
+
if (hasLocalConfig) {
|
|
271
|
+
base = parseYaml(configPath);
|
|
272
|
+
}
|
|
273
|
+
// 팀 임계값: 로컬 .laxy.yml에 thresholds가 없을 때만 적용
|
|
274
|
+
const team = options.teamThresholds ?? null;
|
|
275
|
+
const teamThresholdFallback = (!hasLocalConfig || !base.thresholds) && team
|
|
276
|
+
? {
|
|
277
|
+
performance: team.performance,
|
|
278
|
+
accessibility: team.accessibility,
|
|
279
|
+
seo: team.seo,
|
|
280
|
+
bestPractices: team.best_practices,
|
|
281
|
+
}
|
|
282
|
+
: {};
|
|
283
|
+
const teamFailOnFallback = (!hasLocalConfig || !base.fail_on) && team ? team.fail_on : undefined;
|
|
284
|
+
const config = {
|
|
285
|
+
...DEFAULT_CONFIG,
|
|
286
|
+
framework: base.framework ?? DEFAULT_CONFIG.framework,
|
|
287
|
+
build_command: base.build_command ?? DEFAULT_CONFIG.build_command,
|
|
288
|
+
dev_command: base.dev_command ?? DEFAULT_CONFIG.dev_command,
|
|
289
|
+
package_manager: base.package_manager ?? DEFAULT_CONFIG.package_manager,
|
|
290
|
+
port: base.port ?? DEFAULT_CONFIG.port,
|
|
291
|
+
build_timeout: base.build_timeout ?? DEFAULT_CONFIG.build_timeout,
|
|
292
|
+
dev_timeout: base.dev_timeout ?? DEFAULT_CONFIG.dev_timeout,
|
|
293
|
+
lighthouse_runs: base.lighthouse_runs ?? DEFAULT_CONFIG.lighthouse_runs,
|
|
294
|
+
fail_on: base.fail_on ?? teamFailOnFallback ?? DEFAULT_CONFIG.fail_on,
|
|
295
|
+
scenarios: base.scenarios,
|
|
296
|
+
crawl: base.crawl ?? DEFAULT_CONFIG.crawl,
|
|
297
|
+
max_crawl_depth: base.max_crawl_depth ?? DEFAULT_CONFIG.max_crawl_depth,
|
|
298
|
+
max_crawl_pages: base.max_crawl_pages ?? DEFAULT_CONFIG.max_crawl_pages,
|
|
299
|
+
browsers: base.browsers ?? DEFAULT_CONFIG.browsers,
|
|
300
|
+
lighthouse_routes: base.lighthouse_routes,
|
|
301
|
+
extra_routes: base.extra_routes,
|
|
302
|
+
max_lighthouse_routes: base.max_lighthouse_routes ?? DEFAULT_CONFIG.max_lighthouse_routes,
|
|
303
|
+
visual_diff: {
|
|
304
|
+
...DEFAULT_CONFIG.visual_diff,
|
|
305
|
+
...(base.visual_diff ?? {}),
|
|
306
|
+
},
|
|
307
|
+
typecheck: base.typecheck ?? DEFAULT_CONFIG.typecheck,
|
|
308
|
+
secret_scan: base.secret_scan ?? DEFAULT_CONFIG.secret_scan,
|
|
309
|
+
secret_scan_ignore_paths: base.secret_scan_ignore_paths ?? DEFAULT_CONFIG.secret_scan_ignore_paths,
|
|
310
|
+
bundle_size: base.bundle_size ?? DEFAULT_CONFIG.bundle_size,
|
|
311
|
+
outdated_check: base.outdated_check ?? DEFAULT_CONFIG.outdated_check,
|
|
312
|
+
a11y_deep: base.a11y_deep ?? DEFAULT_CONFIG.a11y_deep,
|
|
313
|
+
seo_deep: base.seo_deep ?? DEFAULT_CONFIG.seo_deep,
|
|
314
|
+
vitals_budget: base.vitals_budget ?? DEFAULT_CONFIG.vitals_budget,
|
|
315
|
+
};
|
|
316
|
+
config.thresholds = {
|
|
317
|
+
...DEFAULT_CONFIG.thresholds,
|
|
318
|
+
...teamThresholdFallback,
|
|
319
|
+
...(base.thresholds ?? {}),
|
|
320
|
+
};
|
|
321
|
+
// CLI flag overrides
|
|
322
|
+
if (options.cliFlags?.failOn !== undefined) {
|
|
323
|
+
if (!VALID_FAIL_ON.includes(options.cliFlags.failOn)) {
|
|
324
|
+
throw new ConfigParseError(`Invalid --fail-on value: "${options.cliFlags.failOn}". Must be one of: ${VALID_FAIL_ON.join(", ")}`);
|
|
325
|
+
}
|
|
326
|
+
config.fail_on = options.cliFlags.failOn;
|
|
327
|
+
}
|
|
328
|
+
// CI mode: apply CI defaults
|
|
329
|
+
const ciMode = options.ciMode;
|
|
330
|
+
if (ciMode) {
|
|
331
|
+
// dev_timeout: 90s in CI
|
|
332
|
+
if (!base.dev_timeout) {
|
|
333
|
+
config.dev_timeout = 90;
|
|
334
|
+
}
|
|
335
|
+
// lighthouse_runs: default to 3 in CI, but explicit config file value wins
|
|
336
|
+
if (!base.lighthouse_runs) {
|
|
337
|
+
config.lighthouse_runs = 3;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Skip lighthouse: max grade is Bronze
|
|
341
|
+
if (options.cliFlags?.skipLighthouse) {
|
|
342
|
+
// Effectively disables Lighthouse grading
|
|
343
|
+
}
|
|
344
|
+
// CLI flag overrides for new checks
|
|
345
|
+
if (options.cliFlags?.typecheck !== undefined)
|
|
346
|
+
config.typecheck = options.cliFlags.typecheck;
|
|
347
|
+
if (options.cliFlags?.secretScan !== undefined)
|
|
348
|
+
config.secret_scan = options.cliFlags.secretScan;
|
|
349
|
+
if (options.cliFlags?.bundleSize !== undefined)
|
|
350
|
+
config.bundle_size = options.cliFlags.bundleSize;
|
|
351
|
+
if (options.cliFlags?.outdatedCheck !== undefined)
|
|
352
|
+
config.outdated_check = options.cliFlags.outdatedCheck;
|
|
353
|
+
if (options.cliFlags?.a11yDeep !== undefined)
|
|
354
|
+
config.a11y_deep = options.cliFlags.a11yDeep;
|
|
355
|
+
if (options.cliFlags?.seoDeep !== undefined)
|
|
356
|
+
config.seo_deep = options.cliFlags.seoDeep;
|
|
357
|
+
if (options.cliFlags?.vitalsBudget !== undefined)
|
|
358
|
+
config.vitals_budget = options.cliFlags.vitalsBudget;
|
|
359
|
+
return { ...config, ciMode };
|
|
360
|
+
}
|