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

Refactor plugin configuration #99

Closed
wants to merge 25 commits into from
Closed

Refactor plugin configuration #99

wants to merge 25 commits into from

Conversation

davidmpaz
Copy link
Contributor

Extract encore built in plugin configuration logic into plugin utils.

First step toward allowing to be able to configure plugins through options and have more modular integration for plugins inside encore. From now on only public API with proper options is needed,
for most cases.

This changes are internals and tries to improve plugin architecture inside encore. Please take a look on it and feedback are welcome :). So far I see improvements for several issues:

thanks in advance.

Extract encore built in plugin configuration logic into plugin utils.

First step toward allowing to be able to configure plugins through
options. From now own only public API with proper options is needed
for most cases.
@weaverryan
Copy link
Member

I'll check this out tonight! Excited about this!

@weaverryan
Copy link
Member

Ok, just checked this out! Breaking things into smaller pieces is a win :). To be honest, I'm struggling on a few of the details about "how best" to do this... but since it's internal stuff, we shouldn't sweat it too much.

From now on only public API with proper options is needed, for most cases.

What do you mean by this?

About the individual plugins files, naming the method getPlugins() and allowing each to return an array of plugins looks weird to me (I realize you based it off of my getLoaders()... and it felt weird there too :) ). What if each plugin file exports just a function... and what if we pass plugins to that function and have it modify the array. So:

// clean.js
module.exports = function(plugins, webpackConfig, paths, cleanUpOptions = {}) {
    let config = Object.assign({}, cleanUpOptions, {
        root: webpackConfig.outputPath,
        verbose: false,
    });

    plugins.push(new CleanWebpackPlugin(paths, config));
};

config-generator.js would look like this:

if (this.webpackConfig.cleanupOutput) {
    cleanPluginUtil(plugins, this.webpackConfig, ['**/*']);
}

Actually, we could go a step further, and include the logic of whether or not a plugin should be enabled, inside of that file:

// clean.js
module.exports = function(plugins, webpackConfig, paths, cleanUpOptions = {}) {
    if (!this.webpackConfig.cleanupOutput) {
        return;
    }

    let config = // ...
    // rest of file
};

It still doesn't feel perfect, but maybe good enough for the next step :).

@@ -0,0 +1,28 @@
/*
Copy link
Member

Choose a reason for hiding this comment

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

This isn't really a dev-server plugin... it's a "AssetOutputDisplayPlugin", which happens to be disabled when the dev-server is used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oops !! my bad. I'll update that.

getPlugins(webpackConfig) {
const outputPath = pathUtil.getRelativeOutputPath(webpackConfig);
const friendlyErrorsPlugin = FriendlyErrorsPlugin.getPlugins();
return [new AssetOutputDisplayPlugin(outputPath, friendlyErrorsPlugin[0])];
Copy link
Member

Choose a reason for hiding this comment

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

This is clever, but a problem :). Because FriendlyErrorsPlugin.getPlugins() actually instantiates a new FriendlyErrorsWebpackPlugin internally, when FriendlyErrorsPlugin.getPlugins() is called by config-generator, it will receive one instance. When it's called here, it will receive a different instance. I "got away: with doing stuff like this with the loaders, because those return static config objects, not new instances of objects. In practice, this might not be a problem, but we can do better :).

The only idea I can think of right now, is to change plugins in config-generator to a hash - i.e. create keys for each plugin - e.g.

// old
plugins.push(new FriendlyErrorsPlugin());

// now
plugins.push('friendly_errors_plugin', new FriendlyErrorsPlugin());

then we could use that internal key to find other plugins. At the bottom of buildPluginsConfig() in config-generator, we would still return an actual array (not a hash/object). I don't love this magic, invented friendly_errors_plugin machine name... but it would work, and would still be an internal implementation detail, at least for now (i.e. not exposed to users in any way).

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 see your point. Is this issue present in general for all plugins or it is just this particular case with the FriendlyErrorsWebpackPlugin?

Regarding hashed solution:

... this magic, invented friendly_errors_plugin machine name...

I don't like it either. This case looks to me like a good scenario for a singleton. I did some research about the topic on the nodejs land. I did found some resources:

What do you think if we try to implement one of those solutions for that FriendlyErrorsPlugin?

I think this is the best way we can guaranty the single instance we need.

Copy link
Member

Choose a reason for hiding this comment

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

This case looks to me like a good scenario for a singleton

Yes.... but no :). We allow you to generate multiple webpack configs. And to be strict about it, each separate webpack config should have its own instances of plugins.

So, yes, we need to make sure that we don't create multiple FriendlyErrorsWebpackPlugin while generating one webpack config... but do need to create distinct instances, each time we ConfigGenerator.getWebpackConfig(). If you can take a shot at that, perfect :).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To apply a solution fitting all cases known so far, I take into account:

  • This is a problem applied only for FriendlyErrorsWebpackPlugin?
  • We.... do need to create distinct instances, each time we ConfigGenerator.getWebpackConfig()
  • We need to make sure that we don't create multiple FriendlyErrorsWebpackPlugin while generating one webpack config

From what we have talked I think the best solution is to not refactor this plugin at all :), we leave it as it was originally. That way we met all requirements.

Still, I am sorry to go over this but I would like to know what I am missing :)

FriendlyErrorsWebpackPlugin is configured currently without any options coming from user, we set up same config options, so every time we will be creating same object values. Different instances (memory references etc..) but behavior i believe is the same.

Having the previous in mind. In which way a singleton will be different to the original code when we call Encore.reset() to generate a new configuration?

thanks in advance for taking the time.

Copy link
Member

Choose a reason for hiding this comment

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

In which way a singleton will be different to the original code when we call Encore.reset() to generate a new configuration?

Honestly, in practice, it probably makes not difference for FriendlyErrorsWebpackPlugin. In fact, it's not clear to me what the proper behavior is for this plugin specifically when it comes to multi webpack config setups - there's an option question about it: geowarin/friendly-errors-webpack-plugin#56.

Could we just pass in the FriendlyErrorsPlugin instance as a dependency - i.e. add a friendlyErrorsWebpackPlugin argument to this plugin and pass the instance in from config-generator? It would work and no complexity or magic.

@davidmpaz
Copy link
Contributor Author

To answer your first comment

Regarding:

From now on only public API with proper options is needed, for most cases.

I did mean that with this approach (broken into smaller pieces). Any future plugin (fork-ts-checker-webpack-plugin, browser-sync, etc..) can be easily integrated by creating another plugin module and adding respective public API. It was not a comment for internally used plugins, those are already integrated it ins owns way.

For:

What if each plugin file exports just a function... and what if we pass plugins to that function and have it modify the array.

I like this idea far more than current one, actually I was trying to get some result similar to an interface implementation, as long as all plugins integration follow same pattern i don't see any problem. To your suggestion though I would only change one thing, function signature:

module.exports = function(webpackConfig, paths, cleanUpOptions = {}) {

I think we don't need the plugin array any more because webpackConfig already contain plugins on its own, plugins that are added later on by:

this.webpackConfig.plugins.forEach(function(plugin) {
    plugins.push(plugin);
});

we could push to webpackConfig.plugins directly and get rid of that intermediate piece of code. Since plugin execution in webpack matters, we could ask as convention to users to add any custom plugin through addPlugin() api at the end of Encore calls.

What do you think?

@weaverryan
Copy link
Member

I did mean that with this approach (broken into smaller pieces). Any future plugin (fork-ts-checker-webpack-plugin, browser-sync, etc..) can be easily integrated by creating another plugin module and adding respective public API

I understand!

I think we don't need the plugin array any more because webpackConfig already contain plugins on its own, plugins that are added later on by:

This sounds good... but so far, when we call ConfigGenerator.getWebpackConfig(), we never modify the WebpackConfig. I think we should continue to not modify this object: it's our input only.

Do we agree on enough to get this to a good spot? I feel pretty comfortable - it's internal stuff anyways, which we can continue to iterate on. I don't want to delay this PR too much more :).

Thanks!

@davidmpaz
Copy link
Contributor Author

I feel also comfortable with the result. Thanks for the clarifications, did helped a lot.

@weaverryan
Copy link
Member

Very nice! Thank you David!

@davidmpaz davidmpaz deleted the refactor-plugins branch July 30, 2017 11:59
weaverryan added a commit that referenced this pull request Aug 5, 2017
This PR was squashed before being merged into the master branch (closes #101).

Discussion
----------

Forked typescript type checking

This PR fixes: #63 it should be possible to merge also with #99 directly.

Open points:

Regarding point .1 in #2

> If this plugin is added. we need to check dependency is added using loader-features.js. This module name is getting name from loaders, we are not adding a loader here but a plugin. Maybe some more general module is needed for checking features?

After implementing this I realize that as  soon as you require a module which doesn't exist node throws an error, we don't even go through error catch from encore. So possible solutions I see for this pending point are:

1. We don't refactor `loader-features.js` file and we still will get a loud error when trying to use some plugin not installed. From error is clear whats need to be done.
2. We refactor `loader-features.js` to be named more general and we include plugins dependencies also there as intended initially. But this way we need to check for missing packages earlier in `WebpackConfig.js` like I am doing now in this PR.

I like either solutions the same, I would like to get feedback on what do you guys think about it?

thanks in advance.

Commits
-------

7a7e399 Forked typescript type checking
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.

2 participants