From 8a0cef9ac2d537f4ad864d3d71967ff93cbb0b6e Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Thu, 1 Jul 2021 15:48:55 +0800 Subject: [PATCH 1/7] Update deps --- cli-main.js | 221 +++++++++++++++++++++++++++++++++++++++++ cli.js | 1 + lib/open-report.js | 1 + lib/options-manager.js | 5 +- package.json | 34 +++---- test/cli-main.js | 184 ++++++++++++++++++++++++++++++++++ test/cli.js | 183 ++-------------------------------- 7 files changed, 433 insertions(+), 196 deletions(-) create mode 100755 cli-main.js create mode 100644 test/cli-main.js diff --git a/cli-main.js b/cli-main.js new file mode 100755 index 00000000..6fa2f447 --- /dev/null +++ b/cli-main.js @@ -0,0 +1,221 @@ +#!/usr/bin/env node +import getStdin from 'get-stdin'; +import meow from 'meow'; +import formatterPretty from 'eslint-formatter-pretty'; +import semver from 'semver'; +import openReport from './lib/open-report.js'; +import xo from './index.js'; + +const cli = meow(` + Usage + $ xo [ ...] + + Options + --fix Automagically fix issues + --reporter Reporter to use + --env Environment preset [Can be set multiple times] + --global Global variable [Can be set multiple times] + --ignore Additional paths to ignore [Can be set multiple times] + --space Use space indent instead of tabs [Default: 2] + --no-semicolon Prevent use of semicolons + --prettier Conform to Prettier code style + --node-version Range of Node.js version to support + --plugin Include third-party plugins [Can be set multiple times] + --extend Extend defaults with a custom config [Can be set multiple times] + --open Open files with issues in your editor + --quiet Show only errors and no warnings + --extension Additional extension to lint [Can be set multiple times] + --cwd= Working directory for files + --stdin Validate/fix code from stdin + --stdin-filename Specify a filename for the --stdin option + --print-config Print the effective ESLint config for the given file + + Examples + $ xo + $ xo index.js + $ xo *.js !foo.js + $ xo --space + $ xo --env=node --env=mocha + $ xo --plugin=react + $ xo --plugin=html --extension=html + $ echo 'const x=true' | xo --stdin --fix + $ xo --print-config=index.js + + Tips + - Add XO to your project with \`npm init xo\`. + - Put options in package.json instead of using flags so other tools can read it. +`, { + autoVersion: false, + booleanDefault: undefined, + flags: { + fix: { + type: 'boolean' + }, + reporter: { + type: 'string' + }, + env: { + type: 'string', + isMultiple: true + }, + global: { + type: 'string', + isMultiple: true + }, + ignore: { + type: 'string', + isMultiple: true + }, + space: { + type: 'string' + }, + semicolon: { + type: 'boolean' + }, + prettier: { + type: 'boolean' + }, + nodeVersion: { + type: 'string' + }, + plugin: { + type: 'string', + isMultiple: true + }, + extend: { + type: 'string', + isMultiple: true + }, + open: { + type: 'boolean' + }, + quiet: { + type: 'boolean' + }, + extension: { + type: 'string', + isMultiple: true + }, + cwd: { + type: 'string' + }, + printConfig: { + type: 'string' + }, + stdin: { + type: 'boolean' + }, + stdinFilename: { + type: 'string' + } + } +}); + +const {input, flags: options, showVersion} = cli; + +// TODO: Fix this properly instead of the below workaround. +// Revert behavior of meow >8 to pre-8 (7.1.1) for flags using `isMultiple: true`. +// Otherwise, options defined in package.json can't be merged by lib/options-manager.js `mergeOptions()`. +for (const key in options) { + if (Array.isArray(options[key]) && options[key].length === 0) { + delete options[key]; + } +} + +// Make data types for `options.space` match those of the API +// Check for string type because `xo --no-space` sets `options.space` to `false` +if (typeof options.space === 'string') { + if (/^\d+$/u.test(options.space)) { + options.space = Number.parseInt(options.space, 10); + } else if (options.space === 'true') { + options.space = true; + } else if (options.space === 'false') { + options.space = false; + } else { + if (options.space !== '') { + // Assume `options.space` was set to a filename when run as `xo --space file.js` + input.push(options.space); + } + + options.space = true; + } +} + +if (process.env.GITHUB_ACTIONS && !options.fix && !options.reporter) { + options.quiet = true; +} + +const log = async report => { + const reporter = options.reporter || process.env.GITHUB_ACTIONS ? await xo.getFormatter(options.reporter || 'compact') : formatterPretty; + process.stdout.write(reporter(report.results)); + process.exitCode = report.errorCount === 0 ? 0 : 1; +}; + +// `xo -` => `xo --stdin` +if (input[0] === '-') { + options.stdin = true; + input.shift(); +} + +if (options.version) { + showVersion(); +} + +if (options.nodeVersion) { + if (options.nodeVersion === 'false') { + options.nodeVersion = false; + } else if (!semver.validRange(options.nodeVersion)) { + console.error('The `--node-engine` flag must be a valid semver range (for example `>=6`)'); + process.exit(1); + } +} + +(async () => { + if (options.printConfig) { + if (input.length > 0) { + console.error('The `--print-config` flag must be used with exactly one filename'); + process.exit(1); + } + + if (options.stdin) { + console.error('The `--print-config` flag is not supported on stdin'); + process.exit(1); + } + + options.filePath = options.printConfig; + const config = await xo.getConfig(options); + console.log(JSON.stringify(config, undefined, '\t')); + } else if (options.stdin) { + const stdin = await getStdin(); + + if (options.stdinFilename) { + options.filePath = options.stdinFilename; + } + + if (options.fix) { + const {results: [result]} = await xo.lintText(stdin, options); + // If there is no output, pass the stdin back out + process.stdout.write((result && result.output) || stdin); + return; + } + + if (options.open) { + console.error('The `--open` flag is not supported on stdin'); + process.exit(1); + } + + await log(await xo.lintText(stdin, options)); + } else { + const report = await xo.lintFiles(input, options); + + if (options.fix) { + await xo.outputFixes(report); + } + + if (options.open) { + openReport(report); + } + + await log(report); + } +})(); diff --git a/cli.js b/cli.js index 6fa2f447..626ab128 100755 --- a/cli.js +++ b/cli.js @@ -45,6 +45,7 @@ const cli = meow(` - Add XO to your project with \`npm init xo\`. - Put options in package.json instead of using flags so other tools can read it. `, { + importMeta: import.meta, autoVersion: false, booleanDefault: undefined, flags: { diff --git a/lib/open-report.js b/lib/open-report.js index 2361ec69..c742cbf8 100644 --- a/lib/open-report.js +++ b/lib/open-report.js @@ -1,3 +1,4 @@ +'use strict'; import openEditor from 'open-editor'; const sortResults = (a, b) => a.errorCount + b.errorCount > 0 ? (a.errorCount - b.errorCount) : (a.warningCount - b.warningCount); diff --git a/lib/options-manager.js b/lib/options-manager.js index 456f6327..0b409f0b 100644 --- a/lib/options-manager.js +++ b/lib/options-manager.js @@ -28,7 +28,7 @@ import { MODULE_NAME, CONFIG_FILES, MERGE_OPTIONS_CONCAT, - TSCONFIG_DEFAULTS, + TSCONFIG_DEFFAULTS, CACHE_DIR_NAME } from './constants.js'; @@ -46,7 +46,6 @@ resolveFrom.silent = (moduleId, fromDirectory) => { } catch { } }; -// TODO: Use `import.meta.resolve(normalizePackageName(name))` when supported const resolveLocalConfig = name => resolveModule(normalizePackageName(name, 'eslint-config'), import.meta.url); const nodeVersion = process && process.version; @@ -219,7 +218,7 @@ const makeTSConfig = (tsConfig, tsConfigPath, files) => { config.extends = tsConfigPath; config.include = arrify(tsConfig.include).map(pattern => toAbsoluteGlob(pattern, {cwd: path.dirname(tsConfigPath)})); } else { - Object.assign(config, TSCONFIG_DEFAULTS); + Object.assign(config, TSCONFIG_DEFFAULTS); } return config; diff --git a/package.json b/package.json index 6c81e631..711d4c7d 100644 --- a/package.json +++ b/package.json @@ -52,13 +52,13 @@ "typescript" ], "dependencies": { - "@eslint/eslintrc": "^0.4.1", - "@typescript-eslint/eslint-plugin": "^4.22.0", - "@typescript-eslint/parser": "^4.22.0", - "arrify": "^2.0.1", + "@eslint/eslintrc": "^0.4.2", + "@typescript-eslint/eslint-plugin": "^4.28.1", + "@typescript-eslint/parser": "^4.28.1", + "arrify": "^3.0.0", "cosmiconfig": "^7.0.0", "debug": "^4.3.1", - "define-lazy-prop": "^2.0.0", + "define-lazy-prop": "^3.0.0", "eslint": "^7.24.0", "eslint-config-prettier": "^8.2.0", "eslint-config-xo": "^0.36.0", @@ -77,36 +77,36 @@ "find-cache-dir": "^3.3.1", "find-up": "^5.0.0", "fs-extra": "^10.0.0", - "get-stdin": "^8.0.0", + "get-stdin": "^9.0.0", "globby": "^9.2.0", "imurmurhash": "^0.1.4", - "is-path-inside": "^3.0.3", + "is-path-inside": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "json5": "^2.2.0", "lodash-es": "^4.17.21", - "meow": "^9.0.0", + "meow": "^10.0.1", "micromatch": "^4.0.4", "open-editor": "^3.0.0", "p-filter": "^2.1.0", - "p-map": "^4.0.0", - "p-reduce": "^2.1.0", + "p-map": "^5.0.0", + "p-reduce": "^3.0.0", "path-exists": "^4.0.0", - "prettier": "^2.2.1", + "prettier": "^2.3.2", "semver": "^7.3.5", - "slash": "^3.0.0", + "slash": "^4.0.0", "to-absolute-glob": "^2.0.2", - "typescript": "^4.2.4" + "typescript": "^4.3.5" }, "devDependencies": { "ava": "^3.15.0", "eslint-config-xo-react": "^0.25.0", - "eslint-plugin-react": "^7.23.2", + "eslint-plugin-react": "^7.24.0", "eslint-plugin-react-hooks": "^4.2.0", - "execa": "^5.0.0", + "execa": "^5.1.1", "nyc": "^15.1.0", "proxyquire": "^2.1.3", - "temp-write": "^4.0.0", - "webpack": "^5.34.0" + "temp-write": "^5.0.0", + "webpack": "^5.41.1" }, "eslintConfig": { "extends": "eslint-config-xo", diff --git a/test/cli-main.js b/test/cli-main.js new file mode 100644 index 00000000..51999728 --- /dev/null +++ b/test/cli-main.js @@ -0,0 +1,184 @@ +import fs from 'fs'; +import path from 'path'; +import test from 'ava'; +import execa from 'execa'; +import slash from 'slash'; +import tempWrite from 'temp-write'; +import createEsmUtils from 'esm-utils'; + +const {__dirname} = createEsmUtils(import.meta); +process.chdir(__dirname); + +const main = (arguments_, options) => execa(path.join(__dirname, '../cli-main.js'), arguments_, options); + +test('fix option', async t => { + const filepath = await tempWrite('console.log()\n', 'x.js'); + await main(['--fix', filepath]); + t.is(fs.readFileSync(filepath, 'utf8').trim(), 'console.log();'); +}); + +test('fix option with stdin', async t => { + const {stdout} = await main(['--fix', '--stdin'], { + input: 'console.log()' + }); + t.is(stdout, 'console.log();'); +}); + +test('stdin-filename option with stdin', async t => { + const {stdout} = await main(['--stdin', '--stdin-filename=unicorn-file'], { + input: 'console.log()\n', + reject: false + }); + t.regex(stdout, /unicorn-file:/u); +}); + +test('reporter option', async t => { + const filepath = await tempWrite('console.log()\n', 'x.js'); + + const error = await t.throwsAsync(() => + main(['--reporter=compact', filepath]) + ); + t.true(error.stdout.includes('Error - ')); +}); + +test('overrides fixture', async t => { + const cwd = path.join(__dirname, 'fixtures/overrides'); + await t.notThrowsAsync(main([], {cwd})); +}); + +test('overrides work with relative path', async t => { + const cwd = path.join(__dirname, 'fixtures/overrides'); + const file = path.join('test', 'bar.js'); + await t.notThrowsAsync(main([file], {cwd})); +}); + +test('overrides work with relative path starting with `./`', async t => { + const cwd = path.join(__dirname, 'fixtures/overrides'); + const file = '.' + path.sep + path.join('test', 'bar.js'); + await t.notThrowsAsync(main([file], {cwd})); +}); + +test('overrides work with absolute path', async t => { + const cwd = path.join(__dirname, 'fixtures/overrides'); + const file = path.join(cwd, 'test', 'bar.js'); + await t.notThrowsAsync(main([file], {cwd})); +}); + +test.failing('override default ignore', async t => { + const cwd = path.join(__dirname, 'fixtures/ignores'); + await t.throwsAsync(main([], {cwd})); +}); + +test('ignore files in .gitignore', async t => { + const cwd = path.join(__dirname, 'fixtures/gitignore'); + const error = await t.throwsAsync(main(['--reporter=json'], {cwd})); + const reports = JSON.parse(error.stdout); + const files = reports + .map(report => path.relative(cwd, report.filePath)) + .map(report => slash(report)); + t.deepEqual(files.sort(), ['index.js', 'test/bar.js'].sort()); +}); + +test('ignore explicit files when in .gitgnore', async t => { + const cwd = path.join(__dirname, 'fixtures/gitignore'); + await t.notThrowsAsync(main(['test/foo.js', '--reporter=json'], {cwd})); +}); + +test('negative gitignores', async t => { + const cwd = path.join(__dirname, 'fixtures/negative-gitignore'); + const error = await t.throwsAsync(main(['--reporter=json'], {cwd})); + const reports = JSON.parse(error.stdout); + const files = reports.map(report => path.relative(cwd, report.filePath)); + t.deepEqual(files, ['foo.js']); +}); + +test('supports being extended with a shareable config', async t => { + const cwd = path.join(__dirname, 'fixtures/project'); + await t.notThrowsAsync(main([], {cwd})); +}); + +test('quiet option', async t => { + const filepath = await tempWrite('// TODO: quiet\nconsole.log()\n', 'x.js'); + const error = await t.throwsAsync(main(['--quiet', '--reporter=json', filepath])); + const [report] = JSON.parse(error.stdout); + t.is(report.warningCount, 0); +}); + +test('invalid node-engine option', async t => { + const filepath = await tempWrite('console.log()\n', 'x.js'); + const error = await t.throwsAsync(main(['--node-version', 'v', filepath])); + t.is(error.exitCode, 1); +}); + +test('cli option takes precedence over config', async t => { + const cwd = path.join(__dirname, 'fixtures/default-options'); + const input = 'console.log()\n'; + + // Use config from package.json + await t.notThrowsAsync(main(['--stdin'], {cwd, input})); + + // Override package.json config with cli flag + await t.throwsAsync(main(['--semicolon=true', '--stdin'], {cwd, input})); + + // Use XO default (`true`) even if option is not set in package.json nor cli arg + // i.e make sure absent cli flags are not parsed as `false` + await t.throwsAsync(main(['--stdin'], {input})); +}); + +test('space option with number value', async t => { + const cwd = path.join(__dirname, 'fixtures/space'); + const {stdout} = await t.throwsAsync(main(['--space=4', 'one-space.js'], {cwd})); + t.true(stdout.includes('Expected indentation of 4 spaces')); +}); + +test('space option as boolean', async t => { + const cwd = path.join(__dirname, 'fixtures/space'); + const {stdout} = await t.throwsAsync(main(['--space'], {cwd})); + t.true(stdout.includes('Expected indentation of 2 spaces')); +}); + +test('space option as boolean with filename', async t => { + const cwd = path.join(__dirname, 'fixtures/space'); + const {stdout} = await main(['--reporter=json', '--space', 'two-spaces.js'], { + cwd, + reject: false + }); + const reports = JSON.parse(stdout); + + // Only the specified file was checked (filename was not the value of `space`) + t.is(reports.length, 1); + + // The default space value of 2 was expected + t.is(reports[0].errorCount, 0); +}); + +test('space option with boolean strings', async t => { + const cwd = path.join(__dirname, 'fixtures/space'); + const trueResult = await t.throwsAsync(main(['--space=true'], {cwd})); + const falseResult = await t.throwsAsync(main(['--space=false'], {cwd})); + t.true(trueResult.stdout.includes('Expected indentation of 2 spaces')); + t.true(falseResult.stdout.includes('Expected indentation of 1 tab')); +}); + +test('extension option', async t => { + const cwd = path.join(__dirname, 'fixtures/custom-extension'); + const {stdout} = await t.throwsAsync(main(['--reporter=json', '--extension=unknown'], {cwd})); + const reports = JSON.parse(stdout); + + t.is(reports.length, 1); + t.true(reports[0].filePath.endsWith('.unknown')); +}); + +test('invalid print-config flag with stdin', async t => { + const error = await t.throwsAsync(() => + main(['--print-config', 'x.js', '--stdin'], {input: 'console.log()\n'}) + ); + t.is(error.stderr.trim(), 'The `--print-config` flag is not supported on stdin'); +}); + +test('print-config flag requires a single filename', async t => { + const error = await t.throwsAsync(() => + main(['--print-config', 'x.js', 'y.js']) + ); + t.is(error.stderr.trim(), 'The `--print-config` flag must be used with exactly one filename'); +}); diff --git a/test/cli.js b/test/cli.js index 7c5a579c..af2d4349 100644 --- a/test/cli.js +++ b/test/cli.js @@ -1,184 +1,15 @@ -import fs from 'fs'; import path from 'path'; import test from 'ava'; import execa from 'execa'; -import slash from 'slash'; -import tempWrite from 'temp-write'; import createEsmUtils from 'esm-utils'; -const {__dirname} = createEsmUtils(import.meta); -process.chdir(__dirname); +const {__dirname, require} = createEsmUtils(import.meta); -const main = (arguments_, options) => execa(path.join(__dirname, '../cli.js'), arguments_, options); +const cwd = path.dirname(__dirname); +const packageJson = require(path.join(cwd, 'package.json')); +const cli = (args, options) => execa(path.join(cwd, 'cli.js'), args, options); -test('fix option', async t => { - const filepath = await tempWrite('console.log()\n', 'x.js'); - await main(['--fix', filepath]); - t.is(fs.readFileSync(filepath, 'utf8').trim(), 'console.log();'); -}); - -test('fix option with stdin', async t => { - const {stdout} = await main(['--fix', '--stdin'], { - input: 'console.log()' - }); - t.is(stdout, 'console.log();'); -}); - -test('stdin-filename option with stdin', async t => { - const {stdout} = await main(['--stdin', '--stdin-filename=unicorn-file'], { - input: 'console.log()\n', - reject: false - }); - t.regex(stdout, /unicorn-file:/u); -}); - -test('reporter option', async t => { - const filepath = await tempWrite('console.log()\n', 'x.js'); - - const error = await t.throwsAsync(() => - main(['--reporter=compact', filepath]) - ); - t.true(error.stdout.includes('Error - ')); -}); - -test('overrides fixture', async t => { - const cwd = path.join(__dirname, 'fixtures/overrides'); - await t.notThrowsAsync(main([], {cwd})); -}); - -test('overrides work with relative path', async t => { - const cwd = path.join(__dirname, 'fixtures/overrides'); - const file = path.join('test', 'bar.js'); - await t.notThrowsAsync(main([file], {cwd})); -}); - -test('overrides work with relative path starting with `./`', async t => { - const cwd = path.join(__dirname, 'fixtures/overrides'); - const file = '.' + path.sep + path.join('test', 'bar.js'); - await t.notThrowsAsync(main([file], {cwd})); -}); - -test('overrides work with absolute path', async t => { - const cwd = path.join(__dirname, 'fixtures/overrides'); - const file = path.join(cwd, 'test', 'bar.js'); - await t.notThrowsAsync(main([file], {cwd})); -}); - -test.failing('override default ignore', async t => { - const cwd = path.join(__dirname, 'fixtures/ignores'); - await t.throwsAsync(main([], {cwd})); -}); - -test('ignore files in .gitignore', async t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); - const error = await t.throwsAsync(main(['--reporter=json'], {cwd})); - const reports = JSON.parse(error.stdout); - const files = reports - .map(report => path.relative(cwd, report.filePath)) - .map(report => slash(report)); - t.deepEqual(files.sort(), ['index.js', 'test/bar.js'].sort()); -}); - -test('ignore explicit files when in .gitgnore', async t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); - await t.notThrowsAsync(main(['test/foo.js', '--reporter=json'], {cwd})); -}); - -test('negative gitignores', async t => { - const cwd = path.join(__dirname, 'fixtures/negative-gitignore'); - const error = await t.throwsAsync(main(['--reporter=json'], {cwd})); - const reports = JSON.parse(error.stdout); - const files = reports.map(report => path.relative(cwd, report.filePath)); - t.deepEqual(files, ['foo.js']); -}); - -test('supports being extended with a shareable config', async t => { - const cwd = path.join(__dirname, 'fixtures/project'); - await t.notThrowsAsync(main([], {cwd})); -}); - -test('quiet option', async t => { - const filepath = await tempWrite('// TODO: quiet\nconsole.log()\n', 'x.js'); - const error = await t.throwsAsync(main(['--quiet', '--reporter=json', filepath])); - const [report] = JSON.parse(error.stdout); - t.is(report.warningCount, 0); -}); - -test('invalid node-engine option', async t => { - const filepath = await tempWrite('console.log()\n', 'x.js'); - const error = await t.throwsAsync(main(['--node-version', 'v', filepath])); - t.is(error.exitCode, 1); -}); - -test('cli option takes precedence over config', async t => { - const cwd = path.join(__dirname, 'fixtures/default-options'); - const input = 'console.log()\n'; - - // Use config from package.json - await t.notThrowsAsync(main(['--stdin'], {cwd, input})); - - // Override package.json config with cli flag - await t.throwsAsync(main(['--semicolon=true', '--stdin'], {cwd, input})); - - // Use XO default (`true`) even if option is not set in package.json nor cli arg - // i.e make sure absent cli flags are not parsed as `false` - await t.throwsAsync(main(['--stdin'], {input})); -}); - -test('space option with number value', async t => { - const cwd = path.join(__dirname, 'fixtures/space'); - const {stdout} = await t.throwsAsync(main(['--space=4', 'one-space.js'], {cwd})); - t.true(stdout.includes('Expected indentation of 4 spaces')); -}); - -test('space option as boolean', async t => { - const cwd = path.join(__dirname, 'fixtures/space'); - const {stdout} = await t.throwsAsync(main(['--space'], {cwd})); - t.true(stdout.includes('Expected indentation of 2 spaces')); -}); - -test('space option as boolean with filename', async t => { - const cwd = path.join(__dirname, 'fixtures/space'); - const {stdout} = await main(['--reporter=json', '--space', 'two-spaces.js'], { - cwd, - reject: false - }); - const reports = JSON.parse(stdout); - - // Only the specified file was checked (filename was not the value of `space`) - t.is(reports.length, 1); - - // The default space value of 2 was expected - t.is(reports[0].errorCount, 0); -}); - -test('space option with boolean strings', async t => { - const cwd = path.join(__dirname, 'fixtures/space'); - const trueResult = await t.throwsAsync(main(['--space=true'], {cwd})); - const falseResult = await t.throwsAsync(main(['--space=false'], {cwd})); - t.true(trueResult.stdout.includes('Expected indentation of 2 spaces')); - t.true(falseResult.stdout.includes('Expected indentation of 1 tab')); -}); - -test('extension option', async t => { - const cwd = path.join(__dirname, 'fixtures/custom-extension'); - const {stdout} = await t.throwsAsync(main(['--reporter=json', '--extension=unknown'], {cwd})); - const reports = JSON.parse(stdout); - - t.is(reports.length, 1); - t.true(reports[0].filePath.endsWith('.unknown')); -}); - -test('invalid print-config flag with stdin', async t => { - const error = await t.throwsAsync(() => - main(['--print-config', 'x.js', '--stdin'], {input: 'console.log()\n'}) - ); - t.is(error.stderr.trim(), 'The `--print-config` flag is not supported on stdin'); -}); - -test('print-config flag requires a single filename', async t => { - const error = await t.throwsAsync(() => - main(['--print-config', 'x.js', 'y.js']) - ); - t.is(error.stderr.trim(), 'The `--print-config` flag must be used with exactly one filename'); +test('runs no-local install of XO', async t => { + const {stdout} = await cli(['--no-local', '--version'], {cwd}); + t.is(stdout, packageJson.version); }); From 7d8e89e517d63a0c436b00833b454a97ef00c8a7 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Thu, 1 Jul 2021 16:48:45 +0800 Subject: [PATCH 2/7] Update `eslint-plugin-unicorn` --- package.json | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 711d4c7d..8c3e219e 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-promise": "^5.1.0", - "eslint-plugin-unicorn": "^32.0.0", + "eslint-plugin-unicorn": "^34.0.1", "esm-utils": "^1.1.0", "find-cache-dir": "^3.3.1", "find-up": "^5.0.0", @@ -109,11 +109,7 @@ "webpack": "^5.41.1" }, "eslintConfig": { - "extends": "eslint-config-xo", - "rules": { - "unicorn/prefer-module": "off", - "unicorn/prefer-node-protocol": "off" - } + "extends": "eslint-config-xo" }, "eslintIgnore": [ "test/fixtures" From 2e23b54fc26dcd73a78e6dd55bc8ed339bc65e1b Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Thu, 1 Jul 2021 16:50:36 +0800 Subject: [PATCH 3/7] Update deps --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8c3e219e..b9861fbd 100644 --- a/package.json +++ b/package.json @@ -59,15 +59,15 @@ "cosmiconfig": "^7.0.0", "debug": "^4.3.1", "define-lazy-prop": "^3.0.0", - "eslint": "^7.24.0", - "eslint-config-prettier": "^8.2.0", + "eslint": "^7.29.0", + "eslint-config-prettier": "^8.3.0", "eslint-config-xo": "^0.36.0", "eslint-config-xo-typescript": "^0.41.0", - "eslint-formatter-pretty": "^4.0.0", - "eslint-import-resolver-webpack": "^0.13.0", + "eslint-formatter-pretty": "^4.1.0", + "eslint-import-resolver-webpack": "^0.13.1", "eslint-plugin-ava": "^12.0.0", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.22.1", + "eslint-plugin-import": "^2.23.4", "eslint-plugin-no-use-extend-native": "^0.5.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^3.4.0", From 0b67cc6001f5ab1e8accc6ef5d4dbe6b6157a618 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Thu, 1 Jul 2021 16:52:02 +0800 Subject: [PATCH 4/7] Update `eslint-config-xo` --- cli.js | 40 ++++----- index.js | 34 ++++---- lib/constants.js | 54 ++++++------ lib/open-report.js | 2 +- lib/options-manager.js | 68 +++++++-------- package.json | 4 +- test/cli.js | 183 ++++++++++++++++++++++++++++++++++++++-- test/lint-files.js | 60 ++++++------- test/lint-text.js | 40 ++++----- test/open-report.js | 26 +++--- test/options-manager.js | 144 +++++++++++++++---------------- 11 files changed, 412 insertions(+), 243 deletions(-) diff --git a/cli.js b/cli.js index 626ab128..cac9446f 100755 --- a/cli.js +++ b/cli.js @@ -50,66 +50,66 @@ const cli = meow(` booleanDefault: undefined, flags: { fix: { - type: 'boolean' + type: 'boolean', }, reporter: { - type: 'string' + type: 'string', }, env: { type: 'string', - isMultiple: true + isMultiple: true, }, global: { type: 'string', - isMultiple: true + isMultiple: true, }, ignore: { type: 'string', - isMultiple: true + isMultiple: true, }, space: { - type: 'string' + type: 'string', }, semicolon: { - type: 'boolean' + type: 'boolean', }, prettier: { - type: 'boolean' + type: 'boolean', }, nodeVersion: { - type: 'string' + type: 'string', }, plugin: { type: 'string', - isMultiple: true + isMultiple: true, }, extend: { type: 'string', - isMultiple: true + isMultiple: true, }, open: { - type: 'boolean' + type: 'boolean', }, quiet: { - type: 'boolean' + type: 'boolean', }, extension: { type: 'string', - isMultiple: true + isMultiple: true, }, cwd: { - type: 'string' + type: 'string', }, printConfig: { - type: 'string' + type: 'string', }, stdin: { - type: 'boolean' + type: 'boolean', }, stdinFilename: { - type: 'string' - } - } + type: 'string', + }, + }, }); const {input, flags: options, showVersion} = cli; diff --git a/index.js b/index.js index 9325cfde..ce203bf3 100644 --- a/index.js +++ b/index.js @@ -16,7 +16,7 @@ import { mergeWithFileConfig, mergeWithFileConfigs, buildConfig, - mergeOptions + mergeOptions, } from './lib/options-manager.js'; /** Merge multiple reports into a single report */ @@ -24,7 +24,7 @@ const mergeReports = reports => { const report = { results: [], errorCount: 0, - warningCount: 0 + warningCount: 0, }; for (const currentReport of reports) { @@ -41,7 +41,7 @@ const getReportStatistics = results => { errorCount: 0, warningCount: 0, fixableErrorCount: 0, - fixableWarningCount: 0 + fixableWarningCount: 0, }; for (const result of results) { @@ -61,7 +61,7 @@ const processReport = (report, {isQuiet = false} = {}) => { const result = { results: report, - ...getReportStatistics(report) + ...getReportStatistics(report), }; defineLazyProperty(result, 'usedDeprecatedRules', () => { @@ -95,7 +95,7 @@ const runEslint = async (paths, options, processorOptions) => { const globFiles = async (patterns, {ignores, extensions, cwd}) => ( await globby( patterns.length === 0 ? [`**/*.{${extensions.join(',')}}`] : arrify(patterns), - {ignore: ignores, gitignore: true, cwd} + {ignore: ignores, gitignore: true, cwd}, )).filter(file => extensions.includes(path.extname(file).slice(1))).map(file => path.resolve(cwd, file)); const getConfig = async options => { @@ -120,9 +120,9 @@ const lintText = async (string, inputOptions = {}) => { const filename = path.relative(options.cwd, filePath); if ( - micromatch.isMatch(filename, options.baseConfig.ignorePatterns) || - globby.gitignore.sync({cwd: options.cwd, ignore: options.baseConfig.ignorePatterns})(filePath) || - await engine.isPathIgnored(filePath) + micromatch.isMatch(filename, options.baseConfig.ignorePatterns) + || globby.gitignore.sync({cwd: options.cwd, ignore: options.baseConfig.ignorePatterns})(filePath) + || await engine.isPathIgnored(filePath) ) { return { errorCount: 0, @@ -131,8 +131,8 @@ const lintText = async (string, inputOptions = {}) => { errorCount: 0, filePath: filename, messages: [], - warningCount: 0 - }] + warningCount: 0, + }], }; } } @@ -149,17 +149,17 @@ const lintFiles = async (patterns, inputOptions = {}) => { const configFiles = (await Promise.all( (await globby( CONFIG_FILES.map(configFile => `**/${configFile}`), - {ignore: DEFAULT_IGNORES, gitignore: true, cwd: inputOptions.cwd} - )).map(async configFile => configExplorer.load(path.resolve(inputOptions.cwd, configFile))) + {ignore: DEFAULT_IGNORES, gitignore: true, cwd: inputOptions.cwd}, + )).map(async configFile => configExplorer.load(path.resolve(inputOptions.cwd, configFile))), )).filter(Boolean); - const paths = configFiles.length > 0 ? - await pReduce( + const paths = configFiles.length > 0 + ? await pReduce( configFiles, async (paths, {filepath, config}) => [...paths, ...(await globFiles(patterns, {...mergeOptions(inputOptions, config), cwd: path.dirname(filepath)}))], - []) : - await globFiles(patterns, mergeOptions(inputOptions)); + []) + : await globFiles(patterns, mergeOptions(inputOptions)); return mergeReports(await pMap(await mergeWithFileConfigs([...new Set(paths)], inputOptions, configFiles), async ({files, options, prettierOptions}) => runEslint(files, buildConfig(options, prettierOptions), {isQuiet: options.quiet}))); }; @@ -175,5 +175,5 @@ export default { outputFixes: async ({results}) => ESLint.outputFixes(results), getConfig, lintText, - lintFiles + lintFiles, }; diff --git a/lib/constants.js b/lib/constants.js index a4f8a2b3..347082f9 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -7,7 +7,7 @@ const DEFAULT_IGNORES = [ '**/*.min.js', 'vendor/**', 'dist/**', - 'tap-snapshots/*.{cjs,js}' + 'tap-snapshots/*.{cjs,js}', ]; /** @@ -18,12 +18,12 @@ const MERGE_OPTIONS_CONCAT = [ 'extends', 'envs', 'globals', - 'plugins' + 'plugins', ]; const TYPESCRIPT_EXTENSION = [ 'ts', - 'tsx' + 'tsx', ]; const DEFAULT_EXTENSION = [ @@ -31,7 +31,7 @@ const DEFAULT_EXTENSION = [ 'jsx', 'mjs', 'cjs', - ...TYPESCRIPT_EXTENSION + ...TYPESCRIPT_EXTENSION, ]; /** @@ -60,57 +60,57 @@ With `engines.node` set to `>=8` the rule `plugin/rule` will be used with the co */ const ENGINE_RULES = { 'unicorn/prefer-spread': { - '5.0.0': 'off' + '5.0.0': 'off', }, 'unicorn/no-new-buffer': { - '5.10.0': 'off' + '5.10.0': 'off', }, 'prefer-rest-params': { - '6.0.0': 'off' + '6.0.0': 'off', }, 'prefer-destructuring': { - '6.0.0': 'off' + '6.0.0': 'off', }, 'promise/prefer-await-to-then': { - '7.6.0': 'off' + '7.6.0': 'off', }, 'prefer-object-spread': { - '8.3.0': 'off' + '8.3.0': 'off', }, 'node/prefer-global/url-search-params': { - '10.0.0': 'off' + '10.0.0': 'off', }, 'node/prefer-global/url': { - '10.0.0': 'off' + '10.0.0': 'off', }, 'no-useless-catch': { - '10.0.0': 'off' + '10.0.0': 'off', }, 'prefer-named-capture-group': { - '10.0.0': 'off' + '10.0.0': 'off', }, 'node/prefer-global/text-encoder': { - '11.0.0': 'off' + '11.0.0': 'off', }, 'node/prefer-global/text-decoder': { - '11.0.0': 'off' + '11.0.0': 'off', }, 'unicorn/prefer-flat-map': { - '11.0.0': 'off' + '11.0.0': 'off', }, 'node/prefer-promises/dns': { - '11.14.0': 'off' + '11.14.0': 'off', }, 'node/prefer-promises/fs': { - '11.14.0': 'off' - } + '11.14.0': 'off', + }, }; const PRETTIER_CONFIG_OVERRIDE = { 'eslint-plugin-babel': 'prettier/babel', 'eslint-plugin-flowtype': 'prettier/flowtype', 'eslint-plugin-standard': 'prettier/standard', - 'eslint-plugin-vue': 'prettier/vue' + 'eslint-plugin-vue': 'prettier/vue', }; const MODULE_NAME = 'xo'; @@ -122,10 +122,10 @@ const CONFIG_FILES = [ `.${MODULE_NAME}-config.js`, `.${MODULE_NAME}-config.cjs`, `${MODULE_NAME}.config.js`, - `${MODULE_NAME}.config.cjs` + `${MODULE_NAME}.config.cjs`, ]; -const TSCONFIG_DEFAULTS = { +const TSCONFIG_DEFFAULTS = { compilerOptions: { target: 'es2018', newLine: 'lf', @@ -133,8 +133,8 @@ const TSCONFIG_DEFAULTS = { noImplicitReturns: true, noUnusedLocals: true, noUnusedParameters: true, - noFallthroughCasesInSwitch: true - } + noFallthroughCasesInSwitch: true, + }, }; const CACHE_DIR_NAME = 'xo-linter'; @@ -148,6 +148,6 @@ export { MODULE_NAME, CONFIG_FILES, MERGE_OPTIONS_CONCAT, - TSCONFIG_DEFAULTS, - CACHE_DIR_NAME + TSCONFIG_DEFFAULTS, + CACHE_DIR_NAME, }; diff --git a/lib/open-report.js b/lib/open-report.js index c742cbf8..6ce16cb6 100644 --- a/lib/open-report.js +++ b/lib/open-report.js @@ -28,7 +28,7 @@ const resultToFile = result => { return { file: result.filePath, line: message.line, - column: message.column + column: message.column, }; }; diff --git a/lib/options-manager.js b/lib/options-manager.js index 0b409f0b..c77e301d 100644 --- a/lib/options-manager.js +++ b/lib/options-manager.js @@ -29,7 +29,7 @@ import { CONFIG_FILES, MERGE_OPTIONS_CONCAT, TSCONFIG_DEFFAULTS, - CACHE_DIR_NAME + CACHE_DIR_NAME, } from './constants.js'; const {__dirname, json, require} = createEsmUtils(import.meta); @@ -60,9 +60,9 @@ const DEFAULT_CONFIG = { extends: [ resolveLocalConfig('xo'), path.join(__dirname, '../config/overrides.cjs'), - path.join(__dirname, '../config/plugins.cjs') - ] - } + path.join(__dirname, '../config/plugins.cjs'), + ], + }, }; /** @@ -76,8 +76,8 @@ const getEmptyConfig = () => ({ ignorePatterns: [], env: {}, plugins: [], - extends: [] - } + extends: [], + }, }); const getEmptyXOConfig = () => ({ @@ -86,7 +86,7 @@ const getEmptyXOConfig = () => ({ globals: [], envs: [], plugins: [], - extends: [] + extends: [], }); const mergeFn = (previousValue, value, key) => { @@ -178,7 +178,7 @@ const mergeWithFileConfigs = async (files, options, configFiles) => { configs.set(cacheKey, { files: [file, ...(cachedGroup ? cachedGroup.files : [])], options: cachedGroup ? cachedGroup.options : fileOptions, - prettierOptions + prettierOptions, }); return configs; @@ -194,7 +194,7 @@ const mergeWithFileConfigs = async (files, options, configFiles) => { } return outputJson(cachePath, makeTSConfig(tsConfigs[tsConfigPath], tsConfigPath, files)); - } + }, )); return groups; @@ -208,7 +208,7 @@ Hashing based on https://github.com/eslint/eslint/blob/cf38d0d939b62f3670cdd59f0 */ const getTsConfigCachePath = (files, tsConfigPath) => path.join( cacheLocation, - `tsconfig.${murmur(`${pkg.version}_${nodeVersion}_${stringify({files: files.sort(), tsConfigPath})}`).result().toString(36)}.json` + `tsconfig.${murmur(`${pkg.version}_${nodeVersion}_${stringify({files: files.sort(), tsConfigPath})}`).result().toString(36)}.json`, ); const makeTSConfig = (tsConfig, tsConfigPath, files) => { @@ -236,7 +236,7 @@ const normalizeOptions = options => { 'rule', 'setting', 'extend', - 'extension' + 'extension', ]; for (const singular of aliases) { @@ -268,7 +268,7 @@ const mergeOptions = (options, xoOptions = {}, enginesOptions = {}) => { const mergedOptions = normalizeOptions({ ...xoOptions, ...(enginesOptions && enginesOptions.node && semver.validRange(enginesOptions.node) ? {nodeVersion: enginesOptions.node} : {}), - ...options + ...options, }); mergedOptions.extensions = DEFAULT_EXTENSION.concat(mergedOptions.extensions || []); @@ -295,7 +295,7 @@ const buildConfig = (options, prettierOptions) => { buildXOConfig(options), buildTSConfig(options), buildExtendsConfig(options), - buildPrettierConfig(options, prettierOptions) + buildPrettierConfig(options, prettierOptions), )(mergeWith(getEmptyConfig(), DEFAULT_CONFIG, mergeFn)); }; @@ -305,7 +305,7 @@ const buildESLintConfig = options => config => { if (options.rules) { config.baseConfig.rules = { ...config.baseConfig.rules, - ...options.rules + ...options.rules, }; } @@ -322,41 +322,41 @@ const buildESLintConfig = options => config => { if (options.envs) { config.baseConfig.env = { ...config.baseConfig.env, - ...toValueMap(options.envs) + ...toValueMap(options.envs), }; } if (options.globals) { config.baseConfig.globals = { ...config.baseConfig.globals, - ...toValueMap(options.globals, 'readonly') + ...toValueMap(options.globals, 'readonly'), }; } if (options.plugins) { config.baseConfig.plugins = [ ...config.baseConfig.plugins, - ...options.plugins + ...options.plugins, ]; } if (options.ignores) { config.baseConfig.ignorePatterns = [ ...config.baseConfig.ignorePatterns, - ...options.ignores + ...options.ignores, ]; } if (options.parserOptions) { config.baseConfig.parserOptions = { ...config.baseConfig.parserOptions, - ...options.parserOptions + ...options.parserOptions, }; } return { ...config, - ...pick(options, ['cwd', 'filePath', 'fix']) + ...pick(options, ['cwd', 'filePath', 'fix']), }; }; @@ -401,7 +401,7 @@ const buildXOConfig = options => config => { config.baseConfig.rules['semi-spacing'] = ['error', { before: false, - after: true + after: true, }]; } @@ -473,13 +473,13 @@ const buildPrettierConfig = (options, prettierConfig) => config => { }; const mergeWithPrettierConfig = (options, prettierOptions) => { - if ((options.semicolon === true && prettierOptions.semi === false) || - (options.semicolon === false && prettierOptions.semi === true)) { + if ((options.semicolon === true && prettierOptions.semi === false) + || (options.semicolon === false && prettierOptions.semi === true)) { throw new Error(`The Prettier config \`semi\` is ${prettierOptions.semi} while XO \`semicolon\` is ${options.semicolon}`); } - if (((options.space === true || typeof options.space === 'number') && prettierOptions.useTabs === true) || - ((options.space === false) && prettierOptions.useTabs === false)) { + if (((options.space === true || typeof options.space === 'number') && prettierOptions.useTabs === true) + || ((options.space === false) && prettierOptions.useTabs === false)) { throw new Error(`The Prettier config \`useTabs\` is ${prettierOptions.useTabs} while XO \`space\` is ${options.space}`); } @@ -496,10 +496,10 @@ const mergeWithPrettierConfig = (options, prettierOptions) => { trailingComma: 'none', tabWidth: normalizeSpaces(options), useTabs: !options.space, - semi: options.semicolon !== false + semi: options.semicolon !== false, }, prettierOptions, - mergeFn + mergeFn, ); }; @@ -513,9 +513,9 @@ const buildTSConfig = options => config => { ecmaFeatures: {jsx: true}, project: options.tsConfigPath, projectFolderIgnoreList: - options.parserOptions && options.parserOptions.projectFolderIgnoreList ? - options.parserOptions.projectFolderIgnoreList : - [new RegExp(`/node_modules/(?!.*\\.cache/${CACHE_DIR_NAME})`)] + options.parserOptions && options.parserOptions.projectFolderIgnoreList + ? options.parserOptions.projectFolderIgnoreList + : [new RegExp(`/node_modules/(?!.*\\.cache/${CACHE_DIR_NAME})`)], }; delete config.tsConfigPath; @@ -561,7 +561,7 @@ const findApplicableOverrides = (path, overrides) => { return { hash, - applicable + applicable, }; }; @@ -596,8 +596,8 @@ const gatherImportResolvers = options => { ...resolvers, webpack: { ...resolvers.webpack, - ...webpackResolverSettings - } + ...webpackResolverSettings, + }, }; } @@ -613,5 +613,5 @@ export { mergeWithFileConfig, buildConfig, applyOverrides, - mergeOptions + mergeOptions, }; diff --git a/package.json b/package.json index b9861fbd..a5d604e5 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,8 @@ "define-lazy-prop": "^3.0.0", "eslint": "^7.29.0", "eslint-config-prettier": "^8.3.0", - "eslint-config-xo": "^0.36.0", - "eslint-config-xo-typescript": "^0.41.0", + "eslint-config-xo": "^0.37.0", + "eslint-config-xo-typescript": "^0.42.0", "eslint-formatter-pretty": "^4.1.0", "eslint-import-resolver-webpack": "^0.13.1", "eslint-plugin-ava": "^12.0.0", diff --git a/test/cli.js b/test/cli.js index af2d4349..cfee0a66 100644 --- a/test/cli.js +++ b/test/cli.js @@ -1,15 +1,184 @@ +import fs from 'fs'; import path from 'path'; import test from 'ava'; import execa from 'execa'; +import slash from 'slash'; +import tempWrite from 'temp-write'; import createEsmUtils from 'esm-utils'; -const {__dirname, require} = createEsmUtils(import.meta); +const {__dirname} = createEsmUtils(import.meta); +process.chdir(__dirname); -const cwd = path.dirname(__dirname); -const packageJson = require(path.join(cwd, 'package.json')); -const cli = (args, options) => execa(path.join(cwd, 'cli.js'), args, options); +const main = (arguments_, options) => execa(path.join(__dirname, '../cli.js'), arguments_, options); -test('runs no-local install of XO', async t => { - const {stdout} = await cli(['--no-local', '--version'], {cwd}); - t.is(stdout, packageJson.version); +test('fix option', async t => { + const filepath = await tempWrite('console.log()\n', 'x.js'); + await main(['--fix', filepath]); + t.is(fs.readFileSync(filepath, 'utf8').trim(), 'console.log();'); +}); + +test('fix option with stdin', async t => { + const {stdout} = await main(['--fix', '--stdin'], { + input: 'console.log()', + }); + t.is(stdout, 'console.log();'); +}); + +test('stdin-filename option with stdin', async t => { + const {stdout} = await main(['--stdin', '--stdin-filename=unicorn-file'], { + input: 'console.log()\n', + reject: false, + }); + t.regex(stdout, /unicorn-file:/u); +}); + +test('reporter option', async t => { + const filepath = await tempWrite('console.log()\n', 'x.js'); + + const error = await t.throwsAsync(() => + main(['--reporter=compact', filepath]), + ); + t.true(error.stdout.includes('Error - ')); +}); + +test('overrides fixture', async t => { + const cwd = path.join(__dirname, 'fixtures/overrides'); + await t.notThrowsAsync(main([], {cwd})); +}); + +test('overrides work with relative path', async t => { + const cwd = path.join(__dirname, 'fixtures/overrides'); + const file = path.join('test', 'bar.js'); + await t.notThrowsAsync(main([file], {cwd})); +}); + +test('overrides work with relative path starting with `./`', async t => { + const cwd = path.join(__dirname, 'fixtures/overrides'); + const file = '.' + path.sep + path.join('test', 'bar.js'); + await t.notThrowsAsync(main([file], {cwd})); +}); + +test('overrides work with absolute path', async t => { + const cwd = path.join(__dirname, 'fixtures/overrides'); + const file = path.join(cwd, 'test', 'bar.js'); + await t.notThrowsAsync(main([file], {cwd})); +}); + +test.failing('override default ignore', async t => { + const cwd = path.join(__dirname, 'fixtures/ignores'); + await t.throwsAsync(main([], {cwd})); +}); + +test('ignore files in .gitignore', async t => { + const cwd = path.join(__dirname, 'fixtures/gitignore'); + const error = await t.throwsAsync(main(['--reporter=json'], {cwd})); + const reports = JSON.parse(error.stdout); + const files = reports + .map(report => path.relative(cwd, report.filePath)) + .map(report => slash(report)); + t.deepEqual(files.sort(), ['index.js', 'test/bar.js'].sort()); +}); + +test('ignore explicit files when in .gitgnore', async t => { + const cwd = path.join(__dirname, 'fixtures/gitignore'); + await t.notThrowsAsync(main(['test/foo.js', '--reporter=json'], {cwd})); +}); + +test('negative gitignores', async t => { + const cwd = path.join(__dirname, 'fixtures/negative-gitignore'); + const error = await t.throwsAsync(main(['--reporter=json'], {cwd})); + const reports = JSON.parse(error.stdout); + const files = reports.map(report => path.relative(cwd, report.filePath)); + t.deepEqual(files, ['foo.js']); +}); + +test('supports being extended with a shareable config', async t => { + const cwd = path.join(__dirname, 'fixtures/project'); + await t.notThrowsAsync(main([], {cwd})); +}); + +test('quiet option', async t => { + const filepath = await tempWrite('// TODO: quiet\nconsole.log()\n', 'x.js'); + const error = await t.throwsAsync(main(['--quiet', '--reporter=json', filepath])); + const [report] = JSON.parse(error.stdout); + t.is(report.warningCount, 0); +}); + +test('invalid node-engine option', async t => { + const filepath = await tempWrite('console.log()\n', 'x.js'); + const error = await t.throwsAsync(main(['--node-version', 'v', filepath])); + t.is(error.exitCode, 1); +}); + +test('cli option takes precedence over config', async t => { + const cwd = path.join(__dirname, 'fixtures/default-options'); + const input = 'console.log()\n'; + + // Use config from package.json + await t.notThrowsAsync(main(['--stdin'], {cwd, input})); + + // Override package.json config with cli flag + await t.throwsAsync(main(['--semicolon=true', '--stdin'], {cwd, input})); + + // Use XO default (`true`) even if option is not set in package.json nor cli arg + // i.e make sure absent cli flags are not parsed as `false` + await t.throwsAsync(main(['--stdin'], {input})); +}); + +test('space option with number value', async t => { + const cwd = path.join(__dirname, 'fixtures/space'); + const {stdout} = await t.throwsAsync(main(['--space=4', 'one-space.js'], {cwd})); + t.true(stdout.includes('Expected indentation of 4 spaces')); +}); + +test('space option as boolean', async t => { + const cwd = path.join(__dirname, 'fixtures/space'); + const {stdout} = await t.throwsAsync(main(['--space'], {cwd})); + t.true(stdout.includes('Expected indentation of 2 spaces')); +}); + +test('space option as boolean with filename', async t => { + const cwd = path.join(__dirname, 'fixtures/space'); + const {stdout} = await main(['--reporter=json', '--space', 'two-spaces.js'], { + cwd, + reject: false, + }); + const reports = JSON.parse(stdout); + + // Only the specified file was checked (filename was not the value of `space`) + t.is(reports.length, 1); + + // The default space value of 2 was expected + t.is(reports[0].errorCount, 0); +}); + +test('space option with boolean strings', async t => { + const cwd = path.join(__dirname, 'fixtures/space'); + const trueResult = await t.throwsAsync(main(['--space=true'], {cwd})); + const falseResult = await t.throwsAsync(main(['--space=false'], {cwd})); + t.true(trueResult.stdout.includes('Expected indentation of 2 spaces')); + t.true(falseResult.stdout.includes('Expected indentation of 1 tab')); +}); + +test('extension option', async t => { + const cwd = path.join(__dirname, 'fixtures/custom-extension'); + const {stdout} = await t.throwsAsync(main(['--reporter=json', '--extension=unknown'], {cwd})); + const reports = JSON.parse(stdout); + + t.is(reports.length, 1); + t.true(reports[0].filePath.endsWith('.unknown')); +}); + +test('invalid print-config flag with stdin', async t => { + const error = await t.throwsAsync(() => + main(['--print-config', 'x.js', '--stdin'], {input: 'console.log()\n'}), + ); + t.is(error.stderr.trim(), 'The `--print-config` flag is not supported on stdin'); +}); + +test('print-config flag requires a single filename', async t => { + const error = await t.throwsAsync(() => + main(['--print-config', 'x.js', 'y.js']), + ); + t.is(error.stderr.trim(), 'The `--print-config` flag must be used with exactly one filename'); }); diff --git a/test/lint-files.js b/test/lint-files.js index 1d0c4446..4cceaf48 100644 --- a/test/lint-files.js +++ b/test/lint-files.js @@ -110,16 +110,16 @@ test('enable rules based on nodeVersion', async t => { hasRule( results, path.resolve('fixtures/engines-overrides/promise-then-transpile.js'), - 'promise/prefer-await-to-then' - ) + 'promise/prefer-await-to-then', + ), ); // The non transpiled files can use `.then` t.false( hasRule( results, path.resolve('fixtures/engines-overrides/promise-then.js'), - 'promise/prefer-await-to-then' - ) + 'promise/prefer-await-to-then', + ), ); }); @@ -141,32 +141,32 @@ test('find configurations close to linted file', async t => { hasRule( results, path.resolve('fixtures/nested-configs/child/semicolon.js'), - 'semi' - ) + 'semi', + ), ); t.true( hasRule( results, path.resolve('fixtures/nested-configs/child-override/child-prettier-override/semicolon.js'), - 'prettier/prettier' - ) + 'prettier/prettier', + ), ); t.true( hasRule( results, path.resolve('fixtures/nested-configs/no-semicolon.js'), - 'semi' - ) + 'semi', + ), ); t.true( hasRule( results, path.resolve('fixtures/nested-configs/child-override/two-spaces.js'), - 'indent' - ) + 'indent', + ), ); }); @@ -177,24 +177,24 @@ test('typescript files', async t => { hasRule( results, path.resolve('fixtures/typescript/two-spaces.tsx'), - '@typescript-eslint/indent' - ) + '@typescript-eslint/indent', + ), ); t.true( hasRule( results, path.resolve('fixtures/typescript/child/extra-semicolon.ts'), - '@typescript-eslint/no-extra-semi' - ) + '@typescript-eslint/no-extra-semi', + ), ); t.true( hasRule( results, path.resolve('fixtures/typescript/child/sub-child/four-spaces.ts'), - '@typescript-eslint/indent' - ) + '@typescript-eslint/indent', + ), ); }); @@ -218,8 +218,8 @@ test('webpack import resolver is used if webpack.config.js is found', async t => const {results} = await xo.lintFiles(path.resolve(cwd, 'file1.js'), { cwd, rules: { - 'import/no-unresolved': 2 - } + 'import/no-unresolved': 2, + }, }); t.is(results[0].errorCount, 1, JSON.stringify(results[0].messages)); @@ -237,14 +237,14 @@ test('webpack import resolver config can be passed through webpack option', asyn config: { resolve: { alias: { - file2alias: path.resolve(__dirname, cwd, './file2.js') - } - } - } + file2alias: path.resolve(__dirname, cwd, './file2.js'), + }, + }, + }, }, rules: { - 'import/no-unresolved': 2 - } + 'import/no-unresolved': 2, + }, }); t.is(results[0].errorCount, 1, JSON.stringify(results[0].messages)); @@ -258,8 +258,8 @@ test('webpack import resolver is used if {webpack: true}', async t => { webpack: true, rules: { 'import/no-unresolved': 2, - 'import/no-webpack-loader-syntax': 0 - } + 'import/no-webpack-loader-syntax': 0, + }, }); t.is(results[0].errorCount, 0, JSON.stringify(results[0])); @@ -272,8 +272,8 @@ async function configType(t, {dir}) { hasRule( results, path.resolve('fixtures', 'config-files', dir, 'file.js'), - 'indent' - ) + 'indent', + ), ); } diff --git a/test/lint-text.js b/test/lint-text.js index cec122cb..d993cecc 100644 --- a/test/lint-text.js +++ b/test/lint-text.js @@ -16,7 +16,7 @@ test('.lintText()', async t => { test('default `ignores`', async t => { const result = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n', { - filePath: 'node_modules/ignored/index.js' + filePath: 'node_modules/ignored/index.js', }); t.is(result.errorCount, 0); t.is(result.warningCount, 0); @@ -25,7 +25,7 @@ test('default `ignores`', async t => { test('`ignores` option', async t => { const result = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n', { filePath: 'ignored/index.js', - ignores: ['ignored/**/*.js'] + ignores: ['ignored/**/*.js'], }); t.is(result.errorCount, 0); t.is(result.warningCount, 0); @@ -34,7 +34,7 @@ test('`ignores` option', async t => { test('`ignores` option without cwd', async t => { const result = await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n', { filePath: 'ignored/index.js', - ignores: ['ignored/**/*.js'] + ignores: ['ignored/**/*.js'], }); t.is(result.errorCount, 0); t.is(result.warningCount, 0); @@ -47,13 +47,13 @@ test('respect overrides', async t => { overrides: [ { files: ['ignored/**/*.js'], - ignores: [] - } + ignores: [], + }, ], rules: { 'unicorn/prefer-module': 'off', - 'unicorn/prefer-node-protocol': 'off' - } + 'unicorn/prefer-node-protocol': 'off', + }, }); t.is(result.errorCount, 1); t.is(result.warningCount, 0); @@ -65,9 +65,9 @@ test('overriden ignore', async t => { overrides: [ { files: ['unignored.js'], - ignores: ['unignored.js'] - } - ] + ignores: ['unignored.js'], + }, + ], }); t.is(result.errorCount, 0); t.is(result.warningCount, 0); @@ -76,7 +76,7 @@ test('overriden ignore', async t => { test('`ignores` option without filename', async t => { await t.throwsAsync(async () => { await xo.lintText('\'use strict\'\nconsole.log(\'unicorn\');\n', { - ignores: ['ignored/**/*.js'] + ignores: ['ignored/**/*.js'], }); }, {message: /The `ignores` option requires the `filePath` option to be defined./u}); }); @@ -89,7 +89,7 @@ test('JSX support', async t => { test('plugin support', async t => { const {results} = await xo.lintText('var React;\nReact.render();\n', { plugins: ['react'], - rules: {'react/jsx-no-undef': 'error'} + rules: {'react/jsx-no-undef': 'error'}, }); t.true(hasRule(results, 'react/jsx-no-undef')); }); @@ -101,7 +101,7 @@ test('prevent use of extended native objects', async t => { test('extends support', async t => { const {results} = await xo.lintText('var React;\nReact.render();\n', { - extends: 'xo-react' + extends: 'xo-react', }); t.true(hasRule(results, 'react/jsx-no-undef')); }); @@ -130,7 +130,7 @@ test('extends `react` support with `prettier` option', async t => { test('regression test for #71', async t => { const {results} = await xo.lintText('const foo = { key: \'value\' };\nconsole.log(foo);\n', { - extends: path.join(__dirname, 'fixtures/extends.js') + extends: path.join(__dirname, 'fixtures/extends.js'), }); t.is(results[0].errorCount, 0); }); @@ -227,9 +227,9 @@ test('enable rules based on nodeVersion in override', async t => { overrides: [ { files: 'promise-*.js', - nodeVersion: '>=6.0.0' - } - ] + nodeVersion: '>=6.0.0', + }, + ], }); t.false(hasRule(results, 'promise/prefer-await-to-then')); @@ -239,9 +239,9 @@ test('enable rules based on nodeVersion in override', async t => { overrides: [ { files: 'promise-*.js', - nodeVersion: '>=8.0.0' - } - ] + nodeVersion: '>=8.0.0', + }, + ], })); t.true(hasRule(results, 'promise/prefer-await-to-then')); }); diff --git a/test/open-report.js b/test/open-report.js index f8e1688d..02c57a2d 100644 --- a/test/open-report.js +++ b/test/open-report.js @@ -16,7 +16,7 @@ test.skip('opens nothing when there are no errors nor warnings', async t => { if (files.length !== 0) { t.fail(); } - } + }, }); openReport(results); @@ -31,24 +31,24 @@ test.skip('only opens errors if there are errors and warnings', async t => { { file: path.join(__dirname, 'fixtures/open-report/errors/one.js'), line: 1, - column: 7 + column: 7, }, { file: path.join(__dirname, 'fixtures/open-report/errors/two-with-warnings.js'), line: 1, - column: 1 + column: 1, }, { file: path.join(__dirname, 'fixtures/open-report/errors/three.js'), line: 1, - column: 7 - } + column: 7, + }, ]; const openReport = proxyquire('../lib/open-report', { 'open-editor': files => { t.deepEqual(files, expected); - } + }, }); openReport(results); }); @@ -61,12 +61,12 @@ test.skip('if a file has errors and warnings, it opens the first error', async t { file: path.join(__dirname, 'fixtures/open-report/errors/two-with-warnings.js'), line: 1, - column: 1 - } + column: 1, + }, ]; const openReport = proxyquire('../lib/open-report', { - 'open-editor': files => t.deepEqual(files, expected) + 'open-editor': files => t.deepEqual(files, expected), }); openReport(results); }); @@ -79,17 +79,17 @@ test.skip('only opens warnings if there are no errors', async t => { { file: path.join(__dirname, 'fixtures/open-report/warnings/one.js'), line: 1, - column: 1 + column: 1, }, { file: path.join(__dirname, 'fixtures/open-report/warnings/three.js'), line: 1, - column: 1 - } + column: 1, + }, ]; const openReport = proxyquire('../lib/open-report', { - 'open-editor': files => t.deepEqual(files, expected) + 'open-editor': files => t.deepEqual(files, expected), }); openReport(results); }); diff --git a/test/options-manager.js b/test/options-manager.js index 2d3f8fd0..f8e38ddf 100644 --- a/test/options-manager.js +++ b/test/options-manager.js @@ -25,34 +25,34 @@ test('normalizeOptions: makes all the options plural and arrays', t => { rule: {'my-rule': 'foo'}, setting: {'my-rule': 'bar'}, extend: 'foo', - extension: 'html' + extension: 'html', }); t.deepEqual(options, { envs: [ - 'node' + 'node', ], extends: [ - 'foo' + 'foo', ], extensions: [ - 'html' + 'html', ], globals: [ - 'foo' + 'foo', ], ignores: [ - 'test.js' + 'test.js', ], plugins: [ - 'my-plugin' + 'my-plugin', ], rules: { - 'my-rule': 'foo' + 'my-rule': 'foo', }, settings: { - 'my-rule': 'bar' - } + 'my-rule': 'bar', + }, }); }); @@ -97,7 +97,7 @@ test('buildConfig: prettier: true', t => { semi: true, singleQuote: true, tabWidth: 2, - trailingComma: 'none' + trailingComma: 'none', }]); // eslint-prettier-config must always be last t.is(config.baseConfig.extends[config.baseConfig.extends.length - 1], 'prettier'); @@ -122,7 +122,7 @@ test('buildConfig: prettier: true, typescript file', t => { semi: true, singleQuote: true, tabWidth: 2, - trailingComma: 'none' + trailingComma: 'none', }]); // eslint-prettier-config must always be last @@ -148,7 +148,7 @@ test('buildConfig: prettier: true, semicolon: false', t => { semi: false, singleQuote: true, tabWidth: 2, - trailingComma: 'none' + trailingComma: 'none', }]); // Indent rule is not enabled t.is(config.baseConfig.rules.indent, undefined); @@ -169,7 +169,7 @@ test('buildConfig: prettier: true, space: 4', t => { semi: true, singleQuote: true, tabWidth: 4, - trailingComma: 'none' + trailingComma: 'none', }]); // Indent rule is not enabled t.is(config.baseConfig.rules.indent, undefined); @@ -190,7 +190,7 @@ test('buildConfig: prettier: true, space: true', t => { semi: true, singleQuote: true, tabWidth: 2, - trailingComma: 'none' + trailingComma: 'none', }]); // Indent rule is not enabled t.is(config.baseConfig.rules.indent, undefined); @@ -252,7 +252,7 @@ test('buildConfig: nodeVersion: >=6', t => { t.deepEqual(config.baseConfig.rules['node/no-unsupported-features/es-builtins'], ['error', {version: '>=6'}]); t.deepEqual( config.baseConfig.rules['node/no-unsupported-features/es-syntax'], - ['error', {version: '>=6', ignores: ['modules']}] + ['error', {version: '>=6', ignores: ['modules']}], ); t.deepEqual(config.baseConfig.rules['node/no-unsupported-features/node-builtins'], ['error', {version: '>=6'}]); }); @@ -266,7 +266,7 @@ test('buildConfig: nodeVersion: >=8', t => { t.deepEqual(config.baseConfig.rules['node/no-unsupported-features/es-builtins'], ['error', {version: '>=8'}]); t.deepEqual( config.baseConfig.rules['node/no-unsupported-features/es-syntax'], - ['error', {version: '>=8', ignores: ['modules']}] + ['error', {version: '>=8', ignores: ['modules']}], ); t.deepEqual(config.baseConfig.rules['node/no-unsupported-features/node-builtins'], ['error', {version: '>=8'}]); }); @@ -276,7 +276,7 @@ test('mergeWithPrettierConfig: use `singleQuote`, `trailingComma`, `bracketSpaci singleQuote: false, trailingComma: 'all', bracketSpacing: false, - jsxBracketSameLine: false + jsxBracketSameLine: false, }; const result = manager.mergeWithPrettierConfig({}, prettierOptions); const expected = { @@ -284,7 +284,7 @@ test('mergeWithPrettierConfig: use `singleQuote`, `trailingComma`, `bracketSpaci ...prettierOptions, tabWidth: 2, useTabs: true, - semi: true + semi: true, }; t.deepEqual(result, expected); }); @@ -293,7 +293,7 @@ test('mergeWithPrettierConfig: determine `tabWidth`, `useTabs`, `semi` from xo c const prettierOptions = { tabWidth: 4, useTabs: false, - semi: false + semi: false, }; const result = manager.mergeWithPrettierConfig({space: 4, semicolon: false}, {}); const expected = { @@ -301,7 +301,7 @@ test('mergeWithPrettierConfig: determine `tabWidth`, `useTabs`, `semi` from xo c jsxBracketSameLine: false, singleQuote: true, trailingComma: 'none', - ...prettierOptions + ...prettierOptions, }; t.deepEqual(result, expected); }); @@ -310,7 +310,7 @@ test('mergeWithPrettierConfig: determine `tabWidth`, `useTabs`, `semi` from pret const prettierOptions = { useTabs: false, semi: false, - tabWidth: 4 + tabWidth: 4, }; const result = manager.mergeWithPrettierConfig({}, prettierOptions); const expected = { @@ -318,7 +318,7 @@ test('mergeWithPrettierConfig: determine `tabWidth`, `useTabs`, `semi` from pret jsxBracketSameLine: false, singleQuote: true, trailingComma: 'none', - ...prettierOptions + ...prettierOptions, }; t.deepEqual(result, expected); }); @@ -326,16 +326,16 @@ test('mergeWithPrettierConfig: determine `tabWidth`, `useTabs`, `semi` from pret test('mergeWithPrettierConfig: throw error is `semi`/`semicolon` conflicts', t => { t.throws(() => manager.mergeWithPrettierConfig( {semicolon: true}, - {semi: false} + {semi: false}, )); t.throws(() => manager.mergeWithPrettierConfig( {semicolon: false}, - {semi: true} + {semi: true}, )); t.notThrows(() => manager.mergeWithPrettierConfig( {semicolon: true}, - {semi: true} + {semi: true}, )); t.notThrows(() => manager.mergeWithPrettierConfig({semicolon: false}, {semi: false})); }); @@ -416,13 +416,13 @@ test('buildConfig: extends', t => { const config = manager.buildConfig({ extends: [ 'plugin:foo/bar', - 'eslint-config-prettier' - ] + 'eslint-config-prettier', + ], }); t.deepEqual(config.baseConfig.extends.slice(-2), [ 'plugin:foo/bar', - path.resolve('../node_modules/eslint-config-prettier/index.js') + path.resolve('../node_modules/eslint-config-prettier/index.js'), ]); }); @@ -435,7 +435,7 @@ test('buildConfig: typescript', t => { warnOnUnsupportedTypeScriptVersion: false, ecmaFeatures: {jsx: true}, project: './tsconfig.json', - projectFolderIgnoreList: [/\/node_modules\/(?!.*\.cache\/xo-linter)/] + projectFolderIgnoreList: [/\/node_modules\/(?!.*\.cache\/xo-linter)/], }); }); @@ -443,7 +443,7 @@ test('buildConfig: typescript with parserOption', t => { const config = manager.buildConfig({ ts: true, parserOptions: {projectFolderIgnoreList: [], sourceType: 'script'}, - tsConfigPath: 'path/to/tmp-tsconfig.json' + tsConfigPath: 'path/to/tmp-tsconfig.json', }, {}); t.is(config.baseConfig.parser, require.resolve('@typescript-eslint/parser')); @@ -452,15 +452,15 @@ test('buildConfig: typescript with parserOption', t => { ecmaFeatures: {jsx: true}, projectFolderIgnoreList: [], project: 'path/to/tmp-tsconfig.json', - sourceType: 'script' + sourceType: 'script', }); }); test('buildConfig: parserOptions', t => { const config = manager.buildConfig({ parserOptions: { - sourceType: 'script' - } + sourceType: 'script', + }, }); t.is(config.baseConfig.parserOptions.sourceType, 'script'); @@ -469,11 +469,11 @@ test('buildConfig: parserOptions', t => { test('buildConfig: prevents useEslintrc option', t => { t.throws(() => { manager.buildConfig({ - useEslintrc: true + useEslintrc: true, }); }, { instanceOf: Error, - message: 'The `useEslintrc` option is not supported' + message: 'The `useEslintrc` option is not supported', }); }); @@ -482,13 +482,13 @@ test('findApplicableOverrides', t => { {files: '**/f*.js'}, {files: '**/bar.js'}, {files: '**/*oo.js'}, - {files: '**/*.txt'} + {files: '**/*.txt'}, ]); t.is(result.hash, 0b1010); t.deepEqual(result.applicable, [ {files: '**/f*.js'}, - {files: '**/*oo.js'} + {files: '**/*oo.js'}, ]); }); @@ -551,13 +551,13 @@ test('mergeWithFileConfig: typescript files', async t => { ignores: DEFAULT_IGNORES, cwd, semicolon: false, - ts: true + ts: true, }; t.deepEqual(omit(options, 'tsConfigPath'), expected); t.deepEqual(await readJson(options.tsConfigPath), { extends: path.resolve(cwd, 'tsconfig.json'), files: [path.resolve(cwd, 'file.ts')], - include: [slash(path.resolve(cwd, '**/*.ts')), slash(path.resolve(cwd, '**/*.tsx'))] + include: [slash(path.resolve(cwd, '**/*.ts')), slash(path.resolve(cwd, '**/*.tsx'))], }); }); @@ -571,13 +571,13 @@ test('mergeWithFileConfig: tsx files', async t => { ignores: DEFAULT_IGNORES, cwd, semicolon: false, - ts: true + ts: true, }; t.deepEqual(omit(options, 'tsConfigPath'), expected); t.deepEqual(await readJson(options.tsConfigPath), { extends: path.resolve(cwd, 'tsconfig.json'), files: [path.resolve(cwd, 'file.tsx')], - include: [slash(path.resolve(cwd, '**/*.ts')), slash(path.resolve(cwd, '**/*.tsx'))] + include: [slash(path.resolve(cwd, '**/*.ts')), slash(path.resolve(cwd, '**/*.tsx'))], }); }); @@ -587,19 +587,19 @@ test('mergeWithFileConfigs: nested configs with prettier', async t => { 'no-semicolon.js', 'child/semicolon.js', 'child-override/two-spaces.js', - 'child-override/child-prettier-override/semicolon.js' + 'child-override/child-prettier-override/semicolon.js', ].map(file => path.resolve(cwd, file)); const result = await manager.mergeWithFileConfigs(paths, {cwd}, [ { filepath: path.resolve(cwd, 'child-override', 'child-prettier-override', 'package.json'), - config: {overrides: [{files: 'semicolon.js', prettier: true}]} + config: {overrides: [{files: 'semicolon.js', prettier: true}]}, }, {filepath: path.resolve(cwd, 'package.json'), config: {semicolon: true}}, { filepath: path.resolve(cwd, 'child-override', 'package.json'), - config: {overrides: [{files: 'two-spaces.js', space: 4}]} + config: {overrides: [{files: 'two-spaces.js', space: 4}]}, }, - {filepath: path.resolve(cwd, 'child', 'package.json'), config: {semicolon: false}} + {filepath: path.resolve(cwd, 'child', 'package.json'), config: {semicolon: false}}, ]); t.deepEqual(result, [ @@ -609,9 +609,9 @@ test('mergeWithFileConfigs: nested configs with prettier', async t => { semicolon: true, cwd, extensions: DEFAULT_EXTENSION, - ignores: DEFAULT_IGNORES + ignores: DEFAULT_IGNORES, }, - prettierOptions: {} + prettierOptions: {}, }, { files: [path.resolve(cwd, 'child/semicolon.js')], @@ -619,9 +619,9 @@ test('mergeWithFileConfigs: nested configs with prettier', async t => { semicolon: false, cwd: path.resolve(cwd, 'child'), extensions: DEFAULT_EXTENSION, - ignores: DEFAULT_IGNORES + ignores: DEFAULT_IGNORES, }, - prettierOptions: {} + prettierOptions: {}, }, { files: [path.resolve(cwd, 'child-override/two-spaces.js')], @@ -635,9 +635,9 @@ test('mergeWithFileConfigs: nested configs with prettier', async t => { extends: [], cwd: path.resolve(cwd, 'child-override'), extensions: DEFAULT_EXTENSION, - ignores: DEFAULT_IGNORES + ignores: DEFAULT_IGNORES, }, - prettierOptions: {} + prettierOptions: {}, }, { files: [path.resolve(cwd, 'child-override/child-prettier-override/semicolon.js')], @@ -651,10 +651,10 @@ test('mergeWithFileConfigs: nested configs with prettier', async t => { extends: [], cwd: path.resolve(cwd, 'child-override', 'child-prettier-override'), extensions: DEFAULT_EXTENSION, - ignores: DEFAULT_IGNORES + ignores: DEFAULT_IGNORES, }, - prettierOptions: {endOfLine: 'lf', semi: false, useTabs: true} - } + prettierOptions: {endOfLine: 'lf', semi: false, useTabs: true}, + }, ]); }); @@ -664,7 +664,7 @@ test('mergeWithFileConfigs: typescript files', async t => { const configFiles = [ {filepath: path.resolve(cwd, 'child/sub-child/package.json'), config: {space: 2}}, {filepath: path.resolve(cwd, 'package.json'), config: {space: 4}}, - {filepath: path.resolve(cwd, 'child/package.json'), config: {semicolon: false}} + {filepath: path.resolve(cwd, 'child/package.json'), config: {semicolon: false}}, ]; const result = await manager.mergeWithFileConfigs(paths, {cwd}, configFiles); @@ -675,9 +675,9 @@ test('mergeWithFileConfigs: typescript files', async t => { cwd, extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, - ts: true + ts: true, }, - prettierOptions: {} + prettierOptions: {}, }); t.deepEqual(await readJson(result[0].options.tsConfigPath), { files: [path.resolve(cwd, 'two-spaces.tsx')], @@ -688,8 +688,8 @@ test('mergeWithFileConfigs: typescript files', async t => { noUnusedLocals: true, noUnusedParameters: true, strict: true, - target: 'es2018' - } + target: 'es2018', + }, }); t.deepEqual(omit(result[1], 'options.tsConfigPath'), { @@ -699,9 +699,9 @@ test('mergeWithFileConfigs: typescript files', async t => { cwd: path.resolve(cwd, 'child'), extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, - ts: true + ts: true, }, - prettierOptions: {} + prettierOptions: {}, }); t.deepEqual(omit(result[2], 'options.tsConfigPath'), { @@ -711,9 +711,9 @@ test('mergeWithFileConfigs: typescript files', async t => { cwd: path.resolve(cwd, 'child/sub-child'), extensions: DEFAULT_EXTENSION, ignores: DEFAULT_IGNORES, - ts: true + ts: true, }, - prettierOptions: {} + prettierOptions: {}, }); // Verify that we use the same temporary tsconfig.json for both files group sharing the same original tsconfig.json even if they have different xo config @@ -723,8 +723,8 @@ test('mergeWithFileConfigs: typescript files', async t => { files: [path.resolve(cwd, 'child/extra-semicolon.ts'), path.resolve(cwd, 'child/sub-child/four-spaces.ts')], include: [ slash(path.resolve(cwd, 'child/**/*.ts')), - slash(path.resolve(cwd, 'child/**/*.tsx')) - ] + slash(path.resolve(cwd, 'child/**/*.tsx')), + ], }); const secondResult = await manager.mergeWithFileConfigs(paths, {cwd}, configFiles); @@ -745,14 +745,14 @@ test('applyOverrides', t => { rules: {'rule-2': 'c'}, extends: ['overrride-extend'], globals: ['override'], - plugins: ['override-plugin'] - } + plugins: ['override-plugin'], + }, ], rules: {'rule-1': 'a', 'rule-2': 'b'}, extends: ['base-extend'], globals: ['base'], plugins: ['base-plugin'], - cwd: '.' + cwd: '.', }), { options: { @@ -762,9 +762,9 @@ test('applyOverrides', t => { plugins: ['base-plugin', 'override-plugin'], envs: [], settings: {}, - cwd: '.' + cwd: '.', }, - hash: 1 - } + hash: 1, + }, ); }); From ce715f30a8d444ccbc210b61f3ccce985e555e5c Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Thu, 1 Jul 2021 17:00:25 +0800 Subject: [PATCH 5/7] Fix tests --- test/fixtures/space/one-space.js | 2 +- test/fixtures/space/two-spaces.js | 2 +- test/fixtures/typescript/child/sub-child/four-spaces.ts | 2 +- test/fixtures/typescript/two-spaces.tsx | 2 +- test/lint-text.js | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/fixtures/space/one-space.js b/test/fixtures/space/one-space.js index 0b1b5e7d..cf203662 100644 --- a/test/fixtures/space/one-space.js +++ b/test/fixtures/space/one-space.js @@ -1,3 +1,3 @@ console.log([ - 1 + 1, ]); diff --git a/test/fixtures/space/two-spaces.js b/test/fixtures/space/two-spaces.js index baa2c240..6b010f41 100644 --- a/test/fixtures/space/two-spaces.js +++ b/test/fixtures/space/two-spaces.js @@ -1,3 +1,3 @@ console.log([ - 1 + 1, ]); diff --git a/test/fixtures/typescript/child/sub-child/four-spaces.ts b/test/fixtures/typescript/child/sub-child/four-spaces.ts index 39806b0b..9b5220b1 100644 --- a/test/fixtures/typescript/child/sub-child/four-spaces.ts +++ b/test/fixtures/typescript/child/sub-child/four-spaces.ts @@ -1,3 +1,3 @@ console.log([ - 4 + 4, ]); diff --git a/test/fixtures/typescript/two-spaces.tsx b/test/fixtures/typescript/two-spaces.tsx index 70a5ead6..5984f43a 100644 --- a/test/fixtures/typescript/two-spaces.tsx +++ b/test/fixtures/typescript/two-spaces.tsx @@ -1,3 +1,3 @@ console.log([ - 2 + 2, ]); diff --git a/test/lint-text.js b/test/lint-text.js index d993cecc..615afb85 100644 --- a/test/lint-text.js +++ b/test/lint-text.js @@ -278,14 +278,14 @@ test('find configurations close to linted file', async t => { test('typescript files', async t => { let {results} = await xo.lintText(`console.log([ - 2 + 2, ]); `, {filePath: 'fixtures/typescript/two-spaces.tsx'}); t.true(hasRule(results, '@typescript-eslint/indent')); ({results} = await xo.lintText(`console.log([ - 2 + 2, ]); `, {filePath: 'fixtures/typescript/two-spaces.tsx', space: 2})); t.is(results[0].errorCount, 0); @@ -297,13 +297,13 @@ test('typescript files', async t => { t.is(results[0].errorCount, 0); ({results} = await xo.lintText(`console.log([ - 4 + 4, ]); `, {filePath: 'fixtures/typescript/child/sub-child/four-spaces.ts'})); t.true(hasRule(results, '@typescript-eslint/indent')); ({results} = await xo.lintText(`console.log([ - 4 + 4, ]); `, {filePath: 'fixtures/typescript/child/sub-child/four-spaces.ts', space: 4})); t.is(results[0].errorCount, 0); From a2593d4c4ae439d25623ab502c8505876e08ef3b Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Thu, 1 Jul 2021 18:12:15 +0800 Subject: [PATCH 6/7] Clean --- cli-main.js | 221 ----------------------------------------------- test/cli-main.js | 184 --------------------------------------- 2 files changed, 405 deletions(-) delete mode 100755 cli-main.js delete mode 100644 test/cli-main.js diff --git a/cli-main.js b/cli-main.js deleted file mode 100755 index 6fa2f447..00000000 --- a/cli-main.js +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env node -import getStdin from 'get-stdin'; -import meow from 'meow'; -import formatterPretty from 'eslint-formatter-pretty'; -import semver from 'semver'; -import openReport from './lib/open-report.js'; -import xo from './index.js'; - -const cli = meow(` - Usage - $ xo [ ...] - - Options - --fix Automagically fix issues - --reporter Reporter to use - --env Environment preset [Can be set multiple times] - --global Global variable [Can be set multiple times] - --ignore Additional paths to ignore [Can be set multiple times] - --space Use space indent instead of tabs [Default: 2] - --no-semicolon Prevent use of semicolons - --prettier Conform to Prettier code style - --node-version Range of Node.js version to support - --plugin Include third-party plugins [Can be set multiple times] - --extend Extend defaults with a custom config [Can be set multiple times] - --open Open files with issues in your editor - --quiet Show only errors and no warnings - --extension Additional extension to lint [Can be set multiple times] - --cwd= Working directory for files - --stdin Validate/fix code from stdin - --stdin-filename Specify a filename for the --stdin option - --print-config Print the effective ESLint config for the given file - - Examples - $ xo - $ xo index.js - $ xo *.js !foo.js - $ xo --space - $ xo --env=node --env=mocha - $ xo --plugin=react - $ xo --plugin=html --extension=html - $ echo 'const x=true' | xo --stdin --fix - $ xo --print-config=index.js - - Tips - - Add XO to your project with \`npm init xo\`. - - Put options in package.json instead of using flags so other tools can read it. -`, { - autoVersion: false, - booleanDefault: undefined, - flags: { - fix: { - type: 'boolean' - }, - reporter: { - type: 'string' - }, - env: { - type: 'string', - isMultiple: true - }, - global: { - type: 'string', - isMultiple: true - }, - ignore: { - type: 'string', - isMultiple: true - }, - space: { - type: 'string' - }, - semicolon: { - type: 'boolean' - }, - prettier: { - type: 'boolean' - }, - nodeVersion: { - type: 'string' - }, - plugin: { - type: 'string', - isMultiple: true - }, - extend: { - type: 'string', - isMultiple: true - }, - open: { - type: 'boolean' - }, - quiet: { - type: 'boolean' - }, - extension: { - type: 'string', - isMultiple: true - }, - cwd: { - type: 'string' - }, - printConfig: { - type: 'string' - }, - stdin: { - type: 'boolean' - }, - stdinFilename: { - type: 'string' - } - } -}); - -const {input, flags: options, showVersion} = cli; - -// TODO: Fix this properly instead of the below workaround. -// Revert behavior of meow >8 to pre-8 (7.1.1) for flags using `isMultiple: true`. -// Otherwise, options defined in package.json can't be merged by lib/options-manager.js `mergeOptions()`. -for (const key in options) { - if (Array.isArray(options[key]) && options[key].length === 0) { - delete options[key]; - } -} - -// Make data types for `options.space` match those of the API -// Check for string type because `xo --no-space` sets `options.space` to `false` -if (typeof options.space === 'string') { - if (/^\d+$/u.test(options.space)) { - options.space = Number.parseInt(options.space, 10); - } else if (options.space === 'true') { - options.space = true; - } else if (options.space === 'false') { - options.space = false; - } else { - if (options.space !== '') { - // Assume `options.space` was set to a filename when run as `xo --space file.js` - input.push(options.space); - } - - options.space = true; - } -} - -if (process.env.GITHUB_ACTIONS && !options.fix && !options.reporter) { - options.quiet = true; -} - -const log = async report => { - const reporter = options.reporter || process.env.GITHUB_ACTIONS ? await xo.getFormatter(options.reporter || 'compact') : formatterPretty; - process.stdout.write(reporter(report.results)); - process.exitCode = report.errorCount === 0 ? 0 : 1; -}; - -// `xo -` => `xo --stdin` -if (input[0] === '-') { - options.stdin = true; - input.shift(); -} - -if (options.version) { - showVersion(); -} - -if (options.nodeVersion) { - if (options.nodeVersion === 'false') { - options.nodeVersion = false; - } else if (!semver.validRange(options.nodeVersion)) { - console.error('The `--node-engine` flag must be a valid semver range (for example `>=6`)'); - process.exit(1); - } -} - -(async () => { - if (options.printConfig) { - if (input.length > 0) { - console.error('The `--print-config` flag must be used with exactly one filename'); - process.exit(1); - } - - if (options.stdin) { - console.error('The `--print-config` flag is not supported on stdin'); - process.exit(1); - } - - options.filePath = options.printConfig; - const config = await xo.getConfig(options); - console.log(JSON.stringify(config, undefined, '\t')); - } else if (options.stdin) { - const stdin = await getStdin(); - - if (options.stdinFilename) { - options.filePath = options.stdinFilename; - } - - if (options.fix) { - const {results: [result]} = await xo.lintText(stdin, options); - // If there is no output, pass the stdin back out - process.stdout.write((result && result.output) || stdin); - return; - } - - if (options.open) { - console.error('The `--open` flag is not supported on stdin'); - process.exit(1); - } - - await log(await xo.lintText(stdin, options)); - } else { - const report = await xo.lintFiles(input, options); - - if (options.fix) { - await xo.outputFixes(report); - } - - if (options.open) { - openReport(report); - } - - await log(report); - } -})(); diff --git a/test/cli-main.js b/test/cli-main.js deleted file mode 100644 index 51999728..00000000 --- a/test/cli-main.js +++ /dev/null @@ -1,184 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import test from 'ava'; -import execa from 'execa'; -import slash from 'slash'; -import tempWrite from 'temp-write'; -import createEsmUtils from 'esm-utils'; - -const {__dirname} = createEsmUtils(import.meta); -process.chdir(__dirname); - -const main = (arguments_, options) => execa(path.join(__dirname, '../cli-main.js'), arguments_, options); - -test('fix option', async t => { - const filepath = await tempWrite('console.log()\n', 'x.js'); - await main(['--fix', filepath]); - t.is(fs.readFileSync(filepath, 'utf8').trim(), 'console.log();'); -}); - -test('fix option with stdin', async t => { - const {stdout} = await main(['--fix', '--stdin'], { - input: 'console.log()' - }); - t.is(stdout, 'console.log();'); -}); - -test('stdin-filename option with stdin', async t => { - const {stdout} = await main(['--stdin', '--stdin-filename=unicorn-file'], { - input: 'console.log()\n', - reject: false - }); - t.regex(stdout, /unicorn-file:/u); -}); - -test('reporter option', async t => { - const filepath = await tempWrite('console.log()\n', 'x.js'); - - const error = await t.throwsAsync(() => - main(['--reporter=compact', filepath]) - ); - t.true(error.stdout.includes('Error - ')); -}); - -test('overrides fixture', async t => { - const cwd = path.join(__dirname, 'fixtures/overrides'); - await t.notThrowsAsync(main([], {cwd})); -}); - -test('overrides work with relative path', async t => { - const cwd = path.join(__dirname, 'fixtures/overrides'); - const file = path.join('test', 'bar.js'); - await t.notThrowsAsync(main([file], {cwd})); -}); - -test('overrides work with relative path starting with `./`', async t => { - const cwd = path.join(__dirname, 'fixtures/overrides'); - const file = '.' + path.sep + path.join('test', 'bar.js'); - await t.notThrowsAsync(main([file], {cwd})); -}); - -test('overrides work with absolute path', async t => { - const cwd = path.join(__dirname, 'fixtures/overrides'); - const file = path.join(cwd, 'test', 'bar.js'); - await t.notThrowsAsync(main([file], {cwd})); -}); - -test.failing('override default ignore', async t => { - const cwd = path.join(__dirname, 'fixtures/ignores'); - await t.throwsAsync(main([], {cwd})); -}); - -test('ignore files in .gitignore', async t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); - const error = await t.throwsAsync(main(['--reporter=json'], {cwd})); - const reports = JSON.parse(error.stdout); - const files = reports - .map(report => path.relative(cwd, report.filePath)) - .map(report => slash(report)); - t.deepEqual(files.sort(), ['index.js', 'test/bar.js'].sort()); -}); - -test('ignore explicit files when in .gitgnore', async t => { - const cwd = path.join(__dirname, 'fixtures/gitignore'); - await t.notThrowsAsync(main(['test/foo.js', '--reporter=json'], {cwd})); -}); - -test('negative gitignores', async t => { - const cwd = path.join(__dirname, 'fixtures/negative-gitignore'); - const error = await t.throwsAsync(main(['--reporter=json'], {cwd})); - const reports = JSON.parse(error.stdout); - const files = reports.map(report => path.relative(cwd, report.filePath)); - t.deepEqual(files, ['foo.js']); -}); - -test('supports being extended with a shareable config', async t => { - const cwd = path.join(__dirname, 'fixtures/project'); - await t.notThrowsAsync(main([], {cwd})); -}); - -test('quiet option', async t => { - const filepath = await tempWrite('// TODO: quiet\nconsole.log()\n', 'x.js'); - const error = await t.throwsAsync(main(['--quiet', '--reporter=json', filepath])); - const [report] = JSON.parse(error.stdout); - t.is(report.warningCount, 0); -}); - -test('invalid node-engine option', async t => { - const filepath = await tempWrite('console.log()\n', 'x.js'); - const error = await t.throwsAsync(main(['--node-version', 'v', filepath])); - t.is(error.exitCode, 1); -}); - -test('cli option takes precedence over config', async t => { - const cwd = path.join(__dirname, 'fixtures/default-options'); - const input = 'console.log()\n'; - - // Use config from package.json - await t.notThrowsAsync(main(['--stdin'], {cwd, input})); - - // Override package.json config with cli flag - await t.throwsAsync(main(['--semicolon=true', '--stdin'], {cwd, input})); - - // Use XO default (`true`) even if option is not set in package.json nor cli arg - // i.e make sure absent cli flags are not parsed as `false` - await t.throwsAsync(main(['--stdin'], {input})); -}); - -test('space option with number value', async t => { - const cwd = path.join(__dirname, 'fixtures/space'); - const {stdout} = await t.throwsAsync(main(['--space=4', 'one-space.js'], {cwd})); - t.true(stdout.includes('Expected indentation of 4 spaces')); -}); - -test('space option as boolean', async t => { - const cwd = path.join(__dirname, 'fixtures/space'); - const {stdout} = await t.throwsAsync(main(['--space'], {cwd})); - t.true(stdout.includes('Expected indentation of 2 spaces')); -}); - -test('space option as boolean with filename', async t => { - const cwd = path.join(__dirname, 'fixtures/space'); - const {stdout} = await main(['--reporter=json', '--space', 'two-spaces.js'], { - cwd, - reject: false - }); - const reports = JSON.parse(stdout); - - // Only the specified file was checked (filename was not the value of `space`) - t.is(reports.length, 1); - - // The default space value of 2 was expected - t.is(reports[0].errorCount, 0); -}); - -test('space option with boolean strings', async t => { - const cwd = path.join(__dirname, 'fixtures/space'); - const trueResult = await t.throwsAsync(main(['--space=true'], {cwd})); - const falseResult = await t.throwsAsync(main(['--space=false'], {cwd})); - t.true(trueResult.stdout.includes('Expected indentation of 2 spaces')); - t.true(falseResult.stdout.includes('Expected indentation of 1 tab')); -}); - -test('extension option', async t => { - const cwd = path.join(__dirname, 'fixtures/custom-extension'); - const {stdout} = await t.throwsAsync(main(['--reporter=json', '--extension=unknown'], {cwd})); - const reports = JSON.parse(stdout); - - t.is(reports.length, 1); - t.true(reports[0].filePath.endsWith('.unknown')); -}); - -test('invalid print-config flag with stdin', async t => { - const error = await t.throwsAsync(() => - main(['--print-config', 'x.js', '--stdin'], {input: 'console.log()\n'}) - ); - t.is(error.stderr.trim(), 'The `--print-config` flag is not supported on stdin'); -}); - -test('print-config flag requires a single filename', async t => { - const error = await t.throwsAsync(() => - main(['--print-config', 'x.js', 'y.js']) - ); - t.is(error.stderr.trim(), 'The `--print-config` flag must be used with exactly one filename'); -}); From 2e068f33eb7e51e16ebb74dfe12f706ea2275b48 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Thu, 1 Jul 2021 18:13:41 +0800 Subject: [PATCH 7/7] Fix --- lib/constants.js | 4 ++-- lib/options-manager.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/constants.js b/lib/constants.js index 347082f9..d0a7dd5a 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -125,7 +125,7 @@ const CONFIG_FILES = [ `${MODULE_NAME}.config.cjs`, ]; -const TSCONFIG_DEFFAULTS = { +const TSCONFIG_DEFAULTS = { compilerOptions: { target: 'es2018', newLine: 'lf', @@ -148,6 +148,6 @@ export { MODULE_NAME, CONFIG_FILES, MERGE_OPTIONS_CONCAT, - TSCONFIG_DEFFAULTS, + TSCONFIG_DEFAULTS, CACHE_DIR_NAME, }; diff --git a/lib/options-manager.js b/lib/options-manager.js index c77e301d..9c87cb76 100644 --- a/lib/options-manager.js +++ b/lib/options-manager.js @@ -28,7 +28,7 @@ import { MODULE_NAME, CONFIG_FILES, MERGE_OPTIONS_CONCAT, - TSCONFIG_DEFFAULTS, + TSCONFIG_DEFAULTS, CACHE_DIR_NAME, } from './constants.js'; @@ -218,7 +218,7 @@ const makeTSConfig = (tsConfig, tsConfigPath, files) => { config.extends = tsConfigPath; config.include = arrify(tsConfig.include).map(pattern => toAbsoluteGlob(pattern, {cwd: path.dirname(tsConfigPath)})); } else { - Object.assign(config, TSCONFIG_DEFFAULTS); + Object.assign(config, TSCONFIG_DEFAULTS); } return config;