laxy-verify 1.2.1 → 1.2.2
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 +12 -17
- package/dist/cli.js +41 -3
- package/dist/config.d.ts +13 -0
- package/dist/config.js +43 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,23 +63,18 @@ This is most useful if you ship frontend apps and want a practical gate before:
|
|
|
63
63
|
- QA handoff
|
|
64
64
|
- production release
|
|
65
65
|
|
|
66
|
-
##
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
|
71
|
-
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
| Ship/hold release decision | yes | score only | no | no | no | no |
|
|
79
|
-
| Zero config to start | yes | no | no | no | partial | no |
|
|
80
|
-
| Free full coverage | yes | yes | limited | limited | yes | enterprise |
|
|
81
|
-
|
|
82
|
-
LHCI gives you Lighthouse. Percy gives you visual diffs. Checkly watches your production uptime. None of them produce a merge or release decision from one command.
|
|
66
|
+
## Why not just LHCI?
|
|
67
|
+
|
|
68
|
+
| | laxy-verify | LHCI | Checkly | Percy |
|
|
69
|
+
|--|--|--|--|--|
|
|
70
|
+
| Production build failure detection | Yes | No | No | No |
|
|
71
|
+
| User-flow E2E verification | Yes | No | Manual setup | No |
|
|
72
|
+
| Lighthouse scoring | Yes | Yes | No | No |
|
|
73
|
+
| Visual regression check | Yes | No | No | Yes |
|
|
74
|
+
| Release decision (`hold` / `client-ready`) | Yes | No, score only | No | No |
|
|
75
|
+
| Zero-config local start | Yes | No | No | No |
|
|
76
|
+
|
|
77
|
+
LHCI measures Lighthouse. `laxy-verify` is for deciding whether this frontend is actually safe to ship.
|
|
83
78
|
|
|
84
79
|
## The failures it is meant to catch
|
|
85
80
|
|
package/dist/cli.js
CHANGED
|
@@ -159,6 +159,7 @@ function parseArgs() {
|
|
|
159
159
|
crawl: flags.crawl !== undefined,
|
|
160
160
|
planOverride: flags["plan-override"],
|
|
161
161
|
port: flags.port !== undefined ? Number(flags.port) : undefined,
|
|
162
|
+
share: flags.share !== undefined,
|
|
162
163
|
help: flags.help !== undefined || flags.h !== undefined,
|
|
163
164
|
};
|
|
164
165
|
}
|
|
@@ -296,6 +297,9 @@ function consoleOutput(result) {
|
|
|
296
297
|
if (result.markdownReportPath) {
|
|
297
298
|
console.log(` Report: ${path.basename(result.markdownReportPath)}`);
|
|
298
299
|
}
|
|
300
|
+
if (result.share?.url) {
|
|
301
|
+
console.log(` Share: ${result.share.url}`);
|
|
302
|
+
}
|
|
299
303
|
console.log(` Exit code: ${result.exitCode}`);
|
|
300
304
|
}
|
|
301
305
|
async function run() {
|
|
@@ -324,6 +328,7 @@ async function run() {
|
|
|
324
328
|
--fail-on unverified | bronze | silver | gold
|
|
325
329
|
--skip-lighthouse Skip Lighthouse but still run build and E2E
|
|
326
330
|
--port <port> Use an already-running dev server on this port (skip build & server start)
|
|
331
|
+
--share Create a public share link for this verification result (Pro)
|
|
327
332
|
--plan-override free | pro | team (testing metadata only)
|
|
328
333
|
--multi-viewport Lighthouse on desktop/tablet/mobile
|
|
329
334
|
--crawl Crawl the app to discover routes before E2E
|
|
@@ -341,6 +346,7 @@ async function run() {
|
|
|
341
346
|
npx laxy-verify . --ci # CI mode
|
|
342
347
|
npx laxy-verify . --fail-on silver # Block Bronze or worse
|
|
343
348
|
npx laxy-verify . --port 3001 # Use existing dev server on port 3001
|
|
349
|
+
npx laxy-verify . --share # Save and share a public verification link
|
|
344
350
|
|
|
345
351
|
Docs: https://github.com/SUNgm24/Laxy/tree/main/laxy-verify
|
|
346
352
|
`);
|
|
@@ -417,6 +423,11 @@ async function run() {
|
|
|
417
423
|
exitGracefully(0);
|
|
418
424
|
return;
|
|
419
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
|
+
}
|
|
420
431
|
let config;
|
|
421
432
|
try {
|
|
422
433
|
config = (0, config_js_1.loadConfig)({
|
|
@@ -427,6 +438,7 @@ async function run() {
|
|
|
427
438
|
failOn: args.failOn,
|
|
428
439
|
skipLighthouse: args.skipLighthouse,
|
|
429
440
|
},
|
|
441
|
+
teamThresholds,
|
|
430
442
|
});
|
|
431
443
|
}
|
|
432
444
|
catch (err) {
|
|
@@ -465,7 +477,9 @@ async function run() {
|
|
|
465
477
|
exitGracefully(2);
|
|
466
478
|
return;
|
|
467
479
|
}
|
|
468
|
-
|
|
480
|
+
if (args.format !== "json") {
|
|
481
|
+
console.log(`Using existing dev server on port ${port} (HTTP ${status})`);
|
|
482
|
+
}
|
|
469
483
|
}
|
|
470
484
|
let buildResult;
|
|
471
485
|
if (useExistingServer) {
|
|
@@ -814,6 +828,9 @@ async function run() {
|
|
|
814
828
|
// Pro 이상: 결과를 서버에 저장 (배지, 공유 링크용)
|
|
815
829
|
const token = (0, auth_js_1.loadToken)();
|
|
816
830
|
const isPro = effectiveFeatures.plan === "pro" || effectiveFeatures.plan === "team";
|
|
831
|
+
if (args.share && (!token || !isPro)) {
|
|
832
|
+
console.warn(" [warn] --share requires a logged-in Pro or Team account.");
|
|
833
|
+
}
|
|
817
834
|
if (token && isPro) {
|
|
818
835
|
try {
|
|
819
836
|
const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
|
|
@@ -828,7 +845,10 @@ async function run() {
|
|
|
828
845
|
repo_id: repoId,
|
|
829
846
|
project_name: path.basename(path.resolve(args.projectDir)),
|
|
830
847
|
grade: unifiedGrade,
|
|
831
|
-
verdict: verificationReport.verdict
|
|
848
|
+
verdict: verificationReport.verdict === "client-ready" || verificationReport.verdict === "release-ready"
|
|
849
|
+
? "client-ready"
|
|
850
|
+
: "hold",
|
|
851
|
+
share: args.share,
|
|
832
852
|
scores: {
|
|
833
853
|
performance: scores?.performance,
|
|
834
854
|
accessibility: scores?.accessibility,
|
|
@@ -840,18 +860,36 @@ async function run() {
|
|
|
840
860
|
console_error_count: e2eConsoleErrors.length,
|
|
841
861
|
broken_link_count: brokenLinksResult?.brokenLinks.length,
|
|
842
862
|
},
|
|
843
|
-
full_result: {
|
|
863
|
+
full_result: {
|
|
864
|
+
framework: detected.framework,
|
|
865
|
+
build: { success: buildResult.success },
|
|
866
|
+
e2e: e2eResult ? { passed: e2eResult.passed, total: e2eResult.total } : null,
|
|
867
|
+
lighthouse: lighthouseResult,
|
|
868
|
+
verification: {
|
|
869
|
+
report: verificationReport,
|
|
870
|
+
},
|
|
871
|
+
},
|
|
844
872
|
}),
|
|
845
873
|
});
|
|
846
874
|
if (!saveRes.ok) {
|
|
847
875
|
const errBody = await saveRes.json().catch(() => ({}));
|
|
848
876
|
console.warn(` [warn] Result save failed (${saveRes.status}): ${errBody.error ?? "unknown error"}`);
|
|
849
877
|
}
|
|
878
|
+
else {
|
|
879
|
+
const saveData = await saveRes.json().catch(() => ({}));
|
|
880
|
+
if (saveData.share_id && saveData.share_url) {
|
|
881
|
+
resultObj.share = {
|
|
882
|
+
id: saveData.share_id,
|
|
883
|
+
url: saveData.share_url,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
}
|
|
850
887
|
}
|
|
851
888
|
catch {
|
|
852
889
|
// 네트워크 오류는 검증 결과에 영향 없음
|
|
853
890
|
}
|
|
854
891
|
}
|
|
892
|
+
writeResultFile(args.projectDir, resultObj);
|
|
855
893
|
if (args.format === "json") {
|
|
856
894
|
console.log(JSON.stringify(resultObj, null, 2));
|
|
857
895
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -38,6 +38,18 @@ export interface LaxyConfig {
|
|
|
38
38
|
export declare class ConfigParseError extends Error {
|
|
39
39
|
constructor(msg: string);
|
|
40
40
|
}
|
|
41
|
+
export interface TeamThresholdsConfig {
|
|
42
|
+
performance: number;
|
|
43
|
+
accessibility: number;
|
|
44
|
+
seo: number;
|
|
45
|
+
best_practices: number;
|
|
46
|
+
fail_on: FailOn;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 로그인된 CLI 토큰으로 팀 공통 임계값을 서버에서 가져온다.
|
|
50
|
+
* 토큰 없음 / 팀 없음 / 네트워크 오류 시 null 반환 (graceful degradation).
|
|
51
|
+
*/
|
|
52
|
+
export declare function fetchTeamThresholds(): Promise<TeamThresholdsConfig | null>;
|
|
41
53
|
export interface LoadConfigOptions {
|
|
42
54
|
dir: string;
|
|
43
55
|
configPath?: string;
|
|
@@ -46,6 +58,7 @@ export interface LoadConfigOptions {
|
|
|
46
58
|
skipLighthouse?: boolean;
|
|
47
59
|
};
|
|
48
60
|
ciMode: boolean;
|
|
61
|
+
teamThresholds?: TeamThresholdsConfig | null;
|
|
49
62
|
}
|
|
50
63
|
export declare function loadConfig(options: LoadConfigOptions): LaxyConfig & {
|
|
51
64
|
ciMode: boolean;
|
package/dist/config.js
CHANGED
|
@@ -34,10 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.ConfigParseError = void 0;
|
|
37
|
+
exports.fetchTeamThresholds = fetchTeamThresholds;
|
|
37
38
|
exports.loadConfig = loadConfig;
|
|
38
39
|
const fs = __importStar(require("node:fs"));
|
|
39
40
|
const path = __importStar(require("node:path"));
|
|
40
41
|
const yaml = __importStar(require("js-yaml"));
|
|
42
|
+
const auth_js_1 = require("./auth.js");
|
|
41
43
|
const DEFAULT_CONFIG = {
|
|
42
44
|
framework: "auto",
|
|
43
45
|
build_command: "",
|
|
@@ -164,12 +166,46 @@ function parseYaml(filePath) {
|
|
|
164
166
|
}
|
|
165
167
|
return result;
|
|
166
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* 로그인된 CLI 토큰으로 팀 공통 임계값을 서버에서 가져온다.
|
|
171
|
+
* 토큰 없음 / 팀 없음 / 네트워크 오류 시 null 반환 (graceful degradation).
|
|
172
|
+
*/
|
|
173
|
+
async function fetchTeamThresholds() {
|
|
174
|
+
const token = (0, auth_js_1.loadToken)();
|
|
175
|
+
if (!token)
|
|
176
|
+
return null;
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetch(`${auth_js_1.LAXY_API_URL}/api/v1/team-thresholds`, {
|
|
179
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
180
|
+
signal: AbortSignal.timeout(5000),
|
|
181
|
+
});
|
|
182
|
+
if (!res.ok)
|
|
183
|
+
return null;
|
|
184
|
+
const data = (await res.json());
|
|
185
|
+
return data.thresholds ?? null;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
167
191
|
function loadConfig(options) {
|
|
168
192
|
const configPath = options.configPath ?? path.join(options.dir, ".laxy.yml");
|
|
169
193
|
let base = {};
|
|
170
|
-
|
|
194
|
+
const hasLocalConfig = fs.existsSync(configPath);
|
|
195
|
+
if (hasLocalConfig) {
|
|
171
196
|
base = parseYaml(configPath);
|
|
172
197
|
}
|
|
198
|
+
// 팀 임계값: 로컬 .laxy.yml에 thresholds가 없을 때만 적용
|
|
199
|
+
const team = options.teamThresholds ?? null;
|
|
200
|
+
const teamThresholdFallback = (!hasLocalConfig || !base.thresholds) && team
|
|
201
|
+
? {
|
|
202
|
+
performance: team.performance,
|
|
203
|
+
accessibility: team.accessibility,
|
|
204
|
+
seo: team.seo,
|
|
205
|
+
bestPractices: team.best_practices,
|
|
206
|
+
}
|
|
207
|
+
: {};
|
|
208
|
+
const teamFailOnFallback = (!hasLocalConfig || !base.fail_on) && team ? team.fail_on : undefined;
|
|
173
209
|
const config = {
|
|
174
210
|
...DEFAULT_CONFIG,
|
|
175
211
|
framework: base.framework ?? DEFAULT_CONFIG.framework,
|
|
@@ -180,14 +216,18 @@ function loadConfig(options) {
|
|
|
180
216
|
build_timeout: base.build_timeout ?? DEFAULT_CONFIG.build_timeout,
|
|
181
217
|
dev_timeout: base.dev_timeout ?? DEFAULT_CONFIG.dev_timeout,
|
|
182
218
|
lighthouse_runs: base.lighthouse_runs ?? DEFAULT_CONFIG.lighthouse_runs,
|
|
183
|
-
fail_on: base.fail_on ?? DEFAULT_CONFIG.fail_on,
|
|
219
|
+
fail_on: base.fail_on ?? teamFailOnFallback ?? DEFAULT_CONFIG.fail_on,
|
|
184
220
|
scenarios: base.scenarios,
|
|
185
221
|
crawl: base.crawl ?? DEFAULT_CONFIG.crawl,
|
|
186
222
|
max_crawl_depth: base.max_crawl_depth ?? DEFAULT_CONFIG.max_crawl_depth,
|
|
187
223
|
max_crawl_pages: base.max_crawl_pages ?? DEFAULT_CONFIG.max_crawl_pages,
|
|
188
224
|
browsers: base.browsers ?? DEFAULT_CONFIG.browsers,
|
|
189
225
|
};
|
|
190
|
-
config.thresholds = {
|
|
226
|
+
config.thresholds = {
|
|
227
|
+
...DEFAULT_CONFIG.thresholds,
|
|
228
|
+
...teamThresholdFallback,
|
|
229
|
+
...(base.thresholds ?? {}),
|
|
230
|
+
};
|
|
191
231
|
// CLI flag overrides
|
|
192
232
|
if (options.cliFlags?.failOn !== undefined) {
|
|
193
233
|
if (!VALID_FAIL_ON.includes(options.cliFlags.failOn)) {
|