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.
- package/dist/multi-viewport.js +129 -54
- package/package.json +2 -2
package/dist/multi-viewport.js
CHANGED
|
@@ -39,88 +39,163 @@ exports.allViewportsPass = allViewportsPass;
|
|
|
39
39
|
/**
|
|
40
40
|
* Pro+ multi-viewport Lighthouse checks.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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(` [
|
|
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(` [
|
|
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
|
|
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:
|
|
106
|
-
accessibility:
|
|
107
|
-
seo:
|
|
108
|
-
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
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
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",
|