Skip to content

Commit 2a1a18a

Browse files
committed
feature #243 #232 Added support for the eslint-loader (pinoniq, Lyrkan)
This PR was merged into the master branch. Discussion ---------- #232 Added support for the eslint-loader I wasn't sure about whether to include the eslint-loader in the devDependencies or dependencies. Commits ------- 95af361 Remove eslint-plugin-import from required dependencies for enableEslintLoader() d06a0b3 Fix eslint-loader test cases 27b641e Removed the default import/resolver config 22fcb14 Fix misplaced cache option for the eslint-loader 0738691 Use the apply-options-callback method for the eslint-loader 87e1a02 Added the cache configuration for the eslint-loader 03bc596 Added a small functional test for eslint output a19414e Removed a default eslint rule Corrected spelling mistakes Reverted unrelated changes ce8c66d #232 Added support for the eslint-loader
2 parents fcecd24 + 95af361 commit 2a1a18a

File tree

11 files changed

+660
-148
lines changed

11 files changed

+660
-148
lines changed

fixtures/js/eslint.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
const a = 'foobar';

index.js

+34
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,40 @@ class Encore {
695695
return this;
696696
}
697697

698+
/**
699+
* If enabled, the eslint-loader is enabled.
700+
*
701+
* https://github.com/MoOx/eslint-loader
702+
*
703+
* // enables the eslint loaded using the default eslint configuration.
704+
* Encore.enableEslintLoader();
705+
*
706+
* // Optionally, you can pass in the configuration eslint should extend.
707+
* Encore.enableEslintLoader('airbnb');
708+
*
709+
* // You can also pass in an object of options
710+
* // that will be passed on to the eslint-loader
711+
* Encore.enableEslintLoader({
712+
* extends: 'airbnb',
713+
emitWarning: false
714+
* });
715+
*
716+
* // For a more advanced usage you can pass in a callback
717+
* // https://github.com/MoOx/eslint-loader#options
718+
* Encore.enableEslintLoader((options) => {
719+
* options.extends = 'airbnb';
720+
* options.emitWarning = false;
721+
* });
722+
*
723+
* @param {string|object|function} eslintLoaderOptionsOrCallback
724+
* @returns {Encore}
725+
*/
726+
enableEslintLoader(eslintLoaderOptionsOrCallback = () => {}) {
727+
webpackConfig.enableEslintLoader(eslintLoaderOptionsOrCallback);
728+
729+
return this;
730+
}
731+
698732
/**
699733
* If enabled, display build notifications using
700734
* webpack-notifier.

lib/WebpackConfig.js

+27
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class WebpackConfig {
6161
this.useReact = false;
6262
this.usePreact = false;
6363
this.useVueLoader = false;
64+
this.useEslintLoader = false;
6465
this.useTypeScriptLoader = false;
6566
this.useCoffeeScriptLoader = false;
6667
this.useForkedTypeScriptTypeChecking = false;
@@ -86,6 +87,7 @@ class WebpackConfig {
8687
this.stylusLoaderOptionsCallback = () => {};
8788
this.babelConfigurationCallback = () => {};
8889
this.vueLoaderOptionsCallback = () => {};
90+
this.eslintLoaderOptionsCallback = () => {};
8991
this.tsConfigurationCallback = () => {};
9092
this.coffeeScriptConfigurationCallback = () => {};
9193
this.handlebarsConfigurationCallback = () => {};
@@ -439,6 +441,31 @@ class WebpackConfig {
439441
this.vueLoaderOptionsCallback = vueLoaderOptionsCallback;
440442
}
441443

444+
enableEslintLoader(eslintLoaderOptionsOrCallback = () => {}) {
445+
this.useEslintLoader = true;
446+
447+
if (typeof eslintLoaderOptionsOrCallback === 'function') {
448+
this.eslintLoaderOptionsCallback = eslintLoaderOptionsOrCallback;
449+
return;
450+
}
451+
452+
if (typeof eslintLoaderOptionsOrCallback === 'string') {
453+
this.eslintLoaderOptionsCallback = (options) => {
454+
options.extends = eslintLoaderOptionsOrCallback;
455+
};
456+
return;
457+
}
458+
459+
if (typeof eslintLoaderOptionsOrCallback === 'object') {
460+
this.eslintLoaderOptionsCallback = (options) => {
461+
Object.assign(options, eslintLoaderOptionsOrCallback);
462+
};
463+
return;
464+
}
465+
466+
throw new Error('Argument 1 to enableEslintLoader() must be either a string, object or callback function.');
467+
}
468+
442469
enableBuildNotifications(enabled = true, notifierPluginOptionsCallback = () => {}) {
443470
if (typeof notifierPluginOptionsCallback !== 'function') {
444471
throw new Error('Argument 2 to enableBuildNotifications() must be a callback function.');

lib/config-generator.js

+11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const tsLoaderUtil = require('./loaders/typescript');
2222
const coffeeScriptLoaderUtil = require('./loaders/coffee-script');
2323
const vueLoaderUtil = require('./loaders/vue');
2424
const handlebarsLoaderUtil = require('./loaders/handlebars');
25+
const eslintLoaderUtil = require('./loaders/eslint');
2526
// plugins utils
2627
const extractTextPluginUtil = require('./plugins/extract-text');
2728
const deleteUnusedEntriesPluginUtil = require('./plugins/delete-unused-entries');
@@ -228,6 +229,16 @@ class ConfigGenerator {
228229
});
229230
}
230231

232+
if (this.webpackConfig.useEslintLoader) {
233+
rules.push({
234+
test: /\.jsx?$/,
235+
loader: 'eslint-loader',
236+
exclude: /node_modules/,
237+
enforce: 'pre',
238+
options: eslintLoaderUtil.getOptions(this.webpackConfig)
239+
});
240+
}
241+
231242
if (this.webpackConfig.useTypeScriptLoader) {
232243
rules.push({
233244
test: /\.tsx?$/,

lib/features.js

+6
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ const features = {
6868
packages: ['vue', 'vue-loader', 'vue-template-compiler'],
6969
description: 'load VUE files'
7070
},
71+
eslint: {
72+
method: 'enableEslintLoader()',
73+
// eslint is needed so the end-user can do things
74+
packages: ['eslint', 'eslint-loader', 'babel-eslint'],
75+
description: 'Enable ESLint checks'
76+
},
7177
notifier: {
7278
method: 'enableBuildNotifications()',
7379
packages: ['webpack-notifier'],

lib/loaders/eslint.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* This file is part of the Symfony Webpack Encore package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
12+
const loaderFeatures = require('../features');
13+
const applyOptionsCallback = require('../utils/apply-options-callback');
14+
15+
/**
16+
* @param {WebpackConfig} webpackConfig
17+
* @return {Object} of options to use for eslint-loader options.
18+
*/
19+
module.exports = {
20+
getOptions(webpackConfig) {
21+
loaderFeatures.ensurePackagesExist('eslint');
22+
23+
const eslintLoaderOptions = {
24+
cache: true,
25+
parser: 'babel-eslint',
26+
emitWarning: true
27+
};
28+
29+
return applyOptionsCallback(webpackConfig.eslintLoaderOptionsCallback, eslintLoaderOptions);
30+
}
31+
};

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,18 @@
5151
},
5252
"devDependencies": {
5353
"autoprefixer": "^6.7.7",
54+
"babel-eslint": "^8.2.1",
5455
"babel-plugin-transform-react-jsx": "^6.24.1",
5556
"babel-preset-es2015": "^6.24.1",
5657
"babel-preset-react": "^6.23.0",
5758
"chai": "^3.5.0",
5859
"chai-fs": "^1.0.0",
5960
"coffee-loader": "^0.9.0",
6061
"coffeescript": "^2.0.2",
61-
"eslint": "^3.19.0",
62+
"eslint": "^4.15.0",
63+
"eslint-loader": "^1.9.0",
6264
"eslint-plugin-header": "^1.0.0",
65+
"eslint-plugin-import": "^2.8.0",
6366
"eslint-plugin-node": "^4.2.2",
6467
"fork-ts-checker-webpack-plugin": "^0.2.7",
6568
"handlebars": "^4.0.11",

test/config-generator.js

+65
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,72 @@ describe('The config-generator function', () => {
377377

378378
expect(JSON.stringify(actualConfig.module.rules)).to.contain('handlebars-loader');
379379
});
380+
});
381+
382+
describe('enableEslintLoader() adds the eslint-loader', () => {
383+
it('without enableEslintLoader()', () => {
384+
const config = createConfig();
385+
config.addEntry('main', './main');
386+
config.publicPath = '/';
387+
config.outputPath = '/tmp';
388+
389+
const actualConfig = configGenerator(config);
390+
391+
expect(JSON.stringify(actualConfig.module.rules)).to.not.contain('eslint-loader');
392+
});
393+
394+
it('enableEslintLoader()', () => {
395+
const config = createConfig();
396+
config.addEntry('main', './main');
397+
config.publicPath = '/';
398+
config.outputPath = '/tmp';
399+
config.enableEslintLoader();
400+
401+
const actualConfig = configGenerator(config);
402+
403+
expect(JSON.stringify(actualConfig.module.rules)).to.contain('eslint-loader');
404+
});
405+
406+
it('enableEslintLoader("extends-name")', () => {
407+
const config = createConfig();
408+
config.addEntry('main', './main');
409+
config.publicPath = '/';
410+
config.outputPath = '/tmp';
411+
config.enableEslintLoader('extends-name');
412+
413+
const actualConfig = configGenerator(config);
414+
415+
expect(JSON.stringify(actualConfig.module.rules)).to.contain('eslint-loader');
416+
expect(JSON.stringify(actualConfig.module.rules)).to.contain('extends-name');
417+
});
418+
419+
it('enableEslintLoader({extends: "extends-name"})', () => {
420+
const config = createConfig();
421+
config.addEntry('main', './main');
422+
config.publicPath = '/';
423+
config.outputPath = '/tmp';
424+
config.enableEslintLoader({ extends: 'extends-name' });
425+
426+
const actualConfig = configGenerator(config);
380427

428+
expect(JSON.stringify(actualConfig.module.rules)).to.contain('eslint-loader');
429+
expect(JSON.stringify(actualConfig.module.rules)).to.contain('extends-name');
430+
});
431+
432+
it('enableEslintLoader((options) => ...)', () => {
433+
const config = createConfig();
434+
config.addEntry('main', './main');
435+
config.publicPath = '/';
436+
config.outputPath = '/tmp';
437+
config.enableEslintLoader((options) => {
438+
options.extends = 'extends-name';
439+
});
440+
441+
const actualConfig = configGenerator(config);
442+
443+
expect(JSON.stringify(actualConfig.module.rules)).to.contain('eslint-loader');
444+
expect(JSON.stringify(actualConfig.module.rules)).to.contain('extends-name');
445+
});
381446
});
382447

383448
describe('addLoader() adds a custom loader', () => {

test/functional.js

+28
Original file line numberDiff line numberDiff line change
@@ -1033,5 +1033,33 @@ module.exports = {
10331033
done();
10341034
});
10351035
});
1036+
1037+
it('When enabled, eslint checks for linting errors', (done) => {
1038+
const config = createWebpackConfig('www/build', 'dev');
1039+
config.setPublicPath('/build');
1040+
config.addEntry('main', './js/eslint');
1041+
config.enableEslintLoader({
1042+
// Force eslint-loader to output errors instead of sometimes
1043+
// using warnings (see: https://github.com/MoOx/eslint-loader#errors-and-warning)
1044+
emitError: true,
1045+
rules: {
1046+
// That is not really needed since it'll use the
1047+
// .eslintrc.js file at the root of the project, but
1048+
// it'll avoid breaking this test if we change these
1049+
// rules later on.
1050+
'indent': ['error', 2],
1051+
'no-unused-vars': ['error', { 'args': 'all' }]
1052+
}
1053+
});
1054+
1055+
testSetup.runWebpack(config, (webpackAssert, stats) => {
1056+
const eslintErrors = stats.toJson().errors[0];
1057+
1058+
expect(eslintErrors).to.contain('Expected indentation of 0 spaces but found 2');
1059+
expect(eslintErrors).to.contain('\'a\' is assigned a value but never used');
1060+
1061+
done();
1062+
}, true);
1063+
});
10361064
});
10371065
});

test/loaders/eslint.js

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* This file is part of the Symfony Webpack Encore package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
12+
const expect = require('chai').expect;
13+
const WebpackConfig = require('../../lib/WebpackConfig');
14+
const RuntimeConfig = require('../../lib/config/RuntimeConfig');
15+
const eslintLoader = require('../../lib/loaders/eslint');
16+
17+
function createConfig() {
18+
const runtimeConfig = new RuntimeConfig();
19+
runtimeConfig.context = __dirname;
20+
runtimeConfig.babelRcFileExists = false;
21+
22+
return new WebpackConfig(runtimeConfig);
23+
}
24+
25+
describe('loaders/eslint', () => {
26+
it('getOptions() full usage', () => {
27+
const config = createConfig();
28+
config.enableEslintLoader();
29+
const actualOptions = eslintLoader.getOptions(config);
30+
31+
expect(actualOptions).to.deep.equal({
32+
cache: true,
33+
parser: 'babel-eslint',
34+
emitWarning: true
35+
});
36+
});
37+
38+
it('getOptions() with extra options', () => {
39+
const config = createConfig();
40+
config.enableEslintLoader((options) => {
41+
options.extends = 'airbnb';
42+
});
43+
44+
const actualOptions = eslintLoader.getOptions(config);
45+
46+
expect(actualOptions).to.deep.equal({
47+
cache: true,
48+
parser: 'babel-eslint',
49+
emitWarning: true,
50+
extends: 'airbnb'
51+
});
52+
});
53+
54+
it('getOptions() with an overridden option', () => {
55+
const config = createConfig();
56+
config.enableEslintLoader((options) => {
57+
options.emitWarning = false;
58+
});
59+
60+
const actualOptions = eslintLoader.getOptions(config);
61+
62+
expect(actualOptions).to.deep.equal({
63+
cache: true,
64+
parser: 'babel-eslint',
65+
emitWarning: false
66+
});
67+
});
68+
69+
it('getOptions() with a callback that returns an object', () => {
70+
const config = createConfig();
71+
config.enableEslintLoader((options) => {
72+
options.custom_option = 'foo';
73+
74+
return { foo: true };
75+
});
76+
77+
const actualOptions = eslintLoader.getOptions(config);
78+
expect(actualOptions).to.deep.equals({ foo: true });
79+
});
80+
});

0 commit comments

Comments
 (0)