xo 2.0.2 → 3.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/cli.d.ts +0 -59
- package/dist/cli.js +139 -88
- package/dist/lib/config.js +2 -407
- package/dist/lib/constants.d.ts +1 -8
- package/dist/lib/constants.js +1 -18
- package/dist/lib/eslint-adapter.d.ts +3 -0
- package/dist/lib/eslint-adapter.js +58 -0
- package/dist/lib/handle-ts-files.d.ts +4 -1
- package/dist/lib/handle-ts-files.js +34 -5
- package/dist/lib/resolve-config.js +8 -7
- package/dist/lib/types.d.ts +10 -5
- package/dist/lib/utils.d.ts +10 -3
- package/dist/lib/utils.js +46 -31
- package/dist/lib/xo-to-eslint.d.ts +2 -6
- package/dist/lib/xo-to-eslint.js +31 -77
- package/dist/lib/xo.d.ts +21 -88
- package/dist/lib/xo.js +383 -261
- package/package.json +25 -64
- package/readme.md +135 -59
- package/dist/lib/rules/no-use-extend-native.d.ts +0 -3
- package/dist/lib/rules/no-use-extend-native.js +0 -386
package/dist/lib/xo.js
CHANGED
|
@@ -6,15 +6,18 @@ import process from 'node:process';
|
|
|
6
6
|
import { ESLint } from 'eslint';
|
|
7
7
|
import findCacheDirectory from 'find-cache-directory';
|
|
8
8
|
import { globby, isDynamicPattern } from 'globby';
|
|
9
|
+
import micromatch from 'micromatch';
|
|
9
10
|
import arrify from 'arrify';
|
|
10
11
|
import defineLazyProperty from 'define-lazy-prop';
|
|
11
|
-
import prettier from 'prettier';
|
|
12
12
|
import { defaultIgnores, cacheDirName, allExtensions, tsFilesGlob, tsconfigDefaults, } from './constants.js';
|
|
13
13
|
import { xoToEslintConfig } from './xo-to-eslint.js';
|
|
14
14
|
import resolveXoConfig from './resolve-config.js';
|
|
15
15
|
import { handleTsconfig } from './handle-ts-files.js';
|
|
16
16
|
import { matchFilesForTsConfig, preProcessXoConfig, typescriptParser, } from './utils.js';
|
|
17
17
|
export const ignoredFileWarningMessage = 'File ignored because of a matching ignore pattern.';
|
|
18
|
+
export const noFilesFoundErrorMessage = 'No files matching the pattern were found.';
|
|
19
|
+
const suppressionsFileMissingErrorMessage = 'The suppressions file does not exist. Please run the command with `--suppress-all` or `--suppress-rule` to create it.';
|
|
20
|
+
const createErrorWithExitCode = (message, exitCode) => Object.assign(new Error(message), { exitCode });
|
|
18
21
|
const createIgnoredLintResult = (filePath) => ({
|
|
19
22
|
filePath,
|
|
20
23
|
messages: [
|
|
@@ -34,6 +37,24 @@ const createIgnoredLintResult = (filePath) => ({
|
|
|
34
37
|
fixableWarningCount: 0,
|
|
35
38
|
usedDeprecatedRules: [],
|
|
36
39
|
});
|
|
40
|
+
const normalizeGlobPath = (filePath) => filePath.split(path.sep).join('/');
|
|
41
|
+
const pathMatchesPattern = (filePath, pattern) => micromatch.isMatch(normalizeGlobPath(filePath), normalizeGlobPath(pattern), { dot: true });
|
|
42
|
+
const isIgnoredByPatterns = (filePath, patterns) => {
|
|
43
|
+
let isIgnored = false;
|
|
44
|
+
for (const pattern of patterns) {
|
|
45
|
+
if (pattern.startsWith('!')) {
|
|
46
|
+
if (pathMatchesPattern(filePath, pattern.slice(1))) {
|
|
47
|
+
isIgnored = false;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (pathMatchesPattern(filePath, pattern)) {
|
|
52
|
+
isIgnored = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return isIgnored;
|
|
56
|
+
};
|
|
57
|
+
const isIgnoredFile = (cwd, filePath, patterns) => isIgnoredByPatterns(path.relative(cwd, filePath), patterns);
|
|
37
58
|
const resolveExplicitFilePath = (cwd, glob) => {
|
|
38
59
|
if (isDynamicPattern(glob)) {
|
|
39
60
|
// Negated and wildcard globs are treated as regular glob filtering, not as explicit file paths that should trigger an ignored-file warning.
|
|
@@ -50,18 +71,110 @@ const resolveExplicitFilePath = (cwd, glob) => {
|
|
|
50
71
|
}
|
|
51
72
|
return undefined;
|
|
52
73
|
};
|
|
53
|
-
const getIgnoredExplicitFileResults = async (cwd, globs, eslint) => {
|
|
74
|
+
const getIgnoredExplicitFileResults = async (cwd, globs, eslint, discoveryIgnores = []) => {
|
|
54
75
|
const explicitFilePaths = [...new Set(globs
|
|
55
76
|
.map(glob => resolveExplicitFilePath(cwd, glob))
|
|
56
77
|
.filter(filePath => filePath !== undefined))];
|
|
57
|
-
const results = await Promise.all(explicitFilePaths.map(async (filePath) =>
|
|
78
|
+
const results = await Promise.all(explicitFilePaths.map(async (filePath) => {
|
|
79
|
+
if (isIgnoredFile(cwd, filePath, discoveryIgnores)) {
|
|
80
|
+
return createIgnoredLintResult(filePath);
|
|
81
|
+
}
|
|
82
|
+
return await eslint.isPathIgnored(filePath) ? createIgnoredLintResult(filePath) : undefined;
|
|
83
|
+
}));
|
|
58
84
|
return results.filter(result => result !== undefined);
|
|
59
85
|
};
|
|
86
|
+
const isGlobalIgnoreConfig = (config) => {
|
|
87
|
+
const keys = Object.keys(config);
|
|
88
|
+
return config.ignores !== undefined && (keys.length === 1 || (keys.length === 2 && config.name !== undefined));
|
|
89
|
+
};
|
|
90
|
+
const expandIgnoreNegationForEslint = (pattern) => {
|
|
91
|
+
const negatedPattern = pattern.slice(1);
|
|
92
|
+
const { base, isGlob } = micromatch.scan(negatedPattern, { parts: true });
|
|
93
|
+
const parentPath = isGlob ? base : path.posix.dirname(negatedPattern);
|
|
94
|
+
if (parentPath === '' || parentPath === '.') {
|
|
95
|
+
return [pattern];
|
|
96
|
+
}
|
|
97
|
+
const expandedPatterns = parentPath.split('/').map((_, index, segments) => `!${segments.slice(0, index + 1).join('/')}`);
|
|
98
|
+
expandedPatterns.push(pattern);
|
|
99
|
+
return expandedPatterns;
|
|
100
|
+
};
|
|
101
|
+
const expandIgnoreNegationsForEslint = (patterns) => patterns.flatMap(pattern => pattern.startsWith('!') ? expandIgnoreNegationForEslint(pattern) : [pattern]);
|
|
102
|
+
const expandGlobalIgnoreConfigForEslint = (config) => {
|
|
103
|
+
if (!isGlobalIgnoreConfig(config)) {
|
|
104
|
+
return config;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
...config,
|
|
108
|
+
ignores: expandIgnoreNegationsForEslint(arrify(config.ignores)),
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
const stripDefaultIgnoreConfigs = (configs) => configs.map(configItem => {
|
|
112
|
+
const { ignores } = configItem;
|
|
113
|
+
const isDefaultIgnoreConfig = ignores !== undefined && ignores.length > 0 && ignores.every(pattern => defaultIgnores.includes(pattern));
|
|
114
|
+
if (!isDefaultIgnoreConfig) {
|
|
115
|
+
return configItem;
|
|
116
|
+
}
|
|
117
|
+
const { ignores: _ignored, ...configWithoutIgnores } = configItem;
|
|
118
|
+
return configWithoutIgnores;
|
|
119
|
+
});
|
|
120
|
+
const defaultIgnoreOverlapsReopenedPattern = (defaultIgnore, pattern) => {
|
|
121
|
+
const { base, isGlob } = micromatch.scan(pattern, { parts: true });
|
|
122
|
+
const patternDirname = path.posix.dirname(pattern);
|
|
123
|
+
const reopenedBase = isGlob ? base : (patternDirname === '' ? pattern : patternDirname);
|
|
124
|
+
const { base: defaultBase, isGlob: isDefaultGlob } = micromatch.scan(defaultIgnore, { parts: true });
|
|
125
|
+
const ignoreBase = isDefaultGlob ? defaultBase : defaultIgnore;
|
|
126
|
+
return micromatch.isMatch(pattern, defaultIgnore, { dot: true })
|
|
127
|
+
|| micromatch.isMatch(defaultIgnore, pattern, { dot: true })
|
|
128
|
+
|| ignoreBase === ''
|
|
129
|
+
|| reopenedBase === ''
|
|
130
|
+
|| ignoreBase.startsWith(`${reopenedBase}/`)
|
|
131
|
+
|| reopenedBase.startsWith(`${ignoreBase}/`);
|
|
132
|
+
};
|
|
133
|
+
const getReopenedDefaultPatterns = (patterns) => patterns
|
|
134
|
+
.filter(pattern => pattern.startsWith('!'))
|
|
135
|
+
.map(pattern => pattern.slice(1))
|
|
136
|
+
.filter(pattern => defaultIgnores.some(defaultIgnore => defaultIgnoreOverlapsReopenedPattern(defaultIgnore, pattern)));
|
|
137
|
+
/**
|
|
138
|
+
XO only compensates for negations that reopen its built-in default ignores.
|
|
139
|
+
User-provided positive ignores are still used only for pruning.
|
|
140
|
+
The flow is:
|
|
141
|
+
1. discover with `defaultIgnores + positive global ignores`
|
|
142
|
+
2. if a negation reopens a built-in default ignore, run one extra pass with only that default ignore relaxed
|
|
143
|
+
3. apply the real global ignore order in XO: default ignores, then config ignores, then CLI ignores
|
|
144
|
+
4. let ESLint make the final ignore decision
|
|
145
|
+
|
|
146
|
+
This keeps the common "lint files XO ignores by default" behavior without trying to fully reimplement ESLint's ignore engine during discovery.
|
|
147
|
+
*/
|
|
148
|
+
const discoverLintFiles = async ({ cwd, globs, positiveGlobalIgnores, discoveryIgnores, reopenedDefaultPatterns }) => {
|
|
149
|
+
const discoveredFiles = await globby(globs, {
|
|
150
|
+
ignore: [...defaultIgnores, ...positiveGlobalIgnores],
|
|
151
|
+
onlyFiles: true,
|
|
152
|
+
gitignore: true,
|
|
153
|
+
globalGitignore: true,
|
|
154
|
+
absolute: true,
|
|
155
|
+
dot: true,
|
|
156
|
+
cwd,
|
|
157
|
+
});
|
|
158
|
+
const effectiveIgnores = [...defaultIgnores, ...discoveryIgnores];
|
|
159
|
+
if (reopenedDefaultPatterns.length === 0) {
|
|
160
|
+
return discoveredFiles.filter(filePath => !isIgnoredFile(cwd, filePath, effectiveIgnores));
|
|
161
|
+
}
|
|
162
|
+
const reopenedFiles = await globby(globs, {
|
|
163
|
+
ignore: [
|
|
164
|
+
...positiveGlobalIgnores,
|
|
165
|
+
...defaultIgnores.filter(defaultIgnore => reopenedDefaultPatterns.every(pattern => !defaultIgnoreOverlapsReopenedPattern(defaultIgnore, pattern))),
|
|
166
|
+
],
|
|
167
|
+
onlyFiles: true,
|
|
168
|
+
gitignore: true,
|
|
169
|
+
globalGitignore: true,
|
|
170
|
+
absolute: true,
|
|
171
|
+
dot: true,
|
|
172
|
+
cwd,
|
|
173
|
+
});
|
|
174
|
+
return [...new Set([...discoveredFiles, ...reopenedFiles])]
|
|
175
|
+
.filter(filePath => !isIgnoredFile(cwd, filePath, effectiveIgnores));
|
|
176
|
+
};
|
|
60
177
|
export class Xo {
|
|
61
|
-
/**
|
|
62
|
-
Static helper to convert an XO config to an ESLint config to be used in `eslint.config.js`.
|
|
63
|
-
*/
|
|
64
|
-
static xoToEslintConfig = xoToEslintConfig;
|
|
65
178
|
/**
|
|
66
179
|
Static helper for backwards compatibility and use in editor extensions and other tools.
|
|
67
180
|
*/
|
|
@@ -73,8 +186,8 @@ export class Xo {
|
|
|
73
186
|
quiet: options.quiet,
|
|
74
187
|
ts: options.ts ?? true,
|
|
75
188
|
configPath: options.configPath,
|
|
189
|
+
suppressionsLocation: options.suppressionsLocation,
|
|
76
190
|
}, {
|
|
77
|
-
react: options.react,
|
|
78
191
|
space: options.space,
|
|
79
192
|
semicolon: options.semicolon,
|
|
80
193
|
prettier: options.prettier,
|
|
@@ -96,8 +209,8 @@ export class Xo {
|
|
|
96
209
|
quiet: options.quiet,
|
|
97
210
|
ts: options.ts,
|
|
98
211
|
configPath: options.configPath,
|
|
212
|
+
suppressionsLocation: options.suppressionsLocation,
|
|
99
213
|
}, {
|
|
100
|
-
react: options.react,
|
|
101
214
|
space: options.space,
|
|
102
215
|
semicolon: options.semicolon,
|
|
103
216
|
prettier: options.prettier,
|
|
@@ -114,303 +227,116 @@ export class Xo {
|
|
|
114
227
|
/**
|
|
115
228
|
Required linter options: `cwd`, `fix`, and `filePath` (in case of `lintText`).
|
|
116
229
|
*/
|
|
117
|
-
linterOptions;
|
|
118
|
-
/**
|
|
119
|
-
Base XO config options that allow configuration from CLI or other sources. Not to be confused with the `xoConfig` property which is the resolved XO config from the flat config AND base config.
|
|
120
|
-
*/
|
|
121
|
-
baseXoConfig;
|
|
230
|
+
#linterOptions;
|
|
122
231
|
/**
|
|
123
232
|
File path to the ESLint cache.
|
|
124
233
|
*/
|
|
125
|
-
cacheLocation;
|
|
126
|
-
/**
|
|
127
|
-
A re-usable ESLint instance configured with options calculated from the XO config.
|
|
128
|
-
*/
|
|
129
|
-
eslint;
|
|
234
|
+
#cacheLocation;
|
|
130
235
|
/**
|
|
131
236
|
XO config derived from both the base config and the resolved flat config.
|
|
132
237
|
*/
|
|
133
|
-
xoConfig;
|
|
238
|
+
#xoConfig;
|
|
134
239
|
/**
|
|
135
|
-
|
|
136
|
-
*/
|
|
137
|
-
eslintConfig;
|
|
138
|
-
/**
|
|
139
|
-
The flat XO config path, if there is one.
|
|
240
|
+
Base XO config options that allow configuration from CLI or other sources. Not to be confused with the `xoConfig` property which is the resolved XO config from the flat config AND base config.
|
|
140
241
|
*/
|
|
141
|
-
|
|
242
|
+
#baseXoConfig;
|
|
142
243
|
/**
|
|
143
|
-
|
|
244
|
+
A re-usable ESLint instance configured with options calculated from the XO config.
|
|
144
245
|
*/
|
|
145
|
-
|
|
246
|
+
#eslint;
|
|
146
247
|
/**
|
|
147
|
-
The
|
|
248
|
+
The ESLint config calculated from the resolved XO config.
|
|
148
249
|
*/
|
|
149
|
-
|
|
250
|
+
#eslintConfig;
|
|
150
251
|
/**
|
|
151
252
|
The glob pattern for TypeScript files, for which we will handle TS files and tsconfig.
|
|
152
253
|
|
|
153
254
|
We expand this based on the XO config and the files glob patterns.
|
|
154
255
|
*/
|
|
155
|
-
tsFilesGlob = [tsFilesGlob];
|
|
256
|
+
#tsFilesGlob = [tsFilesGlob];
|
|
156
257
|
/**
|
|
157
258
|
We use this to also add negative glob patterns in case a user overrides the parserOptions in their XO config.
|
|
158
259
|
*/
|
|
159
|
-
tsFilesIgnoresGlob = [];
|
|
160
|
-
/**
|
|
161
|
-
Track whether ignores have been added to prevent duplicate ignore configs.
|
|
162
|
-
*/
|
|
163
|
-
ignoresHandled = false;
|
|
260
|
+
#tsFilesIgnoresGlob = [];
|
|
164
261
|
/**
|
|
165
262
|
Store per-file configs separately from base config to prevent unbounded array growth.
|
|
166
263
|
Key: file path, Value: config for that file.
|
|
167
264
|
This prevents memory bloat in long-running processes (e.g., language servers).
|
|
168
265
|
*/
|
|
169
|
-
fileConfigs = new Map();
|
|
266
|
+
#fileConfigs = new Map();
|
|
170
267
|
/**
|
|
171
268
|
Track virtual/stdin files that share a single tsconfig.stdin.json.
|
|
172
269
|
These are handled differently from regular files.
|
|
173
270
|
*/
|
|
174
|
-
virtualFiles = new Set();
|
|
271
|
+
#virtualFiles = new Set();
|
|
175
272
|
constructor(_linterOptions, _baseXoConfig = {}) {
|
|
176
|
-
this
|
|
177
|
-
this
|
|
273
|
+
this.#linterOptions = _linterOptions;
|
|
274
|
+
this.#baseXoConfig = _baseXoConfig;
|
|
178
275
|
// Fix relative cwd paths
|
|
179
|
-
if (!path.isAbsolute(this
|
|
180
|
-
this
|
|
276
|
+
if (!path.isAbsolute(this.#linterOptions.cwd)) {
|
|
277
|
+
this.#linterOptions.cwd = path.resolve(process.cwd(), this.#linterOptions.cwd);
|
|
181
278
|
}
|
|
182
279
|
try {
|
|
183
|
-
this
|
|
280
|
+
this.#linterOptions.cwd = syncFs.realpathSync.native(this.#linterOptions.cwd);
|
|
184
281
|
}
|
|
185
282
|
catch {
|
|
186
283
|
// Ignore invalid paths here; the caller will handle errors later.
|
|
187
284
|
}
|
|
188
285
|
const backupCacheLocation = path.join(os.tmpdir(), cacheDirName);
|
|
189
|
-
this
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
Sets the XO config on the XO instance.
|
|
193
|
-
|
|
194
|
-
@private
|
|
195
|
-
*/
|
|
196
|
-
async setXoConfig() {
|
|
197
|
-
if (this.xoConfig) {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const { flatOptions, flatConfigPath } = await resolveXoConfig({
|
|
201
|
-
...this.linterOptions,
|
|
202
|
-
});
|
|
203
|
-
const { config, tsFilesGlob, tsFilesIgnoresGlob } = preProcessXoConfig([
|
|
204
|
-
this.baseXoConfig,
|
|
205
|
-
...flatOptions,
|
|
206
|
-
]);
|
|
207
|
-
this.xoConfig = config;
|
|
208
|
-
this.tsFilesGlob.push(...tsFilesGlob);
|
|
209
|
-
this.tsFilesIgnoresGlob.push(...tsFilesIgnoresGlob);
|
|
210
|
-
this.prettier = this.xoConfig.some(config => config.prettier);
|
|
211
|
-
this.prettierConfig = await prettier.resolveConfig(flatConfigPath, { editorconfig: true }) ?? {};
|
|
212
|
-
this.flatConfigPath = flatConfigPath;
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
Sets the ESLint config on the XO instance.
|
|
216
|
-
|
|
217
|
-
@private
|
|
218
|
-
*/
|
|
219
|
-
setEslintConfig() {
|
|
220
|
-
if (!this.xoConfig) {
|
|
221
|
-
throw new Error('"Xo.setEslintConfig" failed');
|
|
222
|
-
}
|
|
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 });
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
Sets the ignores on the XO instance.
|
|
232
|
-
|
|
233
|
-
@private
|
|
234
|
-
*/
|
|
235
|
-
setIgnores() {
|
|
236
|
-
if (this.ignoresHandled || !this.baseXoConfig.ignores) {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
let ignores = [];
|
|
240
|
-
if (typeof this.baseXoConfig.ignores === 'string') {
|
|
241
|
-
ignores = arrify(this.baseXoConfig.ignores);
|
|
242
|
-
}
|
|
243
|
-
else if (Array.isArray(this.baseXoConfig.ignores)) {
|
|
244
|
-
ignores = this.baseXoConfig.ignores;
|
|
245
|
-
}
|
|
246
|
-
if (!this.xoConfig) {
|
|
247
|
-
throw new Error('"Xo.setIgnores" failed');
|
|
248
|
-
}
|
|
249
|
-
if (ignores.length === 0) {
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
this.xoConfig.push({ ignores });
|
|
253
|
-
this.ignoresHandled = true;
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
Ensures the cache directory exists. This needs to run once before both tsconfig handling and running ESLint occur.
|
|
257
|
-
|
|
258
|
-
@private
|
|
259
|
-
*/
|
|
260
|
-
async ensureCacheDirectory() {
|
|
261
|
-
try {
|
|
262
|
-
const cacheStats = await fs.stat(this.cacheLocation);
|
|
263
|
-
// If file, re-create as directory
|
|
264
|
-
if (cacheStats.isFile()) {
|
|
265
|
-
await fs.rm(this.cacheLocation, { recursive: true, force: true });
|
|
266
|
-
await fs.mkdir(this.cacheLocation, { recursive: true });
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
catch {
|
|
270
|
-
// If not exists, create the directory
|
|
271
|
-
await fs.mkdir(this.cacheLocation, { recursive: true });
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
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.
|
|
276
|
-
|
|
277
|
-
@param files - The TypeScript files being linted.
|
|
278
|
-
*/
|
|
279
|
-
async handleUnincludedTsFiles(files) {
|
|
280
|
-
if (!this.linterOptions.ts || !files || files.length === 0) {
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
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
|
-
}
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
const { program, existingFiles, virtualFiles } = handleTsconfig({
|
|
293
|
-
files: allTsFiles,
|
|
294
|
-
cwd: this.linterOptions.cwd,
|
|
295
|
-
cacheLocation: this.cacheLocation,
|
|
296
|
-
});
|
|
297
|
-
this.fileConfigs.clear();
|
|
298
|
-
if (existingFiles.length > 0) {
|
|
299
|
-
this.addExistingFilesToConfig(existingFiles, program);
|
|
300
|
-
}
|
|
301
|
-
await this.addVirtualFilesToConfig(virtualFiles);
|
|
286
|
+
this.#cacheLocation = findCacheDirectory({ name: cacheDirName, cwd: this.#linterOptions.cwd }) ?? backupCacheLocation;
|
|
302
287
|
}
|
|
303
288
|
/**
|
|
304
|
-
Initializes the ESLint
|
|
289
|
+
Initializes the ESLint flat config on the XO instance.
|
|
305
290
|
*/
|
|
306
|
-
async
|
|
291
|
+
async prepareEslintConfig(files, cliIgnores = arrify(this.#baseXoConfig.ignores), stripDefaultIgnores = false) {
|
|
307
292
|
await this.setXoConfig();
|
|
308
|
-
this.setIgnores();
|
|
309
293
|
await this.ensureCacheDirectory();
|
|
310
294
|
await this.handleUnincludedTsFiles(files);
|
|
311
|
-
this.setEslintConfig();
|
|
312
|
-
if (!this
|
|
313
|
-
throw new Error('"Xo.
|
|
314
|
-
}
|
|
315
|
-
const eslintOptions = {
|
|
316
|
-
cwd: this.linterOptions.cwd,
|
|
317
|
-
overrideConfig: this.eslintConfig,
|
|
318
|
-
overrideConfigFile: true,
|
|
319
|
-
globInputPaths: false,
|
|
320
|
-
warnIgnored: false,
|
|
321
|
-
cache: true,
|
|
322
|
-
cacheLocation: this.cacheLocation,
|
|
323
|
-
cacheStrategy: 'content',
|
|
324
|
-
fix: this.linterOptions.fix,
|
|
325
|
-
};
|
|
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);
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
Lints the files on the XO instance.
|
|
332
|
-
|
|
333
|
-
@param globs - Glob pattern to pass to `globby`.
|
|
334
|
-
@throws Error
|
|
335
|
-
*/
|
|
336
|
-
async lintFiles(globs) {
|
|
337
|
-
if (!globs || (Array.isArray(globs) && globs.length === 0)) {
|
|
338
|
-
globs = `**/*.{${allExtensions.join(',')}}`;
|
|
339
|
-
}
|
|
340
|
-
globs = arrify(globs);
|
|
341
|
-
const files = await globby(globs, {
|
|
342
|
-
// Merge in command line ignores
|
|
343
|
-
ignore: [...defaultIgnores, ...arrify(this.baseXoConfig.ignores)],
|
|
344
|
-
onlyFiles: true,
|
|
345
|
-
gitignore: true,
|
|
346
|
-
absolute: true,
|
|
347
|
-
dot: true,
|
|
348
|
-
cwd: this.linterOptions.cwd,
|
|
349
|
-
});
|
|
350
|
-
await this.initEslint(files);
|
|
351
|
-
if (!this.eslint) {
|
|
352
|
-
throw new Error('Failed to initialize ESLint');
|
|
295
|
+
this.setEslintConfig(cliIgnores, stripDefaultIgnores);
|
|
296
|
+
if (!this.#eslintConfig) {
|
|
297
|
+
throw new Error('"Xo.prepareEslintConfig" failed');
|
|
353
298
|
}
|
|
354
|
-
|
|
355
|
-
const ignoredResults = await getIgnoredExplicitFileResults(this.linterOptions.cwd, globs, eslint);
|
|
356
|
-
if (files.length === 0) {
|
|
357
|
-
return this.processReport(ignoredResults);
|
|
358
|
-
}
|
|
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 });
|
|
299
|
+
return this.#eslintConfig;
|
|
363
300
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
301
|
+
async discoverFiles(globs) {
|
|
302
|
+
await this.setXoConfig();
|
|
303
|
+
const cliIgnores = arrify(this.#baseXoConfig.ignores);
|
|
304
|
+
const configIgnores = (this.#xoConfig ?? []).slice(1)
|
|
305
|
+
.flatMap(config => isGlobalIgnoreConfig(config) ? arrify(config.ignores) : []);
|
|
306
|
+
const discoveryIgnores = [...configIgnores, ...cliIgnores];
|
|
307
|
+
const positiveGlobalIgnores = discoveryIgnores.filter(pattern => !pattern.startsWith('!'));
|
|
308
|
+
const reopenedDefaultPatterns = getReopenedDefaultPatterns(discoveryIgnores);
|
|
309
|
+
const files = await discoverLintFiles({
|
|
310
|
+
cwd: this.#linterOptions.cwd,
|
|
311
|
+
globs,
|
|
312
|
+
positiveGlobalIgnores,
|
|
313
|
+
discoveryIgnores,
|
|
314
|
+
reopenedDefaultPatterns,
|
|
376
315
|
});
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (!this.eslint) {
|
|
383
|
-
throw new Error('Failed to initialize ESLint');
|
|
384
|
-
}
|
|
385
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
386
|
-
return this.eslint.calculateConfigForFile(filePath);
|
|
387
|
-
}
|
|
388
|
-
async getFormatter(name) {
|
|
389
|
-
await this.initEslint();
|
|
390
|
-
if (!this.eslint) {
|
|
391
|
-
throw new Error('Failed to initialize ESLint');
|
|
392
|
-
}
|
|
393
|
-
return this.eslint.loadFormatter(name);
|
|
316
|
+
return {
|
|
317
|
+
cliIgnores,
|
|
318
|
+
discoveryIgnores,
|
|
319
|
+
files,
|
|
320
|
+
};
|
|
394
321
|
}
|
|
395
322
|
/**
|
|
396
323
|
Add virtual files to the config with a tsconfig approach.
|
|
397
324
|
*/
|
|
398
325
|
async addVirtualFilesToConfig(files) {
|
|
399
|
-
if (!this
|
|
326
|
+
if (!this.#xoConfig) {
|
|
400
327
|
return;
|
|
401
328
|
}
|
|
402
329
|
try {
|
|
403
330
|
const nextVirtualFiles = new Set(files);
|
|
404
|
-
const tsconfigPath = path.join(this
|
|
405
|
-
const configIndex = this
|
|
331
|
+
const tsconfigPath = path.join(this.#cacheLocation, 'tsconfig.stdin.json');
|
|
332
|
+
const configIndex = this.#xoConfig.findIndex(configItem => {
|
|
406
333
|
const { languageOptions } = configItem;
|
|
407
|
-
const
|
|
408
|
-
const parserOptions = parserOptionsCandidate;
|
|
334
|
+
const parserOptions = languageOptions?.['parserOptions'];
|
|
409
335
|
return parserOptions?.project === tsconfigPath;
|
|
410
336
|
});
|
|
411
337
|
if (nextVirtualFiles.size > 0) {
|
|
412
338
|
const filesArray = [...nextVirtualFiles];
|
|
413
|
-
const relativeFiles = filesArray.map(file => path.relative(this
|
|
339
|
+
const relativeFiles = filesArray.map(file => path.relative(this.#linterOptions.cwd, file));
|
|
414
340
|
const tsconfigContent = {
|
|
415
341
|
compilerOptions: {
|
|
416
342
|
...tsconfigDefaults.compilerOptions,
|
|
@@ -426,9 +352,9 @@ export class Xo {
|
|
|
426
352
|
const parserOptions = {
|
|
427
353
|
projectService: false,
|
|
428
354
|
project: tsconfigPath,
|
|
429
|
-
tsconfigRootDir: this
|
|
355
|
+
tsconfigRootDir: this.#linterOptions.cwd,
|
|
430
356
|
};
|
|
431
|
-
this
|
|
357
|
+
this.#xoConfig.push({
|
|
432
358
|
files: relativeFiles,
|
|
433
359
|
languageOptions: {
|
|
434
360
|
parser: typescriptParser,
|
|
@@ -437,22 +363,22 @@ export class Xo {
|
|
|
437
363
|
});
|
|
438
364
|
}
|
|
439
365
|
else {
|
|
440
|
-
const existingConfig = this
|
|
441
|
-
this
|
|
366
|
+
const existingConfig = this.#xoConfig[configIndex];
|
|
367
|
+
this.#xoConfig[configIndex] = {
|
|
442
368
|
...existingConfig,
|
|
443
369
|
files: relativeFiles,
|
|
444
370
|
};
|
|
445
371
|
}
|
|
446
|
-
this
|
|
372
|
+
this.#virtualFiles.clear();
|
|
447
373
|
for (const file of nextVirtualFiles) {
|
|
448
|
-
this
|
|
374
|
+
this.#virtualFiles.add(file);
|
|
449
375
|
}
|
|
450
376
|
return;
|
|
451
377
|
}
|
|
452
378
|
if (configIndex >= 0) {
|
|
453
|
-
this
|
|
379
|
+
this.#xoConfig.splice(configIndex, 1);
|
|
454
380
|
}
|
|
455
|
-
this
|
|
381
|
+
this.#virtualFiles.clear();
|
|
456
382
|
await fs.rm(tsconfigPath, { force: true });
|
|
457
383
|
}
|
|
458
384
|
catch (error) {
|
|
@@ -463,7 +389,7 @@ export class Xo {
|
|
|
463
389
|
Add existing files to the config with an in-memory TypeScript Program.
|
|
464
390
|
*/
|
|
465
391
|
addExistingFilesToConfig(files, program) {
|
|
466
|
-
if (!this
|
|
392
|
+
if (!this.#xoConfig || files.length === 0) {
|
|
467
393
|
return;
|
|
468
394
|
}
|
|
469
395
|
const parserOptions = {
|
|
@@ -474,7 +400,7 @@ export class Xo {
|
|
|
474
400
|
parserOptions.programs = [program];
|
|
475
401
|
}
|
|
476
402
|
const config = {
|
|
477
|
-
files: files.map(file => path.relative(this
|
|
403
|
+
files: files.map(file => path.relative(this.#linterOptions.cwd, file)),
|
|
478
404
|
languageOptions: {
|
|
479
405
|
parser: typescriptParser,
|
|
480
406
|
parserOptions,
|
|
@@ -485,11 +411,11 @@ export class Xo {
|
|
|
485
411
|
// The config is immutable after creation, so sharing is safe.
|
|
486
412
|
// Deduplication happens in setEslintConfig() via Set to avoid duplicate configs in the final array.
|
|
487
413
|
for (const file of files) {
|
|
488
|
-
this
|
|
414
|
+
this.#fileConfigs.set(file, config);
|
|
489
415
|
}
|
|
490
416
|
}
|
|
491
417
|
processReport(report, { rulesMeta = {} } = {}) {
|
|
492
|
-
if (this
|
|
418
|
+
if (this.#linterOptions.quiet) {
|
|
493
419
|
report = ESLint.getErrorResults(report);
|
|
494
420
|
}
|
|
495
421
|
const result = {
|
|
@@ -502,11 +428,10 @@ export class Xo {
|
|
|
502
428
|
const rules = [];
|
|
503
429
|
for (const { usedDeprecatedRules } of report) {
|
|
504
430
|
for (const rule of usedDeprecatedRules) {
|
|
505
|
-
if (seenRules.has(rule.ruleId)) {
|
|
506
|
-
|
|
431
|
+
if (!seenRules.has(rule.ruleId)) {
|
|
432
|
+
seenRules.add(rule.ruleId);
|
|
433
|
+
rules.push(rule);
|
|
507
434
|
}
|
|
508
|
-
seenRules.add(rule.ruleId);
|
|
509
|
-
rules.push(rule);
|
|
510
435
|
}
|
|
511
436
|
}
|
|
512
437
|
return rules;
|
|
@@ -528,6 +453,203 @@ export class Xo {
|
|
|
528
453
|
}
|
|
529
454
|
return statistics;
|
|
530
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
Throws if a suppressions location was provided but the file does not exist.
|
|
458
|
+
*/
|
|
459
|
+
async assertSuppressionsFileExists() {
|
|
460
|
+
if (this.#linterOptions.suppressionsLocation === undefined) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const suppressionsFilePath = path.resolve(this.#linterOptions.cwd, this.#linterOptions.suppressionsLocation);
|
|
464
|
+
try {
|
|
465
|
+
await fs.access(suppressionsFilePath);
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
throw createErrorWithExitCode(suppressionsFileMissingErrorMessage, 2);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
Sets the XO config on the XO instance.
|
|
473
|
+
*/
|
|
474
|
+
async setXoConfig() {
|
|
475
|
+
if (this.#xoConfig) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const { flatOptions } = await resolveXoConfig({
|
|
479
|
+
...this.#linterOptions,
|
|
480
|
+
});
|
|
481
|
+
const { config: xoConfig, tsFilesGlob: tsGlob, tsFilesIgnoresGlob } = preProcessXoConfig([
|
|
482
|
+
this.#baseXoConfig,
|
|
483
|
+
...flatOptions,
|
|
484
|
+
]);
|
|
485
|
+
this.#xoConfig = xoConfig;
|
|
486
|
+
this.#tsFilesGlob.push(...tsGlob);
|
|
487
|
+
this.#tsFilesIgnoresGlob.push(...tsFilesIgnoresGlob);
|
|
488
|
+
}
|
|
489
|
+
setEslintConfig(cliIgnores = arrify(this.#baseXoConfig.ignores), stripDefaultIgnores = false) {
|
|
490
|
+
if (!this.#xoConfig) {
|
|
491
|
+
throw new Error('"Xo.setEslintConfig" failed');
|
|
492
|
+
}
|
|
493
|
+
// Combine base config with per-file configs from Map
|
|
494
|
+
// Deduplicate configs since multiple files can share the same config object
|
|
495
|
+
const [baseConfig = {}, ...resolvedConfigs] = this.#xoConfig;
|
|
496
|
+
const { ignores, ...configWithoutCliIgnores } = baseConfig;
|
|
497
|
+
const expandedResolvedConfigs = resolvedConfigs.map(config => expandGlobalIgnoreConfigForEslint(config));
|
|
498
|
+
const uniqueFileConfigs = [...new Set(this.#fileConfigs.values())];
|
|
499
|
+
const cliIgnoreConfig = cliIgnores.length > 0 ? [{ ignores: expandIgnoreNegationsForEslint(cliIgnores) }] : [];
|
|
500
|
+
const allConfigs = [configWithoutCliIgnores, ...expandedResolvedConfigs, ...cliIgnoreConfig, ...uniqueFileConfigs];
|
|
501
|
+
// Always regenerate to support instance reuse with new files
|
|
502
|
+
this.#eslintConfig = xoToEslintConfig(allConfigs);
|
|
503
|
+
if (stripDefaultIgnores) {
|
|
504
|
+
this.#eslintConfig = stripDefaultIgnoreConfigs(this.#eslintConfig);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
Ensures the cache directory exists. This needs to run once before both tsconfig handling and running ESLint occur.
|
|
509
|
+
*/
|
|
510
|
+
async ensureCacheDirectory() {
|
|
511
|
+
try {
|
|
512
|
+
const cacheStats = await fs.stat(this.#cacheLocation);
|
|
513
|
+
// If file, re-create as directory
|
|
514
|
+
if (cacheStats.isFile()) {
|
|
515
|
+
await fs.rm(this.#cacheLocation, { recursive: true, force: true });
|
|
516
|
+
await fs.mkdir(this.#cacheLocation, { recursive: true });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
// If not exists, create the directory. Rethrow any other error (for example, permission issues).
|
|
521
|
+
if (error.code !== 'ENOENT') {
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
await fs.mkdir(this.#cacheLocation, { recursive: true });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
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.
|
|
529
|
+
|
|
530
|
+
@param files - The TypeScript files being linted.
|
|
531
|
+
*/
|
|
532
|
+
async handleUnincludedTsFiles(files) {
|
|
533
|
+
if (!this.#linterOptions.ts || !files || files.length === 0) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
// Get ALL TypeScript files being linted (both new and previously handled)
|
|
537
|
+
const allTsFiles = matchFilesForTsConfig(this.#linterOptions.cwd, files, this.#tsFilesGlob, this.#tsFilesIgnoresGlob);
|
|
538
|
+
if (allTsFiles.length === 0) {
|
|
539
|
+
this.#fileConfigs.clear();
|
|
540
|
+
if (this.#virtualFiles.size > 0) {
|
|
541
|
+
await this.addVirtualFilesToConfig([]);
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const { program, existingFiles, virtualFiles } = handleTsconfig({
|
|
546
|
+
files: allTsFiles,
|
|
547
|
+
cwd: this.#linterOptions.cwd,
|
|
548
|
+
cacheLocation: this.#cacheLocation,
|
|
549
|
+
});
|
|
550
|
+
this.#fileConfigs.clear();
|
|
551
|
+
if (existingFiles.length > 0) {
|
|
552
|
+
this.addExistingFilesToConfig(existingFiles, program);
|
|
553
|
+
}
|
|
554
|
+
await this.addVirtualFilesToConfig(virtualFiles);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
Initializes the ESLint instance on the XO instance.
|
|
558
|
+
*/
|
|
559
|
+
async initEslint(files, cliIgnores = arrify(this.#baseXoConfig.ignores), stripDefaultIgnores = false) {
|
|
560
|
+
await this.prepareEslintConfig(files, cliIgnores, stripDefaultIgnores);
|
|
561
|
+
if (!this.#xoConfig) {
|
|
562
|
+
throw new Error('"Xo.initEslint" failed');
|
|
563
|
+
}
|
|
564
|
+
const eslintOptions = {
|
|
565
|
+
cwd: this.#linterOptions.cwd,
|
|
566
|
+
overrideConfig: this.#eslintConfig,
|
|
567
|
+
overrideConfigFile: true,
|
|
568
|
+
globInputPaths: false,
|
|
569
|
+
warnIgnored: false,
|
|
570
|
+
cache: true,
|
|
571
|
+
cacheLocation: this.#cacheLocation,
|
|
572
|
+
cacheStrategy: 'content',
|
|
573
|
+
fix: this.#linterOptions.fix,
|
|
574
|
+
applySuppressions: true,
|
|
575
|
+
suppressionsLocation: this.#linterOptions.suppressionsLocation,
|
|
576
|
+
};
|
|
577
|
+
// Always create new instance to support reuse with updated config
|
|
578
|
+
// ESLint's file-based cache (cacheLocation) persists across instances
|
|
579
|
+
this.#eslint = new ESLint(eslintOptions);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
Create an ESLint flat config for editor integrations using the same XO pipeline as the CLI.
|
|
583
|
+
*/
|
|
584
|
+
async getProjectEslintConfig() {
|
|
585
|
+
const { cliIgnores, files } = await this.discoverFiles([`**/*.{${allExtensions.join(',')}}`]);
|
|
586
|
+
return this.prepareEslintConfig(files, cliIgnores);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
Lints the files on the XO instance.
|
|
590
|
+
|
|
591
|
+
@param globs - Glob pattern to pass to `globby`.
|
|
592
|
+
@throws Error
|
|
593
|
+
*/
|
|
594
|
+
async lintFiles(globs) {
|
|
595
|
+
if (globs === undefined || (Array.isArray(globs) && globs.length === 0)) {
|
|
596
|
+
globs = `**/*.{${allExtensions.join(',')}}`;
|
|
597
|
+
}
|
|
598
|
+
globs = arrify(globs);
|
|
599
|
+
// If any explicitly provided pattern is non-dynamic (a literal file path), throw when no files are found.
|
|
600
|
+
// Dynamic glob patterns matching nothing is acceptable — the project may simply have no matching files yet.
|
|
601
|
+
// The default glob substitution above is always dynamic, so this is false when no globs were provided.
|
|
602
|
+
const hasExplicitFilePaths = globs.some(glob => !isDynamicPattern(glob));
|
|
603
|
+
const { cliIgnores, discoveryIgnores, files } = await this.discoverFiles(globs);
|
|
604
|
+
await this.assertSuppressionsFileExists();
|
|
605
|
+
await this.initEslint(files, cliIgnores, true);
|
|
606
|
+
if (!this.#eslint) {
|
|
607
|
+
throw new Error('Failed to initialize ESLint');
|
|
608
|
+
}
|
|
609
|
+
const eslint = this.#eslint;
|
|
610
|
+
const ignoredResults = await getIgnoredExplicitFileResults(this.#linterOptions.cwd, globs, eslint, [...defaultIgnores, ...discoveryIgnores]);
|
|
611
|
+
if (files.length === 0) {
|
|
612
|
+
if (hasExplicitFilePaths && ignoredResults.length === 0) {
|
|
613
|
+
throw new Error(noFilesFoundErrorMessage);
|
|
614
|
+
}
|
|
615
|
+
return this.processReport(ignoredResults);
|
|
616
|
+
}
|
|
617
|
+
const results = await eslint.lintFiles(files);
|
|
618
|
+
const rulesMeta = eslint.getRulesMetaForResults(results);
|
|
619
|
+
// No overlap: `warnIgnored: false` makes ESLint silently drop ignored files from `results`.
|
|
620
|
+
return this.processReport([...results, ...ignoredResults], { rulesMeta });
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
Lints the text on the XO instance.
|
|
624
|
+
*/
|
|
625
|
+
async lintText(code, lintTextOptions) {
|
|
626
|
+
const { filePath, warnIgnored: shouldWarnIgnored } = lintTextOptions;
|
|
627
|
+
await this.assertSuppressionsFileExists();
|
|
628
|
+
await this.initEslint([filePath]);
|
|
629
|
+
if (!this.#eslint) {
|
|
630
|
+
throw new Error('Failed to initialize ESLint');
|
|
631
|
+
}
|
|
632
|
+
const results = await this.#eslint.lintText(code, {
|
|
633
|
+
filePath,
|
|
634
|
+
warnIgnored: shouldWarnIgnored,
|
|
635
|
+
});
|
|
636
|
+
const rulesMeta = this.#eslint.getRulesMetaForResults(results);
|
|
637
|
+
return this.processReport(results, { rulesMeta });
|
|
638
|
+
}
|
|
639
|
+
async calculateConfigForFile(filePath) {
|
|
640
|
+
await this.initEslint([filePath]);
|
|
641
|
+
if (!this.#eslint) {
|
|
642
|
+
throw new Error('Failed to initialize ESLint');
|
|
643
|
+
}
|
|
644
|
+
return this.#eslint.calculateConfigForFile(filePath);
|
|
645
|
+
}
|
|
646
|
+
async getFormatter(name) {
|
|
647
|
+
await this.initEslint();
|
|
648
|
+
if (!this.#eslint) {
|
|
649
|
+
throw new Error('Failed to initialize ESLint');
|
|
650
|
+
}
|
|
651
|
+
return this.#eslint.loadFormatter(name);
|
|
652
|
+
}
|
|
531
653
|
}
|
|
532
654
|
export default Xo;
|
|
533
655
|
//# sourceMappingURL=xo.js.map
|