Skip to content

Updating images and fonts to use Webpack 5 Asset modules #883

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

Merged
merged 7 commits into from
Jan 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,56 @@
# CHANGELOG

## 1.0.0

* [DEPENDENCY UPGRADE] Webpack was upgraded from version 4 to 5.

* [BC BREAK] Image and font processing was changed from using `file-loader`
(and optionally `url-loader` via `configureUrlLoader()`) to Webpack 5's
new [Asset Modules](https://webpack.js.org/guides/asset-modules/).
In practice, unless you have a highly-configured system, this should
not cause significant changes.

* [BC BREAK] The `configureUrlLoader()` method was removed. See
`configureImageRule()` and `configureFontRule()` - specifically the
`maxSize` option and type: 'asset'. The `url-loader` is no longer used.

* [BC BREAK] The `disableImagesLoader()` and `disableFontsLoader()` methods
have been removed. See `configureImageRule()` and `configureFontRule()`
for a new option to disable these.

* [BC BREAK] The `configureFilenames()` method no longer accepts paths
for `images` or `fonts`. See `configureImageRule()` and `configureFontRule()`
for how to configure these filenames. The `configureFilenames()` method
*does* now accept an `assets` option, but out-of-the-box, this will not
result in any filename changes. See `configureFilenames()` for more details.

* [BC BREAK] `css-minimizer-webpack-plugin` was replaced by
`css-minimizer-webpack-plugin` and the `optimizeCssPluginOptionsCallback()`
method was replaced by `cssMinimizerPluginOptionsCallback()`.

* [BC BREAK] The `file-loader` package is no longer required by Encore. If
you use `copyFiles()`, you will need to install it manually (you
will receive a clear error about this).

* [BC BREAK] All previously-deprecated methods & options were removed.

* [DEPENDENCY UPGRADES] The following packages had major version upgrades:
* `css-loader` from 3 to 5
* `mini-css-extract-plugin` from 0.4 to 1
* `style-loader` from 1 to 2
* `terser-webpack-plugin` from 1 to 4
* `webpack-cli` from 3 to 4
* `webpack-manifest-plugin` from 2 to 3

* [BEHAVIOR CHANGE] The `HashedModuleIdsPlugin` was previously used to
help name "modules" when building for production. This has been removed
and we now use Webpack's native `optimization.moduleIds` option, which
is set to `deterministic`.

* [configureMiniCssExtractPlugin()] `configureMiniCssExtractPlugin()` was
added to allow the `MiniCssExtractPlugin.loader` and `MiniCssExtractPlugin`
to be configured.

## 0.33.0

* [disableCssExtraction()] Added boolean argument to `disableCssExtraction()`
Expand Down
111 changes: 67 additions & 44 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1299,30 +1299,6 @@ class Encore {
return this;
}

/**
* Call this if you wish to disable the default
* images loader.
*
* @returns {Encore}
*/
disableImagesLoader() {
webpackConfig.disableImagesLoader();

return this;
}

/**
* Call this if you wish to disable the default
* fonts loader.
*
* @returns {Encore}
*/
disableFontsLoader() {
webpackConfig.disableFontsLoader();

return this;
}

/**
* Call this if you don't want imported CSS to be extracted
* into a .css file. All your styles will then be injected
Expand Down Expand Up @@ -1377,8 +1353,7 @@ class Encore {
* Encore.configureFilenames({
* js: '[name].[contenthash].js',
* css: '[name].[contenthash].css',
* images: 'images/[name].[hash:8].[ext]',
* fonts: 'fonts/[name].[hash:8].[ext]'
* assets: 'assets/[name].[hash:8][ext]',
* });
* ```
*
Expand All @@ -1389,6 +1364,10 @@ class Encore {
* make sure that your "js" and "css" filenames contain
* "[contenthash]".
*
* The "assets" key is used for the output.assetModuleFilename option,
* which is overridden for both fonts and images. See configureImageRule()
* and configureFontRule() to control those filenames.
*
* @param {object} filenames
* @returns {Encore}
*/
Expand All @@ -1399,30 +1378,73 @@ class Encore {
}

/**
* Allows to configure the URL loader.
* Configure how images are loaded/processed under module.rules.
*
* https://webpack.js.org/guides/asset-modules/
*
* https://github.com/webpack-contrib/url-loader
* The most important things can be controlled by passing
* an options object to the first argument:
*
* ```
* Encore.configureUrlLoader({
* images: {
* limit: 8192,
* mimetype: 'image/png'
* },
* fonts: {
* limit: 4096
* }
* Encore.configureImageRule({
* // common values: asset, asset/resource, asset/inline
* // Using "asset" will allow smaller images to be "inlined"
* // instead of copied.
* // javascript/auto caan be used to disable asset images (see next example)
* type: 'asset/resource',
*
* // applicable when for "type: asset": files smaller than this
* // size will be "inlined" into CSS, larer files will be extracted
* // into independent files
* maxSize: 4 * 1024, // 4 kb
*
* // control the output filename of images
* filename: 'images/[name].[hash:8][ext]',
*
* // you can also fully disable the image rule if you want
* // to control things yourself
* enabled: true,
* });
* ```
*
* If a key (e.g. fonts) doesn't exists or contains a
* falsy value the file-loader will be used instead.
* If you need more control, you can also pass a callback to the
* 2nd argument. This will be passed the specific Rule object,
* which you can modify:
*
* @param {object} urlLoaderOptions
* @return {Encore}
* https://webpack.js.org/configuration/module/#rule
*
* ```
* Encore.configureImageRule({}, function(rule) {
* // if you set "type: 'javascript/auto'" in the first argument,
* // then you can now specify a loader manually
* // rule.loader = 'file-loader';
* // rule.options = { filename: 'images/[name].[hash:8][ext]' }
* });
* ```
*
* @param {object} options
* @param {string|object|function} ruleCallback
* @returns {Encore}
*/
configureImageRule(options = {}, ruleCallback = (rule) => {}) {
webpackConfig.configureImageRule(options, ruleCallback);

return this;
}

/**
* Configure how fonts are processed/loaded under module.rules.
*
* https://webpack.js.org/guides/asset-modules/
*
* See configureImageRule() for more details.
*
* @param {object} options
* @param {string|object|function} ruleCallback
* @returns {Encore}
*/
configureUrlLoader(urlLoaderOptions = {}) {
webpackConfig.configureUrlLoader(urlLoaderOptions);
configureFontRule(options = {}, ruleCallback = (rule) => {}) {
webpackConfig.configureFontRule(options, ruleCallback);

return this;
}
Expand Down Expand Up @@ -1597,9 +1619,10 @@ class Encore {
*
* @param {string} environment
* @param {object} options
* @param {boolean} enablePackageJsonCheck
* @returns {Encore}
*/
configureRuntimeEnvironment(environment, options = {}) {
configureRuntimeEnvironment(environment, options = {}, enablePackageJsonCheck = true) {
runtimeConfig = parseRuntime(
Object.assign(
{},
Expand All @@ -1609,7 +1632,7 @@ class Encore {
process.cwd()
);

webpackConfig = new WebpackConfig(runtimeConfig, true);
webpackConfig = new WebpackConfig(runtimeConfig, enablePackageJsonCheck);

return this;
}
Expand Down
74 changes: 49 additions & 25 deletions lib/WebpackConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,18 @@ class WebpackConfig {
this.useSourceMaps = false;
this.cleanupOutput = false;
this.extractCss = true;
this.useImagesLoader = true;
this.useFontsLoader = true;
this.imageRuleOptions = {
type: 'asset/resource',
maxSize: null,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm wondering if we shouldn't override the default value for maxSize.

Based on the documentation everything under 8kb will be inlined... which isn't necessarily a bad thing but it will probably confuse some people/break things (for instance if someone used to import images in order to trigger a copy).

Copy link
Member Author

Choose a reason for hiding this comment

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

which isn't necessarily a bad thing but it will probably confuse some people/break things

I think this will also cause some confusion. On the other hand, if you have:

import image from './images.foo.png';

img.src = image;

I think this will work regardless of whether it was inlined or copied. Unless we defaulted this to 0 (or used asset/resource instead of assets by default), this will surprise people.

So, I'm conflicted: on the one hand, the inlining of 8kb and lower is probably better for performance... but will cause more confusion.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm going to go the "more conservative" route and use asset/resource by default

filename: 'images/[name].[hash:8][ext]',
enabled: true,
};
this.fontRuleOptions = {
type: 'asset/resource',
maxSize: null,
filename: 'fonts/[name].[hash:8][ext]',
enabled: true,
};
this.usePostCssLoader = false;
this.useLessLoader = false;
this.useStylusLoader = false;
Expand Down Expand Up @@ -126,10 +136,6 @@ class WebpackConfig {
this.preactOptions = {
preactCompat: false
};
this.urlLoaderOptions = {
images: false,
fonts: false
};
this.babelOptions = {
exclude: /(node_modules|bower_components)/,
useBuiltIns: false,
Expand All @@ -146,6 +152,8 @@ class WebpackConfig {
};

// Features/Loaders options callbacks
this.imageRuleCallback = () => {};
this.fontRuleCallback = () => {};
this.postCssLoaderOptionsCallback = () => {};
this.sassLoaderOptionsCallback = () => {};
this.lessLoaderOptionsCallback = () => {};
Expand Down Expand Up @@ -813,14 +821,6 @@ class WebpackConfig {
this.handlebarsConfigurationCallback = callback;
}

disableImagesLoader() {
this.useImagesLoader = false;
}

disableFontsLoader() {
this.useFontsLoader = false;
}

disableCssExtraction(disabled = true) {
this.extractCss = !disabled;
}
Expand All @@ -831,30 +831,54 @@ class WebpackConfig {
}

// Check allowed keys
const validKeys = ['js', 'css', 'images', 'fonts'];
const validKeys = ['js', 'css', 'assets'];
for (const key of Object.keys(configuredFilenames)) {
if (!validKeys.includes(key)) {
throw new Error(`"${key}" is not a valid key for configureFilenames(). Valid keys: ${validKeys.join(', ')}.`);
throw new Error(`"${key}" is not a valid key for configureFilenames(). Valid keys: ${validKeys.join(', ')}. Use configureImageRule() or configureFontRule() to control image or font filenames.`);
}
}

this.configuredFilenames = configuredFilenames;
}

configureUrlLoader(urlLoaderOptions = {}) {
if (typeof urlLoaderOptions !== 'object') {
throw new Error('Argument 1 to configureUrlLoader() must be an object.');
configureImageRule(options = {}, ruleCallback = () => {}) {
for (const optionKey of Object.keys(options)) {
if (!(optionKey in this.imageRuleOptions)) {
throw new Error(`Invalid option "${optionKey}" passed to configureImageRule(). Valid keys are ${Object.keys(this.imageRuleOptions).join(', ')}`);
}

this.imageRuleOptions[optionKey] = options[optionKey];
}

// Check allowed keys
const validKeys = ['images', 'fonts'];
for (const key of Object.keys(urlLoaderOptions)) {
if (!validKeys.includes(key)) {
throw new Error(`"${key}" is not a valid key for configureUrlLoader(). Valid keys: ${validKeys.join(', ')}.`);
if (this.imageRuleOptions.maxSize && this.imageRuleOptions.type !== 'asset') {
throw new Error('Invalid option "maxSize" passed to configureImageRule(): this option is only valid when "type" is set to "asset".');
}

if (typeof ruleCallback !== 'function') {
throw new Error('Argument 2 to configureImageRule() must be a callback function.');
}

this.imageRuleCallback = ruleCallback;
}

configureFontRule(options = {}, ruleCallback = () => {}) {
for (const optionKey of Object.keys(options)) {
if (!(optionKey in this.fontRuleOptions)) {
throw new Error(`Invalid option "${optionKey}" passed to configureFontRule(). Valid keys are ${Object.keys(this.fontRuleOptions).join(', ')}`);
}

this.fontRuleOptions[optionKey] = options[optionKey];
}

if (this.fontRuleOptions.maxSize && this.fontRuleOptions.type !== 'asset') {
throw new Error('Invalid option "maxSize" passed to configureFontRule(): this option is only valid when "type" is set to "asset".');
}

if (typeof ruleCallback !== 'function') {
throw new Error('Argument 2 to configureFontRule() must be a callback function.');
}

this.urlLoaderOptions = urlLoaderOptions;
this.fontRuleCallback = ruleCallback;
}

cleanupOutputBeforeBuild(paths = ['**/*'], cleanWebpackPluginOptionsCallback = () => {}) {
Expand Down
Loading