Skip to content

Commit e568e5b

Browse files
committed
Add support for eslint-webpack-plugin, close #847
1 parent 972c882 commit e568e5b

File tree

11 files changed

+442
-3
lines changed

11 files changed

+442
-3
lines changed

index.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,38 @@ class Encore {
12791279
return this;
12801280
}
12811281

1282+
/**
1283+
* If enabled, the eslint-webpack-plugin is enabled.
1284+
*
1285+
* https://github.com/webpack-contrib/eslint-webpack-plugin
1286+
*
1287+
* ```
1288+
* // enables the eslint plugin using the default eslint configuration.
1289+
* Encore.enableEslintPlugin();
1290+
*
1291+
* // You can also pass in an object of options
1292+
* // that will be passed on to the eslint-webpack-plugin
1293+
* Encore.enableEslintPlugin({
1294+
* emitWarning: false
1295+
* });
1296+
*
1297+
* // For a more advanced usage you can pass in a callback
1298+
* // https://github.com/webpack-contrib/eslint-webpack-plugin#options
1299+
* Encore.enableEslintPlugin((options) => {
1300+
* options.extensions.push('vue'); // to lint Vue files
1301+
* options.emitWarning = false;
1302+
* });
1303+
* ```
1304+
*
1305+
* @param {string|object|function} eslintPluginOptionsOrCallback
1306+
* @returns {Encore}
1307+
*/
1308+
enableEslintPlugin(eslintPluginOptionsOrCallback = () => {}) {
1309+
webpackConfig.enableEslintPlugin(eslintPluginOptionsOrCallback);
1310+
1311+
return this;
1312+
}
1313+
12821314
/**
12831315
* If enabled, display build notifications using
12841316
* webpack-notifier.

lib/WebpackConfig.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class WebpackConfig {
120120
this.usePreact = false;
121121
this.useVueLoader = false;
122122
this.useEslintLoader = false;
123+
this.useEslintPlugin = false;
123124
this.useTypeScriptLoader = false;
124125
this.useForkedTypeScriptTypeChecking = false;
125126
this.useBabelTypeScriptPreset = false;
@@ -171,6 +172,7 @@ class WebpackConfig {
171172
this.devServerOptionsConfigurationCallback = () => {};
172173
this.vueLoaderOptionsCallback = () => {};
173174
this.eslintLoaderOptionsCallback = () => {};
175+
this.eslintPluginOptionsCallback = () => {};
174176
this.tsConfigurationCallback = () => {};
175177
this.handlebarsConfigurationCallback = () => {};
176178
this.miniCssExtractLoaderConfigurationCallback = () => {};
@@ -803,6 +805,10 @@ class WebpackConfig {
803805
}
804806

805807
enableEslintLoader(eslintLoaderOptionsOrCallback = () => {}, eslintOptions = {}) {
808+
if (this.useEslintPlugin) {
809+
throw new Error('Encore.enableEslintLoader() can not be called when Encore.enableEslintPlugin() has been called.');
810+
}
811+
806812
this.useEslintLoader = true;
807813

808814
if (typeof eslintLoaderOptionsOrCallback === 'function') {
@@ -831,6 +837,24 @@ class WebpackConfig {
831837
this.eslintOptions = eslintOptions;
832838
}
833839

840+
enableEslintPlugin(eslintPluginOptionsOrCallback = () => {}) {
841+
if (this.useEslintLoader) {
842+
throw new Error('Encore.enableEslintPlugin() can not be called when Encore.enableEslintLoader() has been called.');
843+
}
844+
845+
this.useEslintPlugin = true;
846+
847+
if (typeof eslintPluginOptionsOrCallback === 'function') {
848+
this.eslintPluginOptionsCallback = eslintPluginOptionsOrCallback;
849+
} else if (typeof eslintPluginOptionsOrCallback === 'object') {
850+
this.eslintPluginOptionsCallback = (options) => {
851+
Object.assign(options, eslintPluginOptionsOrCallback);
852+
};
853+
} else {
854+
throw new Error('Argument 1 to enableEslintPlugin() must be either an object or callback function.');
855+
}
856+
}
857+
834858
enableBuildNotifications(enabled = true, notifierPluginOptionsCallback = () => {}) {
835859
if (typeof notifierPluginOptionsCallback !== 'function') {
836860
throw new Error('Argument 2 to enableBuildNotifications() must be a callback function.');

lib/config-generator.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const vuePluginUtil = require('./plugins/vue');
3737
const friendlyErrorPluginUtil = require('./plugins/friendly-errors');
3838
const assetOutputDisplay = require('./plugins/asset-output-display');
3939
const notifierPluginUtil = require('./plugins/notifier');
40+
const eslintPluginUtil = require('./plugins/eslint');
4041
const PluginPriorities = require('./plugins/plugin-priorities');
4142
const stimulusBridge = require('./plugins/stimulus-bridge');
4243
const applyOptionsCallback = require('./utils/apply-options-callback');
@@ -457,6 +458,8 @@ class ConfigGenerator {
457458

458459
vuePluginUtil(plugins, this.webpackConfig);
459460

461+
eslintPluginUtil(plugins, this.webpackConfig);
462+
460463
if (!this.webpackConfig.runtimeConfig.outputJson) {
461464
const friendlyErrorPlugin = friendlyErrorPluginUtil(this.webpackConfig);
462465
plugins.push({

lib/features.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,15 @@ const features = {
126126
],
127127
description: 'Enable ESLint checks'
128128
},
129+
eslint_plugin: {
130+
method: 'enableEslintPlugin()',
131+
// eslint is needed so the end-user can do things
132+
packages: [
133+
{ name: 'eslint' },
134+
{ name: 'eslint-webpack-plugin', enforce_version: true },
135+
],
136+
description: 'Enable ESLint checks'
137+
},
129138
copy_files: {
130139
method: 'copyFiles()',
131140
packages: [

lib/plugins/eslint.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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 WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
13+
const EslintPlugin = require('eslint-webpack-plugin'); //eslint-disable-line node/no-unpublished-require
14+
const applyOptionsCallback = require('../utils/apply-options-callback');
15+
const pluginFeatures = require('../features');
16+
17+
function isMissingConfigError(e) {
18+
if (!e.message || !e.message.includes('No ESLint configuration found')) {
19+
return false;
20+
}
21+
22+
return true;
23+
}
24+
25+
/**
26+
* Support for ESLint.
27+
*
28+
* @param {Array} plugins
29+
* @param {WebpackConfig} webpackConfig
30+
* @return {void}
31+
*/
32+
module.exports = function(plugins, webpackConfig) {
33+
if (webpackConfig.useEslintPlugin) {
34+
pluginFeatures.ensurePackagesExistAndAreCorrectVersion('eslint_plugin');
35+
36+
const { ESLint } = require('eslint'); // eslint-disable-line node/no-unpublished-require
37+
const eslint = new ESLint({
38+
cwd: webpackConfig.runtimeConfig.context,
39+
});
40+
41+
try {
42+
(async function() {
43+
await eslint.calculateConfigForFile('webpack.config.js');
44+
})();
45+
} catch (e) {
46+
if (isMissingConfigError(e)) {
47+
const chalk = require('chalk');
48+
const packageHelper = require('../package-helper');
49+
50+
const message = `No ESLint configuration has been found.
51+
52+
${chalk.bgGreen.black('', 'FIX', '')} Run command ${chalk.yellow('./node_modules/.bin/eslint --init')} or manually create a ${chalk.yellow('.eslintrc.js')} file at the root of your project.
53+
54+
If you prefer to create a ${chalk.yellow('.eslintrc.js')} file by yourself, here is an example to get you started:
55+
56+
${chalk.yellow(`// .eslintrc.js
57+
module.exports = {
58+
parser: 'babel-eslint',
59+
extends: ['eslint:recommended'],
60+
}
61+
`)}
62+
63+
Install ${chalk.yellow('babel-eslint')} to prevent potential parsing issues: ${packageHelper.getInstallCommand([[{ name: 'babel-eslint' }]])}
64+
65+
`;
66+
throw new Error(message);
67+
}
68+
69+
throw e;
70+
}
71+
72+
const eslintPluginOptions = {
73+
emitWarning: true,
74+
extensions: ['js', 'jsx'],
75+
};
76+
77+
plugins.push({
78+
plugin: new EslintPlugin(
79+
applyOptionsCallback(webpackConfig.eslintPluginOptionsCallback, eslintPluginOptions)
80+
),
81+
});
82+
}
83+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"eslint-plugin-header": "^3.0.0",
7272
"eslint-plugin-import": "^2.8.0",
7373
"eslint-plugin-node": "^11.1.0",
74+
"eslint-webpack-plugin": "^2.5.4",
7475
"file-loader": "^6.0.0",
7576
"fork-ts-checker-webpack-plugin": "^5.0.0 || ^6.0.0",
7677
"fs-extra": "^9.0.0",

test/WebpackConfig.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1512,5 +1512,24 @@ describe('WebpackConfig object', () => {
15121512
});
15131513
}).to.throw('"notExisting" is not a valid key for enableEslintLoader(). Valid keys: lintVue.');
15141514
});
1515+
1516+
it('ESLint loader can not be enabled if ESLint Webpack Plugin is already enabled', () => {
1517+
const config = createConfig();
1518+
config.enableEslintPlugin();
1519+
1520+
expect(function() {
1521+
config.enableEslintLoader();
1522+
}).to.throw('Encore.enableEslintLoader() can not be called when Encore.enableEslintPlugin() has been called.');
1523+
});
1524+
});
1525+
describe('enableEslintPlugin', () => {
1526+
it('ESLint loader can not be enabled if ESLint Webpack Plugin is already enabled', () => {
1527+
const config = createConfig();
1528+
config.enableEslintLoader();
1529+
1530+
expect(function() {
1531+
config.enableEslintPlugin();
1532+
}).to.throw('Encore.enableEslintPlugin() can not be called when Encore.enableEslintLoader() has been called.');
1533+
});
15151534
});
15161535
});

test/config-generator.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const configGenerator = require('../lib/config-generator');
1616
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
1717
const { WebpackManifestPlugin } = require('../lib/webpack-manifest-plugin');
1818
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
19+
const ESLintWebpackPlugin = require('eslint-webpack-plugin');
1920
const webpack = require('webpack');
2021
const path = require('path');
2122
const logger = require('../lib/logger');
@@ -437,6 +438,61 @@ describe('The config-generator function', () => {
437438
});
438439
});
439440

441+
describe('enableEslintPlugin() adds the eslint-webpack-plugin', () => {
442+
it('without enableEslintPlugin()', () => {
443+
const config = createConfig();
444+
config.addEntry('main', './main');
445+
config.publicPath = '/';
446+
config.outputPath = '/tmp';
447+
448+
const actualConfig = configGenerator(config);
449+
450+
const eslintPlugin = findPlugin(ESLintWebpackPlugin, actualConfig.plugins);
451+
expect(eslintPlugin).to.be.undefined;
452+
});
453+
454+
it('enableEslintPlugin()', () => {
455+
const config = createConfig();
456+
config.addEntry('main', './main');
457+
config.publicPath = '/';
458+
config.outputPath = '/tmp';
459+
config.enableEslintPlugin();
460+
461+
const actualConfig = configGenerator(config);
462+
463+
const eslintPlugin = findPlugin(ESLintWebpackPlugin, actualConfig.plugins);
464+
expect(eslintPlugin).to.not.be.undefined;
465+
});
466+
467+
it('enableEslintPlugin({baseConfig: {extends: "extends-name"}})', () => {
468+
const config = createConfig();
469+
config.addEntry('main', './main');
470+
config.publicPath = '/';
471+
config.outputPath = '/tmp';
472+
config.enableEslintPlugin({ baseConfig: { extends: 'extends-name' } });
473+
474+
const actualConfig = configGenerator(config);
475+
476+
const eslintPlugin = findPlugin(ESLintWebpackPlugin, actualConfig.plugins);
477+
expect(eslintPlugin.options.baseConfig.extends).to.equal('extends-name');
478+
});
479+
480+
it('enableEslintPlugin((options) => ...)', () => {
481+
const config = createConfig();
482+
config.addEntry('main', './main');
483+
config.publicPath = '/';
484+
config.outputPath = '/tmp';
485+
config.enableEslintPlugin((options) => {
486+
options.extensions.push('vue');
487+
});
488+
489+
const actualConfig = configGenerator(config);
490+
491+
const eslintPlugin = findPlugin(ESLintWebpackPlugin, actualConfig.plugins);
492+
expect(eslintPlugin.options.extensions).to.deep.equal(['js', 'jsx', 'vue']);
493+
});
494+
});
495+
440496
describe('addLoader() adds a custom loader', () => {
441497
it('addLoader()', () => {
442498
const config = createConfig();

0 commit comments

Comments
 (0)