├── .eslintignore ├── .gitignore ├── test ├── .eslintrc.js ├── setup.js ├── integration │ ├── left │ │ └── .eslintrc.js │ ├── right │ │ └── .eslintrc.js │ ├── test_get_config.js │ ├── test_get_literal_config.js │ └── test_cli.js └── unit │ ├── test_index.js │ ├── test_get_score.js │ ├── test_normalize_config.js │ ├── test_render_differences.js │ └── test_get_differences.js ├── bin └── eslint-compare-config ├── .npmignore ├── .eslintrc.js ├── src ├── get_literal_config.js ├── _get_config.js ├── get_score.js ├── normalize_config.js ├── get_config.js ├── get_differences.js ├── cli.js └── render_differences.js ├── index.js ├── eslint-compare-config.sublime-project ├── .github └── workflows │ └── ci.yml ├── LICENSE.txt ├── package.json ├── CHANGELOG.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | !.eslintrc.js 4 | _get_config.js 5 | test/integration 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules 4 | coverage 5 | 6 | npm-debug.log 7 | *.sublime-workspace 8 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { extends: ['@scottnonnenberg/thehelp/test'] }; 4 | -------------------------------------------------------------------------------- /bin/eslint-compare-config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var cli = require('../src/cli'); 4 | 5 | cli(process.argv); 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | coverage 3 | 4 | .gitignore 5 | .npmignore 6 | .eslintignore 7 | .eslintrc.js 8 | .github 9 | 10 | *.sublime-* 11 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const modulePath = require('app-module-path'); 5 | 6 | 7 | modulePath.addPath(path.join(__dirname, '..')); 8 | -------------------------------------------------------------------------------- /test/integration/left/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const left = { 4 | extends: ['@scottnonnenberg/thehelp/scripts'], 5 | 6 | rules: { 7 | one: 'error', 8 | two: 'off', 9 | three: ['error', { setting: 1 }], 10 | }, 11 | }; 12 | 13 | module.exports = left; 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | settings: { 'import/resolver': { node: { paths: [__dirname] } } }, 5 | 6 | extends: ['@scottnonnenberg/thehelp'], 7 | 8 | rules: { 9 | 'filenames/match-exported': 'off', 10 | 'import/no-internal-modules': 'off', 11 | }, 12 | 13 | }; 14 | -------------------------------------------------------------------------------- /src/get_literal_config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-dynamic-require, security/detect-non-literal-require */ 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | 8 | function getLiteralConfig(target) { 9 | return require(path.resolve(target)); 10 | } 11 | 12 | module.exports = getLiteralConfig; 13 | -------------------------------------------------------------------------------- /test/integration/right/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const right = { 4 | extends: ['@scottnonnenberg/thehelp/test'], 5 | 6 | plugins: [ 7 | 'immutable', 8 | 'react', 9 | ], 10 | 11 | rules: { 12 | two: 'off', 13 | three: ['error', { setting: 2 }], 14 | four: 'error', 15 | }, 16 | }; 17 | 18 | module.exports = right; 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | getConfigSync: require('./src/get_config'), 5 | getDifferencesSync: require('./src/get_differences'), 6 | getLiteralConfigSync: require('./src/get_literal_config'), 7 | getScoreSync: require('./src/get_score'), 8 | normalizeConfigSync: require('./src/normalize_config'), 9 | renderDifferencesSync: require('./src/render_differences'), 10 | }; 11 | -------------------------------------------------------------------------------- /eslint-compare-config.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": ".", 6 | "folder_exclude_patterns": [ 7 | "assets", 8 | "node_modules", 9 | "public", 10 | "coverage", 11 | 12 | "dist", 13 | "docs", 14 | "bower_components" 15 | ], 16 | "file_exclude_patterns": [ 17 | ".DS_Store" 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/_get_config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-console, import/no-extraneous-dependencies */ 4 | 5 | var path = require('path'); 6 | var eslint = require('eslint'); 7 | 8 | var ESLint = eslint.ESLint || eslint.CLIEngine; 9 | 10 | var engine = new ESLint(); 11 | var target = path.join(__dirname, '__random.js'); 12 | 13 | if (engine.calculateConfigForFile) { 14 | engine.calculateConfigForFile(target).then(function(config) { 15 | console.log(JSON.stringify(config, null, ' ')); 16 | }); 17 | } else { 18 | var config = engine.getConfigForFile(target); 19 | console.log(JSON.stringify(config, null, ' ')); 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Get Linux distro 11 | run: lsb_release -a 12 | 13 | - name: Get Linux system info 14 | run: uname -a 15 | 16 | - name: Check out the repo 17 | uses: actions/checkout@v2 18 | 19 | - name: Install Node.js 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: '16.13.0' 23 | 24 | - name: Cache node_modules/ 25 | uses: actions/cache@v2 26 | with: 27 | path: node_modules 28 | key: node_modules-${{ hashFiles('package-lock.json') }} 29 | 30 | - name: Install dependencies 31 | run: npm install 32 | 33 | - name: Run tests 34 | run: npm run test 35 | -------------------------------------------------------------------------------- /src/get_score.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MAX = 100; 4 | 5 | module.exports = function getScore(differences) { 6 | const totals = { 7 | rulesMissingFromLeft: getLength(differences.rulesMissingFromLeft), 8 | matchingRules: getLength(differences.matchingRules), 9 | rulesMissingFromRight: getLength(differences.rulesMissingFromRight), 10 | ruleDifferences: getLength(differences.ruleDifferences), 11 | }; 12 | 13 | const totalRules = totals.rulesMissingFromLeft + totals.matchingRules 14 | + totals.rulesMissingFromRight + totals.ruleDifferences; 15 | if (totalRules === 0) { 16 | return MAX; 17 | } 18 | 19 | return Math.round(totals.matchingRules / totalRules * MAX); 20 | }; 21 | 22 | function getLength(array) { 23 | if (array) { 24 | return array.length; 25 | } 26 | 27 | return 0; 28 | } 29 | -------------------------------------------------------------------------------- /test/integration/test_get_config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | const _ = require('lodash'); 6 | 7 | const getConfig = require('src/get_config'); 8 | 9 | 10 | describe('integration/getConfig', () => { 11 | it('loads two config files', () => { 12 | const left = 'test/integration/left'; 13 | const right = 'test/integration/right'; 14 | 15 | const actual = { 16 | left: getConfig(left), 17 | right: getConfig(right), 18 | }; 19 | 20 | expect(actual).to.have.all.keys('left', 'right'); 21 | 22 | expect(_.keys(actual.left.rules)).to.have.length.above(3); 23 | expect(_.keys(actual.right.rules)).to.have.length.above(3); 24 | 25 | expect(actual.left) 26 | .to.have.deep.property('plugins').that.has.length.above(0, 'left'); 27 | expect(actual.right) 28 | .to.have.deep.property('plugins').that.has.length.above(2, 'right'); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/normalize_config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | function fixRuleValue(value) { 6 | if (value === 0) { 7 | return 'off'; 8 | } 9 | else if (value === 1) { 10 | return 'warning'; 11 | } 12 | else if (value === 2) { 13 | return 'error'; 14 | } 15 | return value; 16 | } 17 | 18 | function fixRule(ruleArray) { 19 | const name = ruleArray[0]; 20 | let value = ruleArray[1]; 21 | 22 | if (_.isArray(value)) { 23 | value = value.slice(0); 24 | value[0] = fixRuleValue(value[0]); 25 | } 26 | else { 27 | value = [fixRuleValue(value)]; 28 | } 29 | 30 | return [name, value]; 31 | } 32 | 33 | function normalizeConfig(config) { 34 | if (!config) { 35 | return config; 36 | } 37 | 38 | const rules = _.chain(config.rules) 39 | .toPairs() 40 | .map(fixRule) 41 | .reject(ruleArray => ruleArray[1] === 'off' || ruleArray[1][0] === 'off') 42 | .fromPairs() 43 | .value(); 44 | 45 | return _.assign({}, config, { rules }); 46 | } 47 | 48 | module.exports = normalizeConfig; 49 | -------------------------------------------------------------------------------- /src/get_config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable security/detect-child-process */ 4 | 5 | const path = require('path'); 6 | const { readFileSync, statSync, unlinkSync, writeFileSync } = require('fs'); 7 | const childProcess = require('child_process'); 8 | 9 | 10 | const script = readFileSync(path.join(__dirname, '_get_config.js')).toString(); 11 | 12 | function getConfig(providedPath) { 13 | const startPath = path.resolve(providedPath); 14 | 15 | const stat = statSync(startPath); 16 | const dir = stat.isDirectory() ? startPath : path.dirname(startPath); 17 | const filename = '__get_config.js'; 18 | const target = path.join(dir, filename); 19 | 20 | writeFileSync(target, script); 21 | const components = path.parse(dir); 22 | 23 | // enables proper eslint searches for parent configs 24 | const options = { cwd: components.root }; 25 | 26 | try { 27 | const result = childProcess.execFileSync('node', [target], options).toString(); 28 | return JSON.parse(result); 29 | } 30 | finally { 31 | unlinkSync(target); 32 | } 33 | } 34 | 35 | module.exports = getConfig; 36 | -------------------------------------------------------------------------------- /test/integration/test_get_literal_config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | const _ = require('lodash'); 6 | 7 | const getConfig = require('src/get_literal_config'); 8 | 9 | 10 | describe('integration/getLiteralConfig', () => { 11 | it('loads two config files', () => { 12 | const left = 'test/integration/left/.eslintrc.js'; 13 | const right = 'test/integration/right/.eslintrc.js'; 14 | 15 | const actual = { 16 | left: getConfig(left), 17 | right: getConfig(right), 18 | }; 19 | 20 | expect(actual).to.have.all.keys('left', 'right'); 21 | 22 | expect(_.keys(actual.left.rules)).to.have.length(3); 23 | expect(_.keys(actual.right.rules)).to.have.length(3); 24 | 25 | expect(actual.left).to.have.property('extends').that.has.length(1, 'left'); 26 | expect(actual.right).to.have.property('extends').that.has.length(1, 'right'); 27 | 28 | expect(actual.left).not.to.have.deep.property('plugins', 'left'); 29 | expect(actual.right).to.have.deep.property('plugins').that.has.length(2, 'right'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | Copyright (c) 2014 Scott Nonnenberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | associated documentation files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or 12 | substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /test/unit/test_index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | const _ = require('lodash'); 6 | 7 | const index = require('index'); 8 | 9 | 10 | describe('unit/index', () => { 11 | it('has truthy getConfigSync key', () => { 12 | expect(index).to.have.property('getConfigSync').that.exist; 13 | }); 14 | 15 | it('has truthy getDifferencesSync key', () => { 16 | expect(index).to.have.property('getDifferencesSync').that.exist; 17 | }); 18 | 19 | it('has truthy getLiteralConfigSync key', () => { 20 | expect(index).to.have.property('getLiteralConfigSync').that.exist; 21 | }); 22 | 23 | it('has truthy getScoreSync key', () => { 24 | expect(index).to.have.property('getScoreSync').that.exist; 25 | }); 26 | 27 | it('has truthy normalizeConfigSync key', () => { 28 | expect(index).to.have.property('normalizeConfigSync').that.exist; 29 | }); 30 | 31 | it('has truthy renderDifferencesSync key', () => { 32 | expect(index).to.have.property('renderDifferencesSync').that.exist; 33 | }); 34 | 35 | it('has six keys', () => { 36 | expect(_.keys(index)).to.have.length(6); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/integration/test_cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | 6 | const cli = require('src/cli'); 7 | 8 | 9 | describe('integration/cli', () => { 10 | const left = 'test/integration/left/.eslintrc.js'; 11 | const right = 'test/integration/right/.eslintrc.js'; 12 | 13 | it('throws if only one arg provided', () => { 14 | expect(() => { 15 | cli(['node', 'boot.js', left]); 16 | }).to.throw(Error) 17 | .that.match(/needs two configuration/); 18 | }); 19 | 20 | it('throws if three args provided', () => { 21 | expect(() => { 22 | cli(['node', 'boot.js', left, right, right]); 23 | }).to.throw(Error) 24 | .that.match(/needs two configuration/); 25 | }); 26 | 27 | it('shows help', () => { 28 | cli(['node', 'boot.js', left, right, '--help']); 29 | }); 30 | 31 | it('shows differences between two files', () => { 32 | cli(['node', 'boot.js', left, right]); 33 | }); 34 | 35 | it('shows literal differences between two files', () => { 36 | cli(['node', 'boot.js', left, right, '--literal']); 37 | }); 38 | 39 | it('shows json differences', () => { 40 | cli(['node', 'boot.js', left, right, '--json']); 41 | }); 42 | 43 | it('shows score', () => { 44 | cli(['node', 'boot.js', left, right, '--score']); 45 | }); 46 | 47 | it('shows literal score', () => { 48 | cli(['node', 'boot.js', left, right, '--score', '--literal']); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/unit/test_get_score.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | 6 | const getScore = require('src/get_score'); 7 | 8 | 9 | describe('unit/getScore', () => { 10 | it('returns 100 for empty object', () => { 11 | const differences = {}; 12 | const actual = getScore(differences); 13 | expect(actual).to.equal(100); 14 | }); 15 | 16 | it('returns 100 for no differences', () => { 17 | const differences = { 18 | rulesMissingFromLeft: [], 19 | matchingRules: [], 20 | rulesMissingFromRight: [], 21 | ruleDifferences: [], 22 | }; 23 | 24 | const actual = getScore(differences); 25 | 26 | expect(actual).to.equal(100); 27 | }); 28 | 29 | it('returns all differences', () => { 30 | const differences = { 31 | rulesMissingFromLeft: ['one', 'two', 'three'], 32 | matchingRules: ['four', 'five'], 33 | rulesMissingFromRight: ['six', 'seven'], 34 | ruleDifferences: [ 35 | { 36 | rule: 'eight', 37 | left: ['error', { setting: 1 }], 38 | right: 'off', 39 | }, 40 | ], 41 | }; 42 | const actual = getScore(differences); 43 | 44 | expect(actual).to.equal(25); 45 | }); 46 | 47 | it('returns minimal differences', () => { 48 | const differences = { 49 | rulesMissingFromLeft: [], 50 | matchingRules: ['one'], 51 | rulesMissingFromRight: [], 52 | ruleDifferences: [], 53 | }; 54 | 55 | const actual = getScore(differences); 56 | 57 | expect(actual).to.equal(100); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/get_differences.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable security/detect-object-injection */ 4 | 5 | const _ = require('lodash'); 6 | const equal = require('deep-equal'); 7 | const deepDiff = require('deep-diff'); 8 | 9 | 10 | function compareRules(leftConfig, rightConfig) { 11 | const leftRules = _.keys(leftConfig.rules); 12 | const rightRules = _.keys(rightConfig.rules); 13 | 14 | const leftPlugins = leftConfig.plugins; 15 | const rightPlugins = rightConfig.plugins; 16 | 17 | const leftExtends = leftConfig.extends; 18 | const rightExtends = rightConfig.extends; 19 | 20 | const leftClean = _.omit(leftConfig, ['rules', 'plugins', 'extends']); 21 | const rightClean = _.omit(rightConfig, ['rules', 'plugins', 'extends']); 22 | 23 | const sharedRules = _.intersection(leftRules, rightRules); 24 | const ruleDifferences = _.chain(sharedRules) 25 | .map(rule => ({ 26 | rule, 27 | left: leftConfig.rules[rule], 28 | right: rightConfig.rules[rule], 29 | })) 30 | .reject(item => equal(item.left, item.right)) 31 | .value(); 32 | const ruleDifferenceNames = _.map(ruleDifferences, item => item.rule); 33 | const matchingRules = _.difference(sharedRules, ruleDifferenceNames); 34 | 35 | return { 36 | pluginsMissingFromLeft: _.difference(rightPlugins, leftPlugins), 37 | sharedPlugins: _.intersection(leftPlugins, rightPlugins), 38 | pluginsMissingFromRight: _.difference(leftPlugins, rightPlugins), 39 | 40 | extendsMissingFromLeft: _.difference(rightExtends, leftExtends), 41 | sharedExtends: _.intersection(leftExtends, rightExtends), 42 | extendsMissingFromRight: _.difference(leftExtends, rightExtends), 43 | 44 | rulesMissingFromLeft: _.difference(rightRules, leftRules), 45 | matchingRules, 46 | rulesMissingFromRight: _.difference(leftRules, rightRules), 47 | ruleDifferences, 48 | 49 | differences: deepDiff(leftClean, rightClean) || [], 50 | }; 51 | } 52 | 53 | module.exports = compareRules; 54 | -------------------------------------------------------------------------------- /test/unit/test_normalize_config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | 6 | const normalizeConfig = require('src/normalize_config'); 7 | 8 | 9 | describe('unit/normalizeConfig', () => { 10 | it('returns null for null config', () => { 11 | const config = null; 12 | const expected = null; 13 | 14 | const actual = normalizeConfig(config); 15 | expect(actual).to.deep.equal(expected); 16 | }); 17 | 18 | it('removes raw off', () => { 19 | const config = { rules: { rule: 'off' } }; 20 | 21 | const expected = { rules: {} }; 22 | 23 | const actual = normalizeConfig(config); 24 | expect(actual).to.deep.equal(expected); 25 | }); 26 | 27 | it('removes embedded off', () => { 28 | const config = { rules: { rule: ['off', 'something', { key: 'value' }] } }; 29 | 30 | const expected = { rules: {} }; 31 | 32 | const actual = normalizeConfig(config); 33 | expect(actual).to.deep.equal(expected); 34 | }); 35 | 36 | it('replaces raw 2 with error', () => { 37 | const config = { rules: { rule: 2 } }; 38 | 39 | const expected = { rules: { rule: ['error'] } }; 40 | 41 | const actual = normalizeConfig(config); 42 | expect(actual).to.deep.equal(expected); 43 | }); 44 | 45 | it('replaces raw 1 with warning', () => { 46 | const config = { rules: { rule: 1 } }; 47 | 48 | const expected = { rules: { rule: ['warning'] } }; 49 | 50 | const actual = normalizeConfig(config); 51 | expect(actual).to.deep.equal(expected); 52 | }); 53 | 54 | it('removes raw 0', () => { 55 | const config = { rules: { rule: 0 } }; 56 | 57 | const expected = { rules: {} }; 58 | 59 | const actual = normalizeConfig(config); 60 | expect(actual).to.deep.equal(expected); 61 | }); 62 | 63 | it('replaces embedded 2 with error', () => { 64 | const config = { rules: { rule: [2, 'something', { key: 'value' }] } }; 65 | 66 | const expected = { rules: { rule: ['error', 'something', { key: 'value' }] } }; 67 | 68 | const actual = normalizeConfig(config); 69 | expect(actual).to.deep.equal(expected); 70 | }); 71 | 72 | it('replaces embedded 1 with warning', () => { 73 | const config = { rules: { rule: [1, 'something', { key: 'value' }] } }; 74 | 75 | const expected = { rules: { rule: ['warning', 'something', { key: 'value' }] } }; 76 | 77 | const actual = normalizeConfig(config); 78 | expect(actual).to.deep.equal(expected); 79 | }); 80 | 81 | it('removes embedded 0', () => { 82 | const config = { rules: { rule: [0, 'something', { key: 'value' }] } }; 83 | 84 | const expected = { rules: {} }; 85 | 86 | const actual = normalizeConfig(config); 87 | expect(actual).to.deep.equal(expected); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | 'use strict'; 4 | 5 | const dashdash = require('dashdash'); 6 | const _ = require('lodash'); 7 | 8 | const getConfig = require('./get_config'); 9 | const getLiteralConfig = require('./get_literal_config'); 10 | const getDifferences = require('./get_differences'); 11 | const renderDifferences = require('./render_differences'); 12 | const normalizeConfig = require('./normalize_config'); 13 | const getScore = require('./get_score'); 14 | 15 | 16 | function cli(args) { 17 | const parser = getParser(); 18 | const parsed = parser.parse(args); 19 | 20 | if (showHelp(parsed, parser)) { 21 | return; 22 | } 23 | 24 | if (parsed._args.length !== 2) { 25 | throw new Error('A comparison needs two configuration paths!'); 26 | } 27 | 28 | const left = parsed._args[0]; 29 | const right = parsed._args[1]; 30 | const config = loadConfig(parsed.literal, left, right); 31 | 32 | const differences = getDifferences(config.left, config.right); 33 | 34 | showDifferences(parsed, differences); 35 | } 36 | 37 | module.exports = cli; 38 | 39 | function showDifferences(parsed, differences) { 40 | if (parsed.json) { 41 | console.log(JSON.stringify(differences, null, ' ')); 42 | } 43 | else if (parsed.score) { 44 | console.log(`Score: ${getScore(differences)}% similarity`); 45 | } 46 | else { 47 | console.log(renderDifferences(differences)); 48 | } 49 | } 50 | 51 | function showHelp(parsed, parser) { 52 | if (parsed.help) { 53 | const optionList = parser.help({ includeEnv: true }).trimRight(); 54 | const help = `usage: eslint-compare-config LEFT RIGHT [OPTIONS]\noptions:\n${ 55 | optionList}`; 56 | console.log(help); 57 | return true; 58 | } 59 | 60 | return false; 61 | } 62 | 63 | function loadConfig(literal, left, right) { 64 | const get = literal ? getLiteralConfig : getConfig; 65 | const leftConfig = _.omit(get(left), ['globals']); 66 | const rightConfig = _.omit(get(right), ['globals']); 67 | 68 | return { 69 | left: normalizeConfig(leftConfig), 70 | right: normalizeConfig(rightConfig), 71 | }; 72 | } 73 | 74 | function getParser() { 75 | const options = [ 76 | { 77 | names: ['help', 'h'], 78 | type: 'bool', 79 | help: 'Show this help and exit.', 80 | }, { 81 | names: ['literal', 'l'], 82 | type: 'bool', 83 | help: 'Load eslint config files directly, not through eslint. Default: false', 84 | }, { 85 | names: ['json', 'j'], 86 | type: 'bool', 87 | help: 'Show differences in JSON form instead of text. Default: false', 88 | helpArg: 'NAME', 89 | }, { 90 | names: ['score', 's'], 91 | type: 'bool', 92 | help: 'Show a similarity score instead of detaile differences. Default: false', 93 | }, 94 | ]; 95 | 96 | return dashdash.createParser({ options }); 97 | } 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@scottnonnenberg/eslint-compare-config", 3 | "version": "1.1.0", 4 | "description": "A tool to compare two eslint configurations.", 5 | "main": "index.js", 6 | "bin": { 7 | "eslint-compare-config": "bin/eslint-compare-config" 8 | }, 9 | "scripts": { 10 | "release": "standard-version", 11 | "lint": "eslint .", 12 | "test": "npm run all && npm run lint", 13 | "test-coverage": "npm run all-coverage && npm run lint", 14 | "ci": "npm run test-coverage && npm run send-to-codecov", 15 | "mocha": "NODE_ENV=test mocha --recursive --require test/setup.js", 16 | "mocha-watch": "npm run mocha -- --watch", 17 | "mocha-coverage": "NODE_ENV=test istanbul cover _mocha -- --recursive --require test/setup.js", 18 | "unit": "npm run mocha -- -s 15 test/unit", 19 | "unit-watch": "npm run mocha-watch -- -s 15 test/unit", 20 | "unit-coverage": "npm run mocha-coverage -- test/unit", 21 | "integration": "npm run mocha -- -s 15 test/integration", 22 | "integration-watch": "npm run mocha-watch -- -s 15 test/integration", 23 | "integration-coverage": "npm run mocha-coverage -- test/integration", 24 | "all": "npm run mocha -- -s 15 test/unit test/integration", 25 | "all-watch": "npm run mocha-watch -- -s 15 test/unit test/integration", 26 | "all-coverage": "npm run mocha-coverage -- test/unit test/integration", 27 | "send-to-codecov": "codecov", 28 | "open-coverage": "open coverage/lcov-report/index.html" 29 | }, 30 | "dependencies": { 31 | "chalk": "^4.1.0", 32 | "dashdash": "^2.0.0", 33 | "deep-equal": "^2.0.5", 34 | "deep-diff": "^1.0.2", 35 | "lodash": "^4.17.21" 36 | }, 37 | "devDependencies": { 38 | "@scottnonnenberg/eslint-config-thehelp": "0.9.0", 39 | "@scottnonnenberg/eslint-plugin-thehelp": "0.5.0", 40 | "app-module-path": "2.2.0", 41 | "chai": "4.3.4", 42 | "eslint-plugin-bdd": "2.1.1", 43 | "eslint-plugin-chai-expect": "3.0.0", 44 | "eslint-plugin-filenames": "1.3.2", 45 | "eslint-plugin-immutable": "1.0.0", 46 | "eslint-plugin-import": "2.25.3", 47 | "eslint-plugin-react": "7.27.0", 48 | "eslint-plugin-security": "1.4.0", 49 | "eslint": "8.4.1", 50 | "ghooks": "2.0.4", 51 | "istanbul": "0.4.5", 52 | "mocha": "9.1.3", 53 | "standard-version": "9.3.2", 54 | "strip-ansi": "6.0.0", 55 | "validate-commit-msg": "2.14.0" 56 | }, 57 | "config": { 58 | "ghooks": { 59 | "commit-msg": "validate-commit-msg" 60 | }, 61 | "validate-commit-msg": { 62 | "maxSubjectLength": 72 63 | } 64 | }, 65 | "author": "Scott Nonnenberg ", 66 | "license": "MIT", 67 | "homepage": "https://github.com/scottnonnenberg/eslint-compare-config", 68 | "repository": { 69 | "type": "git", 70 | "url": "git@github.com:scottnonnenberg/eslint-compare-config.git" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.1.0](https://github.com/scottnonnenberg/eslint-compare-config/compare/v1.0.1...v1.1.0) (2021-12-09) 6 | 7 | 8 | ### Features 9 | 10 | * **eslint:** Support for modern eslint versions ([f23fdc4](https://github.com/scottnonnenberg/eslint-compare-config/commit/f23fdc4107c10ea6a8a7dcc8b19dcbd2153b9e04)) 11 | 12 | 13 | ## [1.0.1](https://github.com/scottnonnenberg/eslint-compare-config/compare/v1.0.0...v1.0.1) (2017-01-13) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * **normalization:** coerce plain string rule config to array ([#5](https://github.com/scottnonnenberg/eslint-compare-config/issues/5)) ([1c8115c](https://github.com/scottnonnenberg/eslint-compare-config/commit/1c8115c)) 19 | 20 | 21 | 22 | 23 | # [1.0.0](https://github.com/scottnonnenberg/eslint-compare-config/compare/v0.4.0...v1.0.0) (2016-06-20) 24 | 25 | 26 | ### Features 27 | 28 | * **API:** Add sync postfix to all public functions ([59848b9](https://github.com/scottnonnenberg/eslint-compare-config/commit/59848b9)) 29 | * **API:** Simplify getConfig and getLiteralConfig to take one path ([9c46df4](https://github.com/scottnonnenberg/eslint-compare-config/commit/9c46df4)) 30 | * **diff:** Four rules sections are now mutually exclusive ([c2350af](https://github.com/scottnonnenberg/eslint-compare-config/commit/c2350af)) 31 | 32 | 33 | ### BREAKING CHANGES 34 | 35 | * API: All public methods go from xxx() to xxxSync() to make 36 | it clear that all methods are synchronous. 37 | * API: getConfig and getLiteralConfig previously always took 38 | two parameters, one for each config to be compared. Now takes just one 39 | path, loading one config at a time. 40 | * diff: sharedRules key in diff object was changed to 41 | matchingRules, and no longer includes the rules in the ruleDifferences 42 | array. 43 | 44 | 45 | 46 | 47 | # [0.4.0](https://github.com/scottnonnenberg/eslint-compare-config/compare/v0.3.0...v0.4.0) (2016-06-15) 48 | 49 | 50 | ### Features 51 | 52 | * **output:** Show shared rules, plugins and extends ([438f1d2](https://github.com/scottnonnenberg/eslint-compare-config/commit/438f1d2)) 53 | 54 | 55 | 56 | 57 | # [0.3.0](https://github.com/scottnonnenberg/eslint-compare-config/compare/v0.2.0...v0.3.0) (2016-06-15) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * **dependencies:** Remove eslint as production dependency! ([bf8d6ec](https://github.com/scottnonnenberg/eslint-compare-config/commit/bf8d6ec)) 63 | 64 | 65 | ### Features 66 | 67 | * **normalize:** Normalize configurations before comparison ([3327a66](https://github.com/scottnonnenberg/eslint-compare-config/commit/3327a66)) 68 | * **output:** Show counts for each section in human-readable diff ([e55fbc8](https://github.com/scottnonnenberg/eslint-compare-config/commit/e55fbc8)) 69 | 70 | 71 | 72 | 73 | # [0.2.0](https://github.com/scottnonnenberg/eslint-compare-config/compare/v0.1.0...v0.2.0) (2016-06-14) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * Remove __get_config.js from target dir even if things go wrong ([739c3a1](https://github.com/scottnonnenberg/eslint-compare-config/commit/739c3a1)) 79 | 80 | 81 | ### Features 82 | 83 | * **directories:** Support target directories in normal mode ([e3bf811](https://github.com/scottnonnenberg/eslint-compare-config/commit/e3bf811)) 84 | * **license:** Officially open-source project with the MIT license ([ac64b59](https://github.com/scottnonnenberg/eslint-compare-config/commit/ac64b59)) 85 | * **name:** Moved to scoped package name ([8a1f5f1](https://github.com/scottnonnenberg/eslint-compare-config/commit/8a1f5f1)) 86 | 87 | 88 | 89 | # 0.1.0 (2016-06-03) 90 | 91 | - First version of tool! Normal and literal config loading. Normal, JSON and score modes for difference output. 92 | -------------------------------------------------------------------------------- /src/render_differences.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const chalk = require('chalk'); 5 | const _ = require('lodash'); 6 | 7 | 8 | const INDENT = ' '; 9 | const JOIN = `\n${INDENT}`; 10 | const SEPARATOR = '\n\n'; 11 | 12 | function renderDifferences(result) { 13 | if (!result) { 14 | throw new Error('need to provide the differences!'); 15 | } 16 | 17 | if (noLength(result.rulesMissingFromLeft) 18 | && noLength(result.rulesMissingFromRight) 19 | && noLength(result.extendsMissingFromLeft) 20 | && noLength(result.extendsMissingFromRight) 21 | && noLength(result.pluginsMissingFromLeft) 22 | && noLength(result.pluginsMissingFromRight) 23 | && noLength(result.ruleDifferences) 24 | && noLength(result.differences)) { 25 | return 'No differences.'; 26 | } 27 | 28 | return printPluginDifferences(result) 29 | + SEPARATOR 30 | + printExtendsDifferences(result) 31 | + SEPARATOR 32 | + printRuleDifferences(result) 33 | + SEPARATOR 34 | + printOtherDifferences(result); 35 | } 36 | 37 | module.exports = renderDifferences; 38 | 39 | function noLength(array) { 40 | return !array || !array.length; 41 | } 42 | 43 | function bold(item) { 44 | return chalk.bold(item); 45 | } 46 | 47 | function joinOrNone(array) { 48 | if (array.length) { 49 | return ` ${array.length}\n${INDENT}${array.join(JOIN)}`; 50 | } 51 | 52 | return chalk.dim(' None'); 53 | } 54 | 55 | /* eslint-disable prefer-template */ 56 | 57 | function printPluginDifferences(result) { 58 | return 'Plugins shared:' 59 | + joinOrNone(_.map(result.sharedPlugins, bold)) 60 | + SEPARATOR 61 | + 'Plugins missing from left:' 62 | + joinOrNone(_.map(result.pluginsMissingFromLeft, bold)) 63 | + SEPARATOR 64 | + 'Plugins missing from right:' 65 | + joinOrNone(_.map(result.pluginsMissingFromRight, bold)); 66 | } 67 | 68 | function printExtendsDifferences(result) { 69 | return 'Extends shared:' 70 | + joinOrNone(_.map(result.sharedExtends, bold)) 71 | + SEPARATOR 72 | + 'Extends missing from left:' 73 | + joinOrNone(_.map(result.extendsMissingFromLeft, bold)) 74 | + SEPARATOR 75 | + 'Extends missing from right:' 76 | + joinOrNone(_.map(result.extendsMissingFromRight, bold)); 77 | } 78 | 79 | 80 | function printRuleDifferences(result) { 81 | return 'Rules matching:' 82 | + joinOrNone(_.map(result.matchingRules, bold)) 83 | + SEPARATOR 84 | + 'Rules missing from left:' 85 | + joinOrNone(_.map(result.rulesMissingFromLeft, bold)) 86 | + SEPARATOR 87 | + 'Rules missing from right:' 88 | + joinOrNone(_.map(result.rulesMissingFromRight, bold)) 89 | + SEPARATOR 90 | + 'Rule configuration differences:' 91 | + joinOrNone(_.map(result.ruleDifferences, printRuleDifference)); 92 | } 93 | 94 | function printRuleDifference(rule) { 95 | const left = util.inspect(rule.left); 96 | const right = util.inspect(rule.right); 97 | 98 | const leftIndent = left.split('\n').join(JOIN + INDENT); 99 | const rightIndent = right.split('\n').join(JOIN + INDENT); 100 | 101 | return bold(rule.rule) + ':\n' 102 | + INDENT + INDENT + 'left: ' + leftIndent + '\n' 103 | + INDENT + INDENT + 'right: ' + rightIndent; 104 | } 105 | 106 | function printOtherDifferences(result) { 107 | return 'Differences in other configuration:' 108 | + joinOrNone(_.map(result.differences, renderDiff)); 109 | } 110 | 111 | function renderDiff(diff) { 112 | const left = util.inspect(diff.lhs); 113 | const right = util.inspect(diff.rhs); 114 | 115 | const leftIndent = left.split('\n').join(JOIN + INDENT); 116 | const rightIndent = right.split('\n').join(JOIN + INDENT); 117 | 118 | const path = diff.path.join('.'); 119 | 120 | return bold(path) + ':\n' 121 | + INDENT + INDENT + 'left: ' + leftIndent + '\n' 122 | + INDENT + INDENT + 'right: ' + rightIndent; 123 | } 124 | -------------------------------------------------------------------------------- /test/unit/test_render_differences.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | const strip = require('strip-ansi'); 6 | 7 | const renderDifferences = require('src/render_differences'); 8 | 9 | 10 | describe('unit/renderDifferences', () => { 11 | it('throws if provided null object', () => { 12 | const differences = null; 13 | expect(() => { 14 | renderDifferences(differences); 15 | }).to.throw(Error) 16 | .that.match(/need to provide/); 17 | }); 18 | 19 | it('handles empty object', () => { 20 | const differences = {}; 21 | const actual = renderDifferences(differences); 22 | expect(actual).to.equal('No differences.'); 23 | }); 24 | 25 | it('returns "No differences" for empty object', () => { 26 | const differences = { 27 | pluginsMissingFromLeft: [], 28 | pluginsMissingFromRight: [], 29 | extendsMissingFromLeft: [], 30 | extendsMissingFromRight: [], 31 | rulesMissingFromLeft: [], 32 | rulesMissingFromRight: [], 33 | ruleDifferences: [], 34 | differences: [], 35 | }; 36 | 37 | const actual = renderDifferences(differences); 38 | 39 | expect(actual).to.equal('No differences.'); 40 | }); 41 | 42 | it('returns all differences', () => { 43 | const differences = { 44 | pluginsMissingFromLeft: ['one', 'two'], 45 | pluginsMissingFromRight: ['three', 'four'], 46 | extendsMissingFromLeft: ['_one', '_two'], 47 | extendsMissingFromRight: ['_three', '_four'], 48 | rulesMissingFromLeft: ['+one', '+two'], 49 | rulesMissingFromRight: ['+three', '+four'], 50 | ruleDifferences: [ 51 | { 52 | rule: 'five', 53 | left: ['error', { setting: 1 }], 54 | right: 'off', 55 | }, 56 | ], 57 | differences: [ 58 | { 59 | kind: 'E', 60 | lhs: '15', 61 | path: [ 62 | 'settings', 63 | 'react', 64 | 'version', 65 | ], 66 | rhs: '14', 67 | }, { 68 | kind: 'D', 69 | lhs: 'babel-eslint', 70 | path: ['parser'], 71 | }, 72 | ], 73 | }; 74 | const expected = 75 | 'Plugins shared: None\n' 76 | + '\n' 77 | + 'Plugins missing from left: 2\n' 78 | + ' one\n' 79 | + ' two\n' 80 | + '\n' 81 | + 'Plugins missing from right: 2\n' 82 | + ' three\n' 83 | + ' four\n' 84 | + '\n' 85 | + 'Extends shared: None\n' 86 | + '\n' 87 | + 'Extends missing from left: 2\n' 88 | + ' _one\n' 89 | + ' _two\n' 90 | + '\n' 91 | + 'Extends missing from right: 2\n' 92 | + ' _three\n' 93 | + ' _four\n' 94 | + '\n' 95 | + 'Rules matching: None\n' 96 | + '\n' 97 | + 'Rules missing from left: 2\n' 98 | + ' +one\n' 99 | + ' +two\n' 100 | + '\n' 101 | + 'Rules missing from right: 2\n' 102 | + ' +three\n' 103 | + ' +four\n' 104 | + '\n' 105 | + 'Rule configuration differences: 1\n' 106 | + ' five:\n' 107 | + ' left: [ \'error\', { setting: 1 } ]\n' 108 | + ' right: \'off\'\n' 109 | + '\n' 110 | + 'Differences in other configuration: 2\n' 111 | + ' settings.react.version:\n' 112 | + ' left: \'15\'\n' 113 | + ' right: \'14\'\n' 114 | + ' parser:\n' 115 | + ' left: \'babel-eslint\'\n' 116 | + ' right: undefined'; 117 | 118 | const actual = renderDifferences(differences); 119 | 120 | const stripped = strip(actual); 121 | expect(stripped).to.equal(expected); 122 | }); 123 | 124 | it('returns minimal differences', () => { 125 | const differences = { 126 | pluginsMissingFromLeft: ['one'], 127 | pluginsMissingFromRight: [], 128 | rulesMissingFromLeft: [], 129 | rulesMissingFromRight: [], 130 | ruleDifferences: [], 131 | differences: [], 132 | }; 133 | const expected = 134 | 'Plugins shared: None\n' 135 | + '\n' 136 | + 'Plugins missing from left: 1\n' 137 | + ' one\n' 138 | + '\n' 139 | + 'Plugins missing from right: None\n' 140 | + '\n' 141 | + 'Extends shared: None\n' 142 | + '\n' 143 | + 'Extends missing from left: None\n' 144 | + '\n' 145 | + 'Extends missing from right: None\n' 146 | + '\n' 147 | + 'Rules matching: None\n' 148 | + '\n' 149 | + 'Rules missing from left: None\n' 150 | + '\n' 151 | + 'Rules missing from right: None\n' 152 | + '\n' 153 | + 'Rule configuration differences: None\n' 154 | + '\n' 155 | + 'Differences in other configuration: None'; 156 | 157 | const actual = renderDifferences(differences); 158 | 159 | const stripped = strip(actual); 160 | expect(stripped).to.equal(expected); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /test/unit/test_get_differences.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 'use strict'; 4 | 5 | const chai = require('chai'); 6 | const expect = chai.expect; 7 | const _ = require('lodash'); 8 | 9 | const getDifferences = require('src/get_differences'); 10 | 11 | 12 | describe('unit/getDifferences', () => { 13 | it('returns empty object for empty rules', () => { 14 | const left = {}; 15 | const right = {}; 16 | 17 | const actual = getDifferences(left, right); 18 | 19 | expect(actual).to.have.property('pluginsMissingFromLeft').that.deep.equal([]); 20 | expect(actual).to.have.property('sharedPlugins').that.deep.equal([]); 21 | expect(actual).to.have.property('pluginsMissingFromRight').that.deep.equal([]); 22 | 23 | expect(actual).to.have.property('extendsMissingFromLeft').that.deep.equal([]); 24 | expect(actual).to.have.property('sharedExtends').that.deep.equal([]); 25 | expect(actual).to.have.property('extendsMissingFromRight').that.deep.equal([]); 26 | 27 | expect(actual).to.have.property('rulesMissingFromLeft').that.deep.equal([]); 28 | expect(actual).to.have.property('matchingRules').that.deep.equal([]); 29 | expect(actual).to.have.property('rulesMissingFromRight').that.deep.equal([]); 30 | expect(actual).to.have.property('ruleDifferences').that.deep.equal([]); 31 | 32 | expect(actual).to.have.property('differences').that.deep.equal([]); 33 | 34 | expect(_.keys(actual)).to.have.length(11); 35 | }); 36 | 37 | it('property computes rule diffs', () => { 38 | const left = { 39 | rules: { 40 | one: 'error', 41 | two: 'off', 42 | three: ['error', { setting: 1 }], 43 | four: 'error', 44 | }, 45 | }; 46 | const right = { 47 | rules: { 48 | two: 'off', 49 | three: ['error', { setting: 2 }], 50 | four: 'error', 51 | five: 'error', 52 | }, 53 | }; 54 | 55 | const actual = getDifferences(left, right); 56 | 57 | expect(actual).to.have.property('pluginsMissingFromLeft').that.deep.equal([]); 58 | expect(actual).to.have.property('sharedPlugins').that.deep.equal([]); 59 | expect(actual).to.have.property('pluginsMissingFromRight').that.deep.equal([]); 60 | 61 | expect(actual).to.have.property('extendsMissingFromLeft').that.deep.equal([]); 62 | expect(actual).to.have.property('sharedExtends').that.deep.equal([]); 63 | expect(actual).to.have.property('extendsMissingFromRight').that.deep.equal([]); 64 | 65 | expect(actual).to.have.property('rulesMissingFromLeft') 66 | .that.deep.equal(['five']); 67 | expect(actual).to.have.property('matchingRules') 68 | .that.deep.equal(['two', 'four']); 69 | expect(actual).to.have.property('rulesMissingFromRight') 70 | .that.deep.equal(['one']); 71 | expect(actual).to.have.property('ruleDifferences') 72 | .that.has.length(1) 73 | .and.deep.equal([ 74 | { 75 | rule: 'three', 76 | left: ['error', { setting: 1 }], 77 | right: ['error', { setting: 2 }], 78 | }, 79 | ]); 80 | 81 | expect(actual).to.have.property('differences').that.deep.equal([]); 82 | }); 83 | 84 | it('property computes plugin diffs', () => { 85 | const left = { plugins: ['one', 'two'] }; 86 | const right = { plugins: ['two', 'three'] }; 87 | 88 | const actual = getDifferences(left, right); 89 | 90 | expect(actual).to.have.property('pluginsMissingFromLeft') 91 | .that.deep.equal(['three']); 92 | expect(actual).to.have.property('sharedPlugins') 93 | .that.deep.equal(['two']); 94 | expect(actual).to.have.property('pluginsMissingFromRight') 95 | .that.deep.equal(['one']); 96 | 97 | expect(actual).to.have.property('extendsMissingFromLeft').that.deep.equal([]); 98 | expect(actual).to.have.property('sharedExtends').that.deep.equal([]); 99 | expect(actual).to.have.property('extendsMissingFromRight').that.deep.equal([]); 100 | 101 | expect(actual).to.have.property('rulesMissingFromLeft').that.deep.equal([]); 102 | expect(actual).to.have.property('matchingRules').that.deep.equal([]); 103 | expect(actual).to.have.property('rulesMissingFromRight').that.deep.equal([]); 104 | expect(actual).to.have.property('ruleDifferences').that.deep.equal([]); 105 | 106 | expect(actual).to.have.property('differences').that.deep.equal([]); 107 | }); 108 | 109 | it('property computes diffs for the rest of the config', () => { 110 | const left = { 111 | settings: { react: { version: '15' } }, 112 | parser: 'babel-eslint', 113 | }; 114 | const right = { settings: { react: { version: '14' } } }; 115 | 116 | const actual = getDifferences(left, right); 117 | 118 | expect(actual).to.have.property('pluginsMissingFromLeft').that.deep.equal([]); 119 | expect(actual).to.have.property('sharedPlugins').that.deep.equal([]); 120 | expect(actual).to.have.property('pluginsMissingFromRight').that.deep.equal([]); 121 | 122 | expect(actual).to.have.property('extendsMissingFromLeft').that.deep.equal([]); 123 | expect(actual).to.have.property('sharedExtends').that.deep.equal([]); 124 | expect(actual).to.have.property('extendsMissingFromRight').that.deep.equal([]); 125 | 126 | expect(actual).to.have.property('rulesMissingFromLeft').that.deep.equal([]); 127 | expect(actual).to.have.property('matchingRules').that.deep.equal([]); 128 | expect(actual).to.have.property('rulesMissingFromRight').that.deep.equal([]); 129 | expect(actual).to.have.property('ruleDifferences').that.deep.equal([]); 130 | 131 | expect(actual).to.have.property('differences') 132 | .that.has.length(2) 133 | .and.deep.equal([ 134 | { 135 | kind: 'E', 136 | lhs: '15', 137 | path: [ 138 | 'settings', 139 | 'react', 140 | 'version', 141 | ], 142 | rhs: '14', 143 | }, { 144 | kind: 'D', 145 | lhs: 'babel-eslint', 146 | path: ['parser'], 147 | }, 148 | ]); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @scottnonnenberg/eslint-compare-config 2 | 3 | A little tool to help you compare [ESLint](http://eslint.org/) configurations, the way ESLint sees them. 4 | 5 | Blog post announcing this project: https://blog.scottnonnenberg.com/eslint-part-3-analysis/ 6 | 7 | ## Quickstart 8 | 9 | ```bash 10 | npm install @scottnonnenberg/eslint-compare-config -g 11 | eslint-compare-config projectDirOne/ projectDirTwo/ 12 | ``` 13 | 14 | Here's what you get if you compare [`@scottnonnenberg/thehelp`](https://github.com/scottnonnenberg/eslint-config-thehelp) config versus that config merged with [`@scottnonnenberg/thehelp/test`](https://github.com/scottnonnenberg/eslint-config-thehelp#configurations-in-this-project): 15 | 16 | ``` 17 | Plugins shared: 8 18 | filenames 19 | import 20 | security 21 | @scottnonnenberg/thehelp 22 | immutable 23 | no-loops 24 | jsx-a11y 25 | react 26 | 27 | Plugins missing from left: 2 28 | bdd 29 | chai-expect 30 | 31 | Plugins missing from right: None 32 | 33 | Extends shared: 2 34 | @scottnonnenberg/thehelp/react 35 | @scottnonnenberg/thehelp/functional 36 | 37 | Extends missing from left: 1 38 | @scottnonnenberg/thehelp/test 39 | 40 | Extends missing from right: 1 41 | @scottnonnenberg/thehelp 42 | 43 | Rules matching: 271 44 | [full list omitted for brevity] 45 | 46 | Rules missing from left: 5 47 | bdd/focus 48 | bdd/exclude 49 | chai-expect/missing-assertion 50 | chai-expect/no-inner-compare 51 | chai-expect/terminating-properties 52 | 53 | Rules missing from right: 8 54 | max-nested-callbacks 55 | no-magic-numbers 56 | no-sync 57 | no-undefined 58 | no-unused-expressions 59 | import/no-extraneous-dependencies 60 | security/detect-non-literal-fs-filename 61 | immutable/no-let 62 | 63 | Rule configuration differences: None 64 | 65 | Differences in other configuration: 1 66 | env.mocha: 67 | left: undefined 68 | right: true 69 | ``` 70 | 71 | Because the `test` configuration only adds and disables rules, we don't see any configuration differences. We consider a rule to be 'missing' from a configuration if it is disabled. 72 | 73 | ## Permissions 74 | 75 | Note that, to get around ESLint plugin/module-loading semantics, this tool puts a file in each target directory and runs it with Node.js. This means that you'll need write/delete permissions in the target directory. 76 | 77 | If you don't have that permssion, your best bet is to go with _literal mode_. 78 | 79 | ## Options 80 | 81 | First, you will always need to provide two paths to the tool. The first is the _left_, and the second is the _right_. These terms will be used in the output. 82 | 83 | - `--literal` - by default, the tool uses full ESlint semantics to load configuration. Providing this option changes to a `require()` of the target path, which means normal [Node.js require semantics](https://nodejs.org/api/modules.html#modules_all_together) (various transformations to find JavaScript or JSON to load) apply. Because `require()` is called directly, right now only JavaScript and JSON files are supported. 84 | - `--json` - by default, the difference between the two configurations is displayed in human-readable format (see above). If you'd like to use the data in another tool, this will print the raw JSON.stringified `differences` object to the console. 85 | - `--score` - if you'd like to see the similarity of two configurations at a glance, use this option. 0% is completely different, 100% is exactly the same. 86 | 87 | ## API 88 | 89 | If installed as a dependency, you can `require('@scottnonnenberg/eslint-compare-config')` and get access to a number of functions: 90 | 91 | - `getConfigSync(path)` - puts a file in each of the target directories whichs uses `eslint` APIs to load the configuration for a file in that directory, runs them, then deletes them. 92 | - `getLiteralConfigSync(path)` - loads the target files using `require()`, thus supporting only JavaScript and JSON file formats 93 | - `getDifferencesSync(left, right)` - given two configs, produces an object describing all of their differences. The same thing you get when you use the `--json` option 94 | - `getScoreSync(differences)` - given `getDifferences()` output, returns a similarity score 1-100. 95 | - `normalizeConfigSync(config)` - removes disabled rules, and turns numeric toggles into string (1 = 'warning', 2 = 'error') 96 | - `renderDifferencesSync(differences)` - given `getDifferences()` output, returns string with human-readable comparison (including ANSI color codes) 97 | 98 | ## Contributing 99 | 100 | This project uses [`standard-version`](https://github.com/conventional-changelog/standard-version) to release new versions, automatically updating the version number and [changelog](https://github.com/scottnonnenberg/eslint-compare-config/blob/master/CHANGELOG.md) based on commit messages in [standard format](https://github.com/bcoe/conventional-changelog-standard/blob/master/convention.md). [`ghooks`](https://github.com/gtramontina/ghooks) and [`validate-commit-msg`](https://github.com/kentcdodds/validate-commit-msg) are used to ensure all commit messages match the expected format (see [package.json](https://github.com/scottnonnenberg/eslint-compare-config/blob/master/package.json) for the configuration details). 101 | 102 | It takes some getting used to, but this configuration is absolutely worthwhile. A changelog is way easier to understand than the chaos of a raw commit stream, especially with `standard-version` providing direct links to bugs, commits and [commit ranges](https://github.com/scottnonnenberg/eslint-compare-config/compare/v0.4.0...v1.0.0). 103 | 104 | ## TODO 105 | 106 | - Literal mode: support eslint config in YAML and `package.json` files 107 | - Use each rule's defaults in equivalence: just 'error' is the same as ['error', {something: true}] if that config is the same as the default. How to get the default? 108 | - Determinism: sort rules/plugins/extends by name, rules without / in them first 109 | - Add option to include globals in the diff - there are a lot, so it's too noisy to show by default 110 | 111 | ## License 112 | 113 | (The MIT license) 114 | 115 | Copyright (c) 2016 Scott Nonnenberg 116 | 117 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 118 | associated documentation files (the "Software"), to deal in the Software without restriction, 119 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 120 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 121 | furnished to do so, subject to the following conditions: 122 | 123 | The above copyright notice and this permission notice shall be included in all copies or 124 | substantial portions of the Software. 125 | 126 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 127 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 128 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 129 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 130 | OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 131 | --------------------------------------------------------------------------------