|
1 | | -const httpServer = require('http-server'); |
2 | | -const puppeteer = require('puppeteer'); |
3 | | -const lighthouse = require('lighthouse'); |
4 | | -const chromeLauncher = require('chrome-launcher'); |
5 | 1 | require('dotenv').config(); |
| 2 | +const httpServer = require('http-server'); |
| 3 | +const chalk = require('chalk'); |
| 4 | +const { getBrowserPath, runLighthouse } = require('./lighthouse'); |
6 | 5 |
|
7 | | -const getServer = (url, serveDir) => { |
8 | | - if (url) { |
9 | | - console.log(`Scanning url '${url}'`); |
| 6 | +const getServer = ({ serveDir, auditUrl }) => { |
| 7 | + if (auditUrl) { |
10 | 8 | // return a mock server for readability |
11 | 9 | const server = { |
12 | 10 | listen: async (func) => { |
| 11 | + console.log(`Scanning url ${chalk.magenta(auditUrl)}`); |
13 | 12 | await func(); |
14 | 13 | }, |
15 | 14 | close: () => undefined, |
| 15 | + url: auditUrl, |
16 | 16 | }; |
17 | | - return { server, url }; |
| 17 | + return { server }; |
18 | 18 | } |
19 | 19 |
|
20 | 20 | if (!serveDir) { |
21 | 21 | throw new Error('Empty publish dir'); |
22 | 22 | } |
23 | 23 |
|
24 | | - console.log(`Serving and scanning site from directory '${serveDir}'`); |
25 | 24 | const s = httpServer.createServer({ root: serveDir }); |
26 | 25 | const port = 5000; |
27 | 26 | const host = 'localhost'; |
28 | 27 | const server = { |
29 | | - listen: (func) => s.listen(port, host, func), |
| 28 | + listen: (func) => { |
| 29 | + console.log( |
| 30 | + `Serving and scanning site from directory ${chalk.magenta(serveDir)}`, |
| 31 | + ); |
| 32 | + return s.listen(port, host, func); |
| 33 | + }, |
30 | 34 | close: () => s.close(), |
| 35 | + url: `http://${host}:${port}`, |
31 | 36 | }; |
32 | | - return { url: `http://${host}:${port}`, server }; |
| 37 | + return { server }; |
33 | 38 | }; |
34 | 39 |
|
35 | | -const belowThreshold = (id, expected, results) => { |
36 | | - const category = results.find((c) => c.id === id); |
| 40 | +const belowThreshold = (id, expected, categories) => { |
| 41 | + const category = categories.find((c) => c.id === id); |
37 | 42 | if (!category) { |
38 | | - console.warn('Could not find category', id); |
| 43 | + console.warn(`Could not find category ${chalk.yellow(id)}`); |
39 | 44 | } |
40 | 45 | const actual = category ? category.score : Number.MAX_SAFE_INTEGER; |
41 | 46 | return actual < expected; |
42 | 47 | }; |
43 | 48 |
|
44 | 49 | const getError = (id, expected, results) => { |
45 | 50 | const category = results.find((c) => c.id === id); |
46 | | - return `Expected category '${category.title}' to be greater or equal to '${expected}' but got '${category.score}'`; |
| 51 | + return `Expected category ${chalk.magenta( |
| 52 | + category.title, |
| 53 | + )} to be greater or equal to ${chalk.green(expected)} but got ${chalk.red( |
| 54 | + category.score, |
| 55 | + )}`; |
| 56 | +}; |
| 57 | + |
| 58 | +const formatResults = ({ results, thresholds }) => { |
| 59 | + const categories = Object.values( |
| 60 | + results.lhr.categories, |
| 61 | + ).map(({ title, score, id }) => ({ title, score, id })); |
| 62 | + |
| 63 | + const categoriesBelowThreshold = Object.entries( |
| 64 | + thresholds, |
| 65 | + ).filter(([id, expected]) => belowThreshold(id, expected, categories)); |
| 66 | + |
| 67 | + const errors = categoriesBelowThreshold.map(([id, expected]) => |
| 68 | + getError(id, expected, categories), |
| 69 | + ); |
| 70 | + |
| 71 | + const summary = { |
| 72 | + results: categories.map((cat) => ({ |
| 73 | + ...cat, |
| 74 | + ...(thresholds[cat.id] ? { threshold: thresholds[cat.id] } : {}), |
| 75 | + })), |
| 76 | + }; |
| 77 | + |
| 78 | + return { summary, errors }; |
| 79 | +}; |
| 80 | + |
| 81 | +const getConfiguration = ({ constants, inputs }) => { |
| 82 | + const serveDir = |
| 83 | + (constants && constants.PUBLISH_DIR) || process.env.PUBLISH_DIR; |
| 84 | + const auditUrl = (inputs && inputs.audit_url) || process.env.AUDIT_URL; |
| 85 | + let thresholds = |
| 86 | + (inputs && inputs.thresholds) || process.env.THRESHOLDS || {}; |
| 87 | + if (typeof thresholds === 'string') { |
| 88 | + thresholds = JSON.parse(thresholds); |
| 89 | + } |
| 90 | + |
| 91 | + return { serveDir, auditUrl, thresholds }; |
| 92 | +}; |
| 93 | + |
| 94 | +const getUtils = ({ utils }) => { |
| 95 | + const failBuild = |
| 96 | + (utils && utils.build && utils.build.failBuild) || |
| 97 | + (() => { |
| 98 | + process.exitCode = 1; |
| 99 | + }); |
| 100 | + |
| 101 | + const show = |
| 102 | + (utils && utils.status && utils.status.show) || (() => undefined); |
| 103 | + |
| 104 | + return { failBuild, show }; |
47 | 105 | }; |
48 | 106 |
|
49 | 107 | module.exports = { |
50 | | - onSuccess: async ({ |
51 | | - constants: { PUBLISH_DIR: serveDir = process.env.PUBLISH_DIR } = {}, |
52 | | - utils, |
53 | | - inputs: { |
54 | | - audit_url: auditUrl = process.env.AUDIT_URL, |
55 | | - thresholds = process.env.THRESHOLDS || {}, |
56 | | - } = {}, |
57 | | - } = {}) => { |
| 108 | + onSuccess: async ({ constants, utils, inputs } = {}) => { |
| 109 | + const { failBuild, show } = getUtils({ utils }); |
| 110 | + |
58 | 111 | try { |
59 | | - utils = utils || { |
60 | | - build: { |
61 | | - failBuild: () => { |
62 | | - process.exit(1); |
63 | | - }, |
64 | | - }, |
65 | | - status: { |
66 | | - show: () => undefined, |
67 | | - }, |
68 | | - }; |
69 | | - |
70 | | - if (typeof thresholds === 'string') { |
71 | | - thresholds = JSON.parse(thresholds); |
72 | | - } |
| 112 | + const { serveDir, auditUrl, thresholds } = getConfiguration({ |
| 113 | + constants, |
| 114 | + inputs, |
| 115 | + }); |
73 | 116 |
|
74 | | - const { server, url } = getServer(auditUrl, serveDir); |
75 | | - const browserFetcher = puppeteer.createBrowserFetcher(); |
76 | | - const revisions = await browserFetcher.localRevisions(); |
77 | | - if (revisions.length <= 0) { |
78 | | - throw new Error('Could not find local browser'); |
79 | | - } |
80 | | - const info = await browserFetcher.revisionInfo(revisions[0]); |
| 117 | + const { server } = getServer({ serveDir, auditUrl }); |
| 118 | + |
| 119 | + const browserPath = await getBrowserPath(); |
81 | 120 |
|
82 | 121 | const { error, results } = await new Promise((resolve) => { |
83 | 122 | server.listen(async () => { |
84 | | - let chrome; |
85 | 123 | try { |
86 | | - chrome = await chromeLauncher.launch({ |
87 | | - chromePath: info.executablePath, |
88 | | - chromeFlags: ['--headless', '--no-sandbox', '--disable-gpu'], |
89 | | - }); |
90 | | - const results = await lighthouse(url, { |
91 | | - port: chrome.port, |
92 | | - }); |
93 | | - if (results.lhr.runtimeError) { |
94 | | - resolve({ error: new Error(results.lhr.runtimeError.message) }); |
95 | | - } |
| 124 | + const results = await runLighthouse(browserPath, server.url); |
96 | 125 | resolve({ error: false, results }); |
97 | 126 | } catch (error) { |
98 | 127 | resolve({ error }); |
99 | 128 | } finally { |
100 | | - if (chrome) { |
101 | | - await chrome.kill().catch(() => undefined); |
102 | | - } |
103 | 129 | server.close(); |
104 | 130 | } |
105 | 131 | }); |
106 | 132 | }); |
| 133 | + |
107 | 134 | if (error) { |
108 | 135 | throw error; |
109 | 136 | } else { |
110 | | - const categories = Object.values( |
111 | | - results.lhr.categories, |
112 | | - ).map(({ title, score, id }) => ({ title, score, id })); |
113 | | - |
114 | | - const errors = Object.entries(thresholds) |
115 | | - .filter(([id, expected]) => belowThreshold(id, expected, categories)) |
116 | | - .map(([id, expected]) => getError(id, expected, categories)); |
117 | | - |
118 | | - const summary = JSON.stringify({ results: categories }, null, 2); |
| 137 | + const { summary, errors } = formatResults({ results, thresholds }); |
119 | 138 | console.log(summary); |
120 | | - utils.status.show({ |
| 139 | + show({ |
121 | 140 | summary, |
122 | 141 | }); |
123 | 142 |
|
124 | 143 | if (errors.length > 0) { |
125 | | - throw new Error(errors.join('\n')); |
| 144 | + throw new Error(`\n${errors.join('\n')}`); |
126 | 145 | } |
127 | 146 | } |
128 | 147 | } catch (error) { |
129 | 148 | console.error(`\nError: ${error.message}\n`); |
130 | | - utils.build.failBuild(`failed with error: ${error.message}`); |
| 149 | + failBuild(`Failed with error: ${error.message}`); |
131 | 150 | } |
132 | 151 | }, |
133 | 152 | }; |
0 commit comments