diff --git a/packages/react-scripts/bin/react-scripts.js b/packages/react-scripts/bin/react-scripts.js index 7e6e2902..44896ed4 100755 --- a/packages/react-scripts/bin/react-scripts.js +++ b/packages/react-scripts/bin/react-scripts.js @@ -19,12 +19,17 @@ const spawn = require('react-dev-utils/crossSpawn'); const args = process.argv.slice(2); const scriptIndex = args.findIndex( - x => x === 'build' || x === 'eject' || x === 'start' || x === 'test' + x => + x === 'build' || + x === 'eject' || + x === 'start' || + x === 'test' || + x === 'audit' ); const script = scriptIndex === -1 ? args[0] : args[scriptIndex]; const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : []; -if (['build', 'eject', 'start', 'test'].includes(script)) { +if (['build', 'eject', 'start', 'test', 'audit'].includes(script)) { const result = spawn.sync( 'node', nodeArgs diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 5a879b58..4edc0d7a 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -58,6 +58,7 @@ "jest-environment-jsdom-fourteen": "0.1.0", "jest-resolve": "24.9.0", "jest-watch-typeahead": "0.4.2", + "lighthouse": "^5.6.0", "mini-css-extract-plugin": "0.8.0", "optimize-css-assets-webpack-plugin": "5.0.3", "pnp-webpack-plugin": "1.5.0", @@ -71,6 +72,7 @@ "resolve": "1.12.0", "resolve-url-loader": "3.1.1", "sass-loader": "8.0.0", + "serve-handler": "^6.1.2", "semver": "6.3.0", "style-loader": "1.0.0", "terser-webpack-plugin": "2.2.1", diff --git a/packages/react-scripts/scripts/audit.js b/packages/react-scripts/scripts/audit.js new file mode 100644 index 00000000..9eefb4a5 --- /dev/null +++ b/packages/react-scripts/scripts/audit.js @@ -0,0 +1,75 @@ +// @remove-file-on-eject +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +'use strict'; + +const { createServer } = require('http'); +const { writeFileSync } = require('fs'); +const { join } = require('path'); +const { choosePort } = require('react-dev-utils/WebpackDevServerUtils'); +const open = require('open'); +const handler = require('serve-handler'); +const lighthouse = require('lighthouse'); +const chromeLauncher = require('chrome-launcher'); +const paths = require('../config/paths'); + +const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; +const HOST = process.env.HOST || '0.0.0.0'; + +// https://github.com/GoogleChrome/lighthouse/blob/master/docs/readme.md#using-programmatically +const launchChromeAndRunLighthouse = (url, opts) => { + return chromeLauncher + .launch({ chromeFlags: opts.chromeFlags }) + .then(chrome => { + opts.port = chrome.port; + return lighthouse(url, opts).then(results => { + return chrome.kill().then(() => results.report); + }); + }); +}; + +const server = createServer((request, response) => + handler(request, response, { + renderSingle: true, + public: paths.appBuild, + }) +); + +choosePort(HOST, DEFAULT_PORT) + .then(() => choosePort(HOST, DEFAULT_PORT)) + .then(port => { + if (port == null) { + console.log('Unable to find a free port'); + process.exit(1); + } + + server.listen(port); + + console.log('Server started, beginning audit...'); + + return launchChromeAndRunLighthouse(`http://${HOST}:${port}`, { + output: 'html', + }); + }) + .then(report => { + console.log('Audit finished, writing report...'); + + const reportPath = join(paths.appPath, 'lighthouse-audit.html'); + writeFileSync(reportPath, report); + + console.log('Opening report in browser...'); + + open(reportPath, { url: true }); + + console.log('Exiting...'); + + server.close(); + }) + .catch(() => { + console.log('Something went wrong, exiting...'); + server.close(); + }); diff --git a/packages/react-scripts/scripts/init.js b/packages/react-scripts/scripts/init.js index fb349061..56fa03e3 100644 --- a/packages/react-scripts/scripts/init.js +++ b/packages/react-scripts/scripts/init.js @@ -114,6 +114,7 @@ module.exports = function( build: 'react-scripts build', test: 'react-scripts test', eject: 'react-scripts eject', + lighthouse: 'react-scripts audit', }; // Setup the eslint config