Skip to content

Commit 8c0c05c

Browse files
committed
Addresses comments
1 parent bab838b commit 8c0c05c

File tree

14 files changed

+309
-170
lines changed

14 files changed

+309
-170
lines changed

lighthouse-core/audits/audit.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,6 @@ class Audit {
3333
throw new Error('Audit meta information must be overridden.');
3434
}
3535

36-
/**
37-
* @throws
38-
*/
39-
static audit() {
40-
throw new Error('Audit audit() function must be overridden.');
41-
}
42-
4336
/**
4437
* @param {!AuditResultInput} result
4538
* @return {!AuditResult}

lighthouse-core/config/index.js

Lines changed: 76 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ function cleanTrace(trace) {
108108
return trace;
109109
}
110110

111-
function filterPasses(passes, audits, paths) {
111+
function filterPasses(passes, audits, rootPath) {
112112
const requiredGatherers = getGatherersNeededByAudits(audits);
113113

114114
// Make sure we only have the gatherers that are needed by the audits
@@ -117,12 +117,8 @@ function filterPasses(passes, audits, paths) {
117117
const freshPass = Object.assign({}, pass);
118118

119119
freshPass.gatherers = freshPass.gatherers.filter(gatherer => {
120-
try {
121-
const GathererClass = GatherRunner.getGathererClass(gatherer, paths);
122-
return requiredGatherers.has(GathererClass.name);
123-
} catch (requireError) {
124-
throw new Error(`Unable to locate gatherer: ${gatherer}`);
125-
}
120+
const GathererClass = GatherRunner.getGathererClass(gatherer, rootPath);
121+
return requiredGatherers.has(GathererClass.name);
126122
});
127123

128124
return freshPass;
@@ -172,77 +168,80 @@ function filterAudits(audits, auditWhitelist) {
172168
return filteredAudits;
173169
}
174170

175-
function expandAudits(audits, paths) {
176-
const rootPath = path.join(__dirname, '../../');
171+
function expandAudits(audits, rootPath) {
172+
const Runner = require('../runner');
177173

178174
return audits.map(audit => {
179-
// Check each path to see if the audit can be located. First match wins.
180-
const AuditClass = paths.reduce((definition, auditPath) => {
181-
// If the definition has already been found, just propagate it. Otherwise try a search
182-
// on the path in this iteration of the loop.
183-
if (definition !== null) {
184-
return definition;
185-
}
175+
// Firstly see if the audit is in Lighthouse itself.
176+
const list = Runner.getAuditList();
177+
const coreAudit = list.find(a => a === `${audit}.js`);
178+
179+
// Assume it's a core audit first.
180+
let requirePath = path.resolve(__dirname, `../audits/${audit}`);
181+
let AuditClass;
186182

187-
const requirePath = auditPath.startsWith('/') ? auditPath : path.join(rootPath, auditPath);
183+
// If not, see if it can be found another way.
184+
if (!coreAudit) {
185+
// Firstly try and see if the audit resolves naturally through the usual means.
188186
try {
189-
return require(`${requirePath}/${audit}`);
187+
require.resolve(audit);
188+
189+
// If the above works, update the path to the absolute value provided.
190+
requirePath = audit;
190191
} catch (requireError) {
191-
return null;
192+
// If that fails, try and find it relative to any config path provided.
193+
if (rootPath) {
194+
requirePath = path.resolve(rootPath, audit);
195+
}
192196
}
193-
}, null);
197+
}
198+
199+
// Now try and require it in. If this fails then the audit file isn't where we expected it.
200+
try {
201+
AuditClass = require(requirePath);
202+
} catch (requireError) {
203+
AuditClass = null;
204+
}
194205

195206
if (!AuditClass) {
196-
throw new Error(`Unable to locate audit: ${audit}`);
207+
throw new Error(`Unable to locate audit: ${audit} (tried at ${requirePath})`);
197208
}
198209

199210
// Confirm that the audit appears valid.
200-
const auditValidation = validateAudit(AuditClass);
201-
if (!auditValidation.valid) {
202-
const errors = Object.keys(auditValidation)
203-
.reduce((errorList, item) => {
204-
// Ignore the valid property as it's generated from the other items in the object.
205-
if (item === 'valid') {
206-
return errorList;
207-
}
208-
209-
return errorList + (auditValidation[item] ? '' : `\n - ${item} is missing`);
210-
}, '');
211-
212-
throw new Error(`Invalid audit class: ${errors}`);
213-
}
211+
assertValidAudit(audit, AuditClass);
214212

215213
return AuditClass;
216214
});
217215
}
218216

219-
function validateAudit(auditDefinition) {
220-
const hasAuditMethod = typeof auditDefinition.audit === 'function';
221-
const hasMeta = typeof auditDefinition.meta === 'object';
222-
const hasMetaName = hasMeta && typeof auditDefinition.meta.name !== 'undefined';
223-
const hasMetaCategory = hasMeta && typeof auditDefinition.meta.category !== 'undefined';
224-
const hasMetaDescription = hasMeta && typeof auditDefinition.meta.description !== 'undefined';
225-
const hasMetaRequiredArtifacts = hasMeta && Array.isArray(auditDefinition.meta.requiredArtifacts);
226-
const hasGenerateAuditResult = typeof auditDefinition.generateAuditResult === 'function';
227-
228-
return {
229-
'valid': (
230-
hasAuditMethod &&
231-
hasMeta &&
232-
hasMetaName &&
233-
hasMetaCategory &&
234-
hasMetaDescription &&
235-
hasMetaRequiredArtifacts &&
236-
hasGenerateAuditResult
237-
),
238-
'audit()': hasAuditMethod,
239-
'meta property': hasMeta,
240-
'meta.name property': hasMetaName,
241-
'meta.category property': hasMetaCategory,
242-
'meta.description property': hasMetaDescription,
243-
'meta.requiredArtifacts array': hasMetaRequiredArtifacts,
244-
'generateAuditResult()': hasGenerateAuditResult
245-
};
217+
function assertValidAudit(audit, auditDefinition) {
218+
if (typeof auditDefinition.audit !== 'function') {
219+
throw new Error(`${audit} has no audit() method.`);
220+
}
221+
222+
if (typeof auditDefinition.meta.name !== 'string') {
223+
throw new Error(`${audit} has no meta.name property, or the property is not a string.`);
224+
}
225+
226+
if (typeof auditDefinition.meta.category !== 'string') {
227+
throw new Error(`${audit} has no meta.category property, or the property is not a string.`);
228+
}
229+
230+
if (typeof auditDefinition.meta.description !== 'string') {
231+
throw new Error(`${audit} has no meta.description property, or the property is not a string.`);
232+
}
233+
234+
if (!Array.isArray(auditDefinition.meta.requiredArtifacts)) {
235+
throw new Error(
236+
`${audit} has no meta.requiredArtifacts property, or the property is not an array.`
237+
);
238+
}
239+
240+
if (typeof auditDefinition.generateAuditResult !== 'function') {
241+
throw new Error(
242+
`${audit} has no generateAuditResult() method. Did you inherit from the proper base class?`
243+
);
244+
}
246245
}
247246

248247
function expandArtifacts(artifacts, includeSpeedline) {
@@ -300,58 +299,34 @@ class Config {
300299
* @constructor
301300
* @param{Object} config
302301
*/
303-
constructor(configJSON, auditWhitelist) {
302+
constructor(configJSON, auditWhitelist, configPath) {
304303
if (!configJSON) {
305304
configJSON = defaultConfig;
306305
}
307306

308-
this._configJSON = this._initRequirePaths(configJSON);
307+
// Store the directory of the config path, if one was provided.
308+
this._configDir = configPath ? path.dirname(configPath) : undefined;
309309

310-
this._audits = this.json.audits ? expandAudits(
311-
filterAudits(this.json.audits, auditWhitelist), this.json.paths.audits
310+
this._audits = configJSON.audits ? expandAudits(
311+
filterAudits(configJSON.audits, auditWhitelist), this._configDir
312312
) : null;
313313
// filterPasses expects audits to have been expanded
314-
this._passes = this.json.passes ?
315-
filterPasses(this.json.passes, this._audits, this.json.paths.gatherers) :
314+
this._passes = configJSON.passes ?
315+
filterPasses(configJSON.passes, this._audits, this._configDir) :
316316
null;
317-
this._auditResults = this.json.auditResults ? Array.from(this.json.auditResults) : null;
317+
this._auditResults = configJSON.auditResults ? Array.from(configJSON.auditResults) : null;
318318
this._artifacts = null;
319-
if (this.json.artifacts) {
320-
this._artifacts = expandArtifacts(this.json.artifacts,
319+
if (configJSON.artifacts) {
320+
this._artifacts = expandArtifacts(configJSON.artifacts,
321321
// If time-to-interactive is present, add the speedline artifact
322-
this.json.audits && this.json.audits.find(a => a === 'time-to-interactive'));
322+
configJSON.audits && configJSON.audits.find(a => a === 'time-to-interactive'));
323323
}
324-
this._aggregations = this.json.aggregations ? Array.from(this.json.aggregations) : null;
325-
}
326-
327-
_initRequirePaths(configJSON) {
328-
if (typeof configJSON.paths !== 'object') {
329-
configJSON.paths = {};
330-
}
331-
332-
if (!Array.isArray(configJSON.paths.audits)) {
333-
configJSON.paths.audits = [];
334-
}
335-
336-
if (!Array.isArray(configJSON.paths.gatherers)) {
337-
configJSON.paths.gatherers = [];
338-
}
339-
340-
// Make sure the default paths are prepended to the list
341-
if (configJSON.paths.audits.indexOf('lighthouse-core/audits') === -1) {
342-
configJSON.paths.audits.unshift('lighthouse-core/audits');
343-
}
344-
345-
if (configJSON.paths.gatherers.indexOf('lighthouse-core/gather/gatherers') === -1) {
346-
configJSON.paths.gatherers.unshift('lighthouse-core/gather/gatherers');
347-
}
348-
349-
return configJSON;
324+
this._aggregations = configJSON.aggregations ? Array.from(configJSON.aggregations) : null;
350325
}
351326

352-
/** @type {!Object} */
353-
get json() {
354-
return this._configJSON;
327+
/** @type {string} */
328+
get configDir() {
329+
return this._configDir;
355330
}
356331

357332
/** @type {Array<!Pass>} */

lighthouse-core/gather/gather-runner.js

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,6 @@ class GatherRunner {
202202
return Promise.reject(new Error('You must provide a config'));
203203
}
204204

205-
const configJSON = options.config.json;
206-
207205
// Default mobile emulation and page loading to true.
208206
// The extension will switch these off initially.
209207
if (typeof options.flags.mobile === 'undefined') {
@@ -214,7 +212,7 @@ class GatherRunner {
214212
options.flags.loadPage = true;
215213
}
216214

217-
passes = this.instantiateGatherers(passes, configJSON.paths.gatherers);
215+
passes = this.instantiateGatherers(passes, options.config.configDir);
218216

219217
return driver.connect()
220218
.then(_ => GatherRunner.setupDriver(driver, options))
@@ -258,44 +256,77 @@ class GatherRunner {
258256
});
259257
}
260258

261-
static getGathererClass(gatherer, paths) {
262-
const rootPath = path.join(__dirname, '../../');
263-
264-
// Check each path to see if the gatherer can be located. First match wins.
265-
const gathererDefinition = paths.reduce((definition, gathererPath) => {
266-
// If the definition has already been found, just propagate it. Otherwise try a search
267-
// on the path in this iteration of the loop.
268-
if (definition !== null) {
269-
return definition;
270-
}
259+
static getGathererClass(gatherer, rootPath) {
260+
const Runner = require('../runner');
261+
const list = Runner.getGathererList();
262+
const coreGatherer = list.find(a => a === `${gatherer}.js`);
271263

272-
const requirePath = gathererPath.startsWith('/') ?
273-
gathererPath :
274-
path.join(rootPath, gathererPath);
264+
// Assume it's a core gatherer first.
265+
let requirePath = path.resolve(__dirname, `./gatherers/${gatherer}`);
266+
let GathererClass;
275267

268+
// If not, see if it can be found another way.
269+
if (!coreGatherer) {
270+
// Firstly try and see if the gatherer resolves naturally through the usual means.
276271
try {
277-
return require(`${requirePath}/${gatherer}`);
272+
require.resolve(gatherer);
273+
274+
// If the above works, update the path to the absolute value provided.
275+
requirePath = gatherer;
278276
} catch (requireError) {
279-
return null;
277+
// If that fails, try and find it relative to any config path provided.
278+
if (rootPath) {
279+
requirePath = path.resolve(rootPath, gatherer);
280+
}
280281
}
281-
}, null);
282+
}
283+
284+
// Now try and require it in. If this fails then the audit file isn't where we expected it.
285+
try {
286+
GathererClass = require(requirePath);
287+
} catch (requireError) {
288+
GathererClass = null;
289+
}
282290

283-
if (!gathererDefinition) {
284-
throw new Error(`Unable to locate gatherer: ${gatherer}`);
291+
if (!GathererClass) {
292+
throw new Error(`Unable to locate gatherer: ${gatherer} (tried at: ${requirePath})`);
285293
}
286294

287-
return gathererDefinition;
295+
// Confirm that the gatherer appears valid.
296+
this.assertValidGatherer(gatherer, GathererClass);
297+
298+
return GathererClass;
299+
}
300+
301+
static assertValidGatherer(gatherer, GathererDefinition) {
302+
const gathererInstance = new GathererDefinition();
303+
304+
if (typeof gathererInstance.beforePass !== 'function') {
305+
throw new Error(`${gatherer} has no beforePass() method.`);
306+
}
307+
308+
if (typeof gathererInstance.pass !== 'function') {
309+
throw new Error(`${gatherer} has no pass() method.`);
310+
}
311+
312+
if (typeof gathererInstance.afterPass !== 'function') {
313+
throw new Error(`${gatherer} has no afterPass() method.`);
314+
}
315+
316+
if (typeof gathererInstance.artifact !== 'object') {
317+
throw new Error(`${gatherer} has no artifact property.`);
318+
}
288319
}
289320

290-
static instantiateGatherers(passes, paths) {
321+
static instantiateGatherers(passes, rootPath) {
291322
return passes.map(pass => {
292323
pass.gatherers = pass.gatherers.map(gatherer => {
293324
// If this is already instantiated, don't do anything else.
294325
if (typeof gatherer !== 'string') {
295326
return gatherer;
296327
}
297328

298-
const GathererClass = GatherRunner.getGathererClass(gatherer, paths);
329+
const GathererClass = GatherRunner.getGathererClass(gatherer, rootPath);
299330
return new GathererClass();
300331
});
301332

lighthouse-core/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ module.exports = function(url, flags, configJSON) {
6262
}
6363

6464
// Use ConfigParser to generate a valid config file
65-
const config = new Config(configJSON, flags.auditWhitelist);
65+
const config = new Config(configJSON, flags.auditWhitelist, flags.configPath);
6666

6767
const driver = new ChromeProtocol();
6868

@@ -73,3 +73,5 @@ module.exports = function(url, flags, configJSON) {
7373

7474
module.exports.getAuditList = Runner.getAuditList;
7575
module.exports.traceCategories = require('./gather/drivers/driver').traceCategories;
76+
module.exports.Audit = require('./audits/audit');
77+
module.exports.Gatherer = require('./gather/gatherers/gatherer');

lighthouse-core/runner.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@ class Runner {
128128
.readdirSync(path.join(__dirname, './audits'))
129129
.filter(f => /\.js$/.test(f));
130130
}
131+
132+
/**
133+
* Returns list of gatherer names for external querying.
134+
* @return {!Array<string>}
135+
*/
136+
static getGathererList() {
137+
return fs
138+
.readdirSync(path.join(__dirname, './gather/gatherers'))
139+
.filter(f => /\.js$/.test(f));
140+
}
131141
}
132142

133143
module.exports = Runner;

0 commit comments

Comments
 (0)