xo 1.2.3 → 2.0.1
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/dist/cli.d.ts +4 -0
- package/dist/cli.js +7 -2
- package/dist/lib/config.js +57 -41
- package/dist/lib/handle-ts-files.d.ts +11 -8
- package/dist/lib/handle-ts-files.js +88 -14
- package/dist/lib/open-report.js +2 -2
- package/dist/lib/resolve-config.js +2 -3
- package/dist/lib/rules/no-use-extend-native.d.ts +3 -0
- package/dist/lib/rules/no-use-extend-native.js +386 -0
- package/dist/lib/types.d.ts +3 -1
- package/dist/lib/utils.d.ts +3 -3
- package/dist/lib/utils.js +30 -15
- package/dist/lib/xo-to-eslint.js +59 -4
- package/dist/lib/xo.d.ts +25 -1
- package/dist/lib/xo.js +201 -26
- package/package.json +42 -37
- package/readme.md +72 -5
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ const cli = meow(`
|
|
|
16
16
|
|
|
17
17
|
Options
|
|
18
18
|
--fix Automagically fix issues
|
|
19
|
+
--fix-dry-run Automagically fix issues without saving the changes to the file system
|
|
19
20
|
--reporter Reporter to use
|
|
20
21
|
--space Use space indent instead of tabs [Default: 2]
|
|
21
22
|
--config Path to a XO configuration file
|
|
@@ -46,6 +47,10 @@ const cli = meow(`
|
|
|
46
47
|
type: 'boolean',
|
|
47
48
|
default: false,
|
|
48
49
|
},
|
|
50
|
+
fixDryRun: {
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
default: false,
|
|
53
|
+
},
|
|
49
54
|
reporter: {
|
|
50
55
|
type: 'string',
|
|
51
56
|
},
|
|
@@ -105,7 +110,7 @@ const baseXoConfigOptions = {
|
|
|
105
110
|
react: cliOptions.react,
|
|
106
111
|
};
|
|
107
112
|
const linterOptions = {
|
|
108
|
-
fix: cliOptions.fix,
|
|
113
|
+
fix: cliOptions.fix || cliOptions.fixDryRun,
|
|
109
114
|
cwd: (cliOptions.cwd && path.resolve(cliOptions.cwd)) ?? process.cwd(),
|
|
110
115
|
quiet: cliOptions.quiet,
|
|
111
116
|
ts: true,
|
|
@@ -171,7 +176,7 @@ if (cliOptions.stdin) {
|
|
|
171
176
|
await fs.writeFile(cliOptions.stdinFilename, stdin);
|
|
172
177
|
}
|
|
173
178
|
}
|
|
174
|
-
if (
|
|
179
|
+
if (linterOptions.fix) {
|
|
175
180
|
const xo = new Xo(linterOptions, baseXoConfigOptions);
|
|
176
181
|
const { results: [result] } = await xo.lintText(stdin, {
|
|
177
182
|
filePath: cliOptions.stdinFilename,
|
package/dist/lib/config.js
CHANGED
|
@@ -3,50 +3,61 @@ import pluginUnicorn from 'eslint-plugin-unicorn';
|
|
|
3
3
|
import pluginImport from 'eslint-plugin-import-x';
|
|
4
4
|
import pluginN from 'eslint-plugin-n';
|
|
5
5
|
import pluginComments from '@eslint-community/eslint-plugin-eslint-comments';
|
|
6
|
-
import pluginPromise from 'eslint-plugin-promise';
|
|
7
|
-
import pluginNoUseExtendNative from 'eslint-plugin-no-use-extend-native';
|
|
6
|
+
/// import pluginPromise from 'eslint-plugin-promise';
|
|
8
7
|
import configXoTypescript from 'eslint-config-xo-typescript';
|
|
9
|
-
import stylisticPlugin from '@stylistic/eslint-plugin';
|
|
10
8
|
import globals from 'globals';
|
|
9
|
+
import { fixupPluginRules } from '@eslint/compat';
|
|
11
10
|
import { defaultIgnores, tsExtensions, tsFilesGlob, allFilesGlob, jsExtensions, allExtensions, } from './constants.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
if (!configXoTypescript[1]) {
|
|
11
|
+
import noUseExtendNativeRule from './rules/no-use-extend-native.js';
|
|
12
|
+
if (!configXoTypescript[4]) {
|
|
16
13
|
throw new Error('Invalid eslint-config-xo-typescript');
|
|
17
14
|
}
|
|
15
|
+
const baseLanguageOptions = configXoTypescript[0]?.languageOptions;
|
|
16
|
+
const baseParserOptions = baseLanguageOptions?.parserOptions ?? {};
|
|
17
|
+
const typescriptLanguageOptions = (configXoTypescript[4]?.languageOptions ?? {});
|
|
18
|
+
const typescriptParserOptions = typescriptLanguageOptions.parserOptions ?? {};
|
|
19
|
+
const pluginNoUseExtendNative = {
|
|
20
|
+
rules: {
|
|
21
|
+
'no-use-extend-native': noUseExtendNativeRule,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
// TODO: Remove `fixupPluginRules` wrapping when these plugins support ESLint 10 natively.
|
|
25
|
+
const fixedUpBasePlugins = Object.fromEntries(Object.entries(configXoTypescript[0]?.plugins ?? {}).map(([key, plugin]) => [key, fixupPluginRules(plugin)]));
|
|
26
|
+
const fixedUpTypescriptPlugins = Object.fromEntries(Object.entries(configXoTypescript[4]?.plugins ?? {}).map(([key, plugin]) => [key, fixupPluginRules(plugin)]));
|
|
18
27
|
/**
|
|
19
28
|
The base config that XO builds on top of from user options.
|
|
20
29
|
*/
|
|
21
30
|
export const config = [
|
|
22
31
|
{
|
|
23
|
-
name: '
|
|
32
|
+
name: 'xo/ignores',
|
|
24
33
|
ignores: defaultIgnores,
|
|
25
34
|
},
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
36
|
+
...pluginAva.configs['recommended'],
|
|
26
37
|
{
|
|
27
|
-
name: '
|
|
28
|
-
files: [
|
|
29
|
-
allFilesGlob,
|
|
30
|
-
],
|
|
38
|
+
name: 'xo/base',
|
|
39
|
+
files: [allFilesGlob],
|
|
31
40
|
plugins: {
|
|
41
|
+
...fixedUpBasePlugins,
|
|
42
|
+
...fixedUpTypescriptPlugins,
|
|
32
43
|
'no-use-extend-native': pluginNoUseExtendNative,
|
|
33
44
|
ava: pluginAva,
|
|
34
45
|
unicorn: pluginUnicorn,
|
|
35
46
|
'import-x': pluginImport,
|
|
36
|
-
n: pluginN,
|
|
37
47
|
'@eslint-community/eslint-comments': pluginComments,
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
// TODO: Remove `fixupPluginRules` wrapping when these plugins support ESLint 10 natively.
|
|
49
|
+
n: fixupPluginRules(pluginN),
|
|
50
|
+
/// promise: fixupPluginRules(pluginPromise),
|
|
40
51
|
},
|
|
41
52
|
languageOptions: {
|
|
42
53
|
globals: {
|
|
43
54
|
...globals.es2021,
|
|
44
55
|
...globals.node,
|
|
45
56
|
},
|
|
46
|
-
ecmaVersion:
|
|
47
|
-
sourceType:
|
|
57
|
+
ecmaVersion: baseLanguageOptions?.ecmaVersion,
|
|
58
|
+
sourceType: baseLanguageOptions?.sourceType,
|
|
48
59
|
parserOptions: {
|
|
49
|
-
...
|
|
60
|
+
...baseParserOptions,
|
|
50
61
|
},
|
|
51
62
|
},
|
|
52
63
|
settings: {
|
|
@@ -71,7 +82,6 @@ export const config = [
|
|
|
71
82
|
These are the base rules that are always applied to all js and ts file types
|
|
72
83
|
*/
|
|
73
84
|
rules: {
|
|
74
|
-
...pluginAva?.configs?.['recommended']?.rules,
|
|
75
85
|
...pluginUnicorn.configs?.recommended?.rules,
|
|
76
86
|
'no-use-extend-native/no-use-extend-native': 'error',
|
|
77
87
|
// TODO: Remove this override at some point.
|
|
@@ -203,23 +213,24 @@ export const config = [
|
|
|
203
213
|
'unicorn/no-useless-undefined': 'off',
|
|
204
214
|
// TODO: Temporarily disabled as the rule is buggy.
|
|
205
215
|
'function-call-argument-newline': 'off',
|
|
206
|
-
'
|
|
207
|
-
'promise/
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
'promise/no-
|
|
215
|
-
'promise/
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
'promise/
|
|
216
|
+
// Commented out because it's not ready for ESLint 10.
|
|
217
|
+
// 'promise/param-names': 'error',
|
|
218
|
+
// 'promise/no-return-wrap': [
|
|
219
|
+
// 'error',
|
|
220
|
+
// {
|
|
221
|
+
// allowReject: true,
|
|
222
|
+
// },
|
|
223
|
+
// ],
|
|
224
|
+
// 'promise/no-new-statics': 'error',
|
|
225
|
+
// 'promise/no-return-in-finally': 'error',
|
|
226
|
+
// 'promise/prefer-await-to-then': [
|
|
227
|
+
// 'error',
|
|
228
|
+
// {
|
|
229
|
+
// strict: true,
|
|
230
|
+
// },
|
|
231
|
+
// ],
|
|
232
|
+
// 'promise/prefer-catch': 'error',
|
|
233
|
+
// 'promise/valid-params': 'error',
|
|
223
234
|
'import-x/default': 'error',
|
|
224
235
|
'import-x/export': 'error',
|
|
225
236
|
'import-x/extensions': [
|
|
@@ -362,17 +373,22 @@ export const config = [
|
|
|
362
373
|
},
|
|
363
374
|
},
|
|
364
375
|
{
|
|
365
|
-
name: '
|
|
366
|
-
plugins:
|
|
376
|
+
name: 'xo/typescript',
|
|
377
|
+
plugins: fixedUpTypescriptPlugins,
|
|
367
378
|
files: [tsFilesGlob],
|
|
368
379
|
languageOptions: {
|
|
369
|
-
...
|
|
380
|
+
...typescriptLanguageOptions,
|
|
381
|
+
parserOptions: {
|
|
382
|
+
...typescriptParserOptions,
|
|
383
|
+
// This needs to be explicitly set to `true`
|
|
384
|
+
projectService: true,
|
|
385
|
+
},
|
|
370
386
|
},
|
|
371
387
|
/**
|
|
372
388
|
This turns on rules in `typescript-eslint`` and turns off rules from ESLint that conflict.
|
|
373
389
|
*/
|
|
374
390
|
rules: {
|
|
375
|
-
...configXoTypescript[
|
|
391
|
+
...configXoTypescript[4]?.rules,
|
|
376
392
|
'unicorn/import-style': 'off',
|
|
377
393
|
'n/file-extension-in-import': 'off',
|
|
378
394
|
// Disabled because of https://github.com/benmosher/eslint-plugin-import-x/issues/1590
|
|
@@ -384,7 +400,7 @@ export const config = [
|
|
|
384
400
|
'import-x/named': 'off',
|
|
385
401
|
},
|
|
386
402
|
},
|
|
387
|
-
...configXoTypescript.slice(
|
|
403
|
+
...configXoTypescript.slice(5),
|
|
388
404
|
{
|
|
389
405
|
files: ['xo.config.{js,ts}'],
|
|
390
406
|
rules: {
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
1
2
|
/**
|
|
2
3
|
This function checks if the files are matched by the tsconfig include, exclude, and it returns the unmatched files.
|
|
3
4
|
|
|
4
|
-
If no tsconfig is found, it will create
|
|
5
|
+
If no tsconfig is found, it will create an in-memory TypeScript Program for type-aware linting.
|
|
5
6
|
|
|
6
7
|
@param options
|
|
7
|
-
@returns The unmatched files.
|
|
8
|
+
@returns The unmatched files and an in-memory TypeScript Program.
|
|
8
9
|
*/
|
|
9
|
-
export declare function handleTsconfig({ cwd,
|
|
10
|
-
cwd: string;
|
|
10
|
+
export declare function handleTsconfig({ files, cwd, cacheLocation }: {
|
|
11
11
|
files: string[];
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
cwd: string;
|
|
13
|
+
cacheLocation?: string;
|
|
14
|
+
}): {
|
|
15
|
+
existingFiles: string[];
|
|
16
|
+
virtualFiles: string[];
|
|
17
|
+
program: ts.Program | undefined;
|
|
18
|
+
};
|
|
@@ -1,39 +1,113 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import fs from 'node:fs
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import ts from 'typescript';
|
|
3
4
|
import { getTsconfig, createFilesMatcher } from 'get-tsconfig';
|
|
4
|
-
import { tsconfigDefaults
|
|
5
|
+
import { tsconfigDefaults } from './constants.js';
|
|
6
|
+
const createInMemoryProgram = (files, cwd) => {
|
|
7
|
+
if (files.length === 0) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const compilerOptions = getFallbackCompilerOptions(cwd);
|
|
12
|
+
const program = ts.createProgram(files, { ...compilerOptions });
|
|
13
|
+
Object.defineProperty(program, 'toJSON', {
|
|
14
|
+
value: () => ({
|
|
15
|
+
__type: 'TypeScriptProgram',
|
|
16
|
+
files: files.map(file => path.relative(cwd, file)),
|
|
17
|
+
}),
|
|
18
|
+
configurable: true,
|
|
19
|
+
});
|
|
20
|
+
return program;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.warn('XO: Failed to create TypeScript Program for type-aware linting. Continuing without type information for unincluded files.', error instanceof Error ? error.message : String(error));
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const fallbackCompilerOptionsCache = new Map();
|
|
28
|
+
const getFallbackCompilerOptions = (cwd) => {
|
|
29
|
+
const cacheKey = path.resolve(cwd);
|
|
30
|
+
const cached = fallbackCompilerOptionsCache.get(cacheKey);
|
|
31
|
+
if (cached) {
|
|
32
|
+
return cached;
|
|
33
|
+
}
|
|
34
|
+
const compilerOptionsResult = ts.convertCompilerOptionsFromJson(tsconfigDefaults.compilerOptions ?? {}, cacheKey);
|
|
35
|
+
if (compilerOptionsResult.errors.length > 0) {
|
|
36
|
+
throw new Error('XO: Invalid default TypeScript compiler options');
|
|
37
|
+
}
|
|
38
|
+
const compilerOptions = {
|
|
39
|
+
...compilerOptionsResult.options,
|
|
40
|
+
esModuleInterop: true,
|
|
41
|
+
resolveJsonModules: true,
|
|
42
|
+
allowJs: true,
|
|
43
|
+
skipLibCheck: true,
|
|
44
|
+
skipDefaultLibCheck: true,
|
|
45
|
+
};
|
|
46
|
+
fallbackCompilerOptionsCache.set(cacheKey, compilerOptions);
|
|
47
|
+
return compilerOptions;
|
|
48
|
+
};
|
|
5
49
|
/**
|
|
6
50
|
This function checks if the files are matched by the tsconfig include, exclude, and it returns the unmatched files.
|
|
7
51
|
|
|
8
|
-
If no tsconfig is found, it will create
|
|
52
|
+
If no tsconfig is found, it will create an in-memory TypeScript Program for type-aware linting.
|
|
9
53
|
|
|
10
54
|
@param options
|
|
11
|
-
@returns The unmatched files.
|
|
55
|
+
@returns The unmatched files and an in-memory TypeScript Program.
|
|
12
56
|
*/
|
|
13
|
-
export
|
|
57
|
+
export function handleTsconfig({ files, cwd, cacheLocation }) {
|
|
14
58
|
const unincludedFiles = [];
|
|
59
|
+
const filesMatcherCache = new Map();
|
|
15
60
|
for (const filePath of files) {
|
|
16
61
|
const result = getTsconfig(filePath);
|
|
17
62
|
if (!result) {
|
|
18
63
|
unincludedFiles.push(filePath);
|
|
19
64
|
continue;
|
|
20
65
|
}
|
|
21
|
-
const
|
|
66
|
+
const cacheKey = result.path ? path.resolve(result.path) : filePath;
|
|
67
|
+
let filesMatcher = filesMatcherCache.get(cacheKey);
|
|
68
|
+
if (!filesMatcher) {
|
|
69
|
+
filesMatcher = createFilesMatcher(result);
|
|
70
|
+
filesMatcherCache.set(cacheKey, filesMatcher);
|
|
71
|
+
}
|
|
22
72
|
if (filesMatcher(filePath)) {
|
|
23
73
|
continue;
|
|
24
74
|
}
|
|
25
75
|
unincludedFiles.push(filePath);
|
|
26
76
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
77
|
+
if (unincludedFiles.length === 0) {
|
|
78
|
+
return { existingFiles: [], virtualFiles: [], program: undefined };
|
|
79
|
+
}
|
|
80
|
+
// Separate real files from virtual/cache files
|
|
81
|
+
// Virtual files include: stdin files (in cache dir), non-existent files
|
|
82
|
+
// TypeScript will surface opaque diagnostics for missing files; pre-filter so we only pay the program cost for real files.
|
|
83
|
+
const existingFiles = [];
|
|
84
|
+
const virtualFiles = [];
|
|
85
|
+
for (const file of unincludedFiles) {
|
|
86
|
+
const fileExists = fs.existsSync(file);
|
|
87
|
+
// Files that don't exist are always virtual
|
|
88
|
+
if (!fileExists) {
|
|
89
|
+
virtualFiles.push(file);
|
|
90
|
+
continue;
|
|
32
91
|
}
|
|
33
|
-
|
|
34
|
-
|
|
92
|
+
// Check if file is in cache directory (like stdin files)
|
|
93
|
+
// These need tsconfig treatment even though they exist on disk
|
|
94
|
+
if (cacheLocation) {
|
|
95
|
+
const absolutePath = path.resolve(file);
|
|
96
|
+
const cacheRoot = path.resolve(cacheLocation);
|
|
97
|
+
const relativeToCache = path.relative(cacheRoot, absolutePath);
|
|
98
|
+
// File is inside cache if relative path doesn't escape (no '..')
|
|
99
|
+
const isInCache = !relativeToCache.startsWith('..') && !path.isAbsolute(relativeToCache);
|
|
100
|
+
if (isInCache) {
|
|
101
|
+
virtualFiles.push(file);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
35
104
|
}
|
|
105
|
+
existingFiles.push(file);
|
|
36
106
|
}
|
|
37
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
existingFiles,
|
|
109
|
+
virtualFiles,
|
|
110
|
+
program: createInMemoryProgram(existingFiles, cwd),
|
|
111
|
+
};
|
|
38
112
|
}
|
|
39
113
|
//# sourceMappingURL=handle-ts-files.js.map
|
package/dist/lib/open-report.js
CHANGED
|
@@ -2,7 +2,7 @@ import openEditor from 'open-editor';
|
|
|
2
2
|
const sortResults = (a, b) => a.errorCount + b.errorCount > 0 ? (a.errorCount - b.errorCount) : (a.warningCount - b.warningCount);
|
|
3
3
|
const resultToFile = (result) => {
|
|
4
4
|
const [message] = result.messages
|
|
5
|
-
.
|
|
5
|
+
.toSorted((a, b) => {
|
|
6
6
|
if (a.severity < b.severity) {
|
|
7
7
|
return 1;
|
|
8
8
|
}
|
|
@@ -25,7 +25,7 @@ const resultToFile = (result) => {
|
|
|
25
25
|
};
|
|
26
26
|
const getFiles = (report, predicate) => report.results
|
|
27
27
|
.filter(result => predicate(result))
|
|
28
|
-
.
|
|
28
|
+
.toSorted(sortResults)
|
|
29
29
|
.map(result => resultToFile(result));
|
|
30
30
|
const openReport = async (report) => {
|
|
31
31
|
const count = report.errorCount > 0 ? 'errorCount' : 'warningCount';
|
|
@@ -17,14 +17,11 @@ export async function resolveXoConfig(options) {
|
|
|
17
17
|
searchPlaces: [
|
|
18
18
|
'package.json',
|
|
19
19
|
`${moduleName}.config.js`,
|
|
20
|
-
`${moduleName}.config.cjs`,
|
|
21
20
|
`${moduleName}.config.mjs`,
|
|
22
21
|
`${moduleName}.config.ts`,
|
|
23
|
-
`${moduleName}.config.cts`,
|
|
24
22
|
`${moduleName}.config.mts`,
|
|
25
23
|
],
|
|
26
24
|
loaders: {
|
|
27
|
-
'.cts': defaultLoaders['.ts'], // eslint-disable-line @typescript-eslint/naming-convention
|
|
28
25
|
'.mts': defaultLoaders['.ts'], // eslint-disable-line @typescript-eslint/naming-convention
|
|
29
26
|
},
|
|
30
27
|
stopDir: stopDirectory,
|
|
@@ -33,7 +30,9 @@ export async function resolveXoConfig(options) {
|
|
|
33
30
|
options.filePath &&= path.resolve(options.cwd, options.filePath);
|
|
34
31
|
const searchPath = options.filePath ?? options.cwd;
|
|
35
32
|
let { config: flatOptions = [], filepath: flatConfigPath = '', } = await (options.configPath
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
36
34
|
? flatConfigExplorer.load(path.resolve(options.cwd, options.configPath))
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
37
36
|
: flatConfigExplorer.search(searchPath)) ?? {};
|
|
38
37
|
flatOptions = arrify(flatOptions);
|
|
39
38
|
return {
|