laxy-verify 0.1.2 → 1.0.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.
Files changed (47) hide show
  1. package/README.md +150 -163
  2. package/dist/badge.d.ts +1 -0
  3. package/dist/badge.js +11 -0
  4. package/dist/build.d.ts +11 -0
  5. package/dist/build.js +59 -0
  6. package/dist/cli.d.ts +2 -2
  7. package/dist/cli.js +257 -102
  8. package/dist/comment.d.ts +20 -0
  9. package/dist/comment.js +53 -0
  10. package/dist/config.d.ts +34 -16
  11. package/dist/config.js +118 -55
  12. package/dist/detect.d.ts +10 -0
  13. package/dist/detect.js +97 -0
  14. package/dist/github.d.ts +8 -0
  15. package/dist/github.js +11 -0
  16. package/dist/grade.d.ts +37 -0
  17. package/dist/grade.js +57 -0
  18. package/dist/init.d.ts +1 -0
  19. package/dist/init.js +85 -0
  20. package/dist/lighthouse.d.ts +7 -0
  21. package/dist/lighthouse.js +94 -0
  22. package/dist/serve.d.ts +12 -0
  23. package/dist/serve.js +83 -0
  24. package/dist/status.d.ts +6 -0
  25. package/dist/status.js +33 -0
  26. package/package.json +29 -44
  27. package/action.yml +0 -97
  28. package/dist/build-runner.d.ts +0 -22
  29. package/dist/build-runner.js +0 -156
  30. package/dist/build-runner.js.map +0 -1
  31. package/dist/cli.js.map +0 -1
  32. package/dist/config.js.map +0 -1
  33. package/dist/dev-server.d.ts +0 -8
  34. package/dist/dev-server.js +0 -81
  35. package/dist/dev-server.js.map +0 -1
  36. package/dist/lighthouse-runner.d.ts +0 -10
  37. package/dist/lighthouse-runner.js +0 -119
  38. package/dist/lighthouse-runner.js.map +0 -1
  39. package/dist/project-runtime.d.ts +0 -32
  40. package/dist/project-runtime.js +0 -99
  41. package/dist/project-runtime.js.map +0 -1
  42. package/dist/reporter.d.ts +0 -21
  43. package/dist/reporter.js +0 -100
  44. package/dist/reporter.js.map +0 -1
  45. package/dist/verification.d.ts +0 -42
  46. package/dist/verification.js +0 -105
  47. package/dist/verification.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,102 +1,257 @@
1
- #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import { resolve } from "path";
4
- import { existsSync } from "fs";
5
- import { loadConfig } from "./config.js";
6
- import { runBuild } from "./build-runner.js";
7
- import { runLighthouse } from "./lighthouse-runner.js";
8
- import { getVerificationGrade, getImprovementRecommendations } from "./verification.js";
9
- import { formatReport } from "./reporter.js";
10
- const EXIT_PASS = 0; // Silver or Gold
11
- const EXIT_SOFT_FAIL = 1; // Bronze (build OK, LH failed)
12
- const EXIT_FAIL = 2; // Unverified (build failed)
13
- const EXIT_CONFIG = 3; // Config error
14
- const program = new Command();
15
- program
16
- .name("laxy-verify")
17
- .description("Frontend quality gate: build check + Lighthouse audit + verification grade")
18
- .version("0.1.0")
19
- .argument("[dir]", "Project directory", ".")
20
- .option("--format <type>", "Output format: console, json, md", "console")
21
- .option("--ci", "CI mode: relaxed thresholds, 3 Lighthouse runs")
22
- .option("--skip-lighthouse", "Skip Lighthouse, build-only verification")
23
- .option("--runs <number>", "Number of Lighthouse runs", parseInt)
24
- .option("--port <number>", "Dev server port", parseInt)
25
- .action(async (dir, opts) => {
26
- const projectPath = resolve(dir);
27
- if (!existsSync(projectPath)) {
28
- console.error(`Directory not found: ${projectPath}`);
29
- process.exit(EXIT_CONFIG);
30
- }
31
- if (!existsSync(resolve(projectPath, "package.json"))) {
32
- console.error("Not a Node.js project: package.json not found");
33
- process.exit(EXIT_CONFIG);
34
- }
35
- // Load config
36
- const { config, warnings } = loadConfig(projectPath, opts.ci);
37
- for (const w of warnings)
38
- console.warn(`[warn] ${w}`);
39
- // Override config with CLI flags
40
- if (opts.runs)
41
- config.runs = opts.runs;
42
- if (opts.port)
43
- config.port = opts.port;
44
- // Step 1: Build check
45
- const buildResult = await runBuild(projectPath);
46
- if (!buildResult.success) {
47
- const report = {
48
- grade: "unverified",
49
- build: { success: false, errors: buildResult.errors, duration: buildResult.duration },
50
- lighthouse: { scores: null },
51
- thresholds: config.thresholds,
52
- recommendations: getImprovementRecommendations({
53
- buildSuccess: false,
54
- buildErrors: buildResult.errors,
55
- }),
56
- };
57
- console.log(formatReport(report, opts.format));
58
- process.exit(EXIT_FAIL);
59
- }
60
- // Step 2: Lighthouse (optional)
61
- let lighthouseScores = null;
62
- let lighthouseError;
63
- if (!opts.skipLighthouse) {
64
- const lhResult = await runLighthouse(projectPath, {
65
- port: config.port,
66
- runs: config.runs,
67
- ciMode: opts.ci,
68
- });
69
- lighthouseScores = lhResult.scores;
70
- lighthouseError = lhResult.error;
71
- }
72
- // Step 3: Grade
73
- const grade = getVerificationGrade({
74
- buildSuccess: true,
75
- lighthouseScores: lighthouseScores ?? undefined,
76
- });
77
- const report = {
78
- grade,
79
- build: { success: true, errors: [], duration: buildResult.duration },
80
- lighthouse: { scores: lighthouseScores, error: lighthouseError },
81
- thresholds: config.thresholds,
82
- recommendations: getImprovementRecommendations({
83
- buildSuccess: true,
84
- lighthouseScores: lighthouseScores ?? undefined,
85
- }),
86
- };
87
- console.log(formatReport(report, opts.format));
88
- // Exit codes
89
- switch (grade) {
90
- case "gold":
91
- case "silver":
92
- process.exit(EXIT_PASS);
93
- break;
94
- case "bronze":
95
- process.exit(EXIT_SOFT_FAIL);
96
- break;
97
- default:
98
- process.exit(EXIT_FAIL);
99
- }
100
- });
101
- program.parse();
102
- //# sourceMappingURL=cli.js.map
1
+ #!/usr/bin/env node
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { loadConfig } from "./config.js";
5
+ import { detect } from "./detect.js";
6
+ import { runBuild } from "./build.js";
7
+ import { startDevServer, stopDevServer } from "./serve.js";
8
+ import { runLighthouse } from "./lighthouse.js";
9
+ import { calculateGrade } from "./grade.js";
10
+ import { runInit } from "./init.js";
11
+ import { generateBadge } from "./badge.js";
12
+ import { postPRComment } from "./comment.js";
13
+ import { createStatusCheck } from "./status.js";
14
+ function parseArgs() {
15
+ const raw = process.argv.slice(2);
16
+ // Single-pass: collect flags and first positional
17
+ let projectDir = ".";
18
+ const flags = {};
19
+ for (let i = 0; i < raw.length; i++) {
20
+ const arg = raw[i];
21
+ if (arg.startsWith("--")) {
22
+ const eqIndex = arg.indexOf("=");
23
+ if (eqIndex >= 0) {
24
+ const key = arg.slice(2, eqIndex);
25
+ flags[key] = arg.slice(eqIndex + 1);
26
+ }
27
+ else {
28
+ const key = arg.slice(2);
29
+ // Check if next arg is a value
30
+ if (i + 1 < raw.length && !raw[i + 1].startsWith("-")) {
31
+ flags[key] = raw[++i];
32
+ }
33
+ else {
34
+ flags[key] = "true";
35
+ }
36
+ }
37
+ }
38
+ else {
39
+ // Only first non-flag arg is positional (projectDir)
40
+ if (projectDir === ".") {
41
+ projectDir = arg;
42
+ }
43
+ }
44
+ }
45
+ return {
46
+ projectDir: path.resolve(projectDir),
47
+ format: flags["format"] ?? "console",
48
+ ciMode: flags["ci"] !== undefined || process.env.CI === "true",
49
+ configPath: flags["config"],
50
+ failOn: flags["fail-on"] ?? undefined,
51
+ skipLighthouse: flags["skip-lighthouse"] !== undefined,
52
+ badge: flags["badge"] !== undefined,
53
+ init: flags["init"] !== undefined,
54
+ };
55
+ }
56
+ function writeResultFile(projectDir, result) {
57
+ const filePath = path.join(projectDir, ".laxy-result.json");
58
+ fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
59
+ }
60
+ function consoleOutput(result) {
61
+ const gradeLabel = result.grade;
62
+ const checkEmoji = result.grade !== "Unverified" ? " ✅" : "";
63
+ console.log(`\n Laxy Verify — ${gradeLabel}${checkEmoji}`);
64
+ console.log(` Build: ${result.build.success ? `OK (${result.build.durationMs}ms)` : "FAILED"}`);
65
+ if (result.build.errors.length > 0) {
66
+ const last5 = result.build.errors.slice(-5);
67
+ console.log(` Errors:`);
68
+ for (const e of last5)
69
+ console.error(` ${e}`);
70
+ }
71
+ if (result.lighthouse !== null) {
72
+ const lh = result.lighthouse;
73
+ const t = result.thresholds;
74
+ const check = (passed) => passed ? " ✅" : " ❌";
75
+ console.log(` Lighthouse:`);
76
+ console.log(` Performance: ${lh.performance} / ${t.performance}${check(lh.performance >= t.performance)}`);
77
+ console.log(` Accessibility: ${lh.accessibility} / ${t.accessibility}${check(lh.accessibility >= t.accessibility)}`);
78
+ console.log(` SEO: ${lh.seo} / ${t.seo}${check(lh.seo >= t.seo)}`);
79
+ console.log(` Best Practices: ${lh.bestPractices} / ${t.bestPractices}${check(lh.bestPractices >= t.bestPractices)}`);
80
+ console.log(` Runs: ${lh.runs}`);
81
+ }
82
+ else {
83
+ console.log(` Lighthouse: skipped`);
84
+ }
85
+ if (result.github) {
86
+ if (result.github.status === "comment_posted")
87
+ console.log(` PR comment: posted`);
88
+ if (result.github.status === "status_set")
89
+ console.log(` Status check: ${result.github.grade}`);
90
+ }
91
+ console.log(` Result: .laxy-result.json`);
92
+ console.log(` Exit code: ${result.exitCode}`);
93
+ }
94
+ async function run() {
95
+ const args = parseArgs();
96
+ // --init
97
+ if (args.init) {
98
+ runInit(args.projectDir);
99
+ process.exit(0);
100
+ return;
101
+ }
102
+ // --badge
103
+ if (args.badge) {
104
+ const resultPath = path.join(args.projectDir, ".laxy-result.json");
105
+ if (!fs.existsSync(resultPath)) {
106
+ console.error("Error: .laxy-result.json not found. Run `npx laxy-verify .` first to generate it.");
107
+ process.exit(2);
108
+ return;
109
+ }
110
+ const content = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
111
+ const badge = generateBadge(content.grade);
112
+ console.log(badge);
113
+ process.exit(0);
114
+ return;
115
+ }
116
+ // Load config
117
+ let config;
118
+ try {
119
+ config = loadConfig({
120
+ dir: args.projectDir,
121
+ configPath: args.configPath,
122
+ ciMode: args.ciMode,
123
+ cliFlags: {
124
+ failOn: args.failOn,
125
+ skipLighthouse: args.skipLighthouse,
126
+ },
127
+ });
128
+ }
129
+ catch (err) {
130
+ console.error(`Config error: ${err instanceof Error ? err.message : String(err)}`);
131
+ process.exit(2);
132
+ return;
133
+ }
134
+ // Auto-detect framework + package manager
135
+ let detected;
136
+ try {
137
+ detected = detect(args.projectDir);
138
+ }
139
+ catch (err) {
140
+ console.error(`Detection error: ${err instanceof Error ? err.message : String(err)}`);
141
+ process.exit(2);
142
+ return;
143
+ }
144
+ // Merge config overrides
145
+ const buildCmd = config.build_command || detected.buildCmd;
146
+ const devCmd = config.dev_command || detected.devCmd;
147
+ const port = config.port;
148
+ // Phase 1: Build
149
+ let buildResult;
150
+ try {
151
+ buildResult = await runBuild(buildCmd, config.build_timeout);
152
+ }
153
+ catch (err) {
154
+ buildResult = {
155
+ success: false,
156
+ durationMs: 0,
157
+ errors: err instanceof Error ? [err.message] : [String(err)],
158
+ };
159
+ }
160
+ let scores = undefined;
161
+ let lighthouseResult = null;
162
+ const adjustedThresholds = {
163
+ performance: config.ciMode
164
+ ? config.thresholds.performance - 10
165
+ : config.thresholds.performance,
166
+ accessibility: config.thresholds.accessibility,
167
+ seo: config.thresholds.seo,
168
+ bestPractices: config.thresholds.bestPractices,
169
+ };
170
+ // Phase 2: Dev server + Lighthouse (only if build succeeded and not skipped)
171
+ if (buildResult.success && !args.skipLighthouse) {
172
+ let servePid;
173
+ try {
174
+ const serve = await startDevServer(devCmd, port, config.dev_timeout);
175
+ servePid = serve.pid;
176
+ try {
177
+ const lhResult = await runLighthouse(port, config.lighthouse_runs);
178
+ scores = lhResult.scores ?? undefined;
179
+ if (scores) {
180
+ lighthouseResult = {
181
+ performance: scores.performance,
182
+ accessibility: scores.accessibility,
183
+ seo: scores.seo,
184
+ bestPractices: scores.bestPractices,
185
+ runs: config.lighthouse_runs,
186
+ };
187
+ }
188
+ }
189
+ catch (lhErr) {
190
+ console.error(`Lighthouse error: ${lhErr instanceof Error ? lhErr.message : String(lhErr)}`);
191
+ }
192
+ }
193
+ catch (serveErr) {
194
+ console.error(`Dev server error: ${serveErr instanceof Error ? serveErr.message : String(serveErr)}`);
195
+ }
196
+ finally {
197
+ if (servePid) {
198
+ stopDevServer(servePid);
199
+ }
200
+ }
201
+ }
202
+ // Calculate grade
203
+ const gradeResult = calculateGrade({
204
+ buildSuccess: buildResult.success,
205
+ scores,
206
+ thresholds: adjustedThresholds,
207
+ failOn: config.fail_on,
208
+ });
209
+ // Build result object
210
+ const resultObj = {
211
+ grade: gradeResult.grade.charAt(0).toUpperCase() + gradeResult.grade.slice(1), // Capitalize
212
+ timestamp: new Date().toISOString(),
213
+ build: {
214
+ success: buildResult.success,
215
+ durationMs: buildResult.durationMs,
216
+ errors: buildResult.errors,
217
+ },
218
+ lighthouse: lighthouseResult,
219
+ thresholds: adjustedThresholds,
220
+ ciMode: config.ciMode,
221
+ framework: detected.framework,
222
+ exitCode: gradeResult.exitCode,
223
+ config_fail_on: config.fail_on,
224
+ };
225
+ // GitHub integration (only in Actions)
226
+ const inGitHubActions = !!process.env.GITHUB_ACTIONS;
227
+ if (inGitHubActions) {
228
+ try {
229
+ if (process.env.GITHUB_EVENT_NAME === "pull_request") {
230
+ await postPRComment(resultObj);
231
+ resultObj.github = { status: "comment_posted", grade: resultObj.grade };
232
+ }
233
+ await createStatusCheck({ grade: resultObj.grade, exitCode: resultObj.exitCode });
234
+ resultObj.github ??= { status: "status_set", grade: resultObj.grade };
235
+ }
236
+ catch (ghErr) {
237
+ console.error(`GitHub API warning: ${ghErr instanceof Error ? ghErr.message : String(ghErr)}`);
238
+ }
239
+ }
240
+ writeResultFile(args.projectDir, resultObj);
241
+ // Output
242
+ if (args.format === "json") {
243
+ console.log(JSON.stringify(resultObj, null, 2));
244
+ }
245
+ else {
246
+ consoleOutput(resultObj);
247
+ }
248
+ // Set $GITHUB_OUTPUT if in Actions
249
+ if (inGitHubActions && process.env.GITHUB_OUTPUT) {
250
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `grade=${resultObj.grade}\n`);
251
+ }
252
+ process.exit(gradeResult.exitCode);
253
+ }
254
+ run().catch((err) => {
255
+ console.error(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
256
+ process.exit(1);
257
+ });
@@ -0,0 +1,20 @@
1
+ interface LaxyResult {
2
+ grade: string;
3
+ lighthouse: {
4
+ performance: number;
5
+ accessibility: number;
6
+ seo: number;
7
+ bestPractices: number;
8
+ runs: number;
9
+ } | null;
10
+ thresholds: {
11
+ performance: number;
12
+ accessibility: number;
13
+ seo: number;
14
+ bestPractices: number;
15
+ };
16
+ exitCode: number;
17
+ config_fail_on: string;
18
+ }
19
+ export declare function postPRComment(result: LaxyResult): Promise<void>;
20
+ export {};
@@ -0,0 +1,53 @@
1
+ import * as fs from "node:fs";
2
+ import { getGitHubContext } from "./github.js";
3
+ export async function postPRComment(result) {
4
+ const ctx = getGitHubContext();
5
+ if (!ctx || ctx.eventName !== "pull_request")
6
+ return;
7
+ // Parse PR number from event
8
+ let prNumber = 0;
9
+ if (ctx.eventPath && fs.existsSync(ctx.eventPath)) {
10
+ const event = JSON.parse(fs.readFileSync(ctx.eventPath, "utf-8"));
11
+ prNumber = event.pull_request?.number ?? 0;
12
+ }
13
+ if (!prNumber)
14
+ return;
15
+ const grade = result.grade ?? "Unverified";
16
+ const lh = result.lighthouse;
17
+ const t = result.thresholds;
18
+ let lhTable = "";
19
+ if (lh) {
20
+ lhTable = `| Performance | Accessibility | SEO | Best Practices |\n|---|---|---|---|\n| ${lh.performance} / ${t.performance} | ${lh.accessibility} / ${t.accessibility} | ${lh.seo} / ${t.seo} | ${lh.bestPractices} / ${t.bestPractices} |\n\n`;
21
+ }
22
+ const emoji = grade === "Gold" ? "🥇" :
23
+ grade === "Silver" ? "🥈" :
24
+ grade === "Bronze" ? "🥉" : "⚪";
25
+ const body = `## ${emoji} Laxy Verify — **${grade}**
26
+
27
+ ${grade === "Unverified" ? "The build failed or verification could not be completed." : `Build passed verification with a **${grade}** grade.`}
28
+
29
+ ${lhTable}**Fail-on threshold**: ${result.config_fail_on ?? "bronze"}
30
+
31
+ ---
32
+ [🔍 Laxy Verify](https://github.com/psungmin24/laxy-verify) — Frontend quality gate`;
33
+ const [owner, repo] = ctx.repository.split("/");
34
+ const url = `https://api.github.com/repos/${owner}/${repo}/issues/${prNumber}/comments`;
35
+ try {
36
+ const res = await fetch(url, {
37
+ method: "POST",
38
+ headers: {
39
+ "Authorization": `Bearer ${ctx.token}`,
40
+ "Accept": "application/vnd.github.v3+json",
41
+ "Content-Type": "application/json",
42
+ },
43
+ body: JSON.stringify({ body }),
44
+ });
45
+ if (!res.ok) {
46
+ console.warn(`GitHub PR comment API returned ${res.status} — skipping comment`);
47
+ return;
48
+ }
49
+ }
50
+ catch (err) {
51
+ console.warn(`GitHub PR comment request failed: ${err instanceof Error ? err.message : String(err)}`);
52
+ }
53
+ }
package/dist/config.d.ts CHANGED
@@ -1,16 +1,34 @@
1
- export interface LaxyConfig {
2
- thresholds: {
3
- performance: number;
4
- accessibility: number;
5
- seo: number;
6
- bestPractices: number;
7
- };
8
- buildCommand?: string;
9
- devCommand?: string;
10
- port: number;
11
- runs: number;
12
- }
13
- export declare function loadConfig(projectPath: string, ciMode?: boolean): {
14
- config: LaxyConfig;
15
- warnings: string[];
16
- };
1
+ export type FailOn = "unverified" | "bronze" | "silver" | "gold";
2
+ export interface Thresholds {
3
+ performance: number;
4
+ accessibility: number;
5
+ seo: number;
6
+ bestPractices: number;
7
+ }
8
+ export interface LaxyConfig {
9
+ framework: string;
10
+ build_command: string;
11
+ dev_command: string;
12
+ package_manager: string;
13
+ port: number;
14
+ build_timeout: number;
15
+ dev_timeout: number;
16
+ lighthouse_runs: number;
17
+ thresholds: Thresholds;
18
+ fail_on: FailOn;
19
+ }
20
+ export declare class ConfigParseError extends Error {
21
+ constructor(msg: string);
22
+ }
23
+ export interface LoadConfigOptions {
24
+ dir: string;
25
+ configPath?: string;
26
+ cliFlags?: {
27
+ failOn?: FailOn;
28
+ skipLighthouse?: boolean;
29
+ };
30
+ ciMode: boolean;
31
+ }
32
+ export declare function loadConfig(options: LoadConfigOptions): LaxyConfig & {
33
+ ciMode: boolean;
34
+ };