Skip to content
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

Merged
merged 6 commits into from
Oct 3, 2016

Conversation

mdittmer
Copy link
Contributor

@mdittmer mdittmer commented Oct 2, 2016

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.,

// run-foo-in-lighthouse.js
// ...
lighthouse({
  // ...
  passes: {
    // ...
    gatherers: ['some-core-gatherer', fooObj.toLighthouseGatherer()]
  },
  // ...
  audits: ['some-core-audit', anotherFooObj.toLighthouseAudit()]
});

@mdittmer
Copy link
Contributor Author

mdittmer commented Oct 2, 2016

@brendankenny PTAL!

Copy link
Member

@brendankenny brendankenny left a 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
Copy link
Member

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 and assertValidAudit does a better job of giving feedback if it doesn't. Maybe we should switch to something like assertValidAudit(AuditClass, auditWhatever);, have auditWhatever be optional, and let assertValidAudit handle the fallback to meta.name and even AuditClass.name if need be?

Copy link
Contributor Author

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.
Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

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

Copy link
Contributor Author

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
Copy link
Member

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

Copy link
Contributor Author

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', () => {
Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor Author

@mdittmer mdittmer left a 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.

Copy link
Member

@brendankenny brendankenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks!

@brendankenny brendankenny merged commit 3fa3690 into GoogleChrome:master Oct 3, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants