xo 0.42.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import process from 'node:process';
2
3
  import getStdin from 'get-stdin';
3
4
  import meow from 'meow';
4
5
  import formatterPretty from 'eslint-formatter-pretty';
@@ -172,8 +173,8 @@ if (options.nodeVersion) {
172
173
  }
173
174
 
174
175
  (async () => {
175
- if (options.printConfig) {
176
- if (input.length > 0) {
176
+ if (typeof options.printConfig === 'string') {
177
+ if (input.length > 0 || options.printConfig === '') {
177
178
  console.error('The `--print-config` flag must be used with exactly one filename');
178
179
  process.exit(1);
179
180
  }
@@ -3,7 +3,7 @@
3
3
  module.exports = {
4
4
  // Repeated here from eslint-config-xo in case some plugins set something different
5
5
  parserOptions: {
6
- ecmaVersion: 2021,
6
+ ecmaVersion: 'latest',
7
7
  sourceType: 'module',
8
8
  ecmaFeatures: {
9
9
  jsx: true,
@@ -14,7 +14,8 @@ module.exports = {
14
14
  'no-use-extend-native',
15
15
  'ava',
16
16
  'unicorn',
17
- 'promise',
17
+ // Disabled as the plugin doesn't support ESLint 8 yet.
18
+ // 'promise',
18
19
  'import',
19
20
  'node',
20
21
  'eslint-comments',
@@ -173,17 +174,19 @@ module.exports = {
173
174
  // TODO: Temporarily disabled as the rule is buggy.
174
175
  'function-call-argument-newline': 'off',
175
176
 
176
- 'promise/param-names': 'error',
177
- 'promise/no-return-wrap': [
178
- 'error',
179
- {
180
- allowReject: true,
181
- },
182
- ],
183
- 'promise/no-new-statics': 'error',
184
- 'promise/no-return-in-finally': 'error',
185
- 'promise/valid-params': 'error',
186
- 'promise/prefer-await-to-then': 'error',
177
+ // Disabled as the plugin doesn't support ESLint 8 yet.
178
+ // 'promise/param-names': 'error',
179
+ // 'promise/no-return-wrap': [
180
+ // 'error',
181
+ // {
182
+ // allowReject: true,
183
+ // },
184
+ // ],
185
+ // 'promise/no-new-statics': 'error',
186
+ // 'promise/no-return-in-finally': 'error',
187
+ // 'promise/valid-params': 'error',
188
+ // 'promise/prefer-await-to-then': 'error',
189
+
187
190
  'import/default': 'error',
188
191
  'import/export': 'error',
189
192
  'import/extensions': [
@@ -200,9 +203,8 @@ module.exports = {
200
203
  ],
201
204
  'import/first': 'error',
202
205
 
203
- // Disabled as it doesn't work with TypeScript.
204
- // This issue and some others: https://github.com/benmosher/eslint-plugin-import/issues/1341
205
- // 'import/named': 'error',
206
+ // Enabled, but disabled on TypeScript (https://github.com/xojs/xo/issues/576)
207
+ 'import/named': 'error',
206
208
 
207
209
  'import/namespace': [
208
210
  'error',
@@ -322,7 +324,7 @@ module.exports = {
322
324
  'node/no-deprecated-api': 'error',
323
325
  'node/prefer-global/buffer': [
324
326
  'error',
325
- 'always',
327
+ 'never',
326
328
  ],
327
329
  'node/prefer-global/console': [
328
330
  'error',
@@ -330,7 +332,7 @@ module.exports = {
330
332
  ],
331
333
  'node/prefer-global/process': [
332
334
  'error',
333
- 'always',
335
+ 'never',
334
336
  ],
335
337
  'node/prefer-global/text-decoder': [
336
338
  'error',
package/index.js CHANGED
@@ -1,168 +1,112 @@
1
1
  import path from 'node:path';
2
2
  import {ESLint} from 'eslint';
3
- import globby from 'globby';
4
- import {isEqual} from 'lodash-es';
3
+ import {globby, isGitIgnoredSync} from 'globby';
4
+ import {isEqual, groupBy} from 'lodash-es';
5
5
  import micromatch from 'micromatch';
6
6
  import arrify from 'arrify';
7
- import pReduce from 'p-reduce';
8
- import pMap from 'p-map';
9
- import {cosmiconfig, defaultLoaders} from 'cosmiconfig';
10
- import defineLazyProperty from 'define-lazy-prop';
11
- import pFilter from 'p-filter';
12
7
  import slash from 'slash';
13
- import {CONFIG_FILES, MODULE_NAME, DEFAULT_IGNORES} from './lib/constants.js';
14
8
  import {
15
- normalizeOptions,
9
+ parseOptions,
16
10
  getIgnores,
17
11
  mergeWithFileConfig,
18
- mergeWithFileConfigs,
19
- buildConfig,
20
- mergeOptions,
21
12
  } from './lib/options-manager.js';
13
+ import {mergeReports, processReport, getIgnoredReport} from './lib/report.js';
22
14
 
23
- /** Merge multiple reports into a single report */
24
- const mergeReports = reports => {
25
- const report = {
26
- results: [],
27
- errorCount: 0,
28
- warningCount: 0,
29
- };
30
-
31
- for (const currentReport of reports) {
32
- report.results.push(...currentReport.results);
33
- report.errorCount += currentReport.errorCount;
34
- report.warningCount += currentReport.warningCount;
35
- }
36
-
37
- return report;
38
- };
39
-
40
- const getReportStatistics = results => {
41
- const statistics = {
42
- errorCount: 0,
43
- warningCount: 0,
44
- fixableErrorCount: 0,
45
- fixableWarningCount: 0,
46
- };
47
-
48
- for (const result of results) {
49
- statistics.errorCount += result.errorCount;
50
- statistics.warningCount += result.warningCount;
51
- statistics.fixableErrorCount += result.fixableErrorCount;
52
- statistics.fixableWarningCount += result.fixableWarningCount;
53
- }
54
-
55
- return statistics;
56
- };
57
-
58
- const processReport = (report, {isQuiet = false} = {}) => {
59
- if (isQuiet) {
60
- report = ESLint.getErrorResults(report);
61
- }
62
-
63
- const result = {
64
- results: report,
65
- ...getReportStatistics(report),
66
- };
67
-
68
- defineLazyProperty(result, 'usedDeprecatedRules', () => {
69
- const seenRules = new Set();
70
- const rules = [];
71
-
72
- for (const {usedDeprecatedRules} of report) {
73
- for (const rule of usedDeprecatedRules) {
74
- if (seenRules.has(rule.ruleId)) {
75
- continue;
76
- }
77
-
78
- seenRules.add(rule.ruleId);
79
- rules.push(rule);
80
- }
81
- }
15
+ const globFiles = async (patterns, options) => {
16
+ const {ignores, extensions, cwd} = (await mergeWithFileConfig(options)).options;
82
17
 
83
- return rules;
84
- });
18
+ patterns = patterns.length === 0
19
+ ? [`**/*.{${extensions.join(',')}}`]
20
+ : arrify(patterns).map(pattern => slash(pattern));
85
21
 
86
- return result;
87
- };
88
-
89
- const runEslint = async (paths, options, processorOptions) => {
90
- const engine = new ESLint(options);
22
+ const files = await globby(
23
+ patterns,
24
+ {ignore: ignores, gitignore: true, absolute: true, cwd},
25
+ );
91
26
 
92
- const report = await engine.lintFiles(await pFilter(paths, async path => !(await engine.isPathIgnored(path))));
93
- return processReport(report, processorOptions);
27
+ return files.filter(file => extensions.includes(path.extname(file).slice(1)));
94
28
  };
95
29
 
96
- const globFiles = async (patterns, {ignores, extensions, cwd}) => (
97
- await globby(
98
- patterns.length === 0 ? [`**/*.{${extensions.join(',')}}`] : arrify(patterns).map(pattern => slash(pattern)),
99
- {ignore: ignores, gitignore: true, absolute: true, cwd},
100
- )).filter(file => extensions.includes(path.extname(file).slice(1)));
101
-
102
30
  const getConfig = async options => {
103
- const {options: foundOptions, prettierOptions} = mergeWithFileConfig(normalizeOptions(options));
104
- const {filePath, warnIgnored, ...eslintOptions} = buildConfig(foundOptions, prettierOptions);
31
+ const {filePath, eslintOptions} = await parseOptions(options);
105
32
  const engine = new ESLint(eslintOptions);
106
33
  return engine.calculateConfigForFile(filePath);
107
34
  };
108
35
 
109
- const lintText = async (string, inputOptions = {}) => {
110
- const {options: foundOptions, prettierOptions} = mergeWithFileConfig(normalizeOptions(inputOptions));
111
- const options = buildConfig(foundOptions, prettierOptions);
36
+ const lintText = async (string, options) => {
37
+ options = await parseOptions(options);
38
+ const {filePath, warnIgnored, eslintOptions, isQuiet} = options;
39
+ const {cwd, baseConfig: {ignorePatterns}} = eslintOptions;
112
40
 
113
- if (options.baseConfig.ignorePatterns && !isEqual(getIgnores({}), options.baseConfig.ignorePatterns) && typeof options.filePath !== 'string') {
41
+ if (typeof filePath !== 'string' && !isEqual(getIgnores({}), ignorePatterns)) {
114
42
  throw new Error('The `ignores` option requires the `filePath` option to be defined.');
115
43
  }
116
44
 
117
- const {filePath, warnIgnored, ...eslintOptions} = options;
118
- const engine = new ESLint(eslintOptions);
119
-
120
- if (filePath) {
121
- const filename = path.relative(options.cwd, filePath);
122
-
123
- if (
124
- micromatch.isMatch(filename, options.baseConfig.ignorePatterns)
125
- || globby.gitignore.sync({cwd: options.cwd, ignore: options.baseConfig.ignorePatterns})(filePath)
126
- || await engine.isPathIgnored(filePath)
127
- ) {
128
- return {
129
- errorCount: 0,
130
- warningCount: 0,
131
- results: [{
132
- errorCount: 0,
133
- filePath: filename,
134
- messages: [],
135
- warningCount: 0,
136
- }],
137
- };
138
- }
45
+ if (
46
+ filePath
47
+ && (
48
+ micromatch.isMatch(path.relative(cwd, filePath), ignorePatterns)
49
+ || isGitIgnoredSync({cwd, ignore: ignorePatterns})(filePath)
50
+ )
51
+ ) {
52
+ return getIgnoredReport(filePath);
139
53
  }
140
54
 
141
- const report = await engine.lintText(string, {filePath, warnIgnored});
55
+ const eslint = new ESLint(eslintOptions);
142
56
 
143
- return processReport(report, {isQuiet: inputOptions.quiet});
57
+ if (filePath && await eslint.isPathIgnored(filePath)) {
58
+ return getIgnoredReport(filePath);
59
+ }
60
+
61
+ const report = await eslint.lintText(string, {filePath, warnIgnored});
62
+ return processReport(report, {isQuiet});
144
63
  };
145
64
 
146
- const lintFiles = async (patterns, inputOptions = {}) => {
147
- inputOptions.cwd = path.resolve(inputOptions.cwd || process.cwd());
148
- const configExplorer = cosmiconfig(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: inputOptions.cwd});
149
-
150
- const configFiles = (await Promise.all(
151
- (await globby(
152
- CONFIG_FILES.map(configFile => `**/${configFile}`),
153
- {ignore: DEFAULT_IGNORES, gitignore: true, absolute: true, cwd: inputOptions.cwd},
154
- )).map(configFile => configExplorer.load(configFile)),
155
- )).filter(Boolean);
156
-
157
- const paths = configFiles.length > 0
158
- ? await pReduce(
159
- configFiles,
160
- async (paths, {filepath, config}) =>
161
- [...paths, ...(await globFiles(patterns, {...mergeOptions(inputOptions, config), cwd: path.dirname(filepath)}))],
162
- [])
163
- : await globFiles(patterns, mergeOptions(inputOptions));
164
-
165
- return mergeReports(await pMap(await mergeWithFileConfigs([...new Set(paths)], inputOptions, configFiles), async ({files, options, prettierOptions}) => runEslint(files, buildConfig(options, prettierOptions), {isQuiet: options.quiet})));
65
+ const lintFiles = async (patterns, options) => {
66
+ const files = await globFiles(patterns, options);
67
+
68
+ const allOptions = await Promise.all(
69
+ files.map(filePath => parseOptions({...options, filePath})),
70
+ );
71
+
72
+ // Files with same `xoConfigPath` can lint together
73
+ // https://github.com/xojs/xo/issues/599
74
+ const groups = groupBy(allOptions, 'eslintConfigId');
75
+
76
+ const reports = await Promise.all(
77
+ Object.values(groups)
78
+ .map(async filesWithOptions => {
79
+ const options = filesWithOptions[0];
80
+ const eslint = new ESLint(options.eslintOptions);
81
+ const files = [];
82
+
83
+ for (const options of filesWithOptions) {
84
+ const {filePath, eslintOptions} = options;
85
+ const {cwd, baseConfig: {ignorePatterns}} = eslintOptions;
86
+ if (filePath
87
+ && (
88
+ micromatch.isMatch(path.relative(cwd, filePath), ignorePatterns)
89
+ || isGitIgnoredSync({cwd, ignore: ignorePatterns})(filePath)
90
+ )) {
91
+ continue;
92
+ }
93
+
94
+ // eslint-disable-next-line no-await-in-loop
95
+ if ((await eslint.isPathIgnored(filePath))) {
96
+ continue;
97
+ }
98
+
99
+ files.push(filePath);
100
+ }
101
+
102
+ const report = await eslint.lintFiles(files);
103
+
104
+ return processReport(report, {isQuiet: options.isQuiet});
105
+ }));
106
+
107
+ const report = mergeReports(reports);
108
+
109
+ return report;
166
110
  };
167
111
 
168
112
  const getFormatter = async name => {
package/lib/constants.js CHANGED
@@ -106,13 +106,6 @@ const ENGINE_RULES = {
106
106
  },
107
107
  };
108
108
 
109
- const PRETTIER_CONFIG_OVERRIDE = {
110
- 'eslint-plugin-babel': 'prettier/babel',
111
- 'eslint-plugin-flowtype': 'prettier/flowtype',
112
- 'eslint-plugin-standard': 'prettier/standard',
113
- 'eslint-plugin-vue': 'prettier/vue',
114
- };
115
-
116
109
  const MODULE_NAME = 'xo';
117
110
 
118
111
  const CONFIG_FILES = [
@@ -144,7 +137,6 @@ export {
144
137
  DEFAULT_EXTENSION,
145
138
  TYPESCRIPT_EXTENSION,
146
139
  ENGINE_RULES,
147
- PRETTIER_CONFIG_OVERRIDE,
148
140
  MODULE_NAME,
149
141
  CONFIG_FILES,
150
142
  MERGE_OPTIONS_CONCAT,
@@ -1,29 +1,26 @@
1
+ import {existsSync, promises as fs} from 'node:fs';
2
+ import process from 'node:process';
1
3
  import os from 'node:os';
2
4
  import path from 'node:path';
3
- import fsExtra from 'fs-extra';
4
5
  import arrify from 'arrify';
5
- import {mergeWith, groupBy, flow, pick} from 'lodash-es';
6
- import pathExists from 'path-exists';
7
- import findUp from 'find-up';
6
+ import {mergeWith, flow, pick} from 'lodash-es';
7
+ import {findUpSync} from 'find-up';
8
8
  import findCacheDir from 'find-cache-dir';
9
9
  import prettier from 'prettier';
10
10
  import semver from 'semver';
11
- import {cosmiconfig, cosmiconfigSync, defaultLoaders} from 'cosmiconfig';
12
- import pReduce from 'p-reduce';
11
+ import {cosmiconfig, defaultLoaders} from 'cosmiconfig';
13
12
  import micromatch from 'micromatch';
14
13
  import JSON5 from 'json5';
15
14
  import toAbsoluteGlob from 'to-absolute-glob';
16
15
  import stringify from 'json-stable-stringify-without-jsonify';
17
16
  import murmur from 'imurmurhash';
18
- import isPathInside from 'is-path-inside';
19
- import eslintrc from '@eslint/eslintrc';
17
+ import {Legacy} from '@eslint/eslintrc';
20
18
  import createEsmUtils from 'esm-utils';
21
19
  import {
22
20
  DEFAULT_IGNORES,
23
21
  DEFAULT_EXTENSION,
24
22
  TYPESCRIPT_EXTENSION,
25
23
  ENGINE_RULES,
26
- PRETTIER_CONFIG_OVERRIDE,
27
24
  MODULE_NAME,
28
25
  CONFIG_FILES,
29
26
  MERGE_OPTIONS_CONCAT,
@@ -32,10 +29,8 @@ import {
32
29
  } from './constants.js';
33
30
 
34
31
  const {__dirname, json, require} = createEsmUtils(import.meta);
35
- const pkg = json.loadSync('../package.json');
36
- const {outputJson, outputJsonSync} = fsExtra;
37
- const {normalizePackageName} = eslintrc.Legacy.naming;
38
- const resolveModule = eslintrc.Legacy.ModuleResolver.resolve;
32
+ const {normalizePackageName} = Legacy.naming;
33
+ const resolveModule = Legacy.ModuleResolver.resolve;
39
34
 
40
35
  const resolveFrom = (moduleId, fromDirectory = process.cwd()) => resolveModule(moduleId, path.join(fromDirectory, '__placeholder__.js'));
41
36
 
@@ -48,13 +43,14 @@ resolveFrom.silent = (moduleId, fromDirectory) => {
48
43
  const resolveLocalConfig = name => resolveModule(normalizePackageName(name, 'eslint-config'), import.meta.url);
49
44
 
50
45
  const nodeVersion = process && process.version;
51
- const cacheLocation = findCacheDir({name: CACHE_DIR_NAME}) || path.join(os.homedir() || os.tmpdir(), '.xo-cache/');
46
+ const cacheLocation = cwd => findCacheDir({name: CACHE_DIR_NAME, cwd}) || path.join(os.homedir() || os.tmpdir(), '.xo-cache/');
52
47
 
53
48
  const DEFAULT_CONFIG = {
54
49
  useEslintrc: false,
55
50
  cache: true,
56
- cacheLocation: path.join(cacheLocation, 'xo-cache.json'),
51
+ cacheLocation: path.join(cacheLocation(), 'xo-cache.json'),
57
52
  globInputPaths: false,
53
+ resolvePluginsRelativeTo: __dirname,
58
54
  baseConfig: {
59
55
  extends: [
60
56
  resolveLocalConfig('xo'),
@@ -104,111 +100,90 @@ const isTypescript = file => TYPESCRIPT_EXTENSION.includes(path.extname(file).sl
104
100
  Find config for `lintText`.
105
101
  The config files are searched starting from `options.filePath` if defined or `options.cwd` otherwise.
106
102
  */
107
- const mergeWithFileConfig = options => {
103
+ const mergeWithFileConfig = async options => {
108
104
  options.cwd = path.resolve(options.cwd || process.cwd());
109
- const configExplorer = cosmiconfigSync(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: options.cwd});
110
- const pkgConfigExplorer = cosmiconfigSync('engines', {searchPlaces: ['package.json'], stopDir: options.cwd});
105
+ const configExplorer = cosmiconfig(MODULE_NAME, {searchPlaces: CONFIG_FILES, loaders: {noExt: defaultLoaders['.json']}, stopDir: options.cwd});
106
+ const pkgConfigExplorer = cosmiconfig('engines', {searchPlaces: ['package.json'], stopDir: options.cwd});
111
107
  if (options.filePath) {
112
108
  options.filePath = path.resolve(options.cwd, options.filePath);
113
109
  }
114
110
 
115
111
  const searchPath = options.filePath || options.cwd;
116
112
 
117
- const {config: xoOptions, filepath: xoConfigPath} = configExplorer.search(searchPath) || {};
118
- const {config: enginesOptions} = pkgConfigExplorer.search(searchPath) || {};
113
+ const {config: xoOptions, filepath: xoConfigPath} = (await configExplorer.search(searchPath)) || {};
114
+ const {config: enginesOptions} = (await pkgConfigExplorer.search(searchPath)) || {};
119
115
 
120
116
  options = mergeOptions(options, xoOptions, enginesOptions);
121
117
  options.cwd = xoConfigPath && path.dirname(xoConfigPath) !== options.cwd ? path.resolve(options.cwd, path.dirname(xoConfigPath)) : options.cwd;
122
118
 
119
+ // Very simple way to ensure eslint is ran minimal times across
120
+ // all linted files, once for each unique configuration - xo config path + override hash + tsconfig path
121
+ let eslintConfigId = xoConfigPath;
123
122
  if (options.filePath) {
124
- ({options} = applyOverrides(options.filePath, options));
125
- }
126
-
127
- const prettierOptions = options.prettier ? prettier.resolveConfig.sync(searchPath, {editorconfig: true}) || {} : {};
128
-
129
- if (options.filePath && isTypescript(options.filePath)) {
130
- const tsConfigExplorer = cosmiconfigSync([], {searchPlaces: ['tsconfig.json'], loaders: {'.json': (_, content) => JSON5.parse(content)}});
131
- const {config: tsConfig, filepath: tsConfigPath} = tsConfigExplorer.search(options.filePath) || {};
123
+ const overrides = applyOverrides(options.filePath, options);
124
+ options = overrides.options;
132
125
 
133
- options.tsConfigPath = getTsConfigCachePath([options.filePath], options.tsConfigPath);
134
- options.ts = true;
135
- outputJsonSync(options.tsConfigPath, makeTSConfig(tsConfig, tsConfigPath, [options.filePath]));
126
+ if (overrides.hash) {
127
+ eslintConfigId += overrides.hash;
128
+ }
136
129
  }
137
130
 
138
- return {options, prettierOptions};
139
- };
140
-
141
- /**
142
- Find config for each files found by `lintFiles`.
143
- The config files are searched starting from each files.
144
- */
145
- const mergeWithFileConfigs = async (files, options, configFiles) => {
146
- configFiles = configFiles.sort((a, b) => b.filepath.split(path.sep).length - a.filepath.split(path.sep).length);
147
- const tsConfigs = {};
148
-
149
- const groups = [...(await pReduce(files, async (configs, file) => {
150
- const pkgConfigExplorer = cosmiconfig('engines', {searchPlaces: ['package.json'], stopDir: options.cwd});
151
-
152
- const {config: xoOptions, filepath: xoConfigPath} = findApplicableConfig(file, configFiles) || {};
153
- const {config: enginesOptions, filepath: enginesConfigPath} = await pkgConfigExplorer.search(file) || {};
131
+ const prettierOptions = options.prettier ? await prettier.resolveConfig(searchPath, {editorconfig: true}) || {} : {};
154
132
 
155
- let fileOptions = mergeOptions(options, xoOptions, enginesOptions);
156
- fileOptions.cwd = xoConfigPath && path.dirname(xoConfigPath) !== fileOptions.cwd ? path.resolve(fileOptions.cwd, path.dirname(xoConfigPath)) : fileOptions.cwd;
157
-
158
- const {hash, options: optionsWithOverrides} = applyOverrides(file, fileOptions);
159
- fileOptions = optionsWithOverrides;
160
-
161
- const prettierOptions = fileOptions.prettier ? await prettier.resolveConfig(file, {editorconfig: true}) || {} : {};
133
+ if (options.filePath && isTypescript(options.filePath)) {
134
+ // We can skip looking up the tsconfig if we have it defined
135
+ // in our parser options already. Otherwise we can look it up and create it as normal
136
+ const {project: tsConfigProjectPath, tsconfigRootDir} = options.parserOptions || {};
162
137
 
138
+ let tsConfig;
163
139
  let tsConfigPath;
164
- if (isTypescript(file)) {
165
- let tsConfig;
166
- const tsConfigExplorer = cosmiconfig([], {searchPlaces: ['tsconfig.json'], loaders: {'.json': (_, content) => JSON5.parse(content)}});
167
- ({config: tsConfig, filepath: tsConfigPath} = await tsConfigExplorer.search(file) || {});
168
-
169
- fileOptions.tsConfigPath = tsConfigPath;
170
- tsConfigs[tsConfigPath || ''] = tsConfig;
171
- fileOptions.ts = true;
140
+ if (tsConfigProjectPath) {
141
+ tsConfigPath = path.resolve(options.cwd, tsConfigProjectPath);
142
+ tsConfig = await json.load(tsConfigPath);
143
+ } else {
144
+ const tsConfigExplorer = cosmiconfig([], {
145
+ searchPlaces: ['tsconfig.json'],
146
+ loaders: {'.json': (_, content) => JSON5.parse(content)},
147
+ stopDir: tsconfigRootDir,
148
+ });
149
+ const searchResults = (await tsConfigExplorer.search(options.filePath)) || {};
150
+ tsConfigPath = searchResults.filepath;
151
+ tsConfig = searchResults.config;
172
152
  }
173
153
 
174
- const cacheKey = stringify({xoConfigPath, enginesConfigPath, prettierOptions, hash, tsConfigPath: fileOptions.tsConfigPath, ts: fileOptions.ts});
175
- const cachedGroup = configs.get(cacheKey);
176
-
177
- configs.set(cacheKey, {
178
- files: [file, ...(cachedGroup ? cachedGroup.files : [])],
179
- options: cachedGroup ? cachedGroup.options : fileOptions,
180
- prettierOptions,
181
- });
182
-
183
- return configs;
184
- }, new Map())).values()];
185
-
186
- await Promise.all(Object.entries(groupBy(groups.filter(({options}) => Boolean(options.ts)), group => group.options.tsConfigPath || '')).map(
187
- ([tsConfigPath, groups]) => {
188
- const files = groups.flatMap(group => group.files);
189
- const cachePath = getTsConfigCachePath(files, tsConfigPath);
190
-
191
- for (const group of groups) {
192
- group.options.tsConfigPath = cachePath;
193
- }
154
+ if (tsConfigPath) {
155
+ options.tsConfigPath = tsConfigPath;
156
+ eslintConfigId += tsConfigPath;
157
+ } else {
158
+ const {path: tsConfigCachePath, hash: tsConfigHash} = await getTsConfigCachePath([eslintConfigId], tsConfigPath, options.cwd);
159
+ eslintConfigId += tsConfigHash;
160
+ options.tsConfigPath = tsConfigCachePath;
161
+ const config = makeTSConfig(tsConfig, tsConfigPath, [options.filePath]);
162
+ await fs.mkdir(path.dirname(options.tsConfigPath), {recursive: true});
163
+ await fs.writeFile(options.tsConfigPath, JSON.stringify(config));
164
+ }
194
165
 
195
- return outputJson(cachePath, makeTSConfig(tsConfigs[tsConfigPath], tsConfigPath, files));
196
- },
197
- ));
166
+ options.ts = true;
167
+ }
198
168
 
199
- return groups;
169
+ return {options, prettierOptions, eslintConfigId};
200
170
  };
201
171
 
202
- const findApplicableConfig = (file, configFiles) => configFiles.find(({filepath}) => isPathInside(file, path.dirname(filepath)));
203
-
204
172
  /**
205
173
  Generate a unique and consistent path for the temporary `tsconfig.json`.
206
174
  Hashing based on https://github.com/eslint/eslint/blob/cf38d0d939b62f3670cdd59f0143fd896fccd771/lib/cli-engine/lint-result-cache.js#L30
207
175
  */
208
- const getTsConfigCachePath = (files, tsConfigPath) => path.join(
209
- cacheLocation,
210
- `tsconfig.${murmur(`${pkg.version}_${nodeVersion}_${stringify({files: files.sort(), tsConfigPath})}`).result().toString(36)}.json`,
211
- );
176
+ const getTsConfigCachePath = async (files, tsConfigPath, cwd) => {
177
+ const {version} = await json.load('../package.json');
178
+ const tsConfigHash = murmur(`${version}_${nodeVersion}_${stringify({files: files.sort(), tsConfigPath})}`).result().toString(36);
179
+ return {
180
+ path: path.join(
181
+ cacheLocation(cwd),
182
+ `tsconfig.${tsConfigHash}.json`,
183
+ ),
184
+ hash: tsConfigHash,
185
+ };
186
+ };
212
187
 
213
188
  const makeTSConfig = (tsConfig, tsConfigPath, files) => {
214
189
  const config = {files: files.filter(file => isTypescript(file))};
@@ -270,7 +245,7 @@ const mergeOptions = (options, xoOptions = {}, enginesOptions = {}) => {
270
245
  ...options,
271
246
  });
272
247
 
273
- mergedOptions.extensions = DEFAULT_EXTENSION.concat(mergedOptions.extensions || []);
248
+ mergedOptions.extensions = [...DEFAULT_EXTENSION, ...(mergedOptions.extensions || [])];
274
249
  mergedOptions.ignores = getIgnores(mergedOptions);
275
250
 
276
251
  return mergedOptions;
@@ -413,6 +388,10 @@ const buildXOConfig = options => config => {
413
388
 
414
389
  // Does not work when the TS definition exports a default const.
415
390
  config.baseConfig.rules['import/default'] = 'off';
391
+
392
+ // Disabled as it doesn't work with TypeScript.
393
+ // This issue and some others: https://github.com/benmosher/eslint-plugin-import/issues/1341
394
+ config.baseConfig.rules['import/named'] = 'off';
416
395
  }
417
396
 
418
397
  config.baseConfig.settings['import/resolver'] = gatherImportResolvers(options);
@@ -424,7 +403,7 @@ const buildExtendsConfig = options => config => {
424
403
  if (options.extends && options.extends.length > 0) {
425
404
  const configs = options.extends.map(name => {
426
405
  // Don't do anything if it's a filepath
427
- if (pathExists.sync(name)) {
406
+ if (existsSync(name)) {
428
407
  return name;
429
408
  }
430
409
 
@@ -453,19 +432,11 @@ const buildPrettierConfig = (options, prettierConfig) => config => {
453
432
  // The prettier plugin uses Prettier to format the code with `--fix`
454
433
  config.baseConfig.plugins.push('prettier');
455
434
 
456
- // The prettier config overrides ESLint stylistic rules that are handled by Prettier
457
- config.baseConfig.extends.push('prettier');
435
+ // The prettier plugin overrides ESLint stylistic rules that are handled by Prettier
436
+ config.baseConfig.extends.push('plugin:prettier/recommended');
458
437
 
459
438
  // The `prettier/prettier` rule reports errors if the code is not formatted in accordance to Prettier
460
439
  config.baseConfig.rules['prettier/prettier'] = ['error', mergeWithPrettierConfig(options, prettierConfig)];
461
-
462
- // If the user has the React, Flowtype, or Standard plugin, add the corresponding Prettier rule overrides
463
- // See https://github.com/prettier/eslint-config-prettier for the list of plugins overrrides
464
- for (const [plugin, prettierConfig] of Object.entries(PRETTIER_CONFIG_OVERRIDE)) {
465
- if (options.cwd && resolveFrom.silent(plugin, options.cwd)) {
466
- config.baseConfig.extends.push(prettierConfig);
467
- }
468
- }
469
440
  }
470
441
 
471
442
  return config;
@@ -491,8 +462,8 @@ const mergeWithPrettierConfig = (options, prettierOptions) => {
491
462
  {
492
463
  singleQuote: true,
493
464
  bracketSpacing: false,
494
- jsxBracketSameLine: false,
495
- trailingComma: 'none',
465
+ bracketSameLine: false,
466
+ trailingComma: 'all',
496
467
  tabWidth: normalizeSpaces(options),
497
468
  useTabs: !options.space,
498
469
  semi: options.semicolon !== false,
@@ -504,7 +475,7 @@ const mergeWithPrettierConfig = (options, prettierOptions) => {
504
475
 
505
476
  const buildTSConfig = options => config => {
506
477
  if (options.ts) {
507
- config.baseConfig.extends.push('xo-typescript');
478
+ config.baseConfig.extends.push(require.resolve('eslint-config-xo-typescript'));
508
479
  config.baseConfig.parser = require.resolve('@typescript-eslint/parser');
509
480
  config.baseConfig.parserOptions = {
510
481
  ...config.baseConfig.parserOptions,
@@ -550,11 +521,11 @@ const findApplicableOverrides = (path, overrides) => {
550
521
  const applicable = [];
551
522
 
552
523
  for (const override of overrides) {
553
- hash <<= 1;
524
+ hash <<= 1; // eslint-disable-line no-bitwise
554
525
 
555
526
  if (micromatch.isMatch(path, override.files)) {
556
527
  applicable.push(override);
557
- hash |= 1;
528
+ hash |= 1; // eslint-disable-line no-bitwise
558
529
  }
559
530
  }
560
531
 
@@ -564,7 +535,7 @@ const findApplicableOverrides = (path, overrides) => {
564
535
  };
565
536
  };
566
537
 
567
- const getIgnores = ({ignores}) => DEFAULT_IGNORES.concat(ignores || []);
538
+ const getIgnores = ({ignores}) => [...DEFAULT_IGNORES, ...(ignores || [])];
568
539
 
569
540
  const gatherImportResolvers = options => {
570
541
  let resolvers = {};
@@ -584,7 +555,7 @@ const gatherImportResolvers = options => {
584
555
  webpackResolverSettings = options.webpack === true ? {} : options.webpack;
585
556
  } else if (!(options.webpack === false || resolvers.webpack)) {
586
557
  // If a webpack config file exists, add the import resolver automatically
587
- const webpackConfigPath = findUp.sync('webpack.config.js', {cwd: options.cwd});
558
+ const webpackConfigPath = findUpSync('webpack.config.js', {cwd: options.cwd});
588
559
  if (webpackConfigPath) {
589
560
  webpackResolverSettings = {config: webpackConfigPath};
590
561
  }
@@ -603,14 +574,28 @@ const gatherImportResolvers = options => {
603
574
  return resolvers;
604
575
  };
605
576
 
577
+ const parseOptions = async options => {
578
+ options = normalizeOptions(options);
579
+ const {options: foundOptions, prettierOptions, eslintConfigId} = await mergeWithFileConfig(options);
580
+ const {filePath, warnIgnored, ...eslintOptions} = buildConfig(foundOptions, prettierOptions);
581
+ return {
582
+ filePath,
583
+ warnIgnored,
584
+ isQuiet: options.quiet,
585
+ eslintOptions,
586
+ eslintConfigId,
587
+ };
588
+ };
589
+
606
590
  export {
591
+ parseOptions,
592
+ getIgnores,
593
+ mergeWithFileConfig,
594
+
595
+ // For tests
596
+ applyOverrides,
607
597
  findApplicableOverrides,
608
598
  mergeWithPrettierConfig,
609
599
  normalizeOptions,
610
- getIgnores,
611
- mergeWithFileConfigs,
612
- mergeWithFileConfig,
613
600
  buildConfig,
614
- applyOverrides,
615
- mergeOptions,
616
601
  };
package/lib/report.js ADDED
@@ -0,0 +1,84 @@
1
+ import defineLazyProperty from 'define-lazy-prop';
2
+ import {ESLint} from 'eslint';
3
+
4
+ /** Merge multiple reports into a single report */
5
+ const mergeReports = reports => {
6
+ const report = {
7
+ results: [],
8
+ errorCount: 0,
9
+ warningCount: 0,
10
+ };
11
+
12
+ for (const currentReport of reports) {
13
+ report.results.push(...currentReport.results);
14
+ report.errorCount += currentReport.errorCount;
15
+ report.warningCount += currentReport.warningCount;
16
+ }
17
+
18
+ return report;
19
+ };
20
+
21
+ const processReport = (report, {isQuiet = false} = {}) => {
22
+ if (isQuiet) {
23
+ report = ESLint.getErrorResults(report);
24
+ }
25
+
26
+ const result = {
27
+ results: report,
28
+ ...getReportStatistics(report),
29
+ };
30
+
31
+ defineLazyProperty(result, 'usedDeprecatedRules', () => {
32
+ const seenRules = new Set();
33
+ const rules = [];
34
+
35
+ for (const {usedDeprecatedRules} of report) {
36
+ for (const rule of usedDeprecatedRules) {
37
+ if (seenRules.has(rule.ruleId)) {
38
+ continue;
39
+ }
40
+
41
+ seenRules.add(rule.ruleId);
42
+ rules.push(rule);
43
+ }
44
+ }
45
+
46
+ return rules;
47
+ });
48
+
49
+ return result;
50
+ };
51
+
52
+ const getReportStatistics = results => {
53
+ const statistics = {
54
+ errorCount: 0,
55
+ warningCount: 0,
56
+ fixableErrorCount: 0,
57
+ fixableWarningCount: 0,
58
+ };
59
+
60
+ for (const result of results) {
61
+ statistics.errorCount += result.errorCount;
62
+ statistics.warningCount += result.warningCount;
63
+ statistics.fixableErrorCount += result.fixableErrorCount;
64
+ statistics.fixableWarningCount += result.fixableWarningCount;
65
+ }
66
+
67
+ return statistics;
68
+ };
69
+
70
+ const getIgnoredReport = filePath => ({
71
+ errorCount: 0,
72
+ warningCount: 0,
73
+ results: [
74
+ {
75
+ errorCount: 0,
76
+ warningCount: 0,
77
+ filePath,
78
+ messages: [],
79
+ },
80
+ ],
81
+ isIgnored: true,
82
+ });
83
+
84
+ export {mergeReports, processReport, getIgnoredReport};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xo",
3
- "version": "0.42.0",
3
+ "version": "0.46.0",
4
4
  "description": "JavaScript/TypeScript linter (ESLint wrapper) with great defaults",
5
5
  "license": "MIT",
6
6
  "repository": "xojs/xo",
@@ -16,7 +16,8 @@
16
16
  "node": ">=12.20"
17
17
  },
18
18
  "scripts": {
19
- "test": "eslint --quiet . --ext .js,.cjs && nyc ava"
19
+ "test:clean": "find ./test -type d -name 'node_modules' -prune -not -path ./test/fixtures/project/node_modules -exec rm -rf '{}' +",
20
+ "test": "node cli.js && nyc ava"
20
21
  },
21
22
  "files": [
22
23
  "config",
@@ -51,75 +52,67 @@
51
52
  "javascript",
52
53
  "typescript"
53
54
  ],
55
+ "bundledDependencies": [
56
+ "@typescript-eslint/eslint-plugin",
57
+ "@typescript-eslint/parser",
58
+ "eslint-config-xo-typescript"
59
+ ],
54
60
  "dependencies": {
55
- "@eslint/eslintrc": "^0.4.2",
56
- "@typescript-eslint/eslint-plugin": "^4.28.3",
57
- "@typescript-eslint/parser": "^4.28.3",
61
+ "@eslint/eslintrc": "^1.0.3",
62
+ "@typescript-eslint/eslint-plugin": "^5.2.0",
63
+ "@typescript-eslint/parser": "^5.2.0",
58
64
  "arrify": "^3.0.0",
59
- "cosmiconfig": "^7.0.0",
60
- "debug": "^4.3.2",
65
+ "cosmiconfig": "^7.0.1",
61
66
  "define-lazy-prop": "^3.0.0",
62
- "eslint": "^7.30.0",
67
+ "eslint": "^8.1.0",
63
68
  "eslint-config-prettier": "^8.3.0",
64
- "eslint-config-xo": "^0.37.0",
65
- "eslint-config-xo-typescript": "^0.43.0",
69
+ "eslint-config-xo": "^0.39.0",
70
+ "eslint-config-xo-typescript": "^0.47.0",
66
71
  "eslint-formatter-pretty": "^4.1.0",
67
- "eslint-import-resolver-webpack": "^0.13.1",
68
- "eslint-plugin-ava": "^12.0.0",
72
+ "eslint-import-resolver-webpack": "^0.13.2",
73
+ "eslint-plugin-ava": "^13.1.0",
69
74
  "eslint-plugin-eslint-comments": "^3.2.0",
70
- "eslint-plugin-import": "^2.23.4",
75
+ "eslint-plugin-import": "^2.25.2",
71
76
  "eslint-plugin-no-use-extend-native": "^0.5.0",
72
77
  "eslint-plugin-node": "^11.1.0",
73
- "eslint-plugin-prettier": "^3.4.0",
74
- "eslint-plugin-promise": "^5.1.0",
75
- "eslint-plugin-unicorn": "^34.0.1",
76
- "esm-utils": "^1.1.0",
77
- "find-cache-dir": "^3.3.1",
78
- "find-up": "^5.0.0",
79
- "fs-extra": "^10.0.0",
78
+ "eslint-plugin-prettier": "^4.0.0",
79
+ "eslint-plugin-unicorn": "^37.0.1",
80
+ "esm-utils": "^2.0.0",
81
+ "find-cache-dir": "^3.3.2",
82
+ "find-up": "^6.2.0",
80
83
  "get-stdin": "^9.0.0",
81
- "globby": "^11.0.4",
84
+ "globby": "^12.0.2",
82
85
  "imurmurhash": "^0.1.4",
83
- "is-path-inside": "^4.0.0",
84
86
  "json-stable-stringify-without-jsonify": "^1.0.1",
85
87
  "json5": "^2.2.0",
86
88
  "lodash-es": "^4.17.21",
87
- "meow": "^10.1.0",
89
+ "meow": "^10.1.1",
88
90
  "micromatch": "^4.0.4",
89
91
  "open-editor": "^3.0.0",
90
- "p-filter": "^2.1.0",
91
- "p-map": "^5.0.0",
92
- "p-reduce": "^3.0.0",
93
- "path-exists": "^4.0.0",
94
- "prettier": "^2.3.2",
92
+ "prettier": "^2.4.1",
95
93
  "semver": "^7.3.5",
96
94
  "slash": "^4.0.0",
97
95
  "to-absolute-glob": "^2.0.2",
98
- "typescript": "^4.3.5"
96
+ "typescript": "^4.4.4"
99
97
  },
100
98
  "devDependencies": {
101
99
  "ava": "^3.15.0",
102
100
  "eslint-config-xo-react": "^0.25.0",
103
- "eslint-plugin-react": "^7.24.0",
101
+ "eslint-plugin-react": "^7.26.1",
104
102
  "eslint-plugin-react-hooks": "^4.2.0",
105
103
  "execa": "^5.1.1",
106
104
  "nyc": "^15.1.0",
107
105
  "proxyquire": "^2.1.3",
108
106
  "temp-write": "^5.0.0",
109
- "webpack": "^5.45.0"
107
+ "webpack": "^5.60.0"
110
108
  },
111
- "eslintConfig": {
112
- "extends": [
113
- "eslint-config-xo",
114
- "./config/plugins.cjs",
115
- "./config/overrides.cjs"
109
+ "xo": {
110
+ "ignores": [
111
+ "test/fixtures",
112
+ "test/temp",
113
+ "coverage"
116
114
  ]
117
115
  },
118
- "eslintIgnore": [
119
- "test/fixtures",
120
- "test/temp",
121
- "coverage"
122
- ],
123
116
  "ava": {
124
117
  "files": [
125
118
  "!test/temp"
package/readme.md CHANGED
@@ -40,7 +40,7 @@ It uses [ESLint](https://eslint.org) underneath, so issues regarding built-in ru
40
40
  ## Install
41
41
 
42
42
  ```
43
- $ npm install xo
43
+ $ npm install xo --save-dev
44
44
  ```
45
45
 
46
46
  *You must install XO locally. You can run it directly with `$ npx xo`.*
@@ -169,14 +169,14 @@ module.exports = {
169
169
  };
170
170
  ```
171
171
 
172
- [Globals](https://eslint.org/docs/user-guide/configuring#specifying-globals) and [rules](https://eslint.org/docs/user-guide/configuring#configuring-rules) can be configured inline in files.
172
+ [Globals](https://eslint.org/docs/user-guide/configuring/language-options#specifying-globals) and [rules](https://eslint.org/docs/user-guide/configuring/rules#configuring-rules) can be configured inline in files.
173
173
 
174
174
  ### envs
175
175
 
176
176
  Type: `string[]`\
177
177
  Default: `['es2021', 'node']`
178
178
 
179
- Which [environments](https://eslint.org/docs/user-guide/configuring#specifying-environments) your code is designed to run in. Each environment brings with it a certain set of predefined global variables.
179
+ Which [environments](https://eslint.org/docs/user-guide/configuring/language-options#specifying-environments) your code is designed to run in. Each environment brings with it a certain set of predefined global variables.
180
180
 
181
181
  ### globals
182
182
 
@@ -188,7 +188,7 @@ Additional global variables your code accesses during execution.
188
188
 
189
189
  Type: `string[]`
190
190
 
191
- Some [paths](lib/options-manager.js) are ignored by default, including paths in `.gitignore` and [.eslintignore](https://eslint.org/docs/user-guide/configuring#eslintignore). Additional ignores can be added here.
191
+ Some [paths](lib/options-manager.js) are ignored by default, including paths in `.gitignore` and [.eslintignore](https://eslint.org/docs/user-guide/configuring/ignoring-code#the-eslintignore-file). Additional ignores can be added here.
192
192
 
193
193
  ### space
194
194
 
@@ -221,14 +221,23 @@ Default: `false`
221
221
 
222
222
  Format code with [Prettier](https://github.com/prettier/prettier).
223
223
 
224
- The [Prettier options](https://prettier.io/docs/en/options.html) will be read from the [Prettier config](https://prettier.io/docs/en/configuration.html) and if **not set** will be determined as follow:
224
+ [Prettier options](https://prettier.io/docs/en/options.html) will be based on your [Prettier config](https://prettier.io/docs/en/configuration.html). XO will then **merge** your options with its own defaults:
225
225
  - [semi](https://prettier.io/docs/en/options.html#semicolons): based on [semicolon](#semicolon) option
226
226
  - [useTabs](https://prettier.io/docs/en/options.html#tabs): based on [space](#space) option
227
227
  - [tabWidth](https://prettier.io/docs/en/options.html#tab-width): based on [space](#space) option
228
- - [trailingComma](https://prettier.io/docs/en/options.html#trailing-commas): `none`
228
+ - [trailingComma](https://prettier.io/docs/en/options.html#trailing-commas): `all`
229
229
  - [singleQuote](https://prettier.io/docs/en/options.html#quotes): `true`
230
230
  - [bracketSpacing](https://prettier.io/docs/en/options.html#bracket-spacing): `false`
231
- - [jsxBracketSameLine](https://prettier.io/docs/en/options.html#jsx-brackets): `false`
231
+
232
+ To stick with Prettier's defaults, add this to your Prettier config:
233
+
234
+ ```js
235
+ module.exports = {
236
+ trailingComma: 'es5',
237
+ singleQuote: false,
238
+ bracketSpacing: true,
239
+ };
240
+ ```
232
241
 
233
242
  If contradicting options are set for both Prettier and XO an error will be thrown.
234
243
 
@@ -245,13 +254,13 @@ If set to `false`, no rules specific to a Node.js version will be enabled.
245
254
 
246
255
  Type: `string[]`
247
256
 
248
- Include third-party [plugins](https://eslint.org/docs/user-guide/configuring.html#configuring-plugins).
257
+ Include third-party [plugins](https://eslint.org/docs/user-guide/configuring/plugins#configuring-plugins).
249
258
 
250
259
  ### extends
251
260
 
252
261
  Type: `string | string[]`
253
262
 
254
- Use one or more [shareable configs](https://eslint.org/docs/developer-guide/shareable-configs.html) or [plugin configs](https://eslint.org/docs/user-guide/configuring#using-the-configuration-from-a-plugin) to override any of the default rules (like `rules` above).
263
+ Use one or more [shareable configs](https://eslint.org/docs/developer-guide/shareable-configs) or [plugin configs](https://eslint.org/docs/user-guide/configuring/configuration-files#using-a-configuration-from-a-plugin) to override any of the default rules (like `rules` above).
255
264
 
256
265
  ### extensions
257
266
 
@@ -263,7 +272,7 @@ Allow more extensions to be linted besides `.js`, `.jsx`, `.mjs`, and `.cjs`. Ma
263
272
 
264
273
  Type: `object`
265
274
 
266
- [Shared ESLint settings](https://eslint.org/docs/user-guide/configuring#adding-shared-settings) exposed to rules.
275
+ [Shared ESLint settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings) exposed to rules.
267
276
 
268
277
  ### parser
269
278
 
@@ -275,7 +284,7 @@ ESLint parser. For example, [`@babel/eslint-parser`](https://github.com/babel/ba
275
284
 
276
285
  Type: `string`
277
286
 
278
- [ESLint processor.](https://eslint.org/docs/user-guide/configuring#specifying-processor)
287
+ [ESLint processor.](https://eslint.org/docs/user-guide/configuring/plugins#specifying-processor)
279
288
 
280
289
  ### webpack
281
290