From f870d5ed8a1064f2639f87ea0e4d002ccdea82cd Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 27 Jun 2017 23:16:13 -0700 Subject: [PATCH] [cli] Move bin.ts core into -cli/run.ts (#2602) --- lighthouse-cli/bin.ts | 145 ++------------------------- lighthouse-cli/cli-flags.ts | 8 +- lighthouse-cli/package.json | 2 +- lighthouse-cli/run.ts | 149 ++++++++++++++++++++++++++++ lighthouse-cli/test/cli/run-test.js | 35 +++++++ 5 files changed, 198 insertions(+), 141 deletions(-) create mode 100644 lighthouse-cli/run.ts create mode 100644 lighthouse-cli/test/cli/run-test.js diff --git a/lighthouse-cli/bin.ts b/lighthouse-cli/bin.ts index 3bd332f6704d..776b1ee2760f 100644 --- a/lighthouse-cli/bin.ts +++ b/lighthouse-cli/bin.ts @@ -5,31 +5,23 @@ */ 'use strict'; -const _RUNTIME_ERROR_CODE = 1; -const _PROTOCOL_TIMEOUT_EXIT_CODE = 67; +import * as path from 'path'; -const assetSaver = require('../lighthouse-core/lib/asset-saver.js'); -const getFilenamePrefix = require('../lighthouse-core/lib/file-namer.js').getFilenamePrefix; -import {launch, LaunchedChrome} from '../chrome-launcher/chrome-launcher'; import * as Commands from './commands/commands'; -import {getFlags, Flags} from './cli-flags'; -const lighthouse = require('../lighthouse-core'); +import * as Printer from './printer'; +import {getFlags} from './cli-flags'; +import {runLighthouse} from './run'; + const log = require('lighthouse-logger'); -import * as path from 'path'; const perfOnlyConfig = require('../lighthouse-core/config/perf.json'); -const performanceXServer = require('./performance-experiment/server'); -import * as Printer from './printer'; -import {Results} from './types/types'; const pkg = require('../package.json'); // accept noop modules for these, so the real dependency is optional. -import {opn, updateNotifier} from './shim-modules'; +import {updateNotifier} from './shim-modules'; -updateNotifier({pkg}).notify(); // Tell user if there's a newer version of LH. -interface LighthouseError extends Error { - code?: string -} +// Tell user if there's a newer version of LH. +updateNotifier({pkg}).notify(); const cliFlags = getFlags(); @@ -67,127 +59,6 @@ if (cliFlags.output === Printer.OutputMode[Printer.OutputMode.json] && !cliFlags cliFlags.outputPath = 'stdout'; } -/** - * Attempts to connect to an instance of Chrome with an open remote-debugging - * port. If none is found and the `skipAutolaunch` flag is not true, launches - * a debuggable instance. - */ -async function getDebuggableChrome(flags: Flags) { - return await launch( - {port: flags.port, chromeFlags: flags.chromeFlags.split(' '), logLevel: cliFlags.logLevel}); -} - -function showConnectionError() { - console.error('Unable to connect to Chrome'); - console.error( - 'If you\'re using lighthouse with --skip-autolaunch, ' + - 'make sure you\'re running some other Chrome with a debugger.'); - process.exit(_RUNTIME_ERROR_CODE); -} - -function showRuntimeError(err: LighthouseError) { - console.error('Runtime error encountered:', err); - if (err.stack) { - console.error(err.stack); - } - process.exit(_RUNTIME_ERROR_CODE); -} - -function showProtocolTimeoutError() { - console.error('Debugger protocol timed out while connecting to Chrome.'); - process.exit(_PROTOCOL_TIMEOUT_EXIT_CODE); -} - -function showPageLoadError() { - console.error('Unable to load the page. Please verify the url you are trying to review.'); - process.exit(_RUNTIME_ERROR_CODE); -} - -function handleError(err: LighthouseError) { - if (err.code === 'PAGE_LOAD_ERROR') { - showPageLoadError(); - } else if (err.code === 'ECONNREFUSED') { - showConnectionError(); - } else if (err.code === 'CRI_TIMEOUT') { - showProtocolTimeoutError(); - } else { - showRuntimeError(err); - } -} - -function saveResults(results: Results, artifacts: Object, flags: Flags) { - let promise = Promise.resolve(results); - const cwd = process.cwd(); - // Use the output path as the prefix for all generated files. - // If no output path is set, generate a file prefix using the URL and date. - const configuredPath = !flags.outputPath || flags.outputPath === 'stdout' ? - getFilenamePrefix(results) : - flags.outputPath.replace(/\.\w{2,4}$/, ''); - const resolvedPath = path.resolve(cwd, configuredPath); - - if (flags.saveArtifacts) { - assetSaver.saveArtifacts(artifacts, resolvedPath); - } - - if (flags.saveAssets) { - promise = promise.then(_ => assetSaver.saveAssets(artifacts, results.audits, resolvedPath)); - } - - const typeToExtension = (type: string) => type === 'domhtml' ? 'dom.html' : type; - return promise.then(_ => { - if (Array.isArray(flags.output)) { - return flags.output.reduce((innerPromise, outputType) => { - const outputPath = `${resolvedPath}.report.${typeToExtension(outputType)}`; - return innerPromise.then((_: Results) => Printer.write(results, outputType, outputPath)); - }, Promise.resolve(results)); - } else { - const outputPath = - flags.outputPath || `${resolvedPath}.report.${typeToExtension(flags.output)}`; - return Printer.write(results, flags.output, outputPath).then(results => { - if (flags.output === Printer.OutputMode[Printer.OutputMode.html] || - flags.output === Printer.OutputMode[Printer.OutputMode.domhtml]) { - if (flags.view) { - opn(outputPath, {wait: false}); - } else { - log.log( - 'CLI', - 'Protip: Run lighthouse with `--view` to immediately open the HTML report in your browser'); - } - } - - return results; - }); - } - }); -} - -export async function runLighthouse( - url: string, flags: Flags, config: Object|null): Promise<{}|void> { - let launchedChrome: LaunchedChrome|undefined; - - try { - launchedChrome = await getDebuggableChrome(flags); - flags.port = launchedChrome.port; - const results = await lighthouse(url, flags, config); - - const artifacts = results.artifacts; - delete results.artifacts; - - await saveResults(results, artifacts!, flags); - if (flags.interactive) { - await performanceXServer.hostExperiment({url, flags, config}, results); - } - - return await launchedChrome.kill(); - } catch (err) { - if (typeof launchedChrome !== 'undefined') { - await launchedChrome!.kill(); - } - - return handleError(err); - } -} - export function run() { return runLighthouse(url, cliFlags, config); } diff --git a/lighthouse-cli/cli-flags.ts b/lighthouse-cli/cli-flags.ts index 2352f31aa9f2..c4f962d4e3c8 100644 --- a/lighthouse-cli/cli-flags.ts +++ b/lighthouse-cli/cli-flags.ts @@ -14,11 +14,13 @@ import {GetValidOutputOptions, OutputMode} from './printer'; export interface Flags { skipAutolaunch: boolean, port: number, selectChrome: boolean, chromeFlags: string, output: any, outputPath: string, interactive: boolean, saveArtifacts: boolean, saveAssets: boolean, - view: boolean, maxWaitForLoad: number + view: boolean, maxWaitForLoad: number, logLevel: string } -export function getFlags() { - return yargs.help('help') +export function getFlags(manualArgv?: string) { + const y = manualArgv ? yargs(manualArgv) : yargs; + + return y.help('help') .version(() => pkg.version) .showHelpOnFail(false, 'Specify --help for available options') diff --git a/lighthouse-cli/package.json b/lighthouse-cli/package.json index f1c1cc81332c..9eed1c7328a6 100644 --- a/lighthouse-cli/package.json +++ b/lighthouse-cli/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "tsc", "dev": "tsc -w", - "test": "mocha --reporter dot test/**/*-test.js", + "test": "mocha --reporter dot test/**/*-test.js --timeout 15000", "coverage": "nyc yarn test && nyc report --reporter=text-lcov > lcov.info", "test-formatting": "./test/check-formatting.sh", "format": "clang-format -i -style=file *.ts **/*.ts" diff --git a/lighthouse-cli/run.ts b/lighthouse-cli/run.ts new file mode 100644 index 000000000000..6f817e12a0d8 --- /dev/null +++ b/lighthouse-cli/run.ts @@ -0,0 +1,149 @@ +/** + * @license Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +import * as path from 'path'; + +import * as Printer from './printer'; +import {Results} from './types/types'; +import {Flags} from './cli-flags'; +import {launch, LaunchedChrome} from '../chrome-launcher/chrome-launcher'; +const lighthouse = require('../lighthouse-core'); +const log = require('lighthouse-logger'); +const getFilenamePrefix = require('../lighthouse-core/lib/file-namer.js').getFilenamePrefix; +const assetSaver = require('../lighthouse-core/lib/asset-saver.js'); +const performanceXServer = require('./performance-experiment/server'); + +// accept noop modules for these, so the real dependency is optional. +import {opn} from './shim-modules'; + +const _RUNTIME_ERROR_CODE = 1; +const _PROTOCOL_TIMEOUT_EXIT_CODE = 67; + +interface LighthouseError extends Error { + code?: string +} + +/** + * Attempts to connect to an instance of Chrome with an open remote-debugging + * port. If none is found and the `skipAutolaunch` flag is not true, launches + * a debuggable instance. + */ +async function getDebuggableChrome(flags: Flags) { + return await launch( + {port: flags.port, chromeFlags: flags.chromeFlags.split(' '), logLevel: flags.logLevel}); +} + +function showConnectionError() { + console.error('Unable to connect to Chrome'); + console.error( + 'If you\'re using lighthouse with --skip-autolaunch, ' + + 'make sure you\'re running some other Chrome with a debugger.'); + process.exit(_RUNTIME_ERROR_CODE); +} + +function showRuntimeError(err: LighthouseError) { + console.error('Runtime error encountered:', err); + if (err.stack) { + console.error(err.stack); + } + process.exit(_RUNTIME_ERROR_CODE); +} + +function showProtocolTimeoutError() { + console.error('Debugger protocol timed out while connecting to Chrome.'); + process.exit(_PROTOCOL_TIMEOUT_EXIT_CODE); +} + +function showPageLoadError() { + console.error('Unable to load the page. Please verify the url you are trying to review.'); + process.exit(_RUNTIME_ERROR_CODE); +} + +function handleError(err: LighthouseError) { + if (err.code === 'PAGE_LOAD_ERROR') { + showPageLoadError(); + } else if (err.code === 'ECONNREFUSED') { + showConnectionError(); + } else if (err.code === 'CRI_TIMEOUT') { + showProtocolTimeoutError(); + } else { + showRuntimeError(err); + } +} + +export function saveResults(results: Results, artifacts: Object, flags: Flags) { + let promise = Promise.resolve(results); + const cwd = process.cwd(); + // Use the output path as the prefix for all generated files. + // If no output path is set, generate a file prefix using the URL and date. + const configuredPath = !flags.outputPath || flags.outputPath === 'stdout' ? + getFilenamePrefix(results) : + flags.outputPath.replace(/\.\w{2,4}$/, ''); + const resolvedPath = path.resolve(cwd, configuredPath); + + if (flags.saveArtifacts) { + assetSaver.saveArtifacts(artifacts, resolvedPath); + } + + if (flags.saveAssets) { + promise = promise.then(_ => assetSaver.saveAssets(artifacts, results.audits, resolvedPath)); + } + + const typeToExtension = (type: string) => type === 'domhtml' ? 'dom.html' : type; + return promise.then(_ => { + if (Array.isArray(flags.output)) { + return flags.output.reduce((innerPromise, outputType) => { + const outputPath = `${resolvedPath}.report.${typeToExtension(outputType)}`; + return innerPromise.then((_: Results) => Printer.write(results, outputType, outputPath)); + }, Promise.resolve(results)); + } else { + const outputPath = + flags.outputPath || `${resolvedPath}.report.${typeToExtension(flags.output)}`; + return Printer.write(results, flags.output, outputPath).then(results => { + if (flags.output === Printer.OutputMode[Printer.OutputMode.html] || + flags.output === Printer.OutputMode[Printer.OutputMode.domhtml]) { + if (flags.view) { + opn(outputPath, {wait: false}); + } else { + log.log( + 'CLI', + 'Protip: Run lighthouse with `--view` to immediately open the HTML report in your browser'); + } + } + + return results; + }); + } + }); +} + +export async function runLighthouse( + url: string, flags: Flags, config: Object|null): Promise<{}|void> { + let launchedChrome: LaunchedChrome|undefined; + + try { + launchedChrome = await getDebuggableChrome(flags); + flags.port = launchedChrome.port; + const results = await lighthouse(url, flags, config); + + const artifacts = results.artifacts; + delete results.artifacts; + + await saveResults(results, artifacts!, flags); + if (flags.interactive) { + await performanceXServer.hostExperiment({url, flags, config}, results); + } + + return await launchedChrome.kill(); + } catch (err) { + if (typeof launchedChrome !== 'undefined') { + await launchedChrome!.kill(); + } + + return handleError(err); + } +} diff --git a/lighthouse-cli/test/cli/run-test.js b/lighthouse-cli/test/cli/run-test.js new file mode 100644 index 000000000000..43ca75bdca59 --- /dev/null +++ b/lighthouse-cli/test/cli/run-test.js @@ -0,0 +1,35 @@ +/** + * @license Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +/* eslint-env mocha */ +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const run = require('../../run'); +const fastConfig = { + 'extends': 'lighthouse:default', + 'settings': { + 'onlyAudits': ['viewport'] + } +}; + +const getFlags = require('../../cli-flags').getFlags; + +describe('CLI run', function() { + it('runLighthouse completes a LH round trip', () => { + const url = 'chrome://version'; + const filename = path.join(process.cwd(), 'run.ts.results.json'); + const flags = getFlags(`--output=json --output-path=${filename} ${url}`); + return run.runLighthouse(url, flags, fastConfig).then(_ => { + assert.ok(fs.existsSync(filename)); + const results = JSON.parse(fs.readFileSync(filename, 'utf-8')); + assert.equal(results.audits.viewport.rawValue, false); + fs.unlinkSync(filename); + }); + }); +});