xo 0.45.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.
@@ -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': [
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import {ESLint} from 'eslint';
3
3
  import {globby, isGitIgnoredSync} from 'globby';
4
- import {isEqual} from 'lodash-es';
4
+ import {isEqual, groupBy} from 'lodash-es';
5
5
  import micromatch from 'micromatch';
6
6
  import arrify from 'arrify';
7
7
  import slash from 'slash';
@@ -12,30 +12,6 @@ import {
12
12
  } from './lib/options-manager.js';
13
13
  import {mergeReports, processReport, getIgnoredReport} from './lib/report.js';
14
14
 
15
- const runEslint = async (lint, options) => {
16
- const {filePath, eslintOptions, isQuiet} = options;
17
- const {cwd, baseConfig: {ignorePatterns}} = eslintOptions;
18
-
19
- if (
20
- filePath
21
- && (
22
- micromatch.isMatch(path.relative(cwd, filePath), ignorePatterns)
23
- || isGitIgnoredSync({cwd, ignore: ignorePatterns})(filePath)
24
- )
25
- ) {
26
- return getIgnoredReport(filePath);
27
- }
28
-
29
- const eslint = new ESLint(eslintOptions);
30
-
31
- if (filePath && await eslint.isPathIgnored(filePath)) {
32
- return getIgnoredReport(filePath);
33
- }
34
-
35
- const report = await lint(eslint);
36
- return processReport(report, {isQuiet});
37
- };
38
-
39
15
  const globFiles = async (patterns, options) => {
40
16
  const {ignores, extensions, cwd} = (await mergeWithFileConfig(options)).options;
41
17
 
@@ -59,32 +35,76 @@ const getConfig = async options => {
59
35
 
60
36
  const lintText = async (string, options) => {
61
37
  options = await parseOptions(options);
62
- const {filePath, warnIgnored, eslintOptions} = options;
63
- const {ignorePatterns} = eslintOptions.baseConfig;
38
+ const {filePath, warnIgnored, eslintOptions, isQuiet} = options;
39
+ const {cwd, baseConfig: {ignorePatterns}} = eslintOptions;
64
40
 
65
41
  if (typeof filePath !== 'string' && !isEqual(getIgnores({}), ignorePatterns)) {
66
42
  throw new Error('The `ignores` option requires the `filePath` option to be defined.');
67
43
  }
68
44
 
69
- return runEslint(
70
- eslint => eslint.lintText(string, {filePath, warnIgnored}),
71
- options,
72
- );
73
- };
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);
53
+ }
74
54
 
75
- const lintFile = async (filePath, options) => runEslint(
76
- eslint => eslint.lintFiles([filePath]),
77
- await parseOptions({...options, filePath}),
78
- );
55
+ const eslint = new ESLint(eslintOptions);
56
+
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});
63
+ };
79
64
 
80
65
  const lintFiles = async (patterns, options) => {
81
66
  const files = await globFiles(patterns, options);
82
67
 
83
- const reports = await Promise.all(
84
- files.map(filePath => lintFile(filePath, options)),
68
+ const allOptions = await Promise.all(
69
+ files.map(filePath => parseOptions({...options, filePath})),
85
70
  );
86
71
 
87
- const report = mergeReports(reports.filter(({isIgnored}) => !isIgnored));
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);
88
108
 
89
109
  return report;
90
110
  };
@@ -50,6 +50,7 @@ const DEFAULT_CONFIG = {
50
50
  cache: true,
51
51
  cacheLocation: path.join(cacheLocation(), 'xo-cache.json'),
52
52
  globInputPaths: false,
53
+ resolvePluginsRelativeTo: __dirname,
53
54
  baseConfig: {
54
55
  extends: [
55
56
  resolveLocalConfig('xo'),
@@ -115,24 +116,57 @@ const mergeWithFileConfig = async options => {
115
116
  options = mergeOptions(options, xoOptions, enginesOptions);
116
117
  options.cwd = xoConfigPath && path.dirname(xoConfigPath) !== options.cwd ? path.resolve(options.cwd, path.dirname(xoConfigPath)) : options.cwd;
117
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;
118
122
  if (options.filePath) {
119
- ({options} = applyOverrides(options.filePath, options));
123
+ const overrides = applyOverrides(options.filePath, options);
124
+ options = overrides.options;
125
+
126
+ if (overrides.hash) {
127
+ eslintConfigId += overrides.hash;
128
+ }
120
129
  }
121
130
 
122
131
  const prettierOptions = options.prettier ? await prettier.resolveConfig(searchPath, {editorconfig: true}) || {} : {};
123
132
 
124
133
  if (options.filePath && isTypescript(options.filePath)) {
125
- const tsConfigExplorer = cosmiconfig([], {searchPlaces: ['tsconfig.json'], loaders: {'.json': (_, content) => JSON5.parse(content)}});
126
- const {config: tsConfig, filepath: tsConfigPath} = (await tsConfigExplorer.search(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 || {};
137
+
138
+ let tsConfig;
139
+ let tsConfigPath;
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;
152
+ }
153
+
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
+ }
127
165
 
128
- options.tsConfigPath = await getTsConfigCachePath([options.filePath], options.tsConfigPath, options.cwd);
129
166
  options.ts = true;
130
- const config = makeTSConfig(tsConfig, tsConfigPath, [options.filePath]);
131
- await fs.mkdir(path.dirname(options.tsConfigPath), {recursive: true});
132
- await fs.writeFile(options.tsConfigPath, JSON.stringify(config));
133
167
  }
134
168
 
135
- return {options, prettierOptions};
169
+ return {options, prettierOptions, eslintConfigId};
136
170
  };
137
171
 
138
172
  /**
@@ -141,10 +175,14 @@ Hashing based on https://github.com/eslint/eslint/blob/cf38d0d939b62f3670cdd59f0
141
175
  */
142
176
  const getTsConfigCachePath = async (files, tsConfigPath, cwd) => {
143
177
  const {version} = await json.load('../package.json');
144
- return path.join(
145
- cacheLocation(cwd),
146
- `tsconfig.${murmur(`${version}_${nodeVersion}_${stringify({files: files.sort(), tsConfigPath})}`).result().toString(36)}.json`,
147
- );
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
+ };
148
186
  };
149
187
 
150
188
  const makeTSConfig = (tsConfig, tsConfigPath, files) => {
@@ -437,7 +475,7 @@ const mergeWithPrettierConfig = (options, prettierOptions) => {
437
475
 
438
476
  const buildTSConfig = options => config => {
439
477
  if (options.ts) {
440
- config.baseConfig.extends.push('xo-typescript');
478
+ config.baseConfig.extends.push(require.resolve('eslint-config-xo-typescript'));
441
479
  config.baseConfig.parser = require.resolve('@typescript-eslint/parser');
442
480
  config.baseConfig.parserOptions = {
443
481
  ...config.baseConfig.parserOptions,
@@ -538,13 +576,14 @@ const gatherImportResolvers = options => {
538
576
 
539
577
  const parseOptions = async options => {
540
578
  options = normalizeOptions(options);
541
- const {options: foundOptions, prettierOptions} = await mergeWithFileConfig(options);
579
+ const {options: foundOptions, prettierOptions, eslintConfigId} = await mergeWithFileConfig(options);
542
580
  const {filePath, warnIgnored, ...eslintOptions} = buildConfig(foundOptions, prettierOptions);
543
581
  return {
544
582
  filePath,
545
583
  warnIgnored,
546
584
  isQuiet: options.quiet,
547
585
  eslintOptions,
586
+ eslintConfigId,
548
587
  };
549
588
  };
550
589
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xo",
3
- "version": "0.45.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,6 +16,7 @@
16
16
  "node": ">=12.20"
17
17
  },
18
18
  "scripts": {
19
+ "test:clean": "find ./test -type d -name 'node_modules' -prune -not -path ./test/fixtures/project/node_modules -exec rm -rf '{}' +",
19
20
  "test": "node cli.js && nyc ava"
20
21
  },
21
22
  "files": [
@@ -51,30 +52,34 @@
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": "^1.0.1",
56
- "@typescript-eslint/eslint-plugin": "^4.32.0",
57
- "@typescript-eslint/parser": "^4.32.0",
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
65
  "cosmiconfig": "^7.0.1",
60
66
  "define-lazy-prop": "^3.0.0",
61
- "eslint": "^7.32.0",
67
+ "eslint": "^8.1.0",
62
68
  "eslint-config-prettier": "^8.3.0",
63
69
  "eslint-config-xo": "^0.39.0",
64
- "eslint-config-xo-typescript": "^0.45.0",
70
+ "eslint-config-xo-typescript": "^0.47.0",
65
71
  "eslint-formatter-pretty": "^4.1.0",
66
- "eslint-import-resolver-webpack": "^0.13.1",
67
- "eslint-plugin-ava": "^13.0.0",
72
+ "eslint-import-resolver-webpack": "^0.13.2",
73
+ "eslint-plugin-ava": "^13.1.0",
68
74
  "eslint-plugin-eslint-comments": "^3.2.0",
69
- "eslint-plugin-import": "^2.24.2",
75
+ "eslint-plugin-import": "^2.25.2",
70
76
  "eslint-plugin-no-use-extend-native": "^0.5.0",
71
77
  "eslint-plugin-node": "^11.1.0",
72
78
  "eslint-plugin-prettier": "^4.0.0",
73
- "eslint-plugin-promise": "^5.1.0",
74
- "eslint-plugin-unicorn": "^36.0.0",
79
+ "eslint-plugin-unicorn": "^37.0.1",
75
80
  "esm-utils": "^2.0.0",
76
81
  "find-cache-dir": "^3.3.2",
77
- "find-up": "^6.1.0",
82
+ "find-up": "^6.2.0",
78
83
  "get-stdin": "^9.0.0",
79
84
  "globby": "^12.0.2",
80
85
  "imurmurhash": "^0.1.4",
@@ -88,7 +93,7 @@
88
93
  "semver": "^7.3.5",
89
94
  "slash": "^4.0.0",
90
95
  "to-absolute-glob": "^2.0.2",
91
- "typescript": "^4.4.3"
96
+ "typescript": "^4.4.4"
92
97
  },
93
98
  "devDependencies": {
94
99
  "ava": "^3.15.0",
@@ -99,7 +104,7 @@
99
104
  "nyc": "^15.1.0",
100
105
  "proxyquire": "^2.1.3",
101
106
  "temp-write": "^5.0.0",
102
- "webpack": "^5.56.0"
107
+ "webpack": "^5.60.0"
103
108
  },
104
109
  "xo": {
105
110
  "ignores": [
package/readme.md CHANGED
@@ -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
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
- - [bracketSameLine](https://prettier.io/docs/en/options.html#bracket-line): `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