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
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ae19fc6
Refactor plugin configuration
davidmpaz Jul 17, 2017
2cd7e0c
Rename plugin asset output display
davidmpaz Jul 21, 2017
3e3abdb
Refactor clean output plugin configuration
davidmpaz Jul 23, 2017
8e7ec3d
Refactor config-gernerator to use clean output factory
davidmpaz Jul 23, 2017
2661c81
Refactor friendly error plugin configuration
davidmpaz Jul 23, 2017
55a321e
Refactor config generator to use friendly error plugin
davidmpaz Jul 23, 2017
024ac22
Refactor asset output plugin
davidmpaz Jul 23, 2017
2cc2b0a
Refactor config generator to use asset output display
davidmpaz Jul 23, 2017
ac3f89c
Refactor common chunks configuration
davidmpaz Jul 23, 2017
dea3211
Refactor config generator to use comon chunks
davidmpaz Jul 23, 2017
930bd21
Refactor unused entries plugin configuration
davidmpaz Jul 23, 2017
8d63ab7
Refactor config generator to use unused entries
davidmpaz Jul 23, 2017
c5953cf
Refactor extract text plugin configuration
davidmpaz Jul 23, 2017
effd693
Refactor config generator to use extract assets
davidmpaz Jul 23, 2017
c2b3280
Refactor loader options configuration plugin
davidmpaz Jul 23, 2017
74489eb
refactor config geerator to use loader options
davidmpaz Jul 23, 2017
37a6dc7
Refactor manifest plugin configuration
davidmpaz Jul 23, 2017
2e63a34
Refactor config generator to use manifest
davidmpaz Jul 23, 2017
52f2c96
Refactor define and uglify configuration plugins
davidmpaz Jul 23, 2017
31280bb
Refactor config generator to use define and uglify
davidmpaz Jul 23, 2017
562b98e
Refactor variables provider configuration plugin
davidmpaz Jul 23, 2017
796aca6
Refactor config generator to use variable provider
davidmpaz Jul 23, 2017
63972a4
Refactor version configuration plugin
davidmpaz Jul 23, 2017
b650032
Refactor config generator to use versioning
davidmpaz Jul 23, 2017
8098aa6
Fix loader options linting error
davidmpaz Jul 23, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 47 additions & 160 deletions lib/config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,27 @@

'use strict';

const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractText = require('./loaders/extract-text');
const ManifestPlugin = require('./webpack/webpack-manifest-plugin');
const DeleteUnusedEntriesJSPlugin = require('./webpack/delete-unused-entries-js-plugin');
const AssetOutputDisplayPlugin = require('./friendly-errors/asset-output-display-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const WebpackChunkHash = require('webpack-chunk-hash');
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const missingLoaderTransformer = require('./friendly-errors/transformers/missing-loader');
const missingLoaderFormatter = require('./friendly-errors/formatters/missing-loader');
const missingPostCssConfigTransformer = require('./friendly-errors/transformers/missing-postcss-config');
const missingPostCssConfigFormatter = require('./friendly-errors/formatters/missing-postcss-config');
const vueUnactivatedLoaderTransformer = require('./friendly-errors/transformers/vue-unactivated-loader-error');
const vueUnactivatedLoaderFormatter = require('./friendly-errors/formatters/vue-unactivated-loader-error');
const pathUtil = require('./config/path-util');
// loaders utils
const cssLoaderUtil = require('./loaders/css');
const sassLoaderUtil = require('./loaders/sass');
const lessLoaderUtil = require('./loaders/less');
const babelLoaderUtil = require('./loaders/babel');
const tsLoaderUtil = require('./loaders/typescript');
const vueLoaderUtil = require('./loaders/vue');
// plugins utils
const extractTextPluginUtil = require('./plugins/extract-text');
const deleteUnusedEntriesPluginUtil = require('./plugins/delete-unused-entries');
const manifestPluginUtil = require('./plugins/manifest');
const loaderOptionsPluginUtil = require('./plugins/loader-options');
const versioningPluginUtil = require('./plugins/versioning');
const variableProviderPluginUtil = require('./plugins/variable-provider');
const cleanPluginUtil = require('./plugins/clean');
const commonChunksPluginUtil = require('./plugins/common-chunks');
const productionPluginUtil = require('./plugins/production');
const friendlyErrorPluginUtil = require('./plugins/friendly-errors');
const devServerPluginUtil = require('./plugins/dev-server');

class ConfigGenerator {
/**
Expand Down Expand Up @@ -180,172 +179,60 @@ class ConfigGenerator {
buildPluginsConfig() {
let plugins = [];

/*
* All CSS/SCSS content (due to the loaders above) will be
* extracted into an [entrypointname].css files. The result
* is that NO css will be inlined, *except* CSS that is required
* in an async way (e.g. via require.ensure()).
*
* This may not be ideal in some cases, but it's at least
* predictable. It means that you must manually add a
* link tag for an entry point's CSS (unless no CSS file
* was imported - in which case no CSS file will be dumped).
*/
plugins.push(new ExtractTextPlugin({
filename: this.webpackConfig.useVersioning ? '[name].[contenthash].css' : '[name].css',
// if true, async CSS (e.g. loaded via require.ensure())
// is extracted to the entry point CSS. If false, it's
// inlined in the AJAX-loaded .js file.
allChunks: false
}));
plugins = plugins.concat(
extractTextPluginUtil.getPlugins(this.webpackConfig)
);

// register the pure-style entries that should be deleted
plugins.push(new DeleteUnusedEntriesJSPlugin(
// transform into an Array
[... this.webpackConfig.styleEntries.keys()]
));

/*
* Dump the manifest.json file
*/
let manifestPrefix = this.webpackConfig.manifestKeyPrefix;
if (null === manifestPrefix) {
// by convention, we remove the opening slash on the manifest keys
manifestPrefix = this.webpackConfig.publicPath.replace(/^\//,'');
}
plugins.push(new ManifestPlugin({
basePath: manifestPrefix,
// guarantee the value uses the public path (or CDN public path)
publicPath: this.webpackConfig.getRealPublicPath(),
// always write a manifest.json file, even with webpack-dev-server
writeToFileEmit: true,
}));

/*
* This section is a bit mysterious. The "minimize"
* true is read and used to minify the CSS.
* But as soon as this plugin is included
* at all, SASS begins to have errors, until the context
* and output options are specified. At this time, I'm
* not totally sure what's going on here
* https://github.com/jtangelder/sass-loader/issues/285
*/
plugins.push(new webpack.LoaderOptionsPlugin({
debug: !this.webpackConfig.isProduction(),
options: {
context: this.webpackConfig.getContext(),
output: { path: this.webpackConfig.outputPath }
}
}));

/*
* With versioning, the "chunkhash" used in the filenames and
* the module ids (i.e. the internal names of modules that
* are required) become important. Specifically:
*
* 1) If the contents of a module don't change, then you don't want its
* internal module id to change. Otherwise, whatever file holds the
* webpack "manifest" will change because the module id will change.
* Solved by HashedModuleIdsPlugin or NamedModulesPlugin
*
* 2) Similarly, if the final contents of a file don't change,
* then we also don't want that file to have a new filename.
* The WebpackChunkHash() handles this, by making sure that
* the chunkhash is based off of the file contents.
*
* Even in the webpack community, the ideal setup seems to be
* a bit of a mystery:
* * https://github.com/webpack/webpack/issues/1315
* * https://github.com/webpack/webpack.js.org/issues/652#issuecomment-273324529
* * https://webpack.js.org/guides/caching/#deterministic-hashes
*/
if (this.webpackConfig.isProduction()) {
// shorter, and obfuscated module ids (versus NamedModulesPlugin)
// makes the final assets *slightly* larger, but prevents contents
// from sometimes changing when nothing really changed
plugins.push(new webpack.HashedModuleIdsPlugin());
} else {
// human-readable module names, helps debug in HMR
// enable always when not in production for consistency
plugins.push(new webpack.NamedModulesPlugin());
}
plugins = plugins.concat(
deleteUnusedEntriesPluginUtil.getPlugins(this.webpackConfig)
);

if (this.webpackConfig.useVersioning) {
// enables the [chunkhash] ability
plugins.push(new WebpackChunkHash());
}
// Dump the manifest.json file
plugins = plugins.concat(
manifestPluginUtil.getPlugins(this.webpackConfig)
);

plugins = plugins.concat(
loaderOptionsPluginUtil.getPlugins(this.webpackConfig)
);

plugins = plugins.concat(
versioningPluginUtil.getPlugins(this.webpackConfig)
);

if (Object.keys(this.webpackConfig.providedVariables).length > 0) {
plugins = plugins.concat([
new webpack.ProvidePlugin(this.webpackConfig.providedVariables)
]);
plugins = plugins.concat(
variableProviderPluginUtil.getPlugins(this.webpackConfig)
);
}

if (this.webpackConfig.cleanupOutput) {
plugins.push(
new CleanWebpackPlugin(['**/*'], {
root: this.webpackConfig.outputPath,
verbose: false,
})
plugins = plugins.concat(
cleanPluginUtil.getPlugins(this.webpackConfig, ['**/*'])
);
}

// if we're extracting a vendor chunk, set it up!
if (this.webpackConfig.sharedCommonsEntryName) {
plugins = plugins.concat([
new webpack.optimize.CommonsChunkPlugin({
name: [
this.webpackConfig.sharedCommonsEntryName,
/*
* Always dump a 2nd file - manifest.json that
* will contain the webpack manifest information.
* This changes frequently, and without this line,
* it would be packaged inside the "shared commons entry"
* file - e.g. vendor.js, which would prevent long-term caching.
*/
'manifest'
],
minChunks: Infinity,
}),
]);
plugins = plugins.concat(
commonChunksPluginUtil.getPlugins(this.webpackConfig)
);
}

if (this.webpackConfig.isProduction()) {
plugins = plugins.concat([
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),

// todo - options here should be configurable
new webpack.optimize.UglifyJsPlugin({
sourceMap: this.webpackConfig.useSourceMaps
})
]);
// todo - options here should be configurable
plugins = plugins.concat(
productionPluginUtil.getPlugins(this.webpackConfig)
);
}

const friendlyErrorsPlugin = new FriendlyErrorsWebpackPlugin({
clearConsole: false,
additionalTransformers: [
missingLoaderTransformer,
missingPostCssConfigTransformer,
vueUnactivatedLoaderTransformer
],
additionalFormatters: [
missingLoaderFormatter,
missingPostCssConfigFormatter,
vueUnactivatedLoaderFormatter
],
compilationSuccessInfo: {
messages: []
}
});
plugins.push(friendlyErrorsPlugin);
plugins = plugins.concat(friendlyErrorPluginUtil.getPlugins());

if (!this.webpackConfig.useDevServer()) {
const outputPath = pathUtil.getRelativeOutputPath(this.webpackConfig);
plugins.push(new AssetOutputDisplayPlugin(outputPath, friendlyErrorsPlugin));
plugins = plugins.concat(
devServerPluginUtil.getPlugins(this.webpackConfig)
);
}

this.webpackConfig.plugins.forEach(function(plugin) {
Expand Down
30 changes: 30 additions & 0 deletions lib/plugins/clean.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

'use strict';

const CleanWebpackPlugin = require('clean-webpack-plugin');

/**
* @param {WebpackConfig} webpackConfig
* @param {Array} paths to clean
* @param {Object} cleanUpOptions
* @return {Array} of plugins to add to webpack
*/
module.exports = {
getPlugins(webpackConfig, paths, cleanUpOptions = {}) {

let config = Object.assign({}, cleanUpOptions, {
root: webpackConfig.outputPath,
verbose: false,
});

return [new CleanWebpackPlugin(paths, config)];
}
};
36 changes: 36 additions & 0 deletions lib/plugins/common-chunks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

'use strict';

const webpack = require('webpack');

/**
* @param {WebpackConfig} webpackConfig
* @return {Array} of plugins to add to webpack
*/
module.exports = {
getPlugins(webpackConfig) {

return [new webpack.optimize.CommonsChunkPlugin({
name: [
webpackConfig.sharedCommonsEntryName,
/*
* Always dump a 2nd file - manifest.json that
* will contain the webpack manifest information.
* This changes frequently, and without this line,
* it would be packaged inside the "shared commons entry"
* file - e.g. vendor.js, which would prevent long-term caching.
*/
'manifest'
],
minChunks: Infinity,
})];
}
};
26 changes: 26 additions & 0 deletions lib/plugins/delete-unused-entries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

'use strict';

const DeleteUnusedEntriesJSPlugin = require('../webpack/delete-unused-entries-js-plugin');

/**
* @param {WebpackConfig} webpackConfig
* @return {Array} of plugins to add to webpack
*/
module.exports = {
getPlugins(webpackConfig) {

return [new DeleteUnusedEntriesJSPlugin(
// transform into an Array
[... webpackConfig.styleEntries.keys()]
)];
}
};
28 changes: 28 additions & 0 deletions lib/plugins/dev-server.js
Original file line number Diff line number Diff line change
@@ -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.

* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

'use strict';

const pathUtil = require('../config/path-util');
const AssetOutputDisplayPlugin = require('../friendly-errors/asset-output-display-plugin');
const FriendlyErrorsPlugin = require('./friendly-errors');

/**
* @param {WebpackConfig} webpackConfig
* @param {Array} paths to clean
* @param {Object} cleanUpOptions
* @return {Array} of plugins to add to webpack
*/
module.exports = {
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.

}
};
Loading