Skip to content

Commit

Permalink
Add support for eslint-webpack-plugin, close symfony#847
Browse files Browse the repository at this point in the history
  • Loading branch information
Kocal committed May 31, 2021
1 parent a11e030 commit 2e77db6
Show file tree
Hide file tree
Showing 11 changed files with 445 additions and 6 deletions.
32 changes: 32 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,38 @@ class Encore {
return this;
}

/**
* If enabled, the eslint-webpack-plugin is enabled.
*
* https://github.com/webpack-contrib/eslint-webpack-plugin
*
* ```
* // enables the eslint plugin using the default eslint configuration.
* Encore.enableEslintPlugin();
*
* // You can also pass in an object of options
* // that will be passed on to the eslint-webpack-plugin
* Encore.enableEslintPlugin({
* emitWarning: false
* });
*
* // For a more advanced usage you can pass in a callback
* // https://github.com/webpack-contrib/eslint-webpack-plugin#options
* Encore.enableEslintPlugin((options) => {
* options.extensions.push('vue'); // to lint Vue files
* options.emitWarning = false;
* });
* ```
*
* @param {string|object|function} eslintPluginOptionsOrCallback
* @returns {Encore}
*/
enableEslintPlugin(eslintPluginOptionsOrCallback = () => {}) {
webpackConfig.enableEslintPlugin(eslintPluginOptionsOrCallback);

return this;
}

/**
* If enabled, display build notifications using
* webpack-notifier.
Expand Down
24 changes: 24 additions & 0 deletions lib/WebpackConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class WebpackConfig {
this.usePreact = false;
this.useVueLoader = false;
this.useEslintLoader = false;
this.useEslintPlugin = false;
this.useTypeScriptLoader = false;
this.useForkedTypeScriptTypeChecking = false;
this.useBabelTypeScriptPreset = false;
Expand Down Expand Up @@ -171,6 +172,7 @@ class WebpackConfig {
this.devServerOptionsConfigurationCallback = () => {};
this.vueLoaderOptionsCallback = () => {};
this.eslintLoaderOptionsCallback = () => {};
this.eslintPluginOptionsCallback = () => {};
this.tsConfigurationCallback = () => {};
this.handlebarsConfigurationCallback = () => {};
this.miniCssExtractLoaderConfigurationCallback = () => {};
Expand Down Expand Up @@ -803,6 +805,10 @@ class WebpackConfig {
}

enableEslintLoader(eslintLoaderOptionsOrCallback = () => {}, eslintOptions = {}) {
if (this.useEslintPlugin) {
throw new Error('Encore.enableEslintLoader() can not be called when Encore.enableEslintPlugin() has been called.');
}

this.useEslintLoader = true;

if (typeof eslintLoaderOptionsOrCallback === 'function') {
Expand Down Expand Up @@ -831,6 +837,24 @@ class WebpackConfig {
this.eslintOptions = eslintOptions;
}

enableEslintPlugin(eslintPluginOptionsOrCallback = () => {}) {
if (this.useEslintLoader) {
throw new Error('Encore.enableEslintPlugin() can not be called when Encore.enableEslintLoader() has been called.');
}

this.useEslintPlugin = true;

if (typeof eslintPluginOptionsOrCallback === 'function') {
this.eslintPluginOptionsCallback = eslintPluginOptionsOrCallback;
} else if (typeof eslintPluginOptionsOrCallback === 'object') {
this.eslintPluginOptionsCallback = (options) => {
Object.assign(options, eslintPluginOptionsOrCallback);
};
} else {
throw new Error('Argument 1 to enableEslintPlugin() must be either an object or callback function.');
}
}

enableBuildNotifications(enabled = true, notifierPluginOptionsCallback = () => {}) {
if (typeof notifierPluginOptionsCallback !== 'function') {
throw new Error('Argument 2 to enableBuildNotifications() must be a callback function.');
Expand Down
3 changes: 3 additions & 0 deletions lib/config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const vuePluginUtil = require('./plugins/vue');
const friendlyErrorPluginUtil = require('./plugins/friendly-errors');
const assetOutputDisplay = require('./plugins/asset-output-display');
const notifierPluginUtil = require('./plugins/notifier');
const eslintPluginUtil = require('./plugins/eslint');
const PluginPriorities = require('./plugins/plugin-priorities');
const stimulusBridge = require('./plugins/stimulus-bridge');
const applyOptionsCallback = require('./utils/apply-options-callback');
Expand Down Expand Up @@ -457,6 +458,8 @@ class ConfigGenerator {

vuePluginUtil(plugins, this.webpackConfig);

eslintPluginUtil(plugins, this.webpackConfig);

if (!this.webpackConfig.runtimeConfig.outputJson) {
const friendlyErrorPlugin = friendlyErrorPluginUtil(this.webpackConfig);
plugins.push({
Expand Down
9 changes: 9 additions & 0 deletions lib/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ const features = {
],
description: 'Enable ESLint checks'
},
eslint_plugin: {
method: 'enableEslintPlugin()',
// eslint is needed so the end-user can do things
packages: [
{ name: 'eslint' },
{ name: 'eslint-webpack-plugin', enforce_version: true },
],
description: 'Enable ESLint checks'
},
copy_files: {
method: 'copyFiles()',
packages: [
Expand Down
83 changes: 83 additions & 0 deletions lib/plugins/eslint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

'use strict';

const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const EslintPlugin = require('eslint-webpack-plugin'); //eslint-disable-line node/no-unpublished-require
const applyOptionsCallback = require('../utils/apply-options-callback');
const pluginFeatures = require('../features');

function isMissingConfigError(e) {
if (!e.message || !e.message.includes('No ESLint configuration found')) {
return false;
}

return true;
}

/**
* Support for ESLint.
*
* @param {Array} plugins
* @param {WebpackConfig} webpackConfig
* @return {void}
*/
module.exports = function(plugins, webpackConfig) {
if (webpackConfig.useEslintPlugin) {
pluginFeatures.ensurePackagesExistAndAreCorrectVersion('eslint_plugin');

const { ESLint } = require('eslint'); // eslint-disable-line node/no-unpublished-require
const eslint = new ESLint({
cwd: webpackConfig.runtimeConfig.context,
});

try {
(async function() {
await eslint.calculateConfigForFile('webpack.config.js');
})();
} catch (e) {
if (isMissingConfigError(e)) {
const chalk = require('chalk');
const packageHelper = require('../package-helper');

const message = `No ESLint configuration has been found.
${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.
If you prefer to create a ${chalk.yellow('.eslintrc.js')} file by yourself, here is an example to get you started:
${chalk.yellow(`// .eslintrc.js
module.exports = {
parser: 'babel-eslint',
extends: ['eslint:recommended'],
}
`)}
Install ${chalk.yellow('babel-eslint')} to prevent potential parsing issues: ${packageHelper.getInstallCommand([[{ name: 'babel-eslint' }]])}
`;
throw new Error(message);
}

throw e;
}

const eslintPluginOptions = {
emitWarning: true,
extensions: ['js', 'jsx'],
};

plugins.push({
plugin: new EslintPlugin(
applyOptionsCallback(webpackConfig.eslintPluginOptionsCallback, eslintPluginOptions)
),
});
}
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@
"chai-fs": "^2.0.0",
"chai-subset": "^1.6.0",
"core-js": "^3.0.0",
"eslint": "^6.7.0 || ^7.0.0",
"eslint": "^7.0.0",
"eslint-loader": "^4.0.0",
"eslint-plugin-header": "^3.0.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^11.1.0",
"eslint-webpack-plugin": "^2.5.4",
"file-loader": "^6.0.0",
"fork-ts-checker-webpack-plugin": "^5.0.0 || ^6.0.0",
"fs-extra": "^9.0.0",
Expand Down
19 changes: 19 additions & 0 deletions test/WebpackConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -1512,5 +1512,24 @@ describe('WebpackConfig object', () => {
});
}).to.throw('"notExisting" is not a valid key for enableEslintLoader(). Valid keys: lintVue.');
});

it('ESLint loader can not be enabled if ESLint Webpack Plugin is already enabled', () => {
const config = createConfig();
config.enableEslintPlugin();

expect(function() {
config.enableEslintLoader();
}).to.throw('Encore.enableEslintLoader() can not be called when Encore.enableEslintPlugin() has been called.');
});
});
describe('enableEslintPlugin', () => {
it('ESLint loader can not be enabled if ESLint Webpack Plugin is already enabled', () => {
const config = createConfig();
config.enableEslintLoader();

expect(function() {
config.enableEslintPlugin();
}).to.throw('Encore.enableEslintPlugin() can not be called when Encore.enableEslintLoader() has been called.');
});
});
});
56 changes: 56 additions & 0 deletions test/config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const configGenerator = require('../lib/config-generator');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { WebpackManifestPlugin } = require('../lib/webpack-manifest-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const ESLintWebpackPlugin = require('eslint-webpack-plugin');
const webpack = require('webpack');
const path = require('path');
const logger = require('../lib/logger');
Expand Down Expand Up @@ -437,6 +438,61 @@ describe('The config-generator function', () => {
});
});

describe('enableEslintPlugin() adds the eslint-webpack-plugin', () => {
it('without enableEslintPlugin()', () => {
const config = createConfig();
config.addEntry('main', './main');
config.publicPath = '/';
config.outputPath = '/tmp';

const actualConfig = configGenerator(config);

const eslintPlugin = findPlugin(ESLintWebpackPlugin, actualConfig.plugins);
expect(eslintPlugin).to.be.undefined;
});

it('enableEslintPlugin()', () => {
const config = createConfig();
config.addEntry('main', './main');
config.publicPath = '/';
config.outputPath = '/tmp';
config.enableEslintPlugin();

const actualConfig = configGenerator(config);

const eslintPlugin = findPlugin(ESLintWebpackPlugin, actualConfig.plugins);
expect(eslintPlugin).to.not.be.undefined;
});

it('enableEslintPlugin({baseConfig: {extends: "extends-name"}})', () => {
const config = createConfig();
config.addEntry('main', './main');
config.publicPath = '/';
config.outputPath = '/tmp';
config.enableEslintPlugin({ baseConfig: { extends: 'extends-name' } });

const actualConfig = configGenerator(config);

const eslintPlugin = findPlugin(ESLintWebpackPlugin, actualConfig.plugins);
expect(eslintPlugin.options.baseConfig.extends).to.equal('extends-name');
});

it('enableEslintPlugin((options) => ...)', () => {
const config = createConfig();
config.addEntry('main', './main');
config.publicPath = '/';
config.outputPath = '/tmp';
config.enableEslintPlugin((options) => {
options.extensions.push('vue');
});

const actualConfig = configGenerator(config);

const eslintPlugin = findPlugin(ESLintWebpackPlugin, actualConfig.plugins);
expect(eslintPlugin.options.extensions).to.deep.equal(['js', 'jsx', 'vue']);
});
});

describe('addLoader() adds a custom loader', () => {
it('addLoader()', () => {
const config = createConfig();
Expand Down
Loading

0 comments on commit 2e77db6

Please sign in to comment.