Skip to content

Commit a3b9cf3

Browse files
authored
cli: add --plugins flag to load from the command line (#7407)
1 parent b1d2b83 commit a3b9cf3

File tree

7 files changed

+144
-37
lines changed

7 files changed

+144
-37
lines changed

lighthouse-cli/cli-flags.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ function getFlags(manualArgv) {
104104
'only-audits': 'Only run the specified audits',
105105
'only-categories': 'Only run the specified categories',
106106
'skip-audits': 'Run everything except these audits',
107+
'plugins': 'Run the specified plugins',
107108
'print-config': 'Print the normalized config for the given config and options, then exit.',
108109
})
109110
// set aliases
@@ -136,6 +137,7 @@ function getFlags(manualArgv) {
136137
.array('onlyCategories')
137138
.array('skipAudits')
138139
.array('output')
140+
.array('plugins')
139141
.string('extraHeaders')
140142
.string('channel')
141143
.string('precomputedLanternDataPath')

lighthouse-cli/test/cli/run-test.js

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,42 +19,73 @@ const fastConfig = {
1919
},
2020
};
2121

22+
// Map plugin name to fixture since not actually installed in node_modules/.
23+
jest.mock('lighthouse-plugin-simple', () => {
24+
return require('../../../lighthouse-core/test/fixtures/config-plugins/lighthouse-plugin-simple/');
25+
}, {virtual: true});
26+
2227
const getFlags = require('../../cli-flags').getFlags;
2328

2429
describe('CLI run', function() {
25-
it('runLighthouse completes a LH round trip', () => {
26-
const url = 'chrome://version';
30+
describe('LH round trip', () => {
31+
/** @type {LH.RunnerResult} */
32+
let passedResults;
2733
const filename = path.join(process.cwd(), 'run.ts.results.json');
28-
const timeoutFlag = `--max-wait-for-load=${9000}`;
29-
const flags = getFlags(`--output=json --output-path=${filename} ${timeoutFlag} ${url}`);
30-
return run.runLighthouse(url, flags, fastConfig).then(passedResults => {
31-
if (!passedResults) {
32-
assert.fail('no results');
33-
return;
34+
/** @type {LH.Result} */
35+
let fileResults;
36+
37+
beforeAll(async () => {
38+
const url = 'chrome://version';
39+
const timeoutFlag = `--max-wait-for-load=${9000}`;
40+
const pluginsFlag = '--plugins=lighthouse-plugin-simple';
41+
42+
// eslint-disable-next-line max-len
43+
const flags = getFlags(`--output=json --output-path=${filename} ${pluginsFlag} ${timeoutFlag} ${url}`);
44+
45+
const rawResult = await run.runLighthouse(url, flags, fastConfig);
46+
47+
if (!rawResult) {
48+
return assert.fail('no results');
3449
}
50+
passedResults = rawResult;
3551

36-
const {lhr} = passedResults;
3752
assert.ok(fs.existsSync(filename));
38-
/** @type {LH.Result} */
39-
const results = JSON.parse(fs.readFileSync(filename, 'utf-8'));
40-
assert.equal(results.audits.viewport.rawValue, false);
53+
fileResults = JSON.parse(fs.readFileSync(filename, 'utf-8'));
54+
}, 20 * 1000);
55+
56+
afterAll(() => {
57+
fs.unlinkSync(filename);
58+
});
59+
60+
it('returns results that match the saved results', () => {
61+
const {lhr} = passedResults;
62+
assert.equal(fileResults.audits.viewport.rawValue, false);
4163

4264
// passed results match saved results
43-
assert.strictEqual(results.fetchTime, lhr.fetchTime);
44-
assert.strictEqual(results.requestedUrl, lhr.requestedUrl);
45-
assert.strictEqual(results.finalUrl, lhr.finalUrl);
46-
assert.strictEqual(results.audits.viewport.rawValue, lhr.audits.viewport.rawValue);
65+
assert.strictEqual(fileResults.fetchTime, lhr.fetchTime);
66+
assert.strictEqual(fileResults.requestedUrl, lhr.requestedUrl);
67+
assert.strictEqual(fileResults.finalUrl, lhr.finalUrl);
68+
assert.strictEqual(fileResults.audits.viewport.rawValue, lhr.audits.viewport.rawValue);
4769
assert.strictEqual(
48-
Object.keys(results.audits).length,
70+
Object.keys(fileResults.audits).length,
4971
Object.keys(lhr.audits).length);
50-
assert.deepStrictEqual(results.timing, lhr.timing);
51-
assert.ok(results.timing.total !== 0);
72+
assert.deepStrictEqual(fileResults.timing, lhr.timing);
73+
});
74+
75+
it('includes timing information', () => {
76+
assert.ok(passedResults.lhr.timing.total !== 0);
77+
});
5278

53-
assert.equal(results.configSettings.channel, 'cli');
79+
it('correctly sets the channel', () => {
80+
assert.equal(passedResults.lhr.configSettings.channel, 'cli');
81+
});
5482

55-
fs.unlinkSync(filename);
83+
it('merged the plugin into the config', () => {
84+
// Audits have been pruned because of onlyAudits, but groups get merged in.
85+
const groupNames = Object.keys(passedResults.lhr.categoryGroups || {});
86+
assert.ok(groupNames.includes('lighthouse-plugin-simple-new-group'));
5687
});
57-
}, 20 * 1000);
88+
});
5889
});
5990

6091
describe('flag coercing', () => {

lighthouse-core/config/config.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ class Config {
362362
const configDir = configPath ? path.dirname(configPath) : undefined;
363363

364364
// Validate and merge in plugins (if any).
365-
configJSON = Config.mergePlugins(configJSON, configDir);
365+
configJSON = Config.mergePlugins(configJSON, flags, configDir);
366366

367367
const settings = Config.initSettings(configJSON.settings, flags);
368368

@@ -459,22 +459,23 @@ class Config {
459459

460460
/**
461461
* @param {LH.Config.Json} configJSON
462+
* @param {LH.Flags=} flags
462463
* @param {string=} configDir
463464
* @return {LH.Config.Json}
464465
*/
465-
static mergePlugins(configJSON, configDir) {
466-
const pluginNames = configJSON.plugins;
466+
static mergePlugins(configJSON, flags, configDir) {
467+
const configPlugins = configJSON.plugins || [];
468+
const flagPlugins = (flags && flags.plugins) || [];
469+
const pluginNames = new Set([...configPlugins, ...flagPlugins]);
467470

468-
if (pluginNames) {
469-
for (const pluginName of pluginNames) {
470-
assertValidPluginName(configJSON, pluginName);
471+
for (const pluginName of pluginNames) {
472+
assertValidPluginName(configJSON, pluginName);
471473

472-
const pluginPath = Config.resolveModule(pluginName, configDir, 'plugin');
473-
const rawPluginJson = require(pluginPath);
474-
const pluginJson = ConfigPlugin.parsePlugin(rawPluginJson, pluginName);
474+
const pluginPath = Config.resolveModule(pluginName, configDir, 'plugin');
475+
const rawPluginJson = require(pluginPath);
476+
const pluginJson = ConfigPlugin.parsePlugin(rawPluginJson, pluginName);
475477

476-
configJSON = Config.extendConfigJSON(configJSON, pluginJson);
477-
}
478+
configJSON = Config.extendConfigJSON(configJSON, pluginJson);
478479
}
479480

480481
return configJSON;

lighthouse-core/test/config/config-test.js

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -697,19 +697,33 @@ describe('Config', () => {
697697
});
698698

699699
describe('mergePlugins', () => {
700+
// Include a configPath flag so that config.js looks for the plugins in the fixtures dir.
700701
const configFixturePath = __dirname + '/../fixtures/config-plugins/';
701702

702-
it('should append audits and a group', () => {
703+
it('should append audits', () => {
703704
const configJson = {
704705
audits: ['installable-manifest', 'metrics'],
705706
plugins: ['lighthouse-plugin-simple'],
706707
};
707708
const config = new Config(configJson, {configPath: configFixturePath});
708-
const groupIds = Object.keys(config.groups);
709709
assert.deepStrictEqual(config.audits.map(a => a.path),
710710
['installable-manifest', 'metrics', 'redirects', 'user-timings']);
711-
assert.ok(groupIds.length === 1);
712-
assert.strictEqual(groupIds[groupIds.length - 1], 'lighthouse-plugin-simple-new-group');
711+
});
712+
713+
it('should append and use plugin-prefixed groups', () => {
714+
const configJson = {
715+
audits: ['installable-manifest', 'metrics'],
716+
plugins: ['lighthouse-plugin-simple'],
717+
groups: {
718+
configGroup: {title: 'This is a group in the base config'},
719+
},
720+
};
721+
const config = new Config(configJson, {configPath: configFixturePath});
722+
723+
const groupIds = Object.keys(config.groups);
724+
assert.ok(groupIds.length === 2);
725+
assert.strictEqual(groupIds[0], 'configGroup');
726+
assert.strictEqual(groupIds[1], 'lighthouse-plugin-simple-new-group');
713727
assert.strictEqual(config.groups['lighthouse-plugin-simple-new-group'].title, 'New Group');
714728
assert.strictEqual(config.categories['lighthouse-plugin-simple'].auditRefs[0].group,
715729
'lighthouse-plugin-simple-new-group');
@@ -727,6 +741,39 @@ describe('Config', () => {
727741
assert.strictEqual(config.categories['lighthouse-plugin-simple'].title, 'Simple');
728742
});
729743

744+
it('should load plugins from the config and from passed-in flags', () => {
745+
const baseConfigJson = {
746+
audits: ['installable-manifest'],
747+
categories: {
748+
myManifest: {
749+
auditRefs: [{id: 'installable-manifest', weight: 9000}],
750+
},
751+
},
752+
};
753+
const baseFlags = {configPath: configFixturePath};
754+
const simplePluginName = 'lighthouse-plugin-simple';
755+
const noGroupsPluginName = 'lighthouse-plugin-no-groups';
756+
757+
const allConfigConfigJson = {...baseConfigJson, plugins: [simplePluginName,
758+
noGroupsPluginName]};
759+
const allPluginsInConfigConfig = new Config(allConfigConfigJson, baseFlags);
760+
761+
const allFlagsFlags = {...baseFlags, plugins: [simplePluginName, noGroupsPluginName]};
762+
const allPluginsInFlagsConfig = new Config(baseConfigJson, allFlagsFlags);
763+
764+
const mixedConfigJson = {...baseConfigJson, plugins: [simplePluginName]};
765+
const mixedFlags = {...baseFlags, plugins: [noGroupsPluginName]};
766+
const pluginsInConfigAndFlagsConfig = new Config(mixedConfigJson, mixedFlags);
767+
768+
// Double check that we're not comparing empty objects.
769+
const categoryNames = Object.keys(allPluginsInConfigConfig.categories);
770+
assert.deepStrictEqual(categoryNames,
771+
['myManifest', 'lighthouse-plugin-simple', 'lighthouse-plugin-no-groups']);
772+
773+
assert.deepStrictEqual(allPluginsInConfigConfig, allPluginsInFlagsConfig);
774+
assert.deepStrictEqual(allPluginsInConfigConfig, pluginsInConfigAndFlagsConfig);
775+
});
776+
730777
it('should throw if the plugin is invalid', () => {
731778
const configJson = {
732779
extends: 'lighthouse:default',
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "lighthouse-plugin-no-groups",
3+
"private": true,
4+
"main": "./plugin-no-groups.js"
5+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @license Copyright 2019 Google Inc. All Rights Reserved.
3+
* 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
4+
* 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.
5+
*/
6+
'use strict';
7+
8+
/** @type {LH.Config.Plugin} */
9+
module.exports = {
10+
audits: [
11+
{path: 'uses-rel-preload'},
12+
],
13+
category: {
14+
title: 'NoGroups',
15+
auditRefs: [
16+
{id: 'uses-rel-preload', weight: 1},
17+
],
18+
},
19+
};

types/externs.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ declare global {
142142
logLevel?: 'silent'|'error'|'info'|'verbose';
143143
/** The path to the config JSON. */
144144
configPath?: string;
145+
/** Run the specified plugins. */
146+
plugins?: string[];
145147
}
146148

147149
/**

0 commit comments

Comments
 (0)