@@ -23,6 +23,7 @@ const SpeedlineGatherer = require('../gather/gatherers/speedline');
2323
2424const GatherRunner = require ( '../gather/gather-runner' ) ;
2525const log = require ( '../lib/log' ) ;
26+ const path = require ( 'path' ) ;
2627
2728// cleanTrace is run to remove duplicate TracingStartedInPage events,
2829// and to change TracingStartedInBrowser events into TracingStartedInPage.
@@ -107,7 +108,7 @@ function cleanTrace(trace) {
107108 return trace ;
108109}
109110
110- function filterPasses ( passes , audits ) {
111+ function filterPasses ( passes , audits , paths ) {
111112 const requiredGatherers = getGatherersNeededByAudits ( audits ) ;
112113
113114 // Make sure we only have the gatherers that are needed by the audits
@@ -117,7 +118,7 @@ function filterPasses(passes, audits) {
117118
118119 freshPass . gatherers = freshPass . gatherers . filter ( gatherer => {
119120 try {
120- const GathererClass = GatherRunner . getGathererClass ( gatherer ) ;
121+ const GathererClass = GatherRunner . getGathererClass ( gatherer , paths ) ;
121122 return requiredGatherers . has ( GathererClass . name ) ;
122123 } catch ( requireError ) {
123124 throw new Error ( `Unable to locate gatherer: ${ gatherer } ` ) ;
@@ -171,16 +172,79 @@ function filterAudits(audits, auditWhitelist) {
171172 return filteredAudits ;
172173}
173174
174- function expandAudits ( audits ) {
175+ function expandAudits ( audits , paths ) {
176+ const rootPath = path . join ( __dirname , '../../' ) ;
177+
175178 return audits . map ( audit => {
176- try {
177- return require ( `../audits/${ audit } ` ) ;
178- } catch ( requireError ) {
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+ }
186+
187+ const requirePath = auditPath . startsWith ( '/' ) ? auditPath : path . join ( rootPath , auditPath ) ;
188+ try {
189+ return require ( `${ requirePath } /${ audit } ` ) ;
190+ } catch ( requireError ) {
191+ return null ;
192+ }
193+ } , null ) ;
194+
195+ if ( ! AuditClass ) {
179196 throw new Error ( `Unable to locate audit: ${ audit } ` ) ;
180197 }
198+
199+ // 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+ }
214+
215+ return AuditClass ;
181216 } ) ;
182217}
183218
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+ } ;
246+ }
247+
184248function expandArtifacts ( artifacts , includeSpeedline ) {
185249 const expandedArtifacts = Object . assign ( { } , artifacts ) ;
186250
@@ -241,19 +305,53 @@ class Config {
241305 configJSON = defaultConfig ;
242306 }
243307
244- this . _audits = configJSON . audits ? expandAudits (
245- filterAudits ( configJSON . audits , auditWhitelist )
308+ this . _configJSON = this . _initRequirePaths ( configJSON ) ;
309+
310+ this . _audits = this . json . audits ? expandAudits (
311+ filterAudits ( this . json . audits , auditWhitelist ) , this . json . paths . audits
246312 ) : null ;
247313 // filterPasses expects audits to have been expanded
248- this . _passes = configJSON . passes ? filterPasses ( configJSON . passes , this . _audits ) : null ;
249- this . _auditResults = configJSON . auditResults ? Array . from ( configJSON . auditResults ) : null ;
314+ this . _passes = this . json . passes ?
315+ filterPasses ( this . json . passes , this . _audits , this . json . paths . gatherers ) :
316+ null ;
317+ this . _auditResults = this . json . auditResults ? Array . from ( this . json . auditResults ) : null ;
250318 this . _artifacts = null ;
251- if ( configJSON . artifacts ) {
252- this . _artifacts = expandArtifacts ( configJSON . artifacts ,
319+ if ( this . json . artifacts ) {
320+ this . _artifacts = expandArtifacts ( this . json . artifacts ,
253321 // If time-to-interactive is present, add the speedline artifact
254- configJSON . audits && configJSON . audits . find ( a => a === 'time-to-interactive' ) ) ;
322+ this . json . audits && this . json . audits . find ( a => a === 'time-to-interactive' ) ) ;
323+ }
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 = [ ] ;
255338 }
256- this . _aggregations = configJSON . aggregations ? Array . from ( configJSON . aggregations ) : null ;
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 ;
350+ }
351+
352+ /** @type {!Object } */
353+ get json ( ) {
354+ return this . _configJSON ;
257355 }
258356
259357 /** @type {Array<!Pass> } */
0 commit comments