Skip to content

Commit d51c0c1

Browse files
committed
refactor: use puppeteer chrome
1 parent f35367f commit d51c0c1

File tree

11 files changed

+2063
-2716
lines changed

11 files changed

+2063
-2716
lines changed

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Url to audit
2+
AUDIT_URL=https://www.example.com
3+
# Ignored when AUDIT_URL is configured
4+
PUBLISH_DIR=FULL_PATH_TO_LOCAL_BUILD_DIRECTORY
5+
# JSON string of thresholds to enforce
6+
THRESHOLDS={"performance":0.9,"accessibility":0.9,"best-practices":0.9,"seo":0.9,"pwa":0.9}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
.env
3+
.vscode

CONTRIBUTING.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
## Contributing
1+
# Contributing
22

3-
First fork this project.
3+
First fork this project
44

5-
* git clone <your-forked-repo>
6-
* npm install
5+
```bash
6+
git clone <your-forked-repo>
7+
yarn install
8+
git checkout -b my-fix
9+
```
710

8-
* git checkout -b my-fix
11+
Then fix some code and
912

10-
#### fix some code...
13+
```bash
14+
git commit -m "added this feature"
15+
git push origin my-fix
16+
```
1117

12-
* git commit -m "added this feature"
13-
* git push origin my-fix
14-
15-
Lastly, open a pull request on Github.
18+
Lastly, open a pull request on Github.

README.md

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,30 @@
1-
## Netlify Plugin Lighthouse
1+
# Netlify Plugin Lighthouse
22

3-
this does a basic lighthouse run and reports and stores the results. if it there has been a previous run, it also diffs
4-
the results to report improvements.
5-
6-
This shows the diffs:
7-
8-
![image](https://user-images.githubusercontent.com/6764957/62481118-8749c880-b77f-11e9-91d9-44ae028c452b.png)
3+
A Netlify plugin to generate a lighthouse report for every deploy
94

105
## Usage
116

12-
Inside your `netlify.yml`:
7+
This plugin can be included via npm. Install it as a dependency with the following command:
138

14-
```yaml
15-
plugins:
16-
- type: '@netlify/plugin-lighthouse'
17-
config:
18-
currentVersion: '0.0.3'
19-
compareWithVersion: '0.0.1'
9+
```bash
10+
npm install --save netlify-plugin-lighthouse
2011
```
2112

22-
you can pin lighthouse result versions:
23-
24-
```yaml
25-
plugins:
26-
- type: '@netlify/plugin-lighthouse'
27-
config:
28-
currentVersion: '0.0.3'
29-
compareWithVersion: '0.0.1'
13+
To use a build plugin, create a `plugins` in your `netlify.toml` like so:
14+
15+
```toml
16+
[[plugins]]
17+
package = "netlify-plugin-lighthouse"
18+
[plugins.inputs]
19+
# optional, defaults to scanning the current built version of the site
20+
audit_url = 'https://www.my-custom-site.com'
21+
# optional, fails build when a category is below a threshold
22+
[plugins.inputs.thresholds]
23+
performance = 0.9
24+
accessibility = 0.9
25+
best-practices = 0.9
26+
seo = 0.9
27+
pwa = 0.9
3028
```
3129

32-
## TODO
33-
34-
https://github.com/netlify/build/issues/27
30+
Run `netlify build` locally to check that the plugin works

example/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>Empty Site</title>
6+
</head>
7+
<body>
8+
Empty Site
9+
</body>
10+
</html>

index.js

Lines changed: 121 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,133 @@
1-
const { resolve } = require('path')
1+
const httpServer = require("http-server");
2+
const puppeteer = require("puppeteer");
3+
const lighthouse = require("lighthouse");
4+
const chromeLauncher = require("chrome-launcher");
5+
require("dotenv").config();
26

3-
const execa = require('execa')
4-
const chalk = require('chalk')
5-
const Conf = require('conf') // for simple kv store
7+
const getServer = (url, serveDir) => {
8+
if (url) {
9+
console.log(`Scanning url '${url}'`);
10+
// return a mock server for readability
11+
const server = {
12+
listen: async (func) => {
13+
await func();
14+
},
15+
close: () => undefined,
16+
};
17+
return { server, url };
18+
}
619

7-
// // TODO: enable this in production
8-
// const NETLIFY_BUILD_BASE = '/opt/buildhome'
20+
if (!serveDir) {
21+
throw new Error("Empty publish dir");
22+
}
23+
24+
console.log(`Serving and scanning site from directory '${serveDir}'`);
25+
const s = httpServer.createServer({ root: serveDir });
26+
const port = 5000;
27+
const host = "localhost";
28+
const server = {
29+
listen: (func) => s.listen(port, host, func),
30+
close: () => s.close(),
31+
};
32+
return { url: `http://${host}:${port}`, server };
33+
};
34+
35+
const belowThreshold = (id, expected, results) => {
36+
const category = results.find((c) => c.id === id);
37+
if (!category) {
38+
console.warn("Could not find category", id);
39+
}
40+
const actual = category ? category.score : Number.MAX_SAFE_INTEGER;
41+
return actual < expected;
42+
};
43+
44+
const getError = (id, expected, results) => {
45+
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}'`;
47+
};
948

1049
module.exports = {
11-
name: '@netlify/plugin-lighthouse',
12-
// users will be tempted to use semver, but we really don't care
13-
onPostDeploy: async ({ pluginConfig }) => {
14-
let { site = process.env.SITE, currentVersion, compareWithVersion = 'init' } = pluginConfig
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+
} = {}) => {
58+
try {
59+
utils = utils || {
60+
build: {
61+
failBuild: () => {
62+
process.exit(1);
63+
},
64+
},
65+
status: {
66+
show: () => undefined,
67+
},
68+
};
1569

16-
if (typeof currentVersion === `undefined`) {
17-
console.log(`lighthouseplugin version not specified, auto assigning ${chalk.yellow("currentVersion='init'")}`)
18-
currentVersion = 'init'
19-
}
70+
if (typeof thresholds === "string") {
71+
thresholds = JSON.parse(thresholds);
72+
}
2073

21-
// kvstore in `${NETLIFY_CACHE_DIR}/${name}.json`
22-
// we choose to let the user createStore instead of doing it for them
23-
// bc they may want to set `defaults` and `schema` and `de/serialize`
24-
const store = new Conf({
25-
cwd: resolve('cache'),
26-
configName: 'netlify-plugin-lighthouse',
27-
})
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]);
81+
82+
const { error, results } = await new Promise((resolve) => {
83+
server.listen(async () => {
84+
let chrome;
85+
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+
}
96+
resolve({ error: false, results });
97+
} catch (error) {
98+
resolve({ error });
99+
} finally {
100+
if (chrome) {
101+
await chrome.kill().catch(() => undefined);
102+
}
103+
server.close();
104+
}
105+
});
106+
});
107+
if (error) {
108+
throw error;
109+
} else {
110+
const categories = Object.values(
111+
results.lhr.categories
112+
).map(({ title, score, id }) => ({ title, score, id }));
28113

29-
// TODO: fetch previous scores from cache
30-
await execa('lighthouse-ci', site, { stdio: 'inherit' })
114+
const errors = Object.entries(thresholds)
115+
.filter(([id, expected]) => belowThreshold(id, expected, categories))
116+
.map(([id, expected]) => getError(id, expected, categories));
31117

32-
// serialize response
33-
const curLightHouse = {}
34-
const prevLightHouse = store.get(`lighthouse.${compareWithVersion}`)
35-
let totalImprovement = 0
36-
if (prevLightHouse) {
37-
console.log(
38-
`Comparing lighthouse results from version: ${chalk.yellow(compareWithVersion)} to version: ${chalk.yellow(
39-
currentVersion,
40-
)}:`,
41-
)
42-
Object.entries(curLightHouse).forEach(([k, v]) => {
43-
const prevV = prevLightHouse[k]
44-
if (!prevV) return // we should never get here but just in case lighthouse format changes...
45-
const improvement = v - prevV
46-
if (improvement < 0) {
47-
console.log(`- ${chalk.yellow(k)} ${chalk.magenta('regressed')} from ${prevV} to ${v}`)
48-
} else if (improvement > 0) {
49-
console.log(`- ${chalk.yellow(k)} ${chalk.green('improved')} from ${prevV} to ${v}`)
118+
const summary = JSON.stringify({ results: categories }, null, 2);
119+
console.log(summary);
120+
utils.status.show({
121+
summary,
122+
});
123+
124+
if (errors.length > 0) {
125+
throw new Error(errors.join("\n"));
50126
}
51-
// TODO: print out links to give suggestions on what to do! lets see what lighthouse-ci gives us
52-
totalImprovement += improvement
53-
})
54-
if (Math.abs(totalImprovement) > 2) {
55-
// some significance bar
56-
const color = totalImprovement > 0 ? chalk.green : chalk.magenta
57-
console.log(`${chalk.yellow.bold('Total Improvement')}: ${color(totalImprovement)} points!`)
58-
}
59-
} else {
60-
if (compareWithVersion) {
61-
console.warn(
62-
`Warning: you set ${chalk.yellow('compareWithVersion') +
63-
'=' +
64-
chalk.yellow(compareWithVersion)} but that version was not found in our result storage.`,
65-
)
66127
}
128+
} catch (error) {
129+
console.error(`\nError: ${error.message}\n`);
130+
utils.build.failBuild(`failed with error: ${error.message}`);
67131
}
68-
store.set(`lighthouse.${currentVersion}`, curLightHouse)
69-
console.log(`Saved results as version: ${chalk.yellow(currentVersion)}`)
70132
},
71-
}
133+
};

manifest.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name: netlify-plugin-lighthouse
2+
inputs:
3+
- name: audit_url
4+
required: false
5+
description: Url of the site to audit, defaults to scanning the current built version of the site
6+
- name: thresholds
7+
required: false
8+
description: Key value mapping of thresholds that will fail the build when not passed

netlify.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[build]
2+
command = "echo 'no op'"
3+
publish = "example"
4+
5+
[build.environment]
6+
YARN_FLAGS = "--frozen-lockfile"
7+
YARN_VERSION = "1.22.4"
8+
9+
[[plugins]]
10+
package = "./index.js"
11+
[plugins.inputs.thresholds]
12+
performance = 0.9
13+
accessibility = 0.9
14+
best-practices = 0.9
15+
seo = 0.9
16+
pwa = 0.9

0 commit comments

Comments
 (0)