Skip to content

Commit 112ec4b

Browse files
committed
feature #509 feat: Add method to configure loaders rules (Kocal, weaverryan)
This PR was merged into the master branch. Discussion ---------- feat: Add method to configure loaders rules This PR is a proposal to resolve #473 and #504 in a clean way. `Encore.configureLoaderRule()` is a low-level function, and has for goal to let the user having full access to Webpack loaders rules (what we push into `module.rules`) and configure them. This is the implementation of the idea I had in #504 (comment). For resolving Vue files linting issue, the next example would be the ideal solution I think: ```js Encore .configureLoaderRule('eslint', loader => { loader.test = /\.(jsx?|vue)$/ }); // actually, the equivalent code is something like this: const webpackConfig = Encore.getWebpackConfig(); const eslintLoader = webpackConfig.module.rules.find(rule => rule.loader === 'eslint-loader'); eslintLoader.test = /\.(jsx?|vue)$/; return webpackConfig; ``` For now, only ESLint loader is supported, but we can easily add other loaders. Let me know what you think of this solution, thanks! Commits ------- 94da0c2 language tweak 9dd6ca4 chore(tests): correct some tests due to last feature for CSSModules 4dbe443 chore(cr): add real aliases support 729a696 feat: add warning 5cf3d02 feat: add missing loaders, add more tests 86dc788 refactor(test): `configureLoaderRule()` will be easier to test 39cb9bd chore(cr): move tests into bc0e9bf chore(cr): add shortcut function applyRuleConfigurationCallback bc1d025 chore(cr): more user-friendly error for valid configurable loaders 01880cf Add method on "Encore" 49a4258 add tests 9892516 feat: implement "configureLoaderRule" method
2 parents 356e6e6 + 94da0c2 commit 112ec4b

File tree

5 files changed

+325
-22
lines changed

5 files changed

+325
-22
lines changed

index.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,32 @@ class Encore {
11001100
return this;
11011101
}
11021102

1103+
/**
1104+
* Configure Webpack loaders rules (`module.rules`).
1105+
* This is a low-level function, be careful when using it.
1106+
*
1107+
* https://webpack.js.org/concepts/loaders/#configuration
1108+
*
1109+
* For example, if you are using Vue and ESLint loader,
1110+
* this is how you can configure ESLint to lint Vue files:
1111+
*
1112+
* Encore
1113+
* .enableEslintLoader()
1114+
* .enableVueLoader()
1115+
* .configureLoaderRule('eslint', (loaderRule) => {
1116+
* loaderRule.test = /\.(jsx?|vue)/;
1117+
* });
1118+
*
1119+
* @param {string} name
1120+
* @param {function} callback
1121+
* @return {Encore}
1122+
*/
1123+
configureLoaderRule(name, callback) {
1124+
webpackConfig.configureLoaderRule(name, callback);
1125+
1126+
return this;
1127+
}
1128+
11031129
/**
11041130
* If enabled, the output directory is emptied between each build (to remove old files).
11051131
*

lib/WebpackConfig.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,19 @@ class WebpackConfig {
100100
this.eslintLoaderOptionsCallback = () => {};
101101
this.tsConfigurationCallback = () => {};
102102
this.handlebarsConfigurationCallback = () => {};
103+
this.loaderConfigurationCallbacks = {
104+
javascript: () => {},
105+
css: () => {},
106+
images: () => {},
107+
fonts: () => {},
108+
sass: () => {},
109+
less: () => {},
110+
stylus: () => {},
111+
vue: () => {},
112+
eslint: () => {},
113+
typescript: () => {},
114+
handlebars: () => {},
115+
};
103116

104117
// Plugins options
105118
this.cleanWebpackPluginPaths = ['**/*'];
@@ -716,6 +729,31 @@ class WebpackConfig {
716729
});
717730
}
718731

732+
configureLoaderRule(name, callback) {
733+
logger.warning('Be careful when using Encore.configureLoaderRule(), this is a low-level method that can potentially break Encore and Webpack when not used carefully.');
734+
735+
// Key: alias, Value: existing loader in `this.loaderConfigurationCallbacks`
736+
const aliases = {
737+
js: 'javascript',
738+
ts: 'typescript',
739+
scss: 'sass',
740+
};
741+
742+
if (name in aliases) {
743+
name = aliases[name];
744+
}
745+
746+
if (!(name in this.loaderConfigurationCallbacks)) {
747+
throw new Error(`Loader "${name}" is not configurable. Valid loaders are "${Object.keys(this.loaderConfigurationCallbacks).join('", "')}" and the aliases "${Object.keys(aliases).join('", "')}".`);
748+
}
749+
750+
if (typeof callback !== 'function') {
751+
throw new Error('Argument 2 to configureLoaderRule() must be a callback function.');
752+
}
753+
754+
this.loaderConfigurationCallbacks[name] = callback;
755+
}
756+
719757
useDevServer() {
720758
return this.runtimeConfig.useDevServer;
721759
}

lib/config-generator.js

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -221,14 +221,18 @@ class ConfigGenerator {
221221
}
222222

223223
buildRulesConfig() {
224+
const applyRuleConfigurationCallback = (name, defaultRules) => {
225+
return applyOptionsCallback(this.webpackConfig.loaderConfigurationCallbacks[name], defaultRules);
226+
};
227+
224228
let rules = [
225-
{
229+
applyRuleConfigurationCallback('javascript', {
226230
// match .js and .jsx
227231
test: /\.jsx?$/,
228232
exclude: this.webpackConfig.babelOptions.exclude,
229233
use: babelLoaderUtil.getLoaders(this.webpackConfig)
230-
},
231-
{
234+
}),
235+
applyRuleConfigurationCallback('css', {
232236
test: /\.css$/,
233237
oneOf: [
234238
{
@@ -245,7 +249,7 @@ class ConfigGenerator {
245249
)
246250
}
247251
]
248-
}
252+
})
249253
];
250254

251255
if (this.webpackConfig.useImagesLoader) {
@@ -269,11 +273,11 @@ class ConfigGenerator {
269273
Object.assign(loaderOptions, this.webpackConfig.urlLoaderOptions.images);
270274
}
271275

272-
rules.push({
276+
rules.push(applyRuleConfigurationCallback('images', {
273277
test: /\.(png|jpg|jpeg|gif|ico|svg|webp)$/,
274278
loader: loaderName,
275279
options: loaderOptions
276-
});
280+
}));
277281
}
278282

279283
if (this.webpackConfig.useFontsLoader) {
@@ -297,15 +301,15 @@ class ConfigGenerator {
297301
Object.assign(loaderOptions, this.webpackConfig.urlLoaderOptions.fonts);
298302
}
299303

300-
rules.push({
304+
rules.push(applyRuleConfigurationCallback('fonts', {
301305
test: /\.(woff|woff2|ttf|eot|otf)$/,
302306
loader: loaderName,
303307
options: loaderOptions
304-
});
308+
}));
305309
}
306310

307311
if (this.webpackConfig.useSassLoader) {
308-
rules.push({
312+
rules.push(applyRuleConfigurationCallback('sass', {
309313
test: /\.s[ac]ss$/,
310314
oneOf: [
311315
{
@@ -316,11 +320,11 @@ class ConfigGenerator {
316320
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, sassLoaderUtil.getLoaders(this.webpackConfig))
317321
}
318322
]
319-
});
323+
}));
320324
}
321325

322326
if (this.webpackConfig.useLessLoader) {
323-
rules.push({
327+
rules.push(applyRuleConfigurationCallback('less', {
324328
test: /\.less/,
325329
oneOf: [
326330
{
@@ -331,11 +335,11 @@ class ConfigGenerator {
331335
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, lessLoaderUtil.getLoaders(this.webpackConfig))
332336
}
333337
]
334-
});
338+
}));
335339
}
336340

337341
if (this.webpackConfig.useStylusLoader) {
338-
rules.push({
342+
rules.push(applyRuleConfigurationCallback('stylus', {
339343
test: /\.styl/,
340344
oneOf: [
341345
{
@@ -346,39 +350,39 @@ class ConfigGenerator {
346350
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, stylusLoaderUtil.getLoaders(this.webpackConfig))
347351
}
348352
]
349-
});
353+
}));
350354
}
351355

352356
if (this.webpackConfig.useVueLoader) {
353-
rules.push({
357+
rules.push(applyRuleConfigurationCallback('vue', {
354358
test: /\.vue$/,
355359
use: vueLoaderUtil.getLoaders(this.webpackConfig)
356-
});
360+
}));
357361
}
358362

359363
if (this.webpackConfig.useEslintLoader) {
360-
rules.push({
364+
rules.push(applyRuleConfigurationCallback('eslint', {
361365
test: /\.jsx?$/,
362366
loader: 'eslint-loader',
363367
exclude: /node_modules/,
364368
enforce: 'pre',
365369
options: eslintLoaderUtil.getOptions(this.webpackConfig)
366-
});
370+
}));
367371
}
368372

369373
if (this.webpackConfig.useTypeScriptLoader) {
370-
rules.push({
374+
rules.push(applyRuleConfigurationCallback('typescript', {
371375
test: /\.tsx?$/,
372376
exclude: /node_modules/,
373377
use: tsLoaderUtil.getLoaders(this.webpackConfig)
374-
});
378+
}));
375379
}
376380

377381
if (this.webpackConfig.useHandlebarsLoader) {
378-
rules.push({
382+
rules.push(applyRuleConfigurationCallback('handlebars', {
379383
test: /\.(handlebars|hbs)$/,
380384
use: handlebarsLoaderUtil.getLoaders(this.webpackConfig)
381-
});
385+
}));
382386
}
383387

384388
this.webpackConfig.loaders.forEach((loader) => {

test/WebpackConfig.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,4 +1144,36 @@ describe('WebpackConfig object', () => {
11441144
}).to.throw('Argument 1 to configureWatchOptions() must be a callback function.');
11451145
});
11461146
});
1147+
1148+
describe('configureLoaderRule()', () => {
1149+
it('works properly', () => {
1150+
const config = createConfig();
1151+
const callback = (loader) => {};
1152+
1153+
expect(config.loaderConfigurationCallbacks['eslint']).to.not.equal(callback);
1154+
1155+
config.configureLoaderRule('eslint', callback);
1156+
expect(config.loaderConfigurationCallbacks['eslint']).to.equal(callback);
1157+
});
1158+
1159+
it('Call method with a not supported loader', () => {
1160+
const config = createConfig();
1161+
1162+
expect(() => {
1163+
config.configureLoaderRule('reason');
1164+
}).to.throw('Loader "reason" is not configurable. Valid loaders are "javascript", "css", "images", "fonts", "sass", "less", "stylus", "vue", "eslint", "typescript", "handlebars" and the aliases "js", "ts", "scss".');
1165+
});
1166+
1167+
it('Call method with not a valid callback', () => {
1168+
const config = createConfig();
1169+
1170+
expect(() => {
1171+
config.configureLoaderRule('eslint');
1172+
}).to.throw('Argument 2 to configureLoaderRule() must be a callback function.');
1173+
1174+
expect(() => {
1175+
config.configureLoaderRule('eslint', {});
1176+
}).to.throw('Argument 2 to configureLoaderRule() must be a callback function.');
1177+
});
1178+
});
11471179
});

0 commit comments

Comments
 (0)