laxy-verify 1.1.10 → 1.1.11

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.
@@ -39,88 +39,163 @@ exports.allViewportsPass = allViewportsPass;
39
39
  /**
40
40
  * Pro+ multi-viewport Lighthouse checks.
41
41
  *
42
- * This runs one Lighthouse collection per viewport and summarizes the results
43
- * for desktop, tablet, and mobile. The median logic is kept so the output
44
- * shape stays compatible with the existing verification report flow.
42
+ * Each viewport runs through the same direct Lighthouse execution path used by
43
+ * the main verify flow so Windows cleanup behavior is consistent.
45
44
  */
46
45
  const node_child_process_1 = require("node:child_process");
47
46
  const fs = __importStar(require("node:fs"));
48
47
  const path = __importStar(require("node:path"));
49
- const node_module_1 = require("node:module");
50
- const req = (0, node_module_1.createRequire)(__filename);
51
- function resolveLhciBin() {
52
- return req.resolve("@lhci/cli/src/cli.js");
53
- }
54
48
  const VIEWPORTS = [
55
- { name: "desktop", preset: "desktop" },
56
- { name: "tablet", preset: "desktop", screenEmulation: "1024x768" },
57
- { name: "mobile", preset: "perf" },
49
+ {
50
+ name: "desktop",
51
+ formFactor: "desktop",
52
+ screen: { mobile: false, width: 1350, height: 940, deviceScaleFactor: 1, disabled: false },
53
+ },
54
+ {
55
+ name: "tablet",
56
+ formFactor: "desktop",
57
+ screen: { mobile: false, width: 1024, height: 768, deviceScaleFactor: 1, disabled: false },
58
+ },
59
+ {
60
+ name: "mobile",
61
+ formFactor: "mobile",
62
+ screen: { mobile: true, width: 390, height: 844, deviceScaleFactor: 2, disabled: false },
63
+ },
58
64
  ];
59
- async function runLighthouseForViewport(port, viewport, outputDir) {
60
- const lhciBin = resolveLhciBin();
61
- const vpDir = path.join(outputDir, viewport.name);
62
- if (!fs.existsSync(vpDir))
63
- fs.mkdirSync(vpDir, { recursive: true });
64
- const args = [
65
- lhciBin,
66
- "collect",
67
- `--url=http://localhost:${port}`,
68
- "--numberOfRuns=1",
69
- `--outputDir=${vpDir}`,
70
- `--preset=${viewport.preset}`,
71
- ];
72
- const child = (0, node_child_process_1.spawn)("node", args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
65
+ function sleep(ms) {
66
+ return new Promise((resolve) => setTimeout(resolve, ms));
67
+ }
68
+ async function removeDirWithRetries(dirPath, retries = 5) {
69
+ for (let attempt = 0; attempt < retries; attempt++) {
70
+ try {
71
+ if (fs.existsSync(dirPath)) {
72
+ fs.rmSync(dirPath, { recursive: true, force: true });
73
+ }
74
+ return;
75
+ }
76
+ catch {
77
+ if (attempt === retries - 1)
78
+ return;
79
+ await sleep(250);
80
+ }
81
+ }
82
+ }
83
+ function writeViewportRunnerScript(runnerPath) {
84
+ const source = `import fs from "node:fs/promises";
85
+ import lighthouse from "lighthouse";
86
+ import { launch } from "chrome-launcher";
87
+
88
+ const [url, reportPath, chromeDir, formFactor, screenJson] = process.argv.slice(2);
89
+ const screen = JSON.parse(screenJson);
90
+
91
+ const chrome = await launch({
92
+ logLevel: "error",
93
+ chromeFlags: [
94
+ "--headless=new",
95
+ "--no-sandbox",
96
+ "--disable-dev-shm-usage",
97
+ \`--user-data-dir=\${chromeDir}\`,
98
+ ],
99
+ });
100
+
101
+ try {
102
+ const result = await lighthouse(
103
+ url,
104
+ {
105
+ port: chrome.port,
106
+ output: "json",
107
+ logLevel: "error",
108
+ onlyCategories: ["performance", "accessibility", "seo", "best-practices"],
109
+ },
110
+ {
111
+ extends: "lighthouse:default",
112
+ settings: {
113
+ formFactor,
114
+ screenEmulation: screen,
115
+ },
116
+ }
117
+ );
118
+
119
+ if (!result?.lhr) {
120
+ throw new Error("Lighthouse returned no report.");
121
+ }
122
+
123
+ await fs.writeFile(reportPath, JSON.stringify(result.lhr), "utf8");
124
+ } finally {
125
+ await chrome.kill();
126
+ }
127
+ `;
128
+ fs.writeFileSync(runnerPath, source, "utf-8");
129
+ }
130
+ async function runLighthouseForViewport(port, viewport, tempRoot) {
131
+ const runnerPath = path.join(tempRoot, "run-viewport-lighthouse.mjs");
132
+ const reportPath = path.join(tempRoot, `${viewport.name}.json`);
133
+ const chromeDir = path.join(tempRoot, `chrome-${viewport.name}`);
134
+ writeViewportRunnerScript(runnerPath);
135
+ fs.mkdirSync(chromeDir, { recursive: true });
136
+ const child = (0, node_child_process_1.spawn)("node", [
137
+ runnerPath,
138
+ `http://127.0.0.1:${port}/`,
139
+ reportPath,
140
+ chromeDir,
141
+ viewport.formFactor,
142
+ JSON.stringify(viewport.screen),
143
+ ], {
144
+ shell: false,
145
+ stdio: ["ignore", "pipe", "pipe"],
146
+ env: {
147
+ ...process.env,
148
+ TEMP: tempRoot,
149
+ TMP: tempRoot,
150
+ TMPDIR: tempRoot,
151
+ },
152
+ });
73
153
  child.stdout?.on("data", (chunk) => {
74
154
  const lines = chunk.toString().split("\n").filter(Boolean);
75
155
  for (const line of lines)
76
- console.log(` [lhci:${viewport.name}] ${line}`);
156
+ console.log(` [lh:${viewport.name}] ${line}`);
77
157
  });
78
158
  child.stderr?.on("data", (chunk) => {
79
159
  const lines = chunk.toString().split("\n").filter(Boolean);
80
160
  for (const line of lines)
81
- console.error(` [lhci:${viewport.name}] ${line}`);
161
+ console.error(` [lh:${viewport.name}] ${line}`);
82
162
  });
83
163
  const code = await new Promise((resolve) => child.on("exit", (exitCode) => resolve(exitCode ?? 1)));
84
- if (code !== 0)
164
+ if (code !== 0 || !fs.existsSync(reportPath)) {
165
+ await removeDirWithRetries(chromeDir);
85
166
  return null;
167
+ }
86
168
  try {
87
- const files = fs.readdirSync(vpDir).filter((file) => file.startsWith("lhr-") && file.endsWith(".json"));
88
- if (files.length === 0)
89
- return null;
90
- const scores = files.map((file) => {
91
- const lhr = JSON.parse(fs.readFileSync(path.join(vpDir, file), "utf-8"));
92
- return {
93
- performance: Math.round((lhr.categories.performance?.score ?? 0) * 100),
94
- accessibility: Math.round((lhr.categories.accessibility?.score ?? 0) * 100),
95
- seo: Math.round((lhr.categories.seo?.score ?? 0) * 100),
96
- bestPractices: Math.round((lhr.categories["best-practices"]?.score ?? 0) * 100),
97
- };
98
- });
99
- const median = (values) => {
100
- const sorted = [...values].sort((a, b) => a - b);
101
- const mid = Math.floor(sorted.length / 2);
102
- return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
103
- };
169
+ const lhr = JSON.parse(fs.readFileSync(reportPath, "utf-8"));
104
170
  return {
105
- performance: median(scores.map((score) => score.performance)),
106
- accessibility: median(scores.map((score) => score.accessibility)),
107
- seo: median(scores.map((score) => score.seo)),
108
- bestPractices: median(scores.map((score) => score.bestPractices)),
171
+ performance: Math.round((lhr.categories.performance?.score ?? 0) * 100),
172
+ accessibility: Math.round((lhr.categories.accessibility?.score ?? 0) * 100),
173
+ seo: Math.round((lhr.categories.seo?.score ?? 0) * 100),
174
+ bestPractices: Math.round((lhr.categories["best-practices"]?.score ?? 0) * 100),
109
175
  };
110
176
  }
111
177
  catch {
112
178
  return null;
113
179
  }
180
+ finally {
181
+ await removeDirWithRetries(chromeDir);
182
+ }
114
183
  }
115
184
  async function runMultiViewportLighthouse(port) {
116
185
  console.log("\n [Pro+] Running multi-viewport Lighthouse checks (desktop, tablet, mobile)...");
117
- const outputDir = ".lighthouseci-mvp";
186
+ const tempRoot = path.join(process.cwd(), ".laxy-tmp", `multi-viewport-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
187
+ fs.mkdirSync(tempRoot, { recursive: true });
118
188
  const results = { desktop: null, tablet: null, mobile: null };
119
- for (const viewport of VIEWPORTS) {
120
- console.log(`\n Viewport: ${viewport.name}`);
121
- results[viewport.name] = await runLighthouseForViewport(port, viewport, outputDir);
189
+ try {
190
+ for (const viewport of VIEWPORTS) {
191
+ console.log(`\n Viewport: ${viewport.name}`);
192
+ results[viewport.name] = await runLighthouseForViewport(port, viewport, tempRoot);
193
+ }
194
+ return results;
195
+ }
196
+ finally {
197
+ await removeDirWithRetries(tempRoot);
122
198
  }
123
- return results;
124
199
  }
125
200
  function printMultiViewportResults(scores, thresholds) {
126
201
  const check = (passed) => (passed ? "OK" : "FAIL");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "laxy-verify",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
4
4
  "description": "Frontend quality gate: build + Lighthouse verification",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "@lhci/cli": "^0.14.0",
21
21
  "chrome-launcher": "^0.13.4",
22
- "js-yaml": "^4.1.0",
22
+ "js-yaml": "^4.1.0",
23
23
  "lighthouse": "^12.1.0",
24
24
  "pixelmatch": "^7.1.0",
25
25
  "pngjs": "^7.0.0",