lighthouse-reporting 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.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # lighthouse-reporting
2
+
3
+ > Generate Lighthouse reports in HTML, CSV, and JSON formats, optimized for publishing in Jenkins or GitHub Actions.
4
+
5
+ The reports include trend history support, allowing you to track performance improvements over time.
6
+
7
+ ## Usage
8
+
9
+ ### Jenkins
10
+
11
+ ### GitHub Actions
12
+
13
+ TODO
@@ -0,0 +1,8 @@
1
+ const INDEX_HTML = "index.html";
2
+ const LH_OUT_DIR = "lighthouse";
3
+ const PW_TMP_DIR = "pwlh";
4
+ export {
5
+ INDEX_HTML as I,
6
+ LH_OUT_DIR as L,
7
+ PW_TMP_DIR as P
8
+ };
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ const INDEX_HTML = "index.html";
3
+ const LH_OUT_DIR = "lighthouse";
4
+ const PW_TMP_DIR = "pwlh";
5
+ exports.INDEX_HTML = INDEX_HTML;
6
+ exports.LH_OUT_DIR = LH_OUT_DIR;
7
+ exports.PW_TMP_DIR = PW_TMP_DIR;
@@ -0,0 +1,3 @@
1
+ export declare const INDEX_HTML = "index.html";
2
+ export declare const LH_OUT_DIR = "lighthouse";
3
+ export declare const PW_TMP_DIR = "pwlh";
package/dist/hooks.cjs ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const fse = require("fs-extra");
6
+ const url = require("url");
7
+ const constants = require("./constants-667b8033.cjs");
8
+ let dirname;
9
+ if (typeof __dirname !== "string") {
10
+ const filename = url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : document.currentScript && document.currentScript.src || new URL("hooks.cjs", document.baseURI).href);
11
+ dirname = path.dirname(filename);
12
+ } else {
13
+ dirname = __dirname;
14
+ }
15
+ const reportDir = path.join(process.cwd(), process.env.LH_REPORT_DIR || constants.LH_OUT_DIR);
16
+ const htmlTemplatePath = path.join(dirname, "lighthouse.html");
17
+ async function lighthouseSetup() {
18
+ await fse.ensureDir(reportDir);
19
+ await fse.copyFile(htmlTemplatePath, path.join(reportDir, constants.INDEX_HTML));
20
+ }
21
+ async function lighthousePlaywrightTeardown() {
22
+ await fse.remove(path.join(os.tmpdir(), constants.PW_TMP_DIR));
23
+ }
24
+ exports.lighthousePlaywrightTeardown = lighthousePlaywrightTeardown;
25
+ exports.lighthouseSetup = lighthouseSetup;
@@ -0,0 +1,2 @@
1
+ export declare function lighthouseSetup(): Promise<void>;
2
+ export declare function lighthousePlaywrightTeardown(): Promise<void>;
package/dist/hooks.js ADDED
@@ -0,0 +1,25 @@
1
+ import os from "os";
2
+ import path from "path";
3
+ import fse from "fs-extra";
4
+ import url from "url";
5
+ import { L as LH_OUT_DIR, I as INDEX_HTML, P as PW_TMP_DIR } from "./constants-226e9774.js";
6
+ let dirname;
7
+ if (typeof __dirname !== "string") {
8
+ const filename = url.fileURLToPath(import.meta.url);
9
+ dirname = path.dirname(filename);
10
+ } else {
11
+ dirname = __dirname;
12
+ }
13
+ const reportDir = path.join(process.cwd(), process.env.LH_REPORT_DIR || LH_OUT_DIR);
14
+ const htmlTemplatePath = path.join(dirname, "lighthouse.html");
15
+ async function lighthouseSetup() {
16
+ await fse.ensureDir(reportDir);
17
+ await fse.copyFile(htmlTemplatePath, path.join(reportDir, INDEX_HTML));
18
+ }
19
+ async function lighthousePlaywrightTeardown() {
20
+ await fse.remove(path.join(os.tmpdir(), PW_TMP_DIR));
21
+ }
22
+ export {
23
+ lighthousePlaywrightTeardown,
24
+ lighthouseSetup
25
+ };
package/dist/index.cjs ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const hooks = require("./hooks.cjs");
4
+ const lighthouseReports = require("./lighthouseReports.cjs");
5
+ const lighthouseTest = require("./lighthouseTest.cjs");
6
+ require("os");
7
+ require("path");
8
+ require("fs-extra");
9
+ require("url");
10
+ require("./constants-667b8033.cjs");
11
+ require("get-port");
12
+ require("@playwright/test");
13
+ exports.lighthousePlaywrightTeardown = hooks.lighthousePlaywrightTeardown;
14
+ exports.lighthouseSetup = hooks.lighthouseSetup;
15
+ exports.getScores = lighthouseReports.getScores;
16
+ exports.writeCsvResult = lighthouseReports.writeCsvResult;
17
+ exports.writeHtmlListEntryWithRetry = lighthouseReports.writeHtmlListEntryWithRetry;
18
+ exports.lighthouseTest = lighthouseTest.lighthouseTest;
@@ -0,0 +1,3 @@
1
+ export * from './hooks';
2
+ export * from './lighthouseReports';
3
+ export * from './lighthouseTest';
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ import { lighthousePlaywrightTeardown, lighthouseSetup } from "./hooks.js";
2
+ import { getScores, writeCsvResult, writeHtmlListEntryWithRetry } from "./lighthouseReports.js";
3
+ import { lighthouseTest } from "./lighthouseTest.js";
4
+ import "os";
5
+ import "path";
6
+ import "fs-extra";
7
+ import "url";
8
+ import "./constants-226e9774.js";
9
+ import "get-port";
10
+ import "@playwright/test";
11
+ export {
12
+ getScores,
13
+ lighthousePlaywrightTeardown,
14
+ lighthouseSetup,
15
+ lighthouseTest,
16
+ writeCsvResult,
17
+ writeHtmlListEntryWithRetry
18
+ };
@@ -0,0 +1,202 @@
1
+ (function(global, factory) {
2
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("os"), require("path"), require("fs-extra"), require("url"), require("net"), require("@playwright/test")) : typeof define === "function" && define.amd ? define(["exports", "os", "path", "fs-extra", "url", "net", "@playwright/test"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.lh_rep = {}, global.os, global.path, global.fse, global.url, global.net, global.test));
3
+ })(this, function(exports2, os, path, fse, url, net, test) {
4
+ "use strict";
5
+ const INDEX_HTML = "index.html";
6
+ const LH_OUT_DIR = "lighthouse";
7
+ const PW_TMP_DIR = "pwlh";
8
+ let dirname;
9
+ if (typeof __dirname !== "string") {
10
+ const filename = url.fileURLToPath(typeof document === "undefined" && typeof location === "undefined" ? require("url").pathToFileURL(__filename).href : typeof document === "undefined" ? location.href : document.currentScript && document.currentScript.src || new URL("lighthouse-reporting.umd.cjs", document.baseURI).href);
11
+ dirname = path.dirname(filename);
12
+ } else {
13
+ dirname = __dirname;
14
+ }
15
+ const reportDir = path.join(process.cwd(), process.env.LH_REPORT_DIR || LH_OUT_DIR);
16
+ const htmlTemplatePath = path.join(dirname, "lighthouse.html");
17
+ async function lighthouseSetup() {
18
+ await fse.ensureDir(reportDir);
19
+ await fse.copyFile(htmlTemplatePath, path.join(reportDir, INDEX_HTML));
20
+ }
21
+ async function lighthousePlaywrightTeardown() {
22
+ await fse.remove(path.join(os.tmpdir(), PW_TMP_DIR));
23
+ }
24
+ const writeCsvResult = async (reportDir2, name, scores, thresholds, swimlanes = []) => {
25
+ let csvData = Object.keys(scores).join(",");
26
+ if (swimlanes.length > 0) {
27
+ csvData += "," + swimlanes.map((swimlane) => `${swimlane}_threshold`);
28
+ }
29
+ csvData += "\n";
30
+ csvData += Object.values(scores).join(",");
31
+ if (swimlanes.length > 0) {
32
+ csvData += "," + swimlanes.map((swimlane) => thresholds[swimlane]);
33
+ }
34
+ await fse.writeFile(path.join(reportDir2, `${name}.csv`), csvData);
35
+ };
36
+ const writeHtmlListEntry = async (htmlFilePath, name, scores, thresholds, error, addReportLink) => {
37
+ const htmlFileData = (await fse.readFile(htmlFilePath)).toString("utf-8").split("\n");
38
+ const insertIdx = htmlFileData.findIndex((v) => v.includes("// lighthouse-page-results"));
39
+ if (insertIdx < 0) {
40
+ throw new Error("Failed to write results to index.html");
41
+ }
42
+ htmlFileData.splice(insertIdx + 1, 0, JSON.stringify({ href: `${name}.html`, name, scores, thresholds, error, addReportLink }) + ",");
43
+ await fse.writeFile(htmlFilePath, htmlFileData.join("\n"));
44
+ };
45
+ const writeHtmlListEntryWithRetry = async (htmlFilePath, name, scores, thresholds, comparisonError, addReportLink = true) => {
46
+ let attempts = 10;
47
+ let error;
48
+ while (attempts > 0) {
49
+ attempts--;
50
+ try {
51
+ await writeHtmlListEntry(htmlFilePath, name, scores, thresholds, comparisonError, addReportLink);
52
+ return;
53
+ } catch (e) {
54
+ error = e;
55
+ }
56
+ await new Promise((resolve) => setTimeout(resolve, 10));
57
+ }
58
+ throw error;
59
+ };
60
+ const getScores = (result) => Object.entries(result.lhr.categories).reduce((prev, [key, c]) => {
61
+ prev[key] = Math.floor(c.score * 100);
62
+ return prev;
63
+ }, {});
64
+ class Locked extends Error {
65
+ constructor(port) {
66
+ super(`${port} is locked`);
67
+ }
68
+ }
69
+ const lockedPorts = {
70
+ old: /* @__PURE__ */ new Set(),
71
+ young: /* @__PURE__ */ new Set()
72
+ };
73
+ const releaseOldLockedPortsIntervalMs = 1e3 * 15;
74
+ let interval;
75
+ const getLocalHosts = () => {
76
+ const interfaces = os.networkInterfaces();
77
+ const results = /* @__PURE__ */ new Set([void 0, "0.0.0.0"]);
78
+ for (const _interface of Object.values(interfaces)) {
79
+ for (const config of _interface) {
80
+ results.add(config.address);
81
+ }
82
+ }
83
+ return results;
84
+ };
85
+ const checkAvailablePort = (options) => new Promise((resolve, reject) => {
86
+ const server = net.createServer();
87
+ server.unref();
88
+ server.on("error", reject);
89
+ server.listen(options, () => {
90
+ const { port } = server.address();
91
+ server.close(() => {
92
+ resolve(port);
93
+ });
94
+ });
95
+ });
96
+ const getAvailablePort = async (options, hosts) => {
97
+ if (options.host || options.port === 0) {
98
+ return checkAvailablePort(options);
99
+ }
100
+ for (const host of hosts) {
101
+ try {
102
+ await checkAvailablePort({ port: options.port, host });
103
+ } catch (error) {
104
+ if (!["EADDRNOTAVAIL", "EINVAL"].includes(error.code)) {
105
+ throw error;
106
+ }
107
+ }
108
+ }
109
+ return options.port;
110
+ };
111
+ const portCheckSequence = function* (ports) {
112
+ if (ports) {
113
+ yield* ports;
114
+ }
115
+ yield 0;
116
+ };
117
+ async function getPorts(options) {
118
+ let ports;
119
+ let exclude = /* @__PURE__ */ new Set();
120
+ if (options) {
121
+ if (options.port) {
122
+ ports = typeof options.port === "number" ? [options.port] : options.port;
123
+ }
124
+ if (options.exclude) {
125
+ const excludeIterable = options.exclude;
126
+ if (typeof excludeIterable[Symbol.iterator] !== "function") {
127
+ throw new TypeError("The `exclude` option must be an iterable.");
128
+ }
129
+ for (const element of excludeIterable) {
130
+ if (typeof element !== "number") {
131
+ throw new TypeError("Each item in the `exclude` option must be a number corresponding to the port you want excluded.");
132
+ }
133
+ if (!Number.isSafeInteger(element)) {
134
+ throw new TypeError(`Number ${element} in the exclude option is not a safe integer and can't be used`);
135
+ }
136
+ }
137
+ exclude = new Set(excludeIterable);
138
+ }
139
+ }
140
+ if (interval === void 0) {
141
+ interval = setInterval(() => {
142
+ lockedPorts.old = lockedPorts.young;
143
+ lockedPorts.young = /* @__PURE__ */ new Set();
144
+ }, releaseOldLockedPortsIntervalMs);
145
+ if (interval.unref) {
146
+ interval.unref();
147
+ }
148
+ }
149
+ const hosts = getLocalHosts();
150
+ for (const port of portCheckSequence(ports)) {
151
+ try {
152
+ if (exclude.has(port)) {
153
+ continue;
154
+ }
155
+ let availablePort = await getAvailablePort({ ...options, port }, hosts);
156
+ while (lockedPorts.old.has(availablePort) || lockedPorts.young.has(availablePort)) {
157
+ if (port !== 0) {
158
+ throw new Locked(port);
159
+ }
160
+ availablePort = await getAvailablePort({ ...options, port }, hosts);
161
+ }
162
+ lockedPorts.young.add(availablePort);
163
+ return availablePort;
164
+ } catch (error) {
165
+ if (!["EADDRINUSE", "EACCES"].includes(error.code) && !(error instanceof Locked)) {
166
+ throw error;
167
+ }
168
+ }
169
+ }
170
+ throw new Error("No available ports found");
171
+ }
172
+ const lighthouseTest = test.test.extend({
173
+ port: [
174
+ // eslint-disable-next-line no-empty-pattern
175
+ async ({}, use) => {
176
+ const port = await getPorts();
177
+ await use(port);
178
+ },
179
+ { scope: "worker" }
180
+ ],
181
+ context: [
182
+ async ({ port, launchOptions }, use) => {
183
+ const context = await test.chromium.launchPersistentContext(
184
+ path.join(os.tmpdir(), PW_TMP_DIR, `${Math.random()}`.replace(".", "")),
185
+ {
186
+ args: [...launchOptions.args || [], `--remote-debugging-port=${port}`]
187
+ }
188
+ );
189
+ await use(context);
190
+ await context.close();
191
+ },
192
+ { scope: "test" }
193
+ ]
194
+ });
195
+ exports2.getScores = getScores;
196
+ exports2.lighthousePlaywrightTeardown = lighthousePlaywrightTeardown;
197
+ exports2.lighthouseSetup = lighthouseSetup;
198
+ exports2.lighthouseTest = lighthouseTest;
199
+ exports2.writeCsvResult = writeCsvResult;
200
+ exports2.writeHtmlListEntryWithRetry = writeHtmlListEntryWithRetry;
201
+ Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
202
+ });
@@ -0,0 +1,187 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <title>Lighthouse Results</title>
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <title>Lighthouse</title>
7
+ <style>
8
+ body {
9
+ margin: 0px;
10
+ font-size: 0.9em;
11
+ font-family: sans-serif;
12
+ }
13
+ iframe {
14
+ display: block;
15
+ border: none;
16
+ height: 100vh;
17
+ width: 100vw;
18
+ }
19
+
20
+ #table-filter {
21
+ margin: 10px;
22
+ }
23
+
24
+ #scrollTop {
25
+ margin: 0 10px;
26
+ }
27
+
28
+ #lhTable {
29
+ border-collapse: collapse;
30
+ margin: 10px;
31
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
32
+ }
33
+ .hide,
34
+ #lhTable.hide-passed tbody > tr:not(.has-error) {
35
+ display: none;
36
+ }
37
+ #lhTable .lh-fail {
38
+ color: #a51515;
39
+ }
40
+ #lhTable .lh-pass {
41
+ color: #008000;
42
+ }
43
+ #lhTable thead tr {
44
+ background-color: #4c90e2;
45
+ color: #ffffff;
46
+ text-align: left;
47
+ }
48
+ #lhTable th,
49
+ #lhTable td {
50
+ padding: 12px 15px;
51
+ }
52
+ #lhTable tbody tr {
53
+ border-bottom: 1px solid #dddddd;
54
+ }
55
+ #lhTable tbody tr:nth-of-type(even) {
56
+ background-color: #f3f3f3;
57
+ }
58
+ #lhTable tbody tr:hover {
59
+ background-color: #b7d2f3;
60
+ }
61
+ #lhTable tfoot tr {
62
+ border-top: 2px solid #4c90e2;
63
+ font-weight: bold;
64
+ }
65
+ </style>
66
+ </head>
67
+ <body>
68
+ <div id="table-filter">
69
+ <label for="showPassed"><input type="checkbox" name="showPassed" id="showPassed" />Show passed?</label>
70
+ </div>
71
+
72
+ <table id="lhTable" class="hide-passed">
73
+ <thead></thead>
74
+ <tbody></tbody>
75
+ <tfoot></tfoot>
76
+ </table>
77
+
78
+ <button id="scrollTop" class="hide" aria-label="Scroll Top" title="Close Lighthouse report and scroll top">Scroll Top</button>
79
+
80
+ <iframe id="lh-iframe" height="100%" width="100%" frameborder="0" src="" title="Lighthouse report"></iframe>
81
+ </body>
82
+ <script type="text/javascript">
83
+ window.onbeforeunload = () => window.scrollTo(0, 0)
84
+
85
+ const results = [
86
+ // lighthouse-page-results
87
+ ]
88
+
89
+ let sortKey
90
+ if (results.length > 0 && results[0].scores && typeof results[0].scores[Object.keys(results[0].scores).sort()[0]] === 'number') {
91
+ sortKey = Object.keys(results[0].scores).sort()[0]
92
+ }
93
+ results.sort((a, b) => {
94
+ if (sortKey) {
95
+ return a.scores[sortKey] - b.scores[sortKey]
96
+ }
97
+ return b.error ? 1 : -1
98
+ })
99
+
100
+ const createTr = (data, cellType = 'td', isError = false) => {
101
+ const row = document.createElement('tr')
102
+ if (isError) {
103
+ row.classList.add('has-error')
104
+ }
105
+ data.forEach((el) => {
106
+ const cell = document.createElement(cellType)
107
+ cell.appendChild(el)
108
+ row.appendChild(cell)
109
+ })
110
+ return row
111
+ }
112
+
113
+ const lhTable = document.getElementById('lhTable')
114
+
115
+ // header data
116
+ const lhTableHeader = lhTable.getElementsByTagName('thead')[0]
117
+ const headers = ['Result', ...Object.keys(results[0].scores), 'Page'].map((t) => document.createTextNode(t))
118
+ lhTableHeader.appendChild(createTr(headers, 'th'))
119
+
120
+ // footer data
121
+ const lhTableFooter = lhTable.getElementsByTagName('tfoot')[0]
122
+ lhTableFooter.appendChild(
123
+ createTr(
124
+ ['TOTAL', '', `Failed ${results.filter((r) => r.error).length} of ${results.length}`].map((t) => document.createTextNode(t))
125
+ )
126
+ )
127
+
128
+ // body data
129
+ const lhTableBody = lhTable.getElementsByTagName('tbody')[0]
130
+ results.forEach((r) => {
131
+ const rowData = []
132
+
133
+ // Result
134
+ const resultText = document.createElement('span')
135
+ resultText.classList.add(r.error ? 'lh-fail' : 'lh-pass')
136
+ resultText.appendChild(document.createTextNode(r.error ? 'FAIL' : 'PASS'))
137
+ rowData.push(resultText)
138
+
139
+ // Scores
140
+ Object.entries(r.scores).forEach(([k, v]) => {
141
+ rowData.push(document.createTextNode(`${v} (${r.thresholds[k]})`))
142
+ })
143
+
144
+ // Page
145
+ if (r.addReportLink) {
146
+ const link = document.createElement('a')
147
+ link.href = r.href
148
+ link.appendChild(document.createTextNode(r.name))
149
+ rowData.push(link)
150
+ } else {
151
+ rowData.push(document.createTextNode(r.name))
152
+ }
153
+
154
+ // append row to body
155
+ lhTableBody.appendChild(createTr(rowData, 'td', !!r.error))
156
+ })
157
+
158
+ const iframe = document.getElementById('lh-iframe')
159
+ const scrollTopButton = document.getElementById('scrollTop')
160
+
161
+ // add click listener to open specific lighthouse report
162
+ lhTableBody.addEventListener('click', (e) => {
163
+ if (e.target && e.target.nodeName == 'A') {
164
+ iframe.src = e.target.href
165
+ iframe.scrollIntoView({ behavior: 'smooth' })
166
+ scrollTopButton.classList.remove('hide')
167
+ }
168
+ e.preventDefault()
169
+ e.stopPropagation()
170
+ })
171
+
172
+ // scroll top
173
+ scrollTopButton.addEventListener('click', (e) => {
174
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
175
+ iframe.src = ''
176
+ scrollTopButton.classList.add('hide')
177
+ })
178
+
179
+ // checkbox change listener to show/hide passed
180
+ document
181
+ .getElementById('table-filter')
182
+ .getElementsByTagName('input')[0]
183
+ .addEventListener('change', (e) => {
184
+ lhTable.classList.toggle('hide-passed')
185
+ })
186
+ </script>
187
+ </html>
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const path = require("path");
4
+ const fse = require("fs-extra");
5
+ const writeCsvResult = async (reportDir, name, scores, thresholds, swimlanes = []) => {
6
+ let csvData = Object.keys(scores).join(",");
7
+ if (swimlanes.length > 0) {
8
+ csvData += "," + swimlanes.map((swimlane) => `${swimlane}_threshold`);
9
+ }
10
+ csvData += "\n";
11
+ csvData += Object.values(scores).join(",");
12
+ if (swimlanes.length > 0) {
13
+ csvData += "," + swimlanes.map((swimlane) => thresholds[swimlane]);
14
+ }
15
+ await fse.writeFile(path.join(reportDir, `${name}.csv`), csvData);
16
+ };
17
+ const writeHtmlListEntry = async (htmlFilePath, name, scores, thresholds, error, addReportLink) => {
18
+ const htmlFileData = (await fse.readFile(htmlFilePath)).toString("utf-8").split("\n");
19
+ const insertIdx = htmlFileData.findIndex((v) => v.includes("// lighthouse-page-results"));
20
+ if (insertIdx < 0) {
21
+ throw new Error("Failed to write results to index.html");
22
+ }
23
+ htmlFileData.splice(insertIdx + 1, 0, JSON.stringify({ href: `${name}.html`, name, scores, thresholds, error, addReportLink }) + ",");
24
+ await fse.writeFile(htmlFilePath, htmlFileData.join("\n"));
25
+ };
26
+ const writeHtmlListEntryWithRetry = async (htmlFilePath, name, scores, thresholds, comparisonError, addReportLink = true) => {
27
+ let attempts = 10;
28
+ let error;
29
+ while (attempts > 0) {
30
+ attempts--;
31
+ try {
32
+ await writeHtmlListEntry(htmlFilePath, name, scores, thresholds, comparisonError, addReportLink);
33
+ return;
34
+ } catch (e) {
35
+ error = e;
36
+ }
37
+ await new Promise((resolve) => setTimeout(resolve, 10));
38
+ }
39
+ throw error;
40
+ };
41
+ const getScores = (result) => Object.entries(result.lhr.categories).reduce((prev, [key, c]) => {
42
+ prev[key] = Math.floor(c.score * 100);
43
+ return prev;
44
+ }, {});
45
+ exports.getScores = getScores;
46
+ exports.writeCsvResult = writeCsvResult;
47
+ exports.writeHtmlListEntryWithRetry = writeHtmlListEntryWithRetry;
@@ -0,0 +1,16 @@
1
+ export interface LighthouseResult {
2
+ lhr: {
3
+ categories: {
4
+ [k: string]: {
5
+ score: number;
6
+ };
7
+ };
8
+ };
9
+ comparisonError?: string;
10
+ }
11
+ export declare const writeCsvResult: (reportDir: string, name: string, scores: Record<string, number>, thresholds: Record<string, number>, swimlanes?: Array<string>) => Promise<void>;
12
+ /**
13
+ * workaround conflicts when multiple threads write the same file
14
+ */
15
+ export declare const writeHtmlListEntryWithRetry: (htmlFilePath: string, name: string, scores: Record<string, number>, thresholds: Record<string, number>, comparisonError?: string, addReportLink?: boolean) => Promise<void>;
16
+ export declare const getScores: (result: LighthouseResult) => Record<string, number>;
@@ -0,0 +1,47 @@
1
+ import path from "path";
2
+ import fse from "fs-extra";
3
+ const writeCsvResult = async (reportDir, name, scores, thresholds, swimlanes = []) => {
4
+ let csvData = Object.keys(scores).join(",");
5
+ if (swimlanes.length > 0) {
6
+ csvData += "," + swimlanes.map((swimlane) => `${swimlane}_threshold`);
7
+ }
8
+ csvData += "\n";
9
+ csvData += Object.values(scores).join(",");
10
+ if (swimlanes.length > 0) {
11
+ csvData += "," + swimlanes.map((swimlane) => thresholds[swimlane]);
12
+ }
13
+ await fse.writeFile(path.join(reportDir, `${name}.csv`), csvData);
14
+ };
15
+ const writeHtmlListEntry = async (htmlFilePath, name, scores, thresholds, error, addReportLink) => {
16
+ const htmlFileData = (await fse.readFile(htmlFilePath)).toString("utf-8").split("\n");
17
+ const insertIdx = htmlFileData.findIndex((v) => v.includes("// lighthouse-page-results"));
18
+ if (insertIdx < 0) {
19
+ throw new Error("Failed to write results to index.html");
20
+ }
21
+ htmlFileData.splice(insertIdx + 1, 0, JSON.stringify({ href: `${name}.html`, name, scores, thresholds, error, addReportLink }) + ",");
22
+ await fse.writeFile(htmlFilePath, htmlFileData.join("\n"));
23
+ };
24
+ const writeHtmlListEntryWithRetry = async (htmlFilePath, name, scores, thresholds, comparisonError, addReportLink = true) => {
25
+ let attempts = 10;
26
+ let error;
27
+ while (attempts > 0) {
28
+ attempts--;
29
+ try {
30
+ await writeHtmlListEntry(htmlFilePath, name, scores, thresholds, comparisonError, addReportLink);
31
+ return;
32
+ } catch (e) {
33
+ error = e;
34
+ }
35
+ await new Promise((resolve) => setTimeout(resolve, 10));
36
+ }
37
+ throw error;
38
+ };
39
+ const getScores = (result) => Object.entries(result.lhr.categories).reduce((prev, [key, c]) => {
40
+ prev[key] = Math.floor(c.score * 100);
41
+ return prev;
42
+ }, {});
43
+ export {
44
+ getScores,
45
+ writeCsvResult,
46
+ writeHtmlListEntryWithRetry
47
+ };
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const getPort = require("get-port");
6
+ const test = require("@playwright/test");
7
+ const constants = require("./constants-667b8033.cjs");
8
+ const lighthouseTest = test.test.extend({
9
+ port: [
10
+ // eslint-disable-next-line no-empty-pattern
11
+ async ({}, use) => {
12
+ const port = await getPort();
13
+ await use(port);
14
+ },
15
+ { scope: "worker" }
16
+ ],
17
+ context: [
18
+ async ({ port, launchOptions }, use) => {
19
+ const context = await test.chromium.launchPersistentContext(
20
+ path.join(os.tmpdir(), constants.PW_TMP_DIR, `${Math.random()}`.replace(".", "")),
21
+ {
22
+ args: [...launchOptions.args || [], `--remote-debugging-port=${port}`]
23
+ }
24
+ );
25
+ await use(context);
26
+ await context.close();
27
+ },
28
+ { scope: "test" }
29
+ ]
30
+ });
31
+ exports.lighthouseTest = lighthouseTest;
@@ -0,0 +1,6 @@
1
+ import type { BrowserContext } from '@playwright/test';
2
+ export declare const lighthouseTest: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
3
+ context: BrowserContext;
4
+ }, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions & {
5
+ port: number;
6
+ }>;
@@ -0,0 +1,31 @@
1
+ import os from "os";
2
+ import path from "path";
3
+ import getPort from "get-port";
4
+ import { test, chromium } from "@playwright/test";
5
+ import { P as PW_TMP_DIR } from "./constants-226e9774.js";
6
+ const lighthouseTest = test.extend({
7
+ port: [
8
+ // eslint-disable-next-line no-empty-pattern
9
+ async ({}, use) => {
10
+ const port = await getPort();
11
+ await use(port);
12
+ },
13
+ { scope: "worker" }
14
+ ],
15
+ context: [
16
+ async ({ port, launchOptions }, use) => {
17
+ const context = await chromium.launchPersistentContext(
18
+ path.join(os.tmpdir(), PW_TMP_DIR, `${Math.random()}`.replace(".", "")),
19
+ {
20
+ args: [...launchOptions.args || [], `--remote-debugging-port=${port}`]
21
+ }
22
+ );
23
+ await use(context);
24
+ await context.close();
25
+ },
26
+ { scope: "test" }
27
+ ]
28
+ });
29
+ export {
30
+ lighthouseTest
31
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "lighthouse-reporting",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "build": "vite build && vite build -c vite.umd.config.ts && tsc -p ./tsconfig.build.json",
7
+ "compile": "tsc",
8
+ "lint": "eslint --ext .ts --ignore-path .gitignore .",
9
+ "precommit": "run-p lint compile",
10
+ "semantic-release": "semantic-release"
11
+ },
12
+ "files": [
13
+ "dist/"
14
+ ],
15
+ "main": "./dist/lighthouse-reporting.umd.cjs",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/lighthouse-reporting.umd.cjs",
22
+ "default": "./dist/index.cjs"
23
+ }
24
+ },
25
+ "peerDependencies": {
26
+ "@playwright/test": "1"
27
+ },
28
+ "devDependencies": {
29
+ "@playwright/test": "^1.34.2",
30
+ "@types/fs-extra": "^11.0.1",
31
+ "@types/node": "^20.2.3",
32
+ "@typescript-eslint/eslint-plugin": "^5.59.7",
33
+ "@typescript-eslint/parser": "^5.59.7",
34
+ "eslint": "^8.41.0",
35
+ "eslint-config-prettier": "^8.8.0",
36
+ "eslint-plugin-prettier": "^4.2.1",
37
+ "husky": "^8.0.3",
38
+ "npm-run-all": "^4.1.5",
39
+ "prettier": "^2.8.8",
40
+ "semantic-release": "^21.0.2",
41
+ "typescript": "^5.0.4",
42
+ "vite": "^4.3.8",
43
+ "vite-plugin-static-copy": "^0.15.0"
44
+ },
45
+ "dependencies": {
46
+ "fs-extra": "^11.1.1",
47
+ "get-port": "^6.1.2"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/mgrybyk/lighthouse-reporting.git"
52
+ }
53
+ }