diff --git a/fixtures/css/url-loader.css b/fixtures/css/url-loader.css new file mode 100644 index 00000000..b3941404 --- /dev/null +++ b/fixtures/css/url-loader.css @@ -0,0 +1,8 @@ +@font-face { + font-family: 'Roboto'; + src: url('./../fonts/Roboto.woff2') format('woff2'); +} + +.foo { + background: url('./../images/symfony_logo.png'); +} diff --git a/index.js b/index.js index 61b430db..e345b756 100644 --- a/index.js +++ b/index.js @@ -771,6 +771,33 @@ class Encore { return this; } + /** + * Allows to configure the URL loader. + * + * https://github.com/webpack-contrib/url-loader + * + * Encore.configureUrlLoader({ + * images: { + * limit: 8192, + * mimetype: 'image/png' + * }, + * fonts: { + * limit: 4096 + * } + * }); + * + * If a key (e.g. fonts) doesn't exists or contains a + * falsy value the file-loader will be used instead. + * + * @param {object} urlLoaderOptions + * @return {Encore} + */ + configureUrlLoader(urlLoaderOptions = {}) { + webpackConfig.configureUrlLoader(urlLoaderOptions); + + return this; + } + /** * If enabled, the output directory is emptied between each build (to remove old files). * diff --git a/lib/WebpackConfig.js b/lib/WebpackConfig.js index 68545720..6f505124 100644 --- a/lib/WebpackConfig.js +++ b/lib/WebpackConfig.js @@ -73,6 +73,10 @@ class WebpackConfig { this.preactOptions = { preactCompat: false }; + this.urlLoaderOptions = { + images: false, + fonts: false + }; // Features/Loaders options callbacks this.postCssLoaderOptionsCallback = () => {}; @@ -466,6 +470,22 @@ class WebpackConfig { this.configuredFilenames = configuredFilenames; } + configureUrlLoader(urlLoaderOptions = {}) { + if (typeof urlLoaderOptions !== 'object') { + throw new Error('Argument 1 to configureUrlLoader() must be an object.'); + } + + // Check allowed keys + const validKeys = ['images', 'fonts']; + for (const key of Object.keys(urlLoaderOptions)) { + if (validKeys.indexOf(key) === -1) { + throw new Error(`"${key}" is not a valid key for configureUrlLoader(). Valid keys: ${validKeys.join(', ')}.`); + } + } + + this.urlLoaderOptions = urlLoaderOptions; + } + cleanupOutputBeforeBuild(paths = ['**/*'], cleanWebpackPluginOptionsCallback = () => {}) { if (!Array.isArray(paths)) { throw new Error('Argument 1 to cleanupOutputBeforeBuild() must be an Array of paths - e.g. [\'**/*\']'); diff --git a/lib/config-generator.js b/lib/config-generator.js index b6820a09..36d8af2f 100644 --- a/lib/config-generator.js +++ b/lib/config-generator.js @@ -11,6 +11,7 @@ const extractText = require('./loaders/extract-text'); const pathUtil = require('./config/path-util'); +const loaderFeatures = require('./features'); // loaders utils const cssLoaderUtil = require('./loaders/css'); const sassLoaderUtil = require('./loaders/sass'); @@ -149,13 +150,24 @@ class ConfigGenerator { filename = this.webpackConfig.configuredFilenames.images; } + // The url-loader can be used instead of the default file-loader by + // calling Encore.configureUrlLoader({ images: {/* ... */}}) + let loaderName = 'file-loader'; + const loaderOptions = { + name: filename, + publicPath: this.webpackConfig.getRealPublicPath() + }; + + if (this.webpackConfig.urlLoaderOptions.images) { + loaderFeatures.ensurePackagesExist('urlloader'); + loaderName = 'url-loader'; + Object.assign(loaderOptions, this.webpackConfig.urlLoaderOptions.images); + } + rules.push({ test: /\.(png|jpg|jpeg|gif|ico|svg|webp)$/, - loader: 'file-loader', - options: { - name: filename, - publicPath: this.webpackConfig.getRealPublicPath() - } + loader: loaderName, + options: loaderOptions }); } @@ -166,13 +178,24 @@ class ConfigGenerator { filename = this.webpackConfig.configuredFilenames.fonts; } + // The url-loader can be used instead of the default file-loader by + // calling Encore.configureUrlLoader({ fonts: {/* ... */}}) + let loaderName = 'file-loader'; + const loaderOptions = { + name: filename, + publicPath: this.webpackConfig.getRealPublicPath() + }; + + if (this.webpackConfig.urlLoaderOptions.fonts) { + loaderFeatures.ensurePackagesExist('urlloader'); + loaderName = 'url-loader'; + Object.assign(loaderOptions, this.webpackConfig.urlLoaderOptions.fonts); + } + rules.push({ test: /\.(woff|woff2|ttf|eot|otf)$/, - loader: 'file-loader', - options: { - name: filename, - publicPath: this.webpackConfig.getRealPublicPath() - } + loader: loaderName, + options: loaderOptions }); } diff --git a/lib/features.js b/lib/features.js index 9e8d5fce..5eb92b4c 100644 --- a/lib/features.js +++ b/lib/features.js @@ -72,6 +72,11 @@ const features = { method: 'enableBuildNotifications()', packages: ['webpack-notifier'], description: 'display build notifications' + }, + urlloader: { + method: 'configureUrlLoader()', + packages: ['url-loader'], + description: 'use the url-loader' } }; diff --git a/package.json b/package.json index b1b7b106..6495ac49 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "stylus-loader": "^3.0.1", "ts-loader": "^2.1.0", "typescript": "^2.3.4", + "url-loader": "^1.0.1", "vue": "^2.3.4", "vue-loader": "^12.2.1", "vue-template-compiler": "^2.3.4", diff --git a/test/WebpackConfig.js b/test/WebpackConfig.js index 1a4200da..6d57eefb 100644 --- a/test/WebpackConfig.js +++ b/test/WebpackConfig.js @@ -838,4 +838,37 @@ describe('WebpackConfig object', () => { }).to.throw('"foo" is not a valid key'); }); }); + + describe('configureUrlLoader', () => { + it('Calling method sets it', () => { + const config = createConfig(); + config.configureUrlLoader({ + images: { limit: 8192 }, + fonts: { limit: 4096 } + }); + + expect(config.urlLoaderOptions).to.deep.equals({ + images: { limit: 8192 }, + fonts: { limit: 4096 } + }); + }); + + it('Calling with non-object throws an error', () => { + const config = createConfig(); + + expect(() => { + config.configureUrlLoader('FOO'); + }).to.throw('must be an object'); + }); + + it('Calling with an unknown key throws an error', () => { + const config = createConfig(); + + expect(() => { + config.configureUrlLoader({ + foo: 'bar' + }); + }).to.throw('"foo" is not a valid key'); + }); + }); }); diff --git a/test/config-generator.js b/test/config-generator.js index d755d528..f4a819e7 100644 --- a/test/config-generator.js +++ b/test/config-generator.js @@ -674,6 +674,66 @@ describe('The config-generator function', () => { }); }); + describe('configureUrlLoader() allows to use the URL loader for fonts/images', () => { + it('without configureUrlLoader()', () => { + const config = createConfig(); + config.outputPath = '/tmp/public-path'; + config.publicPath = '/public-path'; + config.addEntry('main', './main'); + + const actualConfig = configGenerator(config); + + const imagesRule = findRule(/\.(png|jpg|jpeg|gif|ico|svg|webp)$/, actualConfig.module.rules); + expect(imagesRule.loader).to.equal('file-loader'); + + const fontsRule = findRule(/\.(woff|woff2|ttf|eot|otf)$/, actualConfig.module.rules); + expect(fontsRule.loader).to.equal('file-loader'); + }); + + it('with configureUrlLoader() and missing keys', () => { + const config = createConfig(); + config.outputPath = '/tmp/public-path'; + config.publicPath = '/public-path'; + config.addEntry('main', './main'); + config.configureUrlLoader({}); + + const actualConfig = configGenerator(config); + + const imagesRule = findRule(/\.(png|jpg|jpeg|gif|ico|svg|webp)$/, actualConfig.module.rules); + expect(imagesRule.loader).to.equal('file-loader'); + + const fontsRule = findRule(/\.(woff|woff2|ttf|eot|otf)$/, actualConfig.module.rules); + expect(fontsRule.loader).to.equal('file-loader'); + }); + + it('with configureUrlLoader()', () => { + const config = createConfig(); + config.outputPath = '/tmp/public-path'; + config.publicPath = '/public-path'; + config.addEntry('main', './main'); + config.configureFilenames({ + images: '[name].foo.[ext]', + fonts: '[name].bar.[ext]' + }); + config.configureUrlLoader({ + images: { limit: 8192 }, + fonts: { limit: 4096 } + }); + + const actualConfig = configGenerator(config); + + const imagesRule = findRule(/\.(png|jpg|jpeg|gif|ico|svg|webp)$/, actualConfig.module.rules); + expect(imagesRule.loader).to.equal('url-loader'); + expect(imagesRule.options.name).to.equal('[name].foo.[ext]'); + expect(imagesRule.options.limit).to.equal(8192); + + const fontsRule = findRule(/\.(woff|woff2|ttf|eot|otf)$/, actualConfig.module.rules); + expect(fontsRule.loader).to.equal('url-loader'); + expect(fontsRule.options.limit).to.equal(4096); + expect(fontsRule.options.name).to.equal('[name].bar.[ext]'); + }); + }); + describe('Test preact preset', () => { describe('Without preact-compat', () => { it('enablePreactPreset() does not add aliases to use preact-compat', () => { diff --git a/test/functional.js b/test/functional.js index 651e7d72..63a504c8 100644 --- a/test/functional.js +++ b/test/functional.js @@ -977,5 +977,35 @@ module.exports = { done(); }, true); }); + + it('configureUrlLoader() allows to use the URL loader for images/fonts', (done) => { + const config = createWebpackConfig('web/build', 'dev'); + config.setPublicPath('/build'); + config.addStyleEntry('url-loader', './css/url-loader.css'); + config.configureUrlLoader({ + images: { limit: 102400 }, + fonts: { limit: 102400 } + }); + + testSetup.runWebpack(config, (webpackAssert) => { + expect(config.outputPath).to.be.a.directory() + .with.files([ + 'url-loader.css', + 'manifest.json' + ]); + + webpackAssert.assertOutputFileContains( + 'url-loader.css', + 'url(data:font/woff2;base64,' + ); + + webpackAssert.assertOutputFileContains( + 'url-loader.css', + 'url(data:image/png;base64,' + ); + + done(); + }); + }); }); }); diff --git a/test/index.js b/test/index.js index 12cbe280..aef7e23b 100644 --- a/test/index.js +++ b/test/index.js @@ -287,6 +287,15 @@ describe('Public API', () => { }); + describe('configureUrlLoader', () => { + + it('must return the API object', () => { + const returnedValue = api.configureUrlLoader({}); + expect(returnedValue).to.equal(api); + }); + + }); + describe('cleanupOutputBeforeBuild', () => { it('must return the API object', () => { diff --git a/yarn.lock b/yarn.lock index a2f6d807..c4a7d854 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4114,6 +4114,10 @@ mime@^1.2.11, mime@^1.3.4, mime@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" +mime@^2.0.3: + version "2.3.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369" + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -5670,6 +5674,13 @@ schema-utils@^0.3.0: dependencies: ajv "^5.0.0" +schema-utils@^0.4.3: + version "0.4.5" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e" + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + scss-tokenizer@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" @@ -6506,6 +6517,14 @@ url-join@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78" +url-loader@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.0.1.tgz#61bc53f1f184d7343da2728a1289ef8722ea45ee" + dependencies: + loader-utils "^1.1.0" + mime "^2.0.3" + schema-utils "^0.4.3" + url-parse@1.0.x: version "1.0.5" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b"