diff --git a/.eslintrc.js b/.eslintrc.js index 7cb37db..e2f6c27 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,12 +4,9 @@ module.exports = { root: true, env: { browser: false, - es6: true, + es2021: true, node: true }, - parserOptions: { - ecmaVersion: 2020 - }, reportUnusedDisableDirectives: true, rules: { 'accessor-pairs': 'error', @@ -34,7 +31,6 @@ module.exports = { 'func-call-spacing': ['error', 'never'], 'generator-star-spacing': ['error', { 'before': true, 'after': false }], 'getter-return': 'error', - 'handle-callback-err': ['error', '^(err|error)$' ], 'indent': ['error', 2, { 'SwitchCase': 1 }], 'key-spacing': ['error', { 'beforeColon': false, 'afterColon': true }], 'keyword-spacing': ['error', { 'before': true, 'after': true }], @@ -42,7 +38,6 @@ module.exports = { 'new-parens': 'error', 'no-array-constructor': 'error', 'no-async-promise-executor': 'error', - 'no-buffer-constructor': 'error', 'no-caller': 'error', 'no-class-assign': 'error', 'no-compare-neg-zero': 'error', @@ -81,6 +76,7 @@ module.exports = { 'no-label-var': 'error', 'no-labels': ['error', { 'allowLoop': true, 'allowSwitch': true }], 'no-lone-blocks': 'error', + 'no-loss-of-precision': 'error', 'no-misleading-character-class': 'error', 'no-mixed-spaces-and-tabs': 'error', 'no-multi-spaces': ['error', { 'ignoreEOLComments': true }], @@ -90,7 +86,6 @@ module.exports = { 'no-new-func': 'error', 'no-new-object': 'error', 'no-new-symbol': 'error', - 'no-new-require': 'error', 'no-new-wrappers': 'error', 'no-obj-calls': 'error', 'no-octal': 'error', @@ -100,7 +95,6 @@ module.exports = { 'no-regex-spaces': 'error', 'no-restricted-exports': 'error', 'no-return-assign': 'error', - 'no-return-await': 'error', 'no-self-assign': 'error', 'no-self-compare': 'error', 'no-sequences': 'error', @@ -116,8 +110,11 @@ module.exports = { 'no-unexpected-multiline': 'error', 'no-unneeded-ternary': ['error', { 'defaultAssignment': false }], 'no-unreachable': 'error', + 'no-unreachable-loop': 'error', 'no-unsafe-finally': 'error', 'no-unsafe-negation': 'error', + 'no-unsafe-optional-chaining': 'error', + 'no-unused-expressions': 'error', 'no-unused-vars': ['error', { 'vars': 'all', 'args': 'none' }], 'no-use-before-define': ['error', 'nofunc'], 'no-useless-backreference': 'error', @@ -149,7 +146,7 @@ module.exports = { 'semi': ['error', 'always'], 'semi-spacing': ['error', { 'before': false, 'after': true }], 'space-before-blocks': ['error', 'always'], - 'space-before-function-paren': ['error', 'always'], + 'space-before-function-paren': ['error', { 'anonymous': 'never', 'named': 'never', 'asyncArrow': 'always' }], 'space-in-parens': ['error', 'never'], 'space-infix-ops': 'error', 'space-unary-ops': ['error', { 'words': true, 'nonwords': false }], diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..78cbbf0 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,28 @@ +name: belly-button CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + node-version: [12.x, 14.x, 16.x] + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test + env: + CI: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c4aa27f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -sudo: false -language: node_js -node_js: - - "10" - - "12" - - "14" diff --git a/LICENSE b/LICENSE index ecf271b..fdfabe6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Colin J. Ihrig +Copyright (c) 2015-2021 Colin J. Ihrig Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/bin/belly-button.js b/bin/belly-button.js index e1fa534..786da52 100755 --- a/bin/belly-button.js +++ b/bin/belly-button.js @@ -1,12 +1,16 @@ #!/usr/bin/env node 'use strict'; -require('../lib/cli').run(process.argv, (err, output, code) => { - if (err) { +async function main() { + try { + const [output, code] = await require('../lib/cli').run(process.argv); + + console.log(output); + process.exit(code); + } catch (err) { console.error(err.message); process.exit(1); } +} - console.log(output); - process.exit(code); -}); +main(); diff --git a/lib/cli.js b/lib/cli.js index 4eba8f6..df20f9d 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -2,8 +2,7 @@ const Path = require('path'); const Util = require('util'); const Bossy = require('@hapi/bossy'); -const Chalk = require('chalk'); -const { CLIEngine } = require('eslint'); +const { ESLint } = require('eslint'); const Glob = require('glob'); const Insync = require('insync'); const getFilesToLint = Util.promisify(getFiles); @@ -65,99 +64,40 @@ const cliArgs = { }; -module.exports.run = async function (argv, callback) { +module.exports.run = async function(argv) { const args = Bossy.parse(cliArgs, { argv }); if (args instanceof Error) { - return callback(new Error(Bossy.usage(cliArgs, args.message))); + throw new Error(Bossy.usage(cliArgs, args.message)); } - const configFile = args.config; const globs = args.input; const ignore = args.ignore; - const fix = args.fix; - const cache = args.cache; const cwd = args.pwd || process.cwd(); - - try { - const files = await getFilesToLint({ globs, ignore, cwd }); - const result = lintFiles({ configFile, fix, cache, files }); - const exitCode = +!!result.errorCount; - const output = formatResults(result); - - callback(null, output, exitCode); - } catch (err) { - callback(err); - } -}; - - -function formatResults (result) { - const totalWarnings = result.warningCount; - const totalErrors = result.errorCount; - const totalIssues = totalWarnings + totalErrors; - - if (totalIssues < 1) { - return Chalk.green.bold('\nNo issues found!\n'); - } - - let output = '\n'; - const position = Chalk.gray.bold; - const error = Chalk.red; - const warning = Chalk.yellow; - const colorCode = { - 2: error, - 1: warning - }; - - result.results.forEach((file) => { - if (file.errorCount < 1 && file.warningCount < 1) { - return; - } - - output += 'Problems in: ' + Chalk.dim(file.filePath) + '\n'; - - file.messages.forEach((msg) => { - const mainStyle = colorCode[msg.severity] || Chalk.gray; - - output += mainStyle('\t' + msg.message.slice(0, -1) + - ' at line ' + position(Util.format('[%s]', msg.line)) + - ', column ' + position(Util.format('[%s]', msg.column))); - output += ' - ' + Chalk.blue(Util.format('(%s)', msg.ruleId)); - output += '\n'; - }); - - output += '\n'; - }); - - output += Chalk.bold('Results\n'); - output += 'Total ' + error.bold('errors') + ': ' + totalErrors + '\n'; - output += 'Total ' + warning.bold('warnings') + ': ' + totalWarnings; - output += '\n'; - return output; -} - - -function lintFiles (options) { - const fix = options.fix; - const linter = new CLIEngine({ - configFile: options.configFile, + const files = await getFilesToLint({ globs, ignore, cwd }); + const linter = new ESLint({ + overrideConfigFile: args.config, useEslintrc: false, - fix, - cache: options.cache + fix: args.fix, + cache: args.cache + }); + const result = await linter.lintFiles(files); + const hasErrors = result.some((current) => { + return current.errorCount > 0; }); - const result = linter.executeOnFiles(options.files); - - if (fix) { - CLIEngine.outputFixes(result); + if (args.fix) { + await ESLint.outputFixes(result); } - return result; -} + const formatter = await linter.loadFormatter('stylish'); + const output = formatter.format(result); + + return [output, +hasErrors]; +}; -function getFiles (options, callback) { +function getFiles(options, callback) { let files = []; const globOptions = { realpath: true, diff --git a/package.json b/package.json index d7581ad..ebf64d3 100644 --- a/package.json +++ b/package.json @@ -21,21 +21,19 @@ "test": "npm run lint && lab -v -t 100" }, "engines": { - "node": ">=10.0.0" + "node": ">=12.0.0" }, "dependencies": { "@hapi/bossy": "5.x.x", - "chalk": "4.x.x", - "eslint": "7.x.x", + "eslint": "8.0.0-beta.2", "glob": "7.x.x", "insync": "2.x.x" }, "devDependencies": { - "@hapi/lab": "22.x.x", + "@hapi/lab": "24.x.x", "cb-barrier": "1.x.x", - "fs-extra": "9.x.x", - "stand-in": "4.x.x", - "strip-ansi": "6.x.x" + "fs-extra": "10.x.x", + "stand-in": "4.x.x" }, "keywords": [ "belly-button", diff --git a/test/cli.js b/test/cli.js index fbb8265..f1cda29 100644 --- a/test/cli.js +++ b/test/cli.js @@ -3,14 +3,11 @@ const Assert = require('assert'); const ChildProcess = require('child_process'); const Path = require('path'); const Barrier = require('cb-barrier'); -const ESLint = require('eslint'); const Fse = require('fs-extra'); const Glob = require('glob'); const Lab = require('@hapi/lab'); const StandIn = require('stand-in'); -const StripAnsi = require('strip-ansi'); const Cli = require('../lib/cli'); -const FakeResults = require('./fixtures/fake-results'); const { describe, it, after } = exports.lab = Lab.script(); const fixturesDirectory = Path.join(__dirname, 'fixtures'); @@ -19,117 +16,75 @@ const failuresDirectory = Path.join(fixturesDirectory, 'fail'); const tempDirectory = Path.join(process.cwd(), 'test-tmp'); describe('Belly Button CLI', () => { - after(() => { - const barrier = new Barrier(); - - Fse.remove(tempDirectory, (err) => { - barrier.pass(err); - }); - - return barrier; + after(async () => { + await Fse.remove(tempDirectory); }); describe('run()', () => { - it('reports errors', () => { - const barrier = new Barrier(); + it('reports errors', async () => { const ignore = Path.join(failuresDirectory, '**'); - - Cli.run([ + const [output, exitCode] = await Cli.run([ '-i', successDirectory, '-i', ignore - ], (err, output, exitCode) => { - Assert.ifError(err); - Assert(/Total errors: 2/.test(StripAnsi(output))); - Assert.strictEqual(exitCode, 1); - barrier.pass(); - }); + ]); - return barrier; + Assert.strictEqual(exitCode, 1); + Assert(/2 problems \(2 errors, 0 warnings\)/.test(output)); }); - it('successfully ignores files', () => { - const barrier = new Barrier(); + it('successfully ignores files', async () => { const ignore = Path.join(failuresDirectory, '**'); - - Cli.run([ + const [output, exitCode] = await Cli.run([ '-i', successDirectory, '-i', ignore, '-I', ignore - ], (err, output, exitCode) => { - Assert.ifError(err); - Assert(typeof output === 'string' && output.length > 0); - Assert.strictEqual(exitCode, 0); - barrier.pass(); - }); + ]); - return barrier; + Assert.strictEqual(exitCode, 0); + Assert(typeof output === 'string'); }); - it('fixes linting errors when possible', () => { - const barrier = new Barrier(); + it('fixes linting errors when possible', async () => { const src = Path.join(failuresDirectory, 'semi.js'); const dest = Path.join(tempDirectory, 'semi.js'); Fse.ensureDirSync(tempDirectory); Fse.copySync(src, dest); - Cli.run(['-w', tempDirectory, '-f'], (err, output, exitCode) => { - Assert.ifError(err); - Assert(typeof output === 'string' && output.length > 0); - Assert.strictEqual(exitCode, 0); - barrier.pass(); - }); + const [output, exitCode] = await Cli.run(['-w', tempDirectory, '-f']); - return barrier; + Assert.strictEqual(exitCode, 0); + Assert(typeof output === 'string'); }); - it('uses process.cwd() as default working directory', () => { - const barrier = new Barrier(); - + it('uses process.cwd() as default working directory', async () => { StandIn.replaceOnce(process, 'cwd', () => { return successDirectory; }); - Cli.run([], (err, output, exitCode) => { - Assert.ifError(err); - Assert(typeof output === 'string' && output.length > 0); - Assert.strictEqual(exitCode, 0); - barrier.pass(); - }); - }); - - it('rejects unknown options', () => { - const barrier = new Barrier(); + const [output, exitCode] = await Cli.run([]); - Cli.run(['--foo'], (err, output, exitCode) => { - Assert(err instanceof Error); - Assert(/Unknown option: foo/.test(err.message)); - Assert.strictEqual(output, undefined); - Assert.strictEqual(exitCode, undefined); - barrier.pass(); - }); + Assert.strictEqual(exitCode, 0); + Assert(typeof output === 'string'); + }); - return barrier; + it('rejects unknown options', async () => { + await Assert.rejects(() => { + return Cli.run(['--foo']); + }, /Unknown option: foo/); }); - it('handles glob errors', () => { - const barrier = new Barrier(); + it('handles glob errors', async () => { const glob = Glob.Glob.prototype._process; - Glob.Glob.prototype._process = function (pattern, index, inGlobStar, callback) { + Glob.Glob.prototype._process = function(pattern, index, inGlobStar, callback) { Glob.Glob.prototype._process = glob; this.emit('error', new Error('glob')); }; - Cli.run(['-w', successDirectory], (err, output, exitCode) => { - Assert(err instanceof Error); - Assert.strictEqual(err.message, 'glob'); - Assert.strictEqual(output, undefined); - Assert.strictEqual(exitCode, undefined); - barrier.pass(); - }); - - return barrier; + await Assert.rejects(async () => { + await Cli.run(['-w', successDirectory]); + }, /Error: glob/); }); it('runs binary successfully', () => { @@ -160,44 +115,6 @@ describe('Belly Button CLI', () => { return barrier; }); - it('only executes print logic when there are errors or warnings', () => { - const barrier = new Barrier(); - - StandIn.replaceOnce(ESLint.CLIEngine.prototype, 'executeOnFiles', () => { - return { - errorCount: 1, - warningCount: 1, - results: [] - }; - }); - - Cli.run(['-w', successDirectory], (err, output, exitCode) => { - Assert.ifError(err); - Assert(/total\s+(errors|warnings).+1/i.test(StripAnsi(output))); - barrier.pass(); - }); - - return barrier; - }); - - it('prints messages when there are lint problems', () => { - const barrier = new Barrier(); - - StandIn.replaceOnce(ESLint.CLIEngine.prototype, 'executeOnFiles', () => { - return FakeResults; - }); - - Cli.run(['-w', successDirectory], (err, output, exitCode) => { - Assert.ifError(err); - const out = StripAnsi(output); - const msg = '\nProblems in: /Home/belly-button/bar.js\n\tFooBar is a weird variable name at line [331], column [1] - (weird-name)\n\tDangling comma at line [12], column [4] - (dangling-comma)\n\tMissing semi colon at line [200], column [3] - (semi-colon)\n\nProblems in: /Home/belly-button/baz.js\n\tDangling comma at line [12], column [4] - (dangling-comma)\n\nResults\nTotal errors: 1\nTotal warnings: 1\n'; - Assert.strictEqual(out, msg); - barrier.pass(); - }); - - return barrier; - }); - it('defaults to belly-button style', () => { const barrier = new Barrier(); const lintFile = Path.join(fixturesDirectory, 'config', 'yoda.js'); diff --git a/test/fixtures/fake-results.js b/test/fixtures/fake-results.js deleted file mode 100644 index aa76fa1..0000000 --- a/test/fixtures/fake-results.js +++ /dev/null @@ -1,44 +0,0 @@ -module.exports = { - results: [{ - filePath: '/Home/belly-button/foo.js', - messages: [], - errorCount: 0, - warningCount: 0 - }, { - filePath: '/Home/belly-button/bar.js', - messages: [{ - message: 'FooBar is a weird variable name.', - column: 1, - line: 331, - ruleId: 'weird-name', - severity: 0 - }, { - message: 'Dangling comma.', - column: 4, - line: 12, - ruleId: 'dangling-comma', - severity: 1 - }, { - message: 'Missing semi colon.', - column: 3, - line: 200, - ruleId: 'semi-colon', - severity: 2 - }], - errorCount: 1, - warningCount: 0 - }, { - filePath: '/Home/belly-button/baz.js', - messages: [{ - message: 'Dangling comma.', - column: 4, - line: 12, - ruleId: 'dangling-comma', - severity: 1 - }], - errorCount: 0, - warningCount: 1 - }], - errorCount: 1, - warningCount: 1 -}; diff --git a/test/fixtures/success/semi.js b/test/fixtures/success/semi.js index ddeeeb0..d445655 100644 --- a/test/fixtures/success/semi.js +++ b/test/fixtures/success/semi.js @@ -1,9 +1,9 @@ 'use strict'; -module.exports.foo = function () { +module.exports.foo = function() { return 42; }; -module.exports.bar = function () { +module.exports.bar = function() { return 85; };