Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#232 Added support for the eslint-loader #243

Merged
merged 9 commits into from
May 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions fixtures/js/eslint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const a = 'foobar';
34 changes: 34 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,40 @@ class Encore {
return this;
}

/**
* If enabled, the eslint-loader is enabled.
*
* https://github.com/MoOx/eslint-loader
*
* // enables the eslint loaded using the default eslint configuration.
* Encore.enableEslintLoader();
*
* // Optionally, you can pass in the configuration eslint should extend.
* Encore.enableEslintLoader('airbnb');
*
* // You can also pass in an object of options
* // that will be passed on to the eslint-loader
* Encore.enableEslintLoader({
* extends: 'airbnb',
emitWarning: false
* });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bah. So, this looks really great. But so far, we've avoided allowing users to pass in options like this... because of potential merging issues between their config and our config. I really can't think of an issue here - their config wins, should be pretty straightforward. But it breaks from our convention. @Lyrkan what do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the main issue I have with it is that we don't do the same thing for other methods...
It isn't a really big issue since in this you can also use a callback but it could confuse people.

I'd say that we have to decide whether or not we'll implement the same kind of trick (and also always do Object.assign(defaultConfig, userConfig)) to other methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like lyrkan said, we should probably aggree on a common interface to configure the different plugins

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, let's keep it. And then we can think about adding this on a case-by-case basis. I think sometimes it's not a good idea, because we'll need to merge the config. But in this case, even though we're merging the config, the config is flat and simple. There may be some cases where the merging could be more complex, and we could possibly not allow this then :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can also remove this "nifty" merging and create a follow up ticket where we can further discuss/address. This to avoid breaking backward capabilities.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as @Lyrkan agrees with the idea of starting to add this config where it makes sense, I'm cool with keeping it here for the new method. It won't break BC - it's just a "new philosophy" for us

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, the callback API now support both editing the config to add more settings, and returning a new object. I'm not sure this automatic merging brings much value here

*
* // For a more advanced usage you can pass in a callback
* // https://github.com/MoOx/eslint-loader#options
* Encore.enableEslintLoader((options) => {
* options.extends = 'airbnb';
* options.emitWarning = false;
* });
*
* @param {string|object|function} eslintLoaderOptionsOrCallback
* @returns {Encore}
*/
enableEslintLoader(eslintLoaderOptionsOrCallback = () => {}) {
webpackConfig.enableEslintLoader(eslintLoaderOptionsOrCallback);

return this;
}

/**
* If enabled, display build notifications using
* webpack-notifier.
Expand Down
27 changes: 27 additions & 0 deletions lib/WebpackConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class WebpackConfig {
this.useReact = false;
this.usePreact = false;
this.useVueLoader = false;
this.useEslintLoader = false;
this.useTypeScriptLoader = false;
this.useCoffeeScriptLoader = false;
this.useForkedTypeScriptTypeChecking = false;
Expand All @@ -86,6 +87,7 @@ class WebpackConfig {
this.stylusLoaderOptionsCallback = () => {};
this.babelConfigurationCallback = () => {};
this.vueLoaderOptionsCallback = () => {};
this.eslintLoaderOptionsCallback = () => {};
this.tsConfigurationCallback = () => {};
this.coffeeScriptConfigurationCallback = () => {};
this.handlebarsConfigurationCallback = () => {};
Expand Down Expand Up @@ -439,6 +441,31 @@ class WebpackConfig {
this.vueLoaderOptionsCallback = vueLoaderOptionsCallback;
}

enableEslintLoader(eslintLoaderOptionsOrCallback = () => {}) {
this.useEslintLoader = true;

if (typeof eslintLoaderOptionsOrCallback === 'function') {
this.eslintLoaderOptionsCallback = eslintLoaderOptionsOrCallback;
return;
}

if (typeof eslintLoaderOptionsOrCallback === 'string') {
this.eslintLoaderOptionsCallback = (options) => {
options.extends = eslintLoaderOptionsOrCallback;
};
return;
}

if (typeof eslintLoaderOptionsOrCallback === 'object') {
this.eslintLoaderOptionsCallback = (options) => {
Object.assign(options, eslintLoaderOptionsOrCallback);
};
return;
}

throw new Error('Argument 1 to enableEslintLoader() must be either a string, 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
11 changes: 11 additions & 0 deletions lib/config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const tsLoaderUtil = require('./loaders/typescript');
const coffeeScriptLoaderUtil = require('./loaders/coffee-script');
const vueLoaderUtil = require('./loaders/vue');
const handlebarsLoaderUtil = require('./loaders/handlebars');
const eslintLoaderUtil = require('./loaders/eslint');
// plugins utils
const extractTextPluginUtil = require('./plugins/extract-text');
const deleteUnusedEntriesPluginUtil = require('./plugins/delete-unused-entries');
Expand Down Expand Up @@ -228,6 +229,16 @@ class ConfigGenerator {
});
}

if (this.webpackConfig.useEslintLoader) {
rules.push({
test: /\.jsx?$/,
loader: 'eslint-loader',
exclude: /node_modules/,
enforce: 'pre',
options: eslintLoaderUtil.getOptions(this.webpackConfig)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use cache: true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add the cache: true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

});
}

if (this.webpackConfig.useTypeScriptLoader) {
rules.push({
test: /\.tsx?$/,
Expand Down
6 changes: 6 additions & 0 deletions lib/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ const features = {
packages: ['vue', 'vue-loader', 'vue-template-compiler'],
description: 'load VUE files'
},
eslint: {
method: 'enableEslintLoader()',
// eslint is needed so the end-user can do things
packages: ['eslint', 'eslint-loader', 'babel-eslint'],
description: 'Enable ESLint checks'
},
notifier: {
method: 'enableBuildNotifications()',
packages: ['webpack-notifier'],
Expand Down
31 changes: 31 additions & 0 deletions lib/loaders/eslint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 loaderFeatures = require('../features');
const applyOptionsCallback = require('../utils/apply-options-callback');

/**
* @param {WebpackConfig} webpackConfig
* @return {Object} of options to use for eslint-loader options.
*/
module.exports = {
getOptions(webpackConfig) {
loaderFeatures.ensurePackagesExist('eslint');

const eslintLoaderOptions = {
cache: true,
parser: 'babel-eslint',
emitWarning: true
};

return applyOptionsCallback(webpackConfig.eslintLoaderOptionsCallback, eslintLoaderOptions);
}
};
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,18 @@
},
"devDependencies": {
"autoprefixer": "^6.7.7",
"babel-eslint": "^8.2.1",
"babel-plugin-transform-react-jsx": "^6.24.1",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.23.0",
"chai": "^3.5.0",
"chai-fs": "^1.0.0",
"coffee-loader": "^0.9.0",
"coffeescript": "^2.0.2",
"eslint": "^3.19.0",
"eslint": "^4.15.0",
"eslint-loader": "^1.9.0",
"eslint-plugin-header": "^1.0.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^4.2.2",
"fork-ts-checker-webpack-plugin": "^0.2.7",
"handlebars": "^4.0.11",
Expand Down
65 changes: 65 additions & 0 deletions test/config-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,72 @@ describe('The config-generator function', () => {

expect(JSON.stringify(actualConfig.module.rules)).to.contain('handlebars-loader');
});
});

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

const actualConfig = configGenerator(config);

expect(JSON.stringify(actualConfig.module.rules)).to.not.contain('eslint-loader');
});

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

const actualConfig = configGenerator(config);

expect(JSON.stringify(actualConfig.module.rules)).to.contain('eslint-loader');
});

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

const actualConfig = configGenerator(config);

expect(JSON.stringify(actualConfig.module.rules)).to.contain('eslint-loader');
expect(JSON.stringify(actualConfig.module.rules)).to.contain('extends-name');
});

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

const actualConfig = configGenerator(config);

expect(JSON.stringify(actualConfig.module.rules)).to.contain('eslint-loader');
expect(JSON.stringify(actualConfig.module.rules)).to.contain('extends-name');
});

it('enableEslintLoader((options) => ...)', () => {
const config = createConfig();
config.addEntry('main', './main');
config.publicPath = '/';
config.outputPath = '/tmp';
config.enableEslintLoader((options) => {
options.extends = 'extends-name';
});

const actualConfig = configGenerator(config);

expect(JSON.stringify(actualConfig.module.rules)).to.contain('eslint-loader');
expect(JSON.stringify(actualConfig.module.rules)).to.contain('extends-name');
});
});

describe('addLoader() adds a custom loader', () => {
Expand Down
28 changes: 28 additions & 0 deletions test/functional.js
Original file line number Diff line number Diff line change
Expand Up @@ -1033,5 +1033,33 @@ module.exports = {
done();
});
});

it('When enabled, eslint checks for linting errors', (done) => {
const config = createWebpackConfig('www/build', 'dev');
config.setPublicPath('/build');
config.addEntry('main', './js/eslint');
config.enableEslintLoader({
// Force eslint-loader to output errors instead of sometimes
// using warnings (see: https://github.com/MoOx/eslint-loader#errors-and-warning)
emitError: true,
rules: {
// That is not really needed since it'll use the
// .eslintrc.js file at the root of the project, but
// it'll avoid breaking this test if we change these
// rules later on.
'indent': ['error', 2],
'no-unused-vars': ['error', { 'args': 'all' }]
}
});

testSetup.runWebpack(config, (webpackAssert, stats) => {
const eslintErrors = stats.toJson().errors[0];

expect(eslintErrors).to.contain('Expected indentation of 0 spaces but found 2');
expect(eslintErrors).to.contain('\'a\' is assigned a value but never used');

done();
}, true);
});
});
});
80 changes: 80 additions & 0 deletions test/loaders/eslint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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 expect = require('chai').expect;
const WebpackConfig = require('../../lib/WebpackConfig');
const RuntimeConfig = require('../../lib/config/RuntimeConfig');
const eslintLoader = require('../../lib/loaders/eslint');

function createConfig() {
const runtimeConfig = new RuntimeConfig();
runtimeConfig.context = __dirname;
runtimeConfig.babelRcFileExists = false;

return new WebpackConfig(runtimeConfig);
}

describe('loaders/eslint', () => {
it('getOptions() full usage', () => {
const config = createConfig();
config.enableEslintLoader();
const actualOptions = eslintLoader.getOptions(config);

expect(actualOptions).to.deep.equal({
cache: true,
parser: 'babel-eslint',
emitWarning: true
});
});

it('getOptions() with extra options', () => {
const config = createConfig();
config.enableEslintLoader((options) => {
options.extends = 'airbnb';
});

const actualOptions = eslintLoader.getOptions(config);

expect(actualOptions).to.deep.equal({
cache: true,
parser: 'babel-eslint',
emitWarning: true,
extends: 'airbnb'
});
});

it('getOptions() with an overridden option', () => {
const config = createConfig();
config.enableEslintLoader((options) => {
options.emitWarning = false;
});

const actualOptions = eslintLoader.getOptions(config);

expect(actualOptions).to.deep.equal({
cache: true,
parser: 'babel-eslint',
emitWarning: false
});
});

it('getOptions() with a callback that returns an object', () => {
const config = createConfig();
config.enableEslintLoader((options) => {
options.custom_option = 'foo';

return { foo: true };
});

const actualOptions = eslintLoader.getOptions(config);
expect(actualOptions).to.deep.equals({ foo: true });
});
});
Loading