xo 1.2.2 → 2.0.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/dist/lib/config.js +57 -41
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.js +2 -1
- 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/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 +60 -4
- package/dist/lib/xo.d.ts +25 -1
- package/dist/lib/xo.js +201 -26
- package/package.json +39 -36
- package/readme.md +73 -7
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: {
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ Only applies to options defined as an Array.
|
|
|
7
7
|
*/
|
|
8
8
|
export declare const tsExtensions: string[];
|
|
9
9
|
export declare const jsExtensions: string[];
|
|
10
|
+
export declare const frameworkExtensions: string[];
|
|
10
11
|
export declare const jsFilesGlob: string;
|
|
11
12
|
export declare const tsFilesGlob: string;
|
|
12
13
|
export declare const allExtensions: string[];
|
package/dist/lib/constants.js
CHANGED
|
@@ -16,9 +16,10 @@ Only applies to options defined as an Array.
|
|
|
16
16
|
*/
|
|
17
17
|
export const tsExtensions = ['ts', 'tsx', 'cts', 'mts'];
|
|
18
18
|
export const jsExtensions = ['js', 'jsx', 'mjs', 'cjs'];
|
|
19
|
+
export const frameworkExtensions = ['vue', 'svelte', 'astro'];
|
|
19
20
|
export const jsFilesGlob = `**/*.{${jsExtensions.join(',')}}`;
|
|
20
21
|
export const tsFilesGlob = `**/*.{${tsExtensions.join(',')}}`;
|
|
21
|
-
export const allExtensions = [...jsExtensions, ...tsExtensions];
|
|
22
|
+
export const allExtensions = [...jsExtensions, ...tsExtensions, ...frameworkExtensions];
|
|
22
23
|
export const allFilesGlob = `**/*.{${allExtensions.join(',')}}`;
|
|
23
24
|
export const moduleName = 'xo';
|
|
24
25
|
export const tsconfigDefaults = {
|
|
@@ -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 {
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -71,9 +71,11 @@ export type XoConfigItem = Simplify<XoConfigOptions & Omit<Linter.Config, 'files
|
|
|
71
71
|
/**
|
|
72
72
|
An array of glob patterns indicating the files that the configuration object should apply to. If not specified, the configuration object applies to all files.
|
|
73
73
|
|
|
74
|
+
Accepts a single glob string, an array of globs, or ESLint's native format where nested arrays create AND patterns.
|
|
75
|
+
|
|
74
76
|
@see [Ignore Patterns](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new#excluding-files-with-ignores)
|
|
75
77
|
*/
|
|
76
|
-
files?: string | string[] | undefined;
|
|
78
|
+
files?: string | Array<string | string[]> | undefined;
|
|
77
79
|
/**
|
|
78
80
|
An array of glob patterns indicating the files that the configuration object should not apply to. If not specified, the configuration object applies to all files matched by files.
|
|
79
81
|
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Linter } from 'eslint';
|
|
2
|
-
import { type SetRequired } from 'type-fest';
|
|
3
2
|
import { type XoConfigItem } from './types.js';
|
|
3
|
+
export declare const typescriptParser: Linter.Parser | undefined;
|
|
4
4
|
/**
|
|
5
5
|
Convert a `xo` config item to an ESLint config item.
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ Files and rules will always be defined and all other ESLint config properties ar
|
|
|
11
11
|
@param xoConfig
|
|
12
12
|
@returns eslintConfig
|
|
13
13
|
*/
|
|
14
|
-
export declare const xoToEslintConfigItem: (xoConfig: XoConfigItem) =>
|
|
14
|
+
export declare const xoToEslintConfigItem: (xoConfig: XoConfigItem) => Linter.Config;
|
|
15
15
|
/**
|
|
16
16
|
Function used to match files which should be included in the `tsconfig.json` files.
|
|
17
17
|
|
|
@@ -21,7 +21,7 @@ Function used to match files which should be included in the `tsconfig.json` fil
|
|
|
21
21
|
@param ignores - The globs to ignore when matching the files.
|
|
22
22
|
@returns An array of file paths that match the globs and do not match the ignores.
|
|
23
23
|
*/
|
|
24
|
-
export declare const matchFilesForTsConfig: (cwd: string, files: string[], globs: string[], ignores: string[]) => string[];
|
|
24
|
+
export declare const matchFilesForTsConfig: (cwd: string, files: string[] | undefined, globs: string[], ignores: string[]) => string[];
|
|
25
25
|
/**
|
|
26
26
|
Once a config is resolved, it is pre-processed to ensure that all properties are set correctly.
|
|
27
27
|
|
package/dist/lib/utils.js
CHANGED
|
@@ -3,6 +3,14 @@ import micromatch from 'micromatch';
|
|
|
3
3
|
import arrify from 'arrify';
|
|
4
4
|
import configXoTypescript from 'eslint-config-xo-typescript';
|
|
5
5
|
import { allFilesGlob, jsExtensions, jsFilesGlob, } from './constants.js';
|
|
6
|
+
const typescriptParserConfig = configXoTypescript.find(config => {
|
|
7
|
+
const languageOptions = config.languageOptions;
|
|
8
|
+
return languageOptions?.parser;
|
|
9
|
+
});
|
|
10
|
+
export const typescriptParser = typescriptParserConfig?.languageOptions?.parser;
|
|
11
|
+
if (!typescriptParser) {
|
|
12
|
+
throw new Error('XO: Failed to locate TypeScript parser in eslint-config-xo-typescript');
|
|
13
|
+
}
|
|
6
14
|
/**
|
|
7
15
|
Convert a `xo` config item to an ESLint config item.
|
|
8
16
|
|
|
@@ -17,8 +25,8 @@ export const xoToEslintConfigItem = (xoConfig) => {
|
|
|
17
25
|
const { files, rules, space, prettier, ignores, semicolon, react, ..._xoConfig } = xoConfig;
|
|
18
26
|
const eslintConfig = {
|
|
19
27
|
..._xoConfig,
|
|
20
|
-
files: arrify(xoConfig.files
|
|
21
|
-
rules: xoConfig.rules
|
|
28
|
+
...(xoConfig.files ? { files: arrify(xoConfig.files) } : {}),
|
|
29
|
+
...(xoConfig.rules ? { rules: xoConfig.rules } : {}),
|
|
22
30
|
};
|
|
23
31
|
eslintConfig.ignores &&= arrify(xoConfig.ignores);
|
|
24
32
|
return eslintConfig;
|
|
@@ -32,7 +40,7 @@ Function used to match files which should be included in the `tsconfig.json` fil
|
|
|
32
40
|
@param ignores - The globs to ignore when matching the files.
|
|
33
41
|
@returns An array of file paths that match the globs and do not match the ignores.
|
|
34
42
|
*/
|
|
35
|
-
export const matchFilesForTsConfig = (cwd, files, globs, ignores) => micromatch(files
|
|
43
|
+
export const matchFilesForTsConfig = (cwd, files, globs, ignores) => micromatch(files?.map(file => path.normalize(path.relative(cwd, file))) ?? [],
|
|
36
44
|
// https://github.com/micromatch/micromatch/issues/217
|
|
37
45
|
globs.map(glob => path.normalize(glob)), {
|
|
38
46
|
dot: true,
|
|
@@ -47,11 +55,14 @@ This includes ensuring that user-defined properties can override XO defaults, an
|
|
|
47
55
|
@param xoConfig - The flat XO config to pre-process.
|
|
48
56
|
@returns The pre-processed flat XO config.
|
|
49
57
|
*/
|
|
58
|
+
// eslint-disable-next-line complexity
|
|
50
59
|
export const preProcessXoConfig = (xoConfig) => {
|
|
51
60
|
const tsFilesGlob = [];
|
|
52
61
|
const tsFilesIgnoresGlob = [];
|
|
53
62
|
const processedConfig = [];
|
|
54
63
|
for (const [idx, { ...config }] of xoConfig.entries()) {
|
|
64
|
+
const languageOptions = config.languageOptions;
|
|
65
|
+
const parserOptions = languageOptions?.parserOptions;
|
|
55
66
|
// We can skip the first config item, as it is the base config item.
|
|
56
67
|
if (idx === 0) {
|
|
57
68
|
processedConfig.push(config);
|
|
@@ -60,8 +71,10 @@ export const preProcessXoConfig = (xoConfig) => {
|
|
|
60
71
|
// Use TS parser/plugin for JS files if the config contains TypeScript rules which are applied to JS files.
|
|
61
72
|
// typescript-eslint rules set to "off" are ignored and not applied to JS files.
|
|
62
73
|
if (config.rules
|
|
63
|
-
|
|
64
|
-
&& !
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
75
|
+
&& !languageOptions?.['parser']
|
|
76
|
+
&& parserOptions?.project === undefined
|
|
77
|
+
&& parserOptions?.programs === undefined
|
|
65
78
|
&& !config.plugins?.['@typescript-eslint']) {
|
|
66
79
|
const hasTsRules = Object.entries(config.rules).some(rulePair => {
|
|
67
80
|
// If its not a @typescript-eslint rule, we don't care
|
|
@@ -77,7 +90,7 @@ export const preProcessXoConfig = (xoConfig) => {
|
|
|
77
90
|
if (hasTsRules) {
|
|
78
91
|
let isAppliedToJsFiles = false;
|
|
79
92
|
if (config.files) {
|
|
80
|
-
const normalizedFiles = arrify(config.files).map(file => path.normalize(file));
|
|
93
|
+
const normalizedFiles = arrify(config.files).flat().map(file => path.normalize(file));
|
|
81
94
|
// Strip the basename off any globs
|
|
82
95
|
const globs = normalizedFiles.map(file => micromatch.scan(file, { dot: true }).glob).filter(Boolean);
|
|
83
96
|
// Check if the files globs match a test file with a js extension
|
|
@@ -89,25 +102,27 @@ export const preProcessXoConfig = (xoConfig) => {
|
|
|
89
102
|
isAppliedToJsFiles = true;
|
|
90
103
|
}
|
|
91
104
|
if (isAppliedToJsFiles) {
|
|
92
|
-
|
|
105
|
+
const updatedLanguageOptions = languageOptions
|
|
106
|
+
? { ...languageOptions, parser: typescriptParser }
|
|
107
|
+
: { parser: typescriptParser };
|
|
108
|
+
config.languageOptions = updatedLanguageOptions;
|
|
93
109
|
config.plugins ??= {};
|
|
94
110
|
config.plugins = {
|
|
95
111
|
...config.plugins,
|
|
96
112
|
...configXoTypescript[1]?.plugins,
|
|
97
113
|
};
|
|
98
|
-
config.
|
|
99
|
-
tsFilesGlob.push(...arrify(config.files ?? allFilesGlob));
|
|
114
|
+
tsFilesGlob.push(...arrify(config.files ?? allFilesGlob).flat());
|
|
100
115
|
tsFilesIgnoresGlob.push(...arrify(config.ignores));
|
|
101
116
|
}
|
|
102
117
|
}
|
|
103
118
|
}
|
|
104
|
-
// If
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
||
|
|
108
|
-
||
|
|
119
|
+
// If the config sets `parserOptions.project`, `projectService`, `tsconfigRootDir`, or `programs`, treat those files as opt-out for XO's automatic program wiring.
|
|
120
|
+
if (parserOptions?.project !== undefined
|
|
121
|
+
|| parserOptions?.projectService !== undefined
|
|
122
|
+
|| parserOptions?.tsconfigRootDir !== undefined
|
|
123
|
+
|| parserOptions?.programs !== undefined) {
|
|
109
124
|
// The glob itself should NOT be negated
|
|
110
|
-
tsFilesIgnoresGlob.push(...arrify(config.files ?? allFilesGlob));
|
|
125
|
+
tsFilesIgnoresGlob.push(...arrify(config.files ?? allFilesGlob).flat());
|
|
111
126
|
}
|
|
112
127
|
processedConfig.push(config);
|
|
113
128
|
}
|
package/dist/lib/xo-to-eslint.js
CHANGED
|
@@ -4,13 +4,57 @@ import arrify from 'arrify';
|
|
|
4
4
|
import configReact from 'eslint-config-xo-react';
|
|
5
5
|
import pluginPrettier from 'eslint-plugin-prettier';
|
|
6
6
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
|
7
|
+
import { fixupConfigRules, fixupPluginRules } from '@eslint/compat';
|
|
7
8
|
import { config } from './config.js';
|
|
8
9
|
import { xoToEslintConfigItem } from './utils.js';
|
|
9
10
|
/**
|
|
11
|
+
Merge all plugins from every config into a single config entry at the start of the array, ensuring user-provided plugins take precedence. This avoids ESLint's flat config rejecting duplicate plugin names.
|
|
12
|
+
*/
|
|
13
|
+
const hoistPlugins = (configs, userPluginOverrides) => {
|
|
14
|
+
const plugins = {};
|
|
15
|
+
const configsWithoutPlugins = [];
|
|
16
|
+
for (const configItem of configs) {
|
|
17
|
+
const { plugins: configPlugins } = configItem;
|
|
18
|
+
if (!configPlugins) {
|
|
19
|
+
configsWithoutPlugins.push(configItem);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
// ESLint flat config rejects duplicate plugin names, so merge all plugins into one config.
|
|
23
|
+
Object.assign(plugins, configPlugins);
|
|
24
|
+
const { plugins: _ignored, ...configWithoutPlugins } = configItem;
|
|
25
|
+
if (Object.keys(configWithoutPlugins).length > 0) {
|
|
26
|
+
configsWithoutPlugins.push(configWithoutPlugins);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const [pluginName, plugin] of userPluginOverrides) {
|
|
30
|
+
plugins[pluginName] = plugin;
|
|
31
|
+
}
|
|
32
|
+
if (Object.keys(plugins).length === 0) {
|
|
33
|
+
return configsWithoutPlugins;
|
|
34
|
+
}
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
name: 'xo/plugins',
|
|
38
|
+
plugins,
|
|
39
|
+
},
|
|
40
|
+
...configsWithoutPlugins,
|
|
41
|
+
];
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
10
44
|
Takes a XO flat config and returns an ESlint flat config.
|
|
11
45
|
*/
|
|
12
46
|
export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
13
47
|
const baseConfig = [...config];
|
|
48
|
+
const userPluginOverrides = new Map();
|
|
49
|
+
for (const xoConfigItem of flatXoConfig ?? []) {
|
|
50
|
+
const { plugins } = xoConfigItem;
|
|
51
|
+
if (!plugins) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
for (const [pluginName, plugin] of Object.entries(plugins)) {
|
|
55
|
+
userPluginOverrides.set(pluginName, plugin);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
14
58
|
/**
|
|
15
59
|
Since configs are merged and the last config takes precedence this means we need to handle both true AND false cases for each option. For example, we need to turn `prettier`, `space`, `semi`, etc. on or off for a specific file.
|
|
16
60
|
*/
|
|
@@ -35,6 +79,7 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
35
79
|
*/
|
|
36
80
|
const eslintConfigItem = xoToEslintConfigItem(xoConfigItem);
|
|
37
81
|
if (xoConfigItem.semicolon === false) {
|
|
82
|
+
eslintConfigItem.rules ??= {};
|
|
38
83
|
eslintConfigItem.rules['@stylistic/semi'] = ['error', 'never'];
|
|
39
84
|
eslintConfigItem.rules['@stylistic/semi-spacing'] = [
|
|
40
85
|
'error',
|
|
@@ -43,6 +88,7 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
43
88
|
}
|
|
44
89
|
if (xoConfigItem.space) {
|
|
45
90
|
const spaces = typeof xoConfigItem.space === 'number' ? xoConfigItem.space : 2;
|
|
91
|
+
eslintConfigItem.rules ??= {};
|
|
46
92
|
eslintConfigItem.rules['@stylistic/indent'] = [
|
|
47
93
|
'error',
|
|
48
94
|
spaces,
|
|
@@ -54,12 +100,14 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
54
100
|
else if (xoConfigItem.space === false) {
|
|
55
101
|
// If a user sets this to false for a small subset of files for some reason,
|
|
56
102
|
// then we need to set them back to their original values.
|
|
103
|
+
eslintConfigItem.rules ??= {};
|
|
57
104
|
eslintConfigItem.rules['@stylistic/indent'] = configXoTypescript[1]?.rules?.['@stylistic/indent'];
|
|
58
105
|
eslintConfigItem.rules['@stylistic/indent-binary-ops'] = configXoTypescript[1]?.rules?.['@stylistic/indent-binary-ops'];
|
|
59
106
|
}
|
|
60
107
|
if (xoConfigItem.react) {
|
|
61
108
|
// Ensure the files applied to the React config are the same as the config they are derived from
|
|
62
|
-
|
|
109
|
+
// TODO: Remove `fixupConfigRules` wrapping when eslint-config-xo-react supports ESLint 10 natively.
|
|
110
|
+
baseConfig.push({ ...fixupConfigRules(configReact)[0], files: eslintConfigItem.files, name: 'xo/react' });
|
|
63
111
|
}
|
|
64
112
|
// Prettier should generally be the last config in the array
|
|
65
113
|
if (xoConfigItem.prettier) {
|
|
@@ -68,7 +116,8 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
68
116
|
}
|
|
69
117
|
else {
|
|
70
118
|
// Validate that Prettier options match other `xoConfig` options.
|
|
71
|
-
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
120
|
+
if ((xoConfigItem.semicolon && prettierOptions.semi === false) || (!xoConfigItem.semicolon && prettierOptions.semi === true)) {
|
|
72
121
|
throw new Error(`The Prettier config \`semi\` is ${prettierOptions.semi} while Xo \`semicolon\` is ${xoConfigItem.semicolon}, also check your .editorconfig for inconsistencies.`);
|
|
73
122
|
}
|
|
74
123
|
if (((xoConfigItem.space ?? typeof xoConfigItem.space === 'number') && prettierOptions.useTabs === true) || (!xoConfigItem.space && prettierOptions.useTabs === false)) {
|
|
@@ -78,9 +127,10 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
78
127
|
throw new Error(`The Prettier config \`tabWidth\` is ${prettierOptions.tabWidth} while Xo \`space\` is ${xoConfigItem.space}, also check your .editorconfig for inconsistencies.`);
|
|
79
128
|
}
|
|
80
129
|
// Add Prettier plugin
|
|
130
|
+
// TODO: Remove `fixupPluginRules` wrapping when eslint-plugin-prettier supports ESLint 10 natively.
|
|
81
131
|
eslintConfigItem.plugins = {
|
|
82
132
|
...eslintConfigItem.plugins,
|
|
83
|
-
prettier: pluginPrettier,
|
|
133
|
+
prettier: fixupPluginRules(pluginPrettier),
|
|
84
134
|
};
|
|
85
135
|
const prettierConfig = {
|
|
86
136
|
singleQuote: true,
|
|
@@ -95,6 +145,7 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
95
145
|
// Configure Prettier rules
|
|
96
146
|
const rulesWithPrettier = {
|
|
97
147
|
...eslintConfigItem.rules,
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
98
149
|
...pluginPrettier.configs?.['recommended']?.rules,
|
|
99
150
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
100
151
|
'prettier/prettier': ['error', prettierConfig],
|
|
@@ -105,11 +156,16 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
|
|
|
105
156
|
}
|
|
106
157
|
else if (xoConfigItem.prettier === false) {
|
|
107
158
|
// Turn Prettier off for a subset of files
|
|
159
|
+
eslintConfigItem.rules ??= {};
|
|
108
160
|
eslintConfigItem.rules['prettier/prettier'] = 'off';
|
|
109
161
|
}
|
|
162
|
+
if (Object.keys(eslintConfigItem).length === 0) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
110
165
|
baseConfig.push(eslintConfigItem);
|
|
111
166
|
}
|
|
112
|
-
|
|
167
|
+
// User plugins should always win, even if XO injects plugins later in the config list.
|
|
168
|
+
return hoistPlugins(baseConfig, userPluginOverrides);
|
|
113
169
|
}
|
|
114
170
|
export default xoToEslintConfig;
|
|
115
171
|
//# sourceMappingURL=xo-to-eslint.js.map
|
package/dist/lib/xo.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ESLint, type Linter } from 'eslint';
|
|
|
2
2
|
import prettier from 'prettier';
|
|
3
3
|
import { type XoLintResult, type LinterOptions, type LintTextOptions, type XoConfigOptions, type XoConfigItem } from './types.js';
|
|
4
4
|
import { xoToEslintConfig } from './xo-to-eslint.js';
|
|
5
|
+
export declare const ignoredFileWarningMessage = "File ignored because of a matching ignore pattern.";
|
|
5
6
|
export declare class Xo {
|
|
6
7
|
/**
|
|
7
8
|
Static helper to convert an XO config to an ESLint config to be used in `eslint.config.js`.
|
|
@@ -65,6 +66,21 @@ export declare class Xo {
|
|
|
65
66
|
We use this to also add negative glob patterns in case a user overrides the parserOptions in their XO config.
|
|
66
67
|
*/
|
|
67
68
|
tsFilesIgnoresGlob: string[];
|
|
69
|
+
/**
|
|
70
|
+
Track whether ignores have been added to prevent duplicate ignore configs.
|
|
71
|
+
*/
|
|
72
|
+
private ignoresHandled;
|
|
73
|
+
/**
|
|
74
|
+
Store per-file configs separately from base config to prevent unbounded array growth.
|
|
75
|
+
Key: file path, Value: config for that file.
|
|
76
|
+
This prevents memory bloat in long-running processes (e.g., language servers).
|
|
77
|
+
*/
|
|
78
|
+
private readonly fileConfigs;
|
|
79
|
+
/**
|
|
80
|
+
Track virtual/stdin files that share a single tsconfig.stdin.json.
|
|
81
|
+
These are handled differently from regular files.
|
|
82
|
+
*/
|
|
83
|
+
private readonly virtualFiles;
|
|
68
84
|
constructor(_linterOptions: LinterOptions, _baseXoConfig?: XoConfigOptions);
|
|
69
85
|
/**
|
|
70
86
|
Sets the XO config on the XO instance.
|
|
@@ -91,7 +107,7 @@ export declare class Xo {
|
|
|
91
107
|
*/
|
|
92
108
|
ensureCacheDirectory(): Promise<void>;
|
|
93
109
|
/**
|
|
94
|
-
Checks every TS file to ensure its included in the tsconfig and any that are not included are added to
|
|
110
|
+
Checks every TS file to ensure its included in the tsconfig and any that are not included are added to an in-memory TypeScript Program for type aware linting.
|
|
95
111
|
|
|
96
112
|
@param files - The TypeScript files being linted.
|
|
97
113
|
*/
|
|
@@ -113,6 +129,14 @@ export declare class Xo {
|
|
|
113
129
|
lintText(code: string, lintTextOptions: LintTextOptions): Promise<XoLintResult>;
|
|
114
130
|
calculateConfigForFile(filePath: string): Promise<Linter.Config>;
|
|
115
131
|
getFormatter(name: string): Promise<ESLint.LoadedFormatter>;
|
|
132
|
+
/**
|
|
133
|
+
Add virtual files to the config with a tsconfig approach.
|
|
134
|
+
*/
|
|
135
|
+
private addVirtualFilesToConfig;
|
|
136
|
+
/**
|
|
137
|
+
Add existing files to the config with an in-memory TypeScript Program.
|
|
138
|
+
*/
|
|
139
|
+
private addExistingFilesToConfig;
|
|
116
140
|
private processReport;
|
|
117
141
|
private getReportStatistics;
|
|
118
142
|
}
|
package/dist/lib/xo.js
CHANGED
|
@@ -1,18 +1,62 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import os from 'node:os';
|
|
3
|
+
import syncFs from 'node:fs';
|
|
3
4
|
import fs from 'node:fs/promises';
|
|
4
5
|
import process from 'node:process';
|
|
5
6
|
import { ESLint } from 'eslint';
|
|
6
7
|
import findCacheDirectory from 'find-cache-directory';
|
|
7
|
-
import { globby } from 'globby';
|
|
8
|
+
import { globby, isDynamicPattern } from 'globby';
|
|
8
9
|
import arrify from 'arrify';
|
|
9
10
|
import defineLazyProperty from 'define-lazy-prop';
|
|
10
11
|
import prettier from 'prettier';
|
|
11
|
-
import { defaultIgnores, cacheDirName, allExtensions, tsFilesGlob, } from './constants.js';
|
|
12
|
+
import { defaultIgnores, cacheDirName, allExtensions, tsFilesGlob, tsconfigDefaults, } from './constants.js';
|
|
12
13
|
import { xoToEslintConfig } from './xo-to-eslint.js';
|
|
13
14
|
import resolveXoConfig from './resolve-config.js';
|
|
14
15
|
import { handleTsconfig } from './handle-ts-files.js';
|
|
15
|
-
import { matchFilesForTsConfig, preProcessXoConfig } from './utils.js';
|
|
16
|
+
import { matchFilesForTsConfig, preProcessXoConfig, typescriptParser, } from './utils.js';
|
|
17
|
+
export const ignoredFileWarningMessage = 'File ignored because of a matching ignore pattern.';
|
|
18
|
+
const createIgnoredLintResult = (filePath) => ({
|
|
19
|
+
filePath,
|
|
20
|
+
messages: [
|
|
21
|
+
{
|
|
22
|
+
ruleId: null,
|
|
23
|
+
severity: 1,
|
|
24
|
+
message: ignoredFileWarningMessage,
|
|
25
|
+
line: 0,
|
|
26
|
+
column: 0,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
suppressedMessages: [],
|
|
30
|
+
errorCount: 0,
|
|
31
|
+
fatalErrorCount: 0,
|
|
32
|
+
warningCount: 1,
|
|
33
|
+
fixableErrorCount: 0,
|
|
34
|
+
fixableWarningCount: 0,
|
|
35
|
+
usedDeprecatedRules: [],
|
|
36
|
+
});
|
|
37
|
+
const resolveExplicitFilePath = (cwd, glob) => {
|
|
38
|
+
if (isDynamicPattern(glob)) {
|
|
39
|
+
// Negated and wildcard globs are treated as regular glob filtering, not as explicit file paths that should trigger an ignored-file warning.
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const absolutePath = path.resolve(cwd, glob);
|
|
43
|
+
try {
|
|
44
|
+
if (syncFs.statSync(absolutePath).isFile()) {
|
|
45
|
+
return absolutePath;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// File does not exist or is inaccessible.
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
};
|
|
53
|
+
const getIgnoredExplicitFileResults = async (cwd, globs, eslint) => {
|
|
54
|
+
const explicitFilePaths = [...new Set(globs
|
|
55
|
+
.map(glob => resolveExplicitFilePath(cwd, glob))
|
|
56
|
+
.filter(filePath => filePath !== undefined))];
|
|
57
|
+
const results = await Promise.all(explicitFilePaths.map(async (filePath) => await eslint.isPathIgnored(filePath) ? createIgnoredLintResult(filePath) : undefined));
|
|
58
|
+
return results.filter(result => result !== undefined);
|
|
59
|
+
};
|
|
16
60
|
export class Xo {
|
|
17
61
|
/**
|
|
18
62
|
Static helper to convert an XO config to an ESLint config to be used in `eslint.config.js`.
|
|
@@ -113,6 +157,21 @@ export class Xo {
|
|
|
113
157
|
We use this to also add negative glob patterns in case a user overrides the parserOptions in their XO config.
|
|
114
158
|
*/
|
|
115
159
|
tsFilesIgnoresGlob = [];
|
|
160
|
+
/**
|
|
161
|
+
Track whether ignores have been added to prevent duplicate ignore configs.
|
|
162
|
+
*/
|
|
163
|
+
ignoresHandled = false;
|
|
164
|
+
/**
|
|
165
|
+
Store per-file configs separately from base config to prevent unbounded array growth.
|
|
166
|
+
Key: file path, Value: config for that file.
|
|
167
|
+
This prevents memory bloat in long-running processes (e.g., language servers).
|
|
168
|
+
*/
|
|
169
|
+
fileConfigs = new Map();
|
|
170
|
+
/**
|
|
171
|
+
Track virtual/stdin files that share a single tsconfig.stdin.json.
|
|
172
|
+
These are handled differently from regular files.
|
|
173
|
+
*/
|
|
174
|
+
virtualFiles = new Set();
|
|
116
175
|
constructor(_linterOptions, _baseXoConfig = {}) {
|
|
117
176
|
this.linterOptions = _linterOptions;
|
|
118
177
|
this.baseXoConfig = _baseXoConfig;
|
|
@@ -120,6 +179,12 @@ export class Xo {
|
|
|
120
179
|
if (!path.isAbsolute(this.linterOptions.cwd)) {
|
|
121
180
|
this.linterOptions.cwd = path.resolve(process.cwd(), this.linterOptions.cwd);
|
|
122
181
|
}
|
|
182
|
+
try {
|
|
183
|
+
this.linterOptions.cwd = syncFs.realpathSync.native(this.linterOptions.cwd);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Ignore invalid paths here; the caller will handle errors later.
|
|
187
|
+
}
|
|
123
188
|
const backupCacheLocation = path.join(os.tmpdir(), cacheDirName);
|
|
124
189
|
this.cacheLocation = findCacheDirectory({ name: cacheDirName, cwd: this.linterOptions.cwd }) ?? backupCacheLocation;
|
|
125
190
|
}
|
|
@@ -155,7 +220,12 @@ export class Xo {
|
|
|
155
220
|
if (!this.xoConfig) {
|
|
156
221
|
throw new Error('"Xo.setEslintConfig" failed');
|
|
157
222
|
}
|
|
158
|
-
|
|
223
|
+
// Combine base config with per-file configs from Map
|
|
224
|
+
// Deduplicate configs since multiple files can share the same config object
|
|
225
|
+
const uniqueFileConfigs = [...new Set(this.fileConfigs.values())];
|
|
226
|
+
const allConfigs = [...this.xoConfig, ...uniqueFileConfigs];
|
|
227
|
+
// Always regenerate to support instance reuse with new files
|
|
228
|
+
this.eslintConfig = xoToEslintConfig(allConfigs, { prettierOptions: this.prettierConfig });
|
|
159
229
|
}
|
|
160
230
|
/**
|
|
161
231
|
Sets the ignores on the XO instance.
|
|
@@ -163,7 +233,7 @@ export class Xo {
|
|
|
163
233
|
@private
|
|
164
234
|
*/
|
|
165
235
|
setIgnores() {
|
|
166
|
-
if (!this.baseXoConfig.ignores) {
|
|
236
|
+
if (this.ignoresHandled || !this.baseXoConfig.ignores) {
|
|
167
237
|
return;
|
|
168
238
|
}
|
|
169
239
|
let ignores = [];
|
|
@@ -180,6 +250,7 @@ export class Xo {
|
|
|
180
250
|
return;
|
|
181
251
|
}
|
|
182
252
|
this.xoConfig.push({ ignores });
|
|
253
|
+
this.ignoresHandled = true;
|
|
183
254
|
}
|
|
184
255
|
/**
|
|
185
256
|
Ensures the cache directory exists. This needs to run once before both tsconfig handling and running ESLint occur.
|
|
@@ -201,7 +272,7 @@ export class Xo {
|
|
|
201
272
|
}
|
|
202
273
|
}
|
|
203
274
|
/**
|
|
204
|
-
Checks every TS file to ensure its included in the tsconfig and any that are not included are added to
|
|
275
|
+
Checks every TS file to ensure its included in the tsconfig and any that are not included are added to an in-memory TypeScript Program for type aware linting.
|
|
205
276
|
|
|
206
277
|
@param files - The TypeScript files being linted.
|
|
207
278
|
*/
|
|
@@ -209,25 +280,25 @@ export class Xo {
|
|
|
209
280
|
if (!this.linterOptions.ts || !files || files.length === 0) {
|
|
210
281
|
return;
|
|
211
282
|
}
|
|
212
|
-
|
|
213
|
-
|
|
283
|
+
// Get ALL TypeScript files being linted (both new and previously handled)
|
|
284
|
+
const allTsFiles = matchFilesForTsConfig(this.linterOptions.cwd, files, this.tsFilesGlob, this.tsFilesIgnoresGlob);
|
|
285
|
+
if (allTsFiles.length === 0) {
|
|
286
|
+
this.fileConfigs.clear();
|
|
287
|
+
if (this.virtualFiles.size > 0) {
|
|
288
|
+
await this.addVirtualFilesToConfig([]);
|
|
289
|
+
}
|
|
214
290
|
return;
|
|
215
291
|
}
|
|
216
|
-
const {
|
|
292
|
+
const { program, existingFiles, virtualFiles } = handleTsconfig({
|
|
293
|
+
files: allTsFiles,
|
|
217
294
|
cwd: this.linterOptions.cwd,
|
|
218
|
-
|
|
295
|
+
cacheLocation: this.cacheLocation,
|
|
219
296
|
});
|
|
220
|
-
|
|
221
|
-
|
|
297
|
+
this.fileConfigs.clear();
|
|
298
|
+
if (existingFiles.length > 0) {
|
|
299
|
+
this.addExistingFilesToConfig(existingFiles, program);
|
|
222
300
|
}
|
|
223
|
-
|
|
224
|
-
config.files = unincludedFiles.map(file => path.relative(this.linterOptions.cwd, file));
|
|
225
|
-
config.languageOptions ??= {};
|
|
226
|
-
config.languageOptions.parserOptions ??= {};
|
|
227
|
-
config.languageOptions.parserOptions['projectService'] = false;
|
|
228
|
-
config.languageOptions.parserOptions['project'] = fallbackTsConfigPath;
|
|
229
|
-
config.languageOptions.parserOptions['tsconfigRootDir'] = this.linterOptions.cwd;
|
|
230
|
-
this.xoConfig.push(config);
|
|
301
|
+
await this.addVirtualFilesToConfig(virtualFiles);
|
|
231
302
|
}
|
|
232
303
|
/**
|
|
233
304
|
Initializes the ESLint instance on the XO instance.
|
|
@@ -249,9 +320,12 @@ export class Xo {
|
|
|
249
320
|
warnIgnored: false,
|
|
250
321
|
cache: true,
|
|
251
322
|
cacheLocation: this.cacheLocation,
|
|
323
|
+
cacheStrategy: 'content',
|
|
252
324
|
fix: this.linterOptions.fix,
|
|
253
325
|
};
|
|
254
|
-
|
|
326
|
+
// Always create new instance to support reuse with updated config
|
|
327
|
+
// ESLint's file-based cache (cacheLocation) persists across instances
|
|
328
|
+
this.eslint = new ESLint(eslintOptions);
|
|
255
329
|
}
|
|
256
330
|
/**
|
|
257
331
|
Lints the files on the XO instance.
|
|
@@ -264,24 +338,28 @@ export class Xo {
|
|
|
264
338
|
globs = `**/*.{${allExtensions.join(',')}}`;
|
|
265
339
|
}
|
|
266
340
|
globs = arrify(globs);
|
|
267
|
-
|
|
341
|
+
const files = await globby(globs, {
|
|
268
342
|
// Merge in command line ignores
|
|
269
343
|
ignore: [...defaultIgnores, ...arrify(this.baseXoConfig.ignores)],
|
|
270
344
|
onlyFiles: true,
|
|
271
345
|
gitignore: true,
|
|
272
346
|
absolute: true,
|
|
347
|
+
dot: true,
|
|
273
348
|
cwd: this.linterOptions.cwd,
|
|
274
349
|
});
|
|
275
350
|
await this.initEslint(files);
|
|
276
351
|
if (!this.eslint) {
|
|
277
352
|
throw new Error('Failed to initialize ESLint');
|
|
278
353
|
}
|
|
354
|
+
const { eslint } = this;
|
|
355
|
+
const ignoredResults = await getIgnoredExplicitFileResults(this.linterOptions.cwd, globs, eslint);
|
|
279
356
|
if (files.length === 0) {
|
|
280
|
-
|
|
357
|
+
return this.processReport(ignoredResults);
|
|
281
358
|
}
|
|
282
|
-
const results = await
|
|
283
|
-
const rulesMeta =
|
|
284
|
-
|
|
359
|
+
const results = await eslint.lintFiles(files);
|
|
360
|
+
const rulesMeta = eslint.getRulesMetaForResults(results);
|
|
361
|
+
// No overlap: `warnIgnored: false` makes ESLint silently drop ignored files from `results`.
|
|
362
|
+
return this.processReport([...results, ...ignoredResults], { rulesMeta });
|
|
285
363
|
}
|
|
286
364
|
/**
|
|
287
365
|
Lints the text on the XO instance.
|
|
@@ -304,6 +382,7 @@ export class Xo {
|
|
|
304
382
|
if (!this.eslint) {
|
|
305
383
|
throw new Error('Failed to initialize ESLint');
|
|
306
384
|
}
|
|
385
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
307
386
|
return this.eslint.calculateConfigForFile(filePath);
|
|
308
387
|
}
|
|
309
388
|
async getFormatter(name) {
|
|
@@ -313,6 +392,102 @@ export class Xo {
|
|
|
313
392
|
}
|
|
314
393
|
return this.eslint.loadFormatter(name);
|
|
315
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
Add virtual files to the config with a tsconfig approach.
|
|
397
|
+
*/
|
|
398
|
+
async addVirtualFilesToConfig(files) {
|
|
399
|
+
if (!this.xoConfig) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const nextVirtualFiles = new Set(files);
|
|
404
|
+
const tsconfigPath = path.join(this.cacheLocation, 'tsconfig.stdin.json');
|
|
405
|
+
const configIndex = this.xoConfig.findIndex(configItem => {
|
|
406
|
+
const { languageOptions } = configItem;
|
|
407
|
+
const parserOptionsCandidate = languageOptions?.parserOptions;
|
|
408
|
+
const parserOptions = parserOptionsCandidate;
|
|
409
|
+
return parserOptions?.project === tsconfigPath;
|
|
410
|
+
});
|
|
411
|
+
if (nextVirtualFiles.size > 0) {
|
|
412
|
+
const filesArray = [...nextVirtualFiles];
|
|
413
|
+
const relativeFiles = filesArray.map(file => path.relative(this.linterOptions.cwd, file));
|
|
414
|
+
const tsconfigContent = {
|
|
415
|
+
compilerOptions: {
|
|
416
|
+
...tsconfigDefaults.compilerOptions,
|
|
417
|
+
module: 'ESNext',
|
|
418
|
+
moduleResolution: 'NodeNext',
|
|
419
|
+
esModuleInterop: true,
|
|
420
|
+
skipLibCheck: true,
|
|
421
|
+
},
|
|
422
|
+
files: filesArray,
|
|
423
|
+
};
|
|
424
|
+
await fs.writeFile(tsconfigPath, JSON.stringify(tsconfigContent, null, 2));
|
|
425
|
+
if (configIndex === -1) {
|
|
426
|
+
const parserOptions = {
|
|
427
|
+
projectService: false,
|
|
428
|
+
project: tsconfigPath,
|
|
429
|
+
tsconfigRootDir: this.linterOptions.cwd,
|
|
430
|
+
};
|
|
431
|
+
this.xoConfig.push({
|
|
432
|
+
files: relativeFiles,
|
|
433
|
+
languageOptions: {
|
|
434
|
+
parser: typescriptParser,
|
|
435
|
+
parserOptions,
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
const existingConfig = this.xoConfig[configIndex];
|
|
441
|
+
this.xoConfig[configIndex] = {
|
|
442
|
+
...existingConfig,
|
|
443
|
+
files: relativeFiles,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
this.virtualFiles.clear();
|
|
447
|
+
for (const file of nextVirtualFiles) {
|
|
448
|
+
this.virtualFiles.add(file);
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (configIndex >= 0) {
|
|
453
|
+
this.xoConfig.splice(configIndex, 1);
|
|
454
|
+
}
|
|
455
|
+
this.virtualFiles.clear();
|
|
456
|
+
await fs.rm(tsconfigPath, { force: true });
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
console.warn('XO: Failed to create tsconfig for virtual files. Type-aware linting will be disabled for these files.', error instanceof Error ? error.message : String(error));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
Add existing files to the config with an in-memory TypeScript Program.
|
|
464
|
+
*/
|
|
465
|
+
addExistingFilesToConfig(files, program) {
|
|
466
|
+
if (!this.xoConfig || files.length === 0) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const parserOptions = {
|
|
470
|
+
project: false,
|
|
471
|
+
projectService: false,
|
|
472
|
+
};
|
|
473
|
+
if (program) {
|
|
474
|
+
parserOptions.programs = [program];
|
|
475
|
+
}
|
|
476
|
+
const config = {
|
|
477
|
+
files: files.map(file => path.relative(this.linterOptions.cwd, file)),
|
|
478
|
+
languageOptions: {
|
|
479
|
+
parser: typescriptParser,
|
|
480
|
+
parserOptions,
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
// IMPORTANT: All files intentionally share the same config object reference for memory efficiency.
|
|
484
|
+
// This prevents unbounded memory growth in long-running processes (e.g., language servers).
|
|
485
|
+
// The config is immutable after creation, so sharing is safe.
|
|
486
|
+
// Deduplication happens in setEslintConfig() via Set to avoid duplicate configs in the final array.
|
|
487
|
+
for (const file of files) {
|
|
488
|
+
this.fileConfigs.set(file, config);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
316
491
|
processReport(report, { rulesMeta = {} } = {}) {
|
|
317
492
|
if (this.linterOptions.quiet) {
|
|
318
493
|
report = ESLint.getErrorResults(report);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "JavaScript/TypeScript linter (ESLint wrapper) with great defaults",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "xojs/xo",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"sideEffects": false,
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=20.
|
|
21
|
+
"node": ">=20.19"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "npm run clean && tsc",
|
|
@@ -65,53 +65,51 @@
|
|
|
65
65
|
"typescript"
|
|
66
66
|
],
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@eslint-community/eslint-plugin-eslint-comments": "^4.
|
|
69
|
-
"@
|
|
70
|
-
"@
|
|
71
|
-
"@typescript-eslint/parser": "^8.37.0",
|
|
68
|
+
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.1",
|
|
69
|
+
"@eslint/compat": "^2.0.2",
|
|
70
|
+
"@sindresorhus/tsconfig": "^8.1.0",
|
|
72
71
|
"arrify": "^3.0.0",
|
|
73
72
|
"cosmiconfig": "^9.0.0",
|
|
74
73
|
"define-lazy-prop": "^3.0.0",
|
|
75
|
-
"eslint": "^
|
|
76
|
-
"eslint-config-prettier": "^10.1.
|
|
77
|
-
"eslint-config-xo-react": "^0.
|
|
78
|
-
"eslint-config-xo-typescript": "^
|
|
79
|
-
"eslint-formatter-pretty": "^
|
|
80
|
-
"eslint-plugin-ava": "^
|
|
74
|
+
"eslint": "^10.0.2",
|
|
75
|
+
"eslint-config-prettier": "^10.1.8",
|
|
76
|
+
"eslint-config-xo-react": "^0.29.0",
|
|
77
|
+
"eslint-config-xo-typescript": "^9.0.0",
|
|
78
|
+
"eslint-formatter-pretty": "^7.0.0",
|
|
79
|
+
"eslint-plugin-ava": "^16.0.0",
|
|
81
80
|
"eslint-plugin-import-x": "^4.16.1",
|
|
82
|
-
"eslint-plugin-n": "^17.
|
|
83
|
-
"eslint-plugin-
|
|
84
|
-
"eslint-plugin-
|
|
85
|
-
"eslint-plugin-promise": "^7.2.1",
|
|
86
|
-
"eslint-plugin-unicorn": "^59.0.1",
|
|
81
|
+
"eslint-plugin-n": "^17.24.0",
|
|
82
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
83
|
+
"eslint-plugin-unicorn": "^63.0.0",
|
|
87
84
|
"find-cache-directory": "^6.0.0",
|
|
88
|
-
"get-stdin": "^
|
|
89
|
-
"get-tsconfig": "^4.
|
|
90
|
-
"globals": "^
|
|
91
|
-
"globby": "^
|
|
92
|
-
"meow": "^
|
|
85
|
+
"get-stdin": "^10.0.0",
|
|
86
|
+
"get-tsconfig": "^4.13.6",
|
|
87
|
+
"globals": "^17.3.0",
|
|
88
|
+
"globby": "^16.1.1",
|
|
89
|
+
"meow": "^14.1.0",
|
|
93
90
|
"micromatch": "^4.0.8",
|
|
94
|
-
"open-editor": "^
|
|
91
|
+
"open-editor": "^6.0.0",
|
|
95
92
|
"path-exists": "^5.0.0",
|
|
96
|
-
"prettier": "^3.
|
|
97
|
-
"type-fest": "^4.
|
|
98
|
-
"typescript
|
|
93
|
+
"prettier": "^3.8.1",
|
|
94
|
+
"type-fest": "^5.4.3",
|
|
95
|
+
"typescript": "^5.9.3",
|
|
96
|
+
"typescript-eslint": "^8.56.1"
|
|
99
97
|
},
|
|
100
98
|
"devDependencies": {
|
|
101
|
-
"@commitlint/cli": "^
|
|
102
|
-
"@commitlint/config-conventional": "^
|
|
103
|
-
"@types/
|
|
104
|
-
"@types/
|
|
99
|
+
"@commitlint/cli": "^20.4.1",
|
|
100
|
+
"@commitlint/config-conventional": "^20.4.1",
|
|
101
|
+
"@types/micromatch": "^4.0.10",
|
|
102
|
+
"@types/node": "^25.2.1",
|
|
105
103
|
"@types/prettier": "^3.0.0",
|
|
106
104
|
"ava": "^6.4.1",
|
|
107
|
-
"dedent": "^1.
|
|
108
|
-
"execa": "^9.
|
|
105
|
+
"dedent": "^1.7.1",
|
|
106
|
+
"execa": "^9.6.1",
|
|
109
107
|
"husky": "^9.1.7",
|
|
110
|
-
"lint-staged": "^16.
|
|
111
|
-
"np": "^
|
|
112
|
-
"npm-package-json-lint": "^9.
|
|
108
|
+
"lint-staged": "^16.2.7",
|
|
109
|
+
"np": "^11.0.2",
|
|
110
|
+
"npm-package-json-lint": "^9.1.0",
|
|
113
111
|
"npm-package-json-lint-config-default": "^8.0.1",
|
|
114
|
-
"prettier-plugin-packagejson": "^
|
|
112
|
+
"prettier-plugin-packagejson": "^3.0.0",
|
|
115
113
|
"temp-dir": "^3.0.0",
|
|
116
114
|
"xo": "file:."
|
|
117
115
|
},
|
|
@@ -143,5 +141,10 @@
|
|
|
143
141
|
"text",
|
|
144
142
|
"lcov"
|
|
145
143
|
]
|
|
144
|
+
},
|
|
145
|
+
"xo": {
|
|
146
|
+
"rules": {
|
|
147
|
+
"ava/no-ignored-test-files": "off"
|
|
148
|
+
}
|
|
146
149
|
}
|
|
147
150
|
}
|
package/readme.md
CHANGED
|
@@ -27,7 +27,7 @@ It uses [ESLint](https://eslint.org) underneath, so issues regarding built-in ru
|
|
|
27
27
|
- No need to specify file paths to lint as it lints all JS/TS files except for [commonly ignored paths](#ignores).
|
|
28
28
|
- [Flat config customization.](#config)
|
|
29
29
|
- [TypeScript supported by default.](#typescript)
|
|
30
|
-
- Includes many useful ESLint plugins, like [`unicorn`](https://github.com/sindresorhus/eslint-plugin-unicorn), [`import`](https://github.com/
|
|
30
|
+
- Includes many useful ESLint plugins, like [`unicorn`](https://github.com/sindresorhus/eslint-plugin-unicorn), [`import-x`](https://github.com/un-ts/eslint-plugin-import-x), [`ava`](https://github.com/avajs/eslint-plugin-ava), [`n`](https://github.com/eslint-community/eslint-plugin-n) and more.
|
|
31
31
|
- Caches results between runs for much better performance.
|
|
32
32
|
- Super simple to add XO to a project with [`$ npm init xo`](https://github.com/xojs/create-xo).
|
|
33
33
|
- Fix many issues automagically with `$ xo --fix`.
|
|
@@ -46,7 +46,7 @@ npm install xo --save-dev
|
|
|
46
46
|
|
|
47
47
|
*You must install XO locally. You can run it directly with `$ npx xo`.*
|
|
48
48
|
|
|
49
|
-
*
|
|
49
|
+
*For framework-specific linting, see [Astro](#astro), [Svelte](#svelte), and [Vue](#vue).*
|
|
50
50
|
|
|
51
51
|
## Usage
|
|
52
52
|
|
|
@@ -108,7 +108,7 @@ Simply run `$ npm init xo` (with any options) to add XO to create an `xo.config.
|
|
|
108
108
|
|
|
109
109
|
## Config
|
|
110
110
|
|
|
111
|
-
You can configure XO options by creating an `xo.config.js` or an `xo.config.ts` file in the root directory of your project, or you can add an `xo` field to your `package.json`. XO supports all js/ts file extensions (js,cjs,mjs,ts,cts,mts) automatically. A XO config is an extension of ESLint's Flat Config. Like ESLint, an XO config exports an array of XO config objects. XO config objects extend [ESLint Configuration Objects](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-objects). This means all the available configuration params for ESLint also work for `XO`. However, `XO` enhances and adds extra params to the configuration objects to make them easier to work with.
|
|
111
|
+
You can configure XO options by creating an `xo.config.js` or an `xo.config.ts` file in the root directory of your project, or you can add an `xo` field to your `package.json`. XO supports all js/ts file extensions (js,cjs,mjs,ts,cts,mts) and popular framework extensions (vue,svelte,astro) automatically. A XO config is an extension of ESLint's Flat Config. Like ESLint, an XO config exports an array of XO config objects. XO config objects extend [ESLint Configuration Objects](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-objects). This means all the available configuration params for ESLint also work for `XO`. However, `XO` enhances and adds extra params to the configuration objects to make them easier to work with.
|
|
112
112
|
|
|
113
113
|
### Config types
|
|
114
114
|
|
|
@@ -136,16 +136,16 @@ export default [...] satisfies import('xo').FlatXoConfig
|
|
|
136
136
|
|
|
137
137
|
### files
|
|
138
138
|
|
|
139
|
-
Type: `string | string
|
|
140
|
-
Default: `**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}`
|
|
139
|
+
Type: `string | (string | string[])[]`\
|
|
140
|
+
Default: `**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx,vue,svelte,astro}`
|
|
141
141
|
|
|
142
|
-
A glob
|
|
142
|
+
A glob string, array of globs, or ESLint's native format (where nested arrays create AND patterns) indicating which files the config object applies to. By default `XO` will apply the configuration to [all files](lib/constants.ts). This is compatible with ESLint plugin configs, so you can spread them directly into your XO config.
|
|
143
143
|
|
|
144
144
|
> Tip: If you are adding additional `@typescript-eslint` rules to your config, these rules will apply to JS files as well unless you separate them appropriately with the `files` option. `@typescript-eslint` rules set to `'off'` or `0`, however, will have no effect on JS linting.
|
|
145
145
|
|
|
146
146
|
### ignores
|
|
147
147
|
|
|
148
|
-
Type: `string[]`
|
|
148
|
+
Type: `string | string[]`
|
|
149
149
|
|
|
150
150
|
Some [paths](lib/constants.ts) are ignored by default, including paths in `.gitignore`. Additional ignores can be added here.
|
|
151
151
|
|
|
@@ -204,6 +204,66 @@ Default: `false`
|
|
|
204
204
|
|
|
205
205
|
Adds `eslint-plugin-react`, `eslint-plugin-react-hooks`, and `eslint-config-xo-react` to get all the React best practices applied automatically.
|
|
206
206
|
|
|
207
|
+
### Astro
|
|
208
|
+
|
|
209
|
+
To lint [Astro](https://astro.build) files, install [`eslint-plugin-astro`](https://github.com/ota-meshi/eslint-plugin-astro):
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
npm install --save-dev eslint-plugin-astro
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Then spread its recommended config in your `xo.config.js`:
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
import astroPlugin from 'eslint-plugin-astro';
|
|
219
|
+
|
|
220
|
+
const xoConfig = [
|
|
221
|
+
...astroPlugin.configs.recommended,
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
export default xoConfig;
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Svelte
|
|
228
|
+
|
|
229
|
+
To lint [Svelte](https://svelte.dev) files, install [`eslint-plugin-svelte`](https://github.com/sveltejs/eslint-plugin-svelte):
|
|
230
|
+
|
|
231
|
+
```sh
|
|
232
|
+
npm install --save-dev eslint-plugin-svelte
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Then spread its recommended config in your `xo.config.js`:
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
import sveltePlugin from 'eslint-plugin-svelte';
|
|
239
|
+
|
|
240
|
+
const xoConfig = [
|
|
241
|
+
...sveltePlugin.configs.recommended,
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
export default xoConfig;
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Vue
|
|
248
|
+
|
|
249
|
+
To lint [Vue](https://vuejs.org) files, install [`eslint-plugin-vue`](https://github.com/vuejs/eslint-plugin-vue):
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
npm install --save-dev eslint-plugin-vue
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Then spread its recommended config in your `xo.config.js`:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
import vuePlugin from 'eslint-plugin-vue';
|
|
259
|
+
|
|
260
|
+
const xoConfig = [
|
|
261
|
+
...vuePlugin.configs['flat/recommended'],
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
export default xoConfig;
|
|
265
|
+
```
|
|
266
|
+
|
|
207
267
|
## TypeScript
|
|
208
268
|
|
|
209
269
|
XO will automatically lint TypeScript files (`.ts`, `.mts`, `.cts`, and `.tsx`) with the rules defined in [eslint-config-xo-typescript#use-with-xo](https://github.com/xojs/eslint-config-xo-typescript#use-with-xo).
|
|
@@ -306,6 +366,12 @@ Show the world you're using XO → [](https://github.com/xojs/xo)
|
|
307
367
|
```
|
|
308
368
|
|
|
369
|
+
Large badge: [](https://github.com/xojs/xo)
|
|
370
|
+
|
|
371
|
+
```md
|
|
372
|
+
[](https://github.com/xojs/xo)
|
|
373
|
+
```
|
|
374
|
+
|
|
309
375
|
Or [customize the badge](https://github.com/xojs/xo/issues/689#issuecomment-1253127616).
|
|
310
376
|
|
|
311
377
|
You can also find some nice dynamic XO badges on [badgen.net](https://badgen.net/#xo).
|