-
Notifications
You must be signed in to change notification settings - Fork 9.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support dynamic plugins via plugin objects stored in lighthouse config #730
Conversation
@brendankenny PTAL! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Mark! Thanks for taking the time to get into this.
This is definitely needed. No reason for someone using Lighthouse as a module to have to write their audits to disk in order to use them :)
@@ -163,7 +163,13 @@ function requireAudits(audits, configPath) { | |||
const coreList = Runner.getAuditList(); | |||
|
|||
return audits.map(audit => { | |||
// First, see if the audit is a Lighthouse core audit. | |||
// If "audit" is an Audit class, return it. This happens when dynamic |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if we necessarily want to require the type hierarchy (e.g. someone is using a class for multiple reasons and it also happens to work as a LH audit), so we do have the runtime structural(ish) typing in assertValidAudit
below.
What if instead here it did something like
let AuditClass;
if (typeof audit === 'string') {
// See if the audit is a Lighthouse core audit.
const coreAudit = coreList.find(a => a === `${audit}.js`);
// existing code, etc
AuditClass = require(requirePath);
} else {
AuditClass = audit;
audit = audit.meta.name;
}
// Confirm that the audit appears valid.
assertValidAudit(audit, AuditClass);
return AuditClass;
the only real issues with this solution are
audit
is becoming an increasingly poor descriptive variable name here :) Feel free to switch to something better.audit.meta.name
might not exist andassertValidAudit
does a better job of giving feedback if it doesn't. Maybe we should switch to something likeassertValidAudit(AuditClass, auditWhatever);
, haveauditWhatever
be optional, and letassertValidAudit
handle the fallback tometa.name
and evenAuditClass.name
if need be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
let inputConfig = configJSON; | ||
configJSON = JSON.parse(JSON.stringify(inputConfig)); | ||
// Copy arrays that could contain plugins to allow for programmatic | ||
// injection of plugins. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this makes me sad, but it's fine for this PR. It's our own fault for using JSON parsing for deep copying. We probably need a real deep copy thats aware of the config format to allow for exceptions like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. You rightly identified that (for this PR) I just implemented the shortest path to making this work. I figure someone more familiar with Lighthouse than me might want design input on some kind of conditional-cloner.
// Copy arrays that could contain plugins to allow for programmatic | ||
// injection of plugins. | ||
if (inputConfig.passes && Array.isArray(inputConfig.passes.gatherers)) { | ||
configJSON.passes.gatherers = Array.from(inputConfig.passes.gatherers); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless I'm missing something tricky we've done here, I believe you're going to have to do a nested copy here. passes
is an array of objects, each with its own array of gatherers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I'll fix it.
@@ -163,17 +165,22 @@ class Runner { | |||
* Resolves the location of the specified plugin and returns an absolute | |||
* string path to the file. Used for loading custom audits and gatherers. | |||
* Throws an error if no plugin is found. | |||
* @param {string} plugin | |||
* @param {(string|Gatherer|Audit)} plugin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be preferable to leave resolvePlugin
as strictly string
input and output, roughly analogous to require.resolve
.
If you take the code you're adding to requireAudits
in config.js and also add it to getGathererClass
in gather-runner.js, the live plugin class will never make it this far, as those two spots can handle that case, and this can stay strings-only
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@@ -33,6 +34,20 @@ describe('Config', () => { | |||
assert.notEqual(config, newConfig); | |||
}); | |||
|
|||
it('doesn\'t change directly injected plugins', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
may also want to add a test to getGathererClass
on GatherRunner
that it correctly exposes and validates a live gatherer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied requested changes. I made the "name" parameter to assertValid*() functions optional, as suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thanks!
It's more flexible for Lighthouse to support plugin classes provided from somewhere other than the filesystem.
Example use case: Library
foo
wants to consume and extend Lighthouse via a[[foo-instance]] => [[lighthouse-plugin-type]]
adapter. That means that the plugin classes are not static modules, but adapted from foo's structures.E.g.,