xo 3.0.1 → 4.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/handle-ts-files.d.ts +5 -12
- package/dist/lib/handle-ts-files.js +5 -88
- package/dist/lib/open-report.js +2 -2
- package/dist/lib/xo.d.ts +7 -9
- package/dist/lib/xo.js +109 -138
- package/package.json +8 -8
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
import ts from 'typescript';
|
|
2
1
|
/**
|
|
3
|
-
This function checks if the files are matched by the tsconfig include
|
|
2
|
+
This function checks if the files are matched by the tsconfig include/exclude, and returns the unmatched files.
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
All unincluded files are routed through a generated tsconfig (`parserOptions.project`) so that autofix works correctly across multiple ESLint passes. The in-memory Program approach is intentionally not used here because `@typescript-eslint/typescript-estree` always returns the AST built from the original file text when using `parserOptions.programs`, ignoring the updated `code` ESLint passes on subsequent fix passes, which causes file corruption.
|
|
6
5
|
|
|
7
6
|
@param options - The options for handling the tsconfig.
|
|
8
7
|
@param options.files - The TypeScript files to check against the tsconfig.
|
|
9
8
|
@param options.cwd - The current working directory.
|
|
10
|
-
@
|
|
11
|
-
@returns The unmatched files and an in-memory TypeScript Program.
|
|
9
|
+
@returns The unincluded files.
|
|
12
10
|
*/
|
|
13
|
-
export declare function handleTsconfig({ files, cwd
|
|
11
|
+
export declare function handleTsconfig({ files, cwd }: {
|
|
14
12
|
files: string[];
|
|
15
13
|
cwd: string;
|
|
16
|
-
|
|
17
|
-
}): {
|
|
18
|
-
existingFiles: string[];
|
|
19
|
-
virtualFiles: string[];
|
|
20
|
-
program: ts.Program | undefined;
|
|
21
|
-
};
|
|
14
|
+
}): string[];
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
2
|
import { createRequire } from 'node:module';
|
|
4
3
|
import ts from 'typescript';
|
|
5
4
|
import { getTsconfig, createFilesMatcher } from 'get-tsconfig';
|
|
6
|
-
import { tsconfigDefaults } from './constants.js';
|
|
7
5
|
let hasWarnedAboutTypeScriptVersion = false;
|
|
8
6
|
/**
|
|
9
7
|
Warns once if the project's own TypeScript is an older major than the version XO bundles. Mixing TypeScript versions in one process can crash type-aware linting (notably under pnpm), because the TypeFlags enum was renumbered in TypeScript 6.
|
|
@@ -29,65 +27,17 @@ const warnOnOutdatedProjectTypeScript = (cwd) => {
|
|
|
29
27
|
hasWarnedAboutTypeScriptVersion = true;
|
|
30
28
|
console.warn(`XO bundles TypeScript ${ts.version}, but your project has TypeScript ${projectVersion}. Mixing TypeScript versions in one process can crash type-aware linting (notably with pnpm). Upgrade your project's \`typescript\` to ${bundledMajor} or later, or pin it (for example, a pnpm \`overrides\` entry).`);
|
|
31
29
|
};
|
|
32
|
-
const createInMemoryProgram = (files, cwd) => {
|
|
33
|
-
if (files.length === 0) {
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const compilerOptions = getFallbackCompilerOptions(cwd);
|
|
38
|
-
const program = ts.createProgram(files, { ...compilerOptions });
|
|
39
|
-
Object.defineProperty(program, 'toJSON', {
|
|
40
|
-
value: () => ({
|
|
41
|
-
__type: 'TypeScriptProgram',
|
|
42
|
-
files: files.map(file => path.relative(cwd, file)),
|
|
43
|
-
}),
|
|
44
|
-
configurable: true,
|
|
45
|
-
});
|
|
46
|
-
return program;
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
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));
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
const fallbackCompilerOptionsCache = new Map();
|
|
54
|
-
const getFallbackCompilerOptions = (cwd) => {
|
|
55
|
-
const cacheKey = path.resolve(cwd);
|
|
56
|
-
const cached = fallbackCompilerOptionsCache.get(cacheKey);
|
|
57
|
-
if (cached) {
|
|
58
|
-
return cached;
|
|
59
|
-
}
|
|
60
|
-
const compilerOptionsResult = ts.convertCompilerOptionsFromJson(tsconfigDefaults.compilerOptions ?? {}, cacheKey);
|
|
61
|
-
if (compilerOptionsResult.errors.length > 0) {
|
|
62
|
-
throw new Error('XO: Invalid default TypeScript compiler options');
|
|
63
|
-
}
|
|
64
|
-
const compilerOptions = {
|
|
65
|
-
...compilerOptionsResult.options,
|
|
66
|
-
// Anchor module and `@types` resolution to the project directory. Without `configFilePath`, an option-only program resolves these from `process.cwd()`, which differs from `cwd` for programmatic callers. The file need not exist; only its directory is used.
|
|
67
|
-
configFilePath: path.join(cacheKey, 'tsconfig.json'),
|
|
68
|
-
esModuleInterop: true,
|
|
69
|
-
resolveJsonModule: true,
|
|
70
|
-
allowJs: true,
|
|
71
|
-
skipLibCheck: true,
|
|
72
|
-
skipDefaultLibCheck: true,
|
|
73
|
-
// TypeScript 6 only auto-includes `@types/*` packages when `types` contains `'*'`. Without this, files outside the tsconfig resolve imports to `any` and type-aware rules misfire.
|
|
74
|
-
types: ['*'],
|
|
75
|
-
};
|
|
76
|
-
fallbackCompilerOptionsCache.set(cacheKey, compilerOptions);
|
|
77
|
-
return compilerOptions;
|
|
78
|
-
};
|
|
79
30
|
/**
|
|
80
|
-
This function checks if the files are matched by the tsconfig include
|
|
31
|
+
This function checks if the files are matched by the tsconfig include/exclude, and returns the unmatched files.
|
|
81
32
|
|
|
82
|
-
|
|
33
|
+
All unincluded files are routed through a generated tsconfig (`parserOptions.project`) so that autofix works correctly across multiple ESLint passes. The in-memory Program approach is intentionally not used here because `@typescript-eslint/typescript-estree` always returns the AST built from the original file text when using `parserOptions.programs`, ignoring the updated `code` ESLint passes on subsequent fix passes, which causes file corruption.
|
|
83
34
|
|
|
84
35
|
@param options - The options for handling the tsconfig.
|
|
85
36
|
@param options.files - The TypeScript files to check against the tsconfig.
|
|
86
37
|
@param options.cwd - The current working directory.
|
|
87
|
-
@
|
|
88
|
-
@returns The unmatched files and an in-memory TypeScript Program.
|
|
38
|
+
@returns The unincluded files.
|
|
89
39
|
*/
|
|
90
|
-
export function handleTsconfig({ files, cwd
|
|
40
|
+
export function handleTsconfig({ files, cwd }) {
|
|
91
41
|
warnOnOutdatedProjectTypeScript(cwd);
|
|
92
42
|
const unincludedFiles = [];
|
|
93
43
|
const filesMatcherCache = new Map();
|
|
@@ -108,39 +58,6 @@ export function handleTsconfig({ files, cwd, cacheLocation }) {
|
|
|
108
58
|
}
|
|
109
59
|
unincludedFiles.push(filePath);
|
|
110
60
|
}
|
|
111
|
-
|
|
112
|
-
return { existingFiles: [], virtualFiles: [], program: undefined };
|
|
113
|
-
}
|
|
114
|
-
// Separate real files from virtual/cache files
|
|
115
|
-
// Virtual files include: stdin files (in cache dir), non-existent files
|
|
116
|
-
// TypeScript will surface opaque diagnostics for missing files; pre-filter so we only pay the program cost for real files.
|
|
117
|
-
const existingFiles = [];
|
|
118
|
-
const virtualFiles = [];
|
|
119
|
-
for (const file of unincludedFiles) {
|
|
120
|
-
// Files that don't exist are always virtual
|
|
121
|
-
if (!fs.existsSync(file)) {
|
|
122
|
-
virtualFiles.push(file);
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
// Check if file is in cache directory (like stdin files)
|
|
126
|
-
// These need tsconfig treatment even though they exist on disk
|
|
127
|
-
if (cacheLocation !== undefined) {
|
|
128
|
-
const absolutePath = path.resolve(file);
|
|
129
|
-
const cacheRoot = path.resolve(cacheLocation);
|
|
130
|
-
const relativeToCache = path.relative(cacheRoot, absolutePath);
|
|
131
|
-
// File is inside cache if relative path doesn't escape (no '..')
|
|
132
|
-
const isInCache = !relativeToCache.startsWith('..') && !path.isAbsolute(relativeToCache);
|
|
133
|
-
if (isInCache) {
|
|
134
|
-
virtualFiles.push(file);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
existingFiles.push(file);
|
|
139
|
-
}
|
|
140
|
-
return {
|
|
141
|
-
existingFiles,
|
|
142
|
-
virtualFiles,
|
|
143
|
-
program: createInMemoryProgram(existingFiles, cwd),
|
|
144
|
-
};
|
|
61
|
+
return unincludedFiles;
|
|
145
62
|
}
|
|
146
63
|
//# sourceMappingURL=handle-ts-files.js.map
|
package/dist/lib/open-report.js
CHANGED
|
@@ -23,8 +23,8 @@ const resultToFile = (result) => {
|
|
|
23
23
|
column: message?.column,
|
|
24
24
|
};
|
|
25
25
|
};
|
|
26
|
-
const getFiles = (report,
|
|
27
|
-
.filter(result =>
|
|
26
|
+
const getFiles = (report, isMatchingResult) => report.results
|
|
27
|
+
.filter(result => isMatchingResult(result))
|
|
28
28
|
.toSorted(sortResults)
|
|
29
29
|
.map(result => resultToFile(result));
|
|
30
30
|
const openReport = async (report) => {
|
package/dist/lib/xo.d.ts
CHANGED
|
@@ -23,13 +23,11 @@ export declare class Xo {
|
|
|
23
23
|
private prepareEslintConfig;
|
|
24
24
|
private discoverFiles;
|
|
25
25
|
/**
|
|
26
|
-
Add
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
Add existing files to the config with an in-memory TypeScript Program.
|
|
26
|
+
Add unincluded files to the config with a generated tsconfig approach. Passing an empty array removes the generated config entry.
|
|
27
|
+
|
|
28
|
+
This uses `parserOptions.project` rather than an in-memory `parserOptions.programs`. A pre-built program's AST is built once from the original file text and is never updated between ESLint's multiple `--fix` passes, so stale offsets corrupt the output. A file-based project is re-read on each pass, so autofix stays correct.
|
|
31
29
|
*/
|
|
32
|
-
private
|
|
30
|
+
private addUnincludedFilesToConfig;
|
|
33
31
|
private processReport;
|
|
34
32
|
private getReportStatistics;
|
|
35
33
|
/**
|
|
@@ -40,13 +38,13 @@ export declare class Xo {
|
|
|
40
38
|
Sets the XO config on the XO instance.
|
|
41
39
|
*/
|
|
42
40
|
setXoConfig(): Promise<void>;
|
|
43
|
-
setEslintConfig(cliIgnores?: string[],
|
|
41
|
+
setEslintConfig(cliIgnores?: string[], shouldStripDefaultIgnores?: boolean): void;
|
|
44
42
|
/**
|
|
45
43
|
Ensures the cache directory exists. This needs to run once before both tsconfig handling and running ESLint occur.
|
|
46
44
|
*/
|
|
47
45
|
ensureCacheDirectory(): Promise<void>;
|
|
48
46
|
/**
|
|
49
|
-
Checks every TS file to ensure
|
|
47
|
+
Checks every TS file to ensure it is included in the tsconfig. Any that are not included are routed through a generated tsconfig (`tsconfig.generated.json`) so that autofix works correctly across multiple ESLint passes.
|
|
50
48
|
|
|
51
49
|
@param files - The TypeScript files being linted.
|
|
52
50
|
*/
|
|
@@ -54,7 +52,7 @@ export declare class Xo {
|
|
|
54
52
|
/**
|
|
55
53
|
Initializes the ESLint instance on the XO instance.
|
|
56
54
|
*/
|
|
57
|
-
initEslint(files?: string[], cliIgnores?: string[],
|
|
55
|
+
initEslint(files?: string[], cliIgnores?: string[], shouldStripDefaultIgnores?: boolean): Promise<void>;
|
|
58
56
|
/**
|
|
59
57
|
Create an ESLint flat config for editor integrations using the same XO pipeline as the CLI.
|
|
60
58
|
*/
|
package/dist/lib/xo.js
CHANGED
|
@@ -3,6 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import syncFs from 'node:fs';
|
|
4
4
|
import fs from 'node:fs/promises';
|
|
5
5
|
import process from 'node:process';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
6
7
|
import { ESLint } from 'eslint';
|
|
7
8
|
import findCacheDirectory from 'find-cache-directory';
|
|
8
9
|
import { globby, isDynamicPattern } from 'globby';
|
|
@@ -38,17 +39,28 @@ const createIgnoredLintResult = (filePath) => ({
|
|
|
38
39
|
usedDeprecatedRules: [],
|
|
39
40
|
});
|
|
40
41
|
const normalizeGlobPath = (filePath) => filePath.split(path.sep).join('/');
|
|
41
|
-
const
|
|
42
|
+
const isPathMatchingPattern = (filePath, pattern) => micromatch.isMatch(normalizeGlobPath(filePath), normalizeGlobPath(pattern), { dot: true });
|
|
43
|
+
const isPathInside = (parentPath, childPath) => {
|
|
44
|
+
const relativePath = path.relative(parentPath, childPath);
|
|
45
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
46
|
+
};
|
|
47
|
+
const getGeneratedTsconfigPath = (directory, files) => {
|
|
48
|
+
const hash = createHash('sha256')
|
|
49
|
+
.update(JSON.stringify(files.toSorted((first, second) => first.localeCompare(second))))
|
|
50
|
+
.digest('hex')
|
|
51
|
+
.slice(0, 16);
|
|
52
|
+
return path.join(directory, `tsconfig.generated.${hash}.json`);
|
|
53
|
+
};
|
|
42
54
|
const isIgnoredByPatterns = (filePath, patterns) => {
|
|
43
55
|
let isIgnored = false;
|
|
44
56
|
for (const pattern of patterns) {
|
|
45
57
|
if (pattern.startsWith('!')) {
|
|
46
|
-
if (
|
|
58
|
+
if (isPathMatchingPattern(filePath, pattern.slice(1))) {
|
|
47
59
|
isIgnored = false;
|
|
48
60
|
}
|
|
49
61
|
continue;
|
|
50
62
|
}
|
|
51
|
-
if (
|
|
63
|
+
if (isPathMatchingPattern(filePath, pattern)) {
|
|
52
64
|
isIgnored = true;
|
|
53
65
|
}
|
|
54
66
|
}
|
|
@@ -117,7 +129,7 @@ const stripDefaultIgnoreConfigs = (configs) => configs.map(configItem => {
|
|
|
117
129
|
const { ignores: _ignored, ...configWithoutIgnores } = configItem;
|
|
118
130
|
return configWithoutIgnores;
|
|
119
131
|
});
|
|
120
|
-
const
|
|
132
|
+
const doesDefaultIgnoreOverlapReopenedPattern = (defaultIgnore, pattern) => {
|
|
121
133
|
const { base, isGlob } = micromatch.scan(pattern, { parts: true });
|
|
122
134
|
const patternDirname = path.posix.dirname(pattern);
|
|
123
135
|
const reopenedBase = isGlob ? base : (patternDirname === '' ? pattern : patternDirname);
|
|
@@ -133,7 +145,7 @@ const defaultIgnoreOverlapsReopenedPattern = (defaultIgnore, pattern) => {
|
|
|
133
145
|
const getReopenedDefaultPatterns = (patterns) => patterns
|
|
134
146
|
.filter(pattern => pattern.startsWith('!'))
|
|
135
147
|
.map(pattern => pattern.slice(1))
|
|
136
|
-
.filter(pattern => defaultIgnores.some(defaultIgnore =>
|
|
148
|
+
.filter(pattern => defaultIgnores.some(defaultIgnore => doesDefaultIgnoreOverlapReopenedPattern(defaultIgnore, pattern)));
|
|
137
149
|
/**
|
|
138
150
|
XO only compensates for negations that reopen its built-in default ignores.
|
|
139
151
|
User-provided positive ignores are still used only for pruning.
|
|
@@ -162,7 +174,7 @@ const discoverLintFiles = async ({ cwd, globs, positiveGlobalIgnores, discoveryI
|
|
|
162
174
|
const reopenedFiles = await globby(globs, {
|
|
163
175
|
ignore: [
|
|
164
176
|
...positiveGlobalIgnores,
|
|
165
|
-
...defaultIgnores.filter(defaultIgnore => reopenedDefaultPatterns.every(pattern => !
|
|
177
|
+
...defaultIgnores.filter(defaultIgnore => reopenedDefaultPatterns.every(pattern => !doesDefaultIgnoreOverlapReopenedPattern(defaultIgnore, pattern))),
|
|
166
178
|
],
|
|
167
179
|
onlyFiles: true,
|
|
168
180
|
gitignore: true,
|
|
@@ -233,6 +245,14 @@ export class Xo {
|
|
|
233
245
|
*/
|
|
234
246
|
#cacheLocation;
|
|
235
247
|
/**
|
|
248
|
+
Directory for generated tsconfigs for unincluded TypeScript files.
|
|
249
|
+
*/
|
|
250
|
+
#generatedTsconfigDirectory;
|
|
251
|
+
/**
|
|
252
|
+
File path to the currently active generated tsconfig.
|
|
253
|
+
*/
|
|
254
|
+
#currentGeneratedTsconfigPath;
|
|
255
|
+
/**
|
|
236
256
|
XO config derived from both the base config and the resolved flat config.
|
|
237
257
|
*/
|
|
238
258
|
#xoConfig;
|
|
@@ -258,17 +278,6 @@ export class Xo {
|
|
|
258
278
|
We use this to also add negative glob patterns in case a user overrides the parserOptions in their XO config.
|
|
259
279
|
*/
|
|
260
280
|
#tsFilesIgnoresGlob = [];
|
|
261
|
-
/**
|
|
262
|
-
Store per-file configs separately from base config to prevent unbounded array growth.
|
|
263
|
-
Key: file path, Value: config for that file.
|
|
264
|
-
This prevents memory bloat in long-running processes (e.g., language servers).
|
|
265
|
-
*/
|
|
266
|
-
#fileConfigs = new Map();
|
|
267
|
-
/**
|
|
268
|
-
Track virtual/stdin files that share a single tsconfig.stdin.json.
|
|
269
|
-
These are handled differently from regular files.
|
|
270
|
-
*/
|
|
271
|
-
#virtualFiles = new Set();
|
|
272
281
|
constructor(_linterOptions, _baseXoConfig = {}) {
|
|
273
282
|
this.#linterOptions = _linterOptions;
|
|
274
283
|
this.#baseXoConfig = _baseXoConfig;
|
|
@@ -284,15 +293,18 @@ export class Xo {
|
|
|
284
293
|
}
|
|
285
294
|
const backupCacheLocation = path.join(os.tmpdir(), cacheDirName);
|
|
286
295
|
this.#cacheLocation = findCacheDirectory({ name: cacheDirName, cwd: this.#linterOptions.cwd }) ?? backupCacheLocation;
|
|
296
|
+
this.#generatedTsconfigDirectory = isPathInside(this.#linterOptions.cwd, this.#cacheLocation)
|
|
297
|
+
? this.#cacheLocation
|
|
298
|
+
: path.join(this.#linterOptions.cwd, 'node_modules', '.cache', cacheDirName);
|
|
287
299
|
}
|
|
288
300
|
/**
|
|
289
301
|
Initializes the ESLint flat config on the XO instance.
|
|
290
302
|
*/
|
|
291
|
-
async prepareEslintConfig(files, cliIgnores = arrify(this.#baseXoConfig.ignores),
|
|
303
|
+
async prepareEslintConfig(files, cliIgnores = arrify(this.#baseXoConfig.ignores), shouldStripDefaultIgnores = false) {
|
|
292
304
|
await this.setXoConfig();
|
|
293
305
|
await this.ensureCacheDirectory();
|
|
294
306
|
await this.handleUnincludedTsFiles(files);
|
|
295
|
-
this.setEslintConfig(cliIgnores,
|
|
307
|
+
this.setEslintConfig(cliIgnores, shouldStripDefaultIgnores);
|
|
296
308
|
if (!this.#eslintConfig) {
|
|
297
309
|
throw new Error('"Xo.prepareEslintConfig" failed');
|
|
298
310
|
}
|
|
@@ -320,100 +332,74 @@ export class Xo {
|
|
|
320
332
|
};
|
|
321
333
|
}
|
|
322
334
|
/**
|
|
323
|
-
Add
|
|
335
|
+
Add unincluded files to the config with a generated tsconfig approach. Passing an empty array removes the generated config entry.
|
|
336
|
+
|
|
337
|
+
This uses `parserOptions.project` rather than an in-memory `parserOptions.programs`. A pre-built program's AST is built once from the original file text and is never updated between ESLint's multiple `--fix` passes, so stale offsets corrupt the output. A file-based project is re-read on each pass, so autofix stays correct.
|
|
324
338
|
*/
|
|
325
|
-
async
|
|
339
|
+
async addUnincludedFilesToConfig(files) {
|
|
326
340
|
if (!this.#xoConfig) {
|
|
327
341
|
return;
|
|
328
342
|
}
|
|
329
343
|
try {
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
compilerOptions: {
|
|
342
|
-
...tsconfigDefaults.compilerOptions,
|
|
343
|
-
module: 'ESNext',
|
|
344
|
-
moduleResolution: 'NodeNext',
|
|
345
|
-
esModuleInterop: true,
|
|
346
|
-
skipLibCheck: true,
|
|
347
|
-
// TypeScript 6 only auto-includes `@types/*` packages when `types` contains `'*'`. Without this, virtual/stdin files resolve imports to `any` and type-aware rules misfire.
|
|
348
|
-
types: ['*'],
|
|
349
|
-
},
|
|
350
|
-
files: filesArray,
|
|
351
|
-
};
|
|
352
|
-
await fs.writeFile(tsconfigPath, JSON.stringify(tsconfigContent, null, 2));
|
|
353
|
-
if (configIndex === -1) {
|
|
354
|
-
const parserOptions = {
|
|
355
|
-
projectService: false,
|
|
356
|
-
project: tsconfigPath,
|
|
357
|
-
tsconfigRootDir: this.#linterOptions.cwd,
|
|
358
|
-
};
|
|
359
|
-
this.#xoConfig.push({
|
|
360
|
-
files: relativeFiles,
|
|
361
|
-
languageOptions: {
|
|
362
|
-
parser: typescriptParser,
|
|
363
|
-
parserOptions,
|
|
364
|
-
},
|
|
365
|
-
});
|
|
344
|
+
const previousTsconfigPath = this.#currentGeneratedTsconfigPath;
|
|
345
|
+
const configIndex = previousTsconfigPath === undefined
|
|
346
|
+
? -1
|
|
347
|
+
: this.#xoConfig.findIndex(configItem => {
|
|
348
|
+
const { languageOptions } = configItem;
|
|
349
|
+
const parserOptions = languageOptions?.['parserOptions'];
|
|
350
|
+
return parserOptions?.project === previousTsconfigPath;
|
|
351
|
+
});
|
|
352
|
+
if (files.length === 0) {
|
|
353
|
+
if (configIndex !== -1) {
|
|
354
|
+
this.#xoConfig.splice(configIndex, 1);
|
|
366
355
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
this.#xoConfig[configIndex] = {
|
|
370
|
-
...existingConfig,
|
|
371
|
-
files: relativeFiles,
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
this.#virtualFiles.clear();
|
|
375
|
-
for (const file of nextVirtualFiles) {
|
|
376
|
-
this.#virtualFiles.add(file);
|
|
356
|
+
if (previousTsconfigPath !== undefined) {
|
|
357
|
+
this.#currentGeneratedTsconfigPath = undefined;
|
|
377
358
|
}
|
|
378
359
|
return;
|
|
379
360
|
}
|
|
380
|
-
|
|
381
|
-
|
|
361
|
+
const tsconfigPath = getGeneratedTsconfigPath(this.#generatedTsconfigDirectory, files);
|
|
362
|
+
// The generated tsconfig references files by absolute path; the ESLint config matches them by path relative to `cwd`.
|
|
363
|
+
const relativeFiles = files.map(file => path.relative(this.#linterOptions.cwd, file));
|
|
364
|
+
const tsconfigContent = {
|
|
365
|
+
compilerOptions: {
|
|
366
|
+
...tsconfigDefaults.compilerOptions,
|
|
367
|
+
module: 'ESNext',
|
|
368
|
+
moduleResolution: 'NodeNext',
|
|
369
|
+
esModuleInterop: true,
|
|
370
|
+
resolveJsonModule: true,
|
|
371
|
+
skipLibCheck: true,
|
|
372
|
+
// JS files are routed here too (type-aware rules apply to JS). Without `allowJs`, TypeScript does not load them as source files, so the parser reports "file not found in project".
|
|
373
|
+
allowJs: true,
|
|
374
|
+
// TypeScript 6 only auto-includes `@types/*` packages when `types` contains `'*'`. Without this, unincluded files resolve imports to `any` and type-aware rules misfire.
|
|
375
|
+
types: ['*'],
|
|
376
|
+
},
|
|
377
|
+
files,
|
|
378
|
+
};
|
|
379
|
+
await fs.mkdir(path.dirname(tsconfigPath), { recursive: true });
|
|
380
|
+
await fs.writeFile(tsconfigPath, JSON.stringify(tsconfigContent, null, 2));
|
|
381
|
+
this.#currentGeneratedTsconfigPath = tsconfigPath;
|
|
382
|
+
const parserOptions = {
|
|
383
|
+
projectService: false,
|
|
384
|
+
project: tsconfigPath,
|
|
385
|
+
tsconfigRootDir: this.#linterOptions.cwd,
|
|
386
|
+
};
|
|
387
|
+
const generatedConfig = {
|
|
388
|
+
files: relativeFiles,
|
|
389
|
+
languageOptions: {
|
|
390
|
+
parser: typescriptParser,
|
|
391
|
+
parserOptions,
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
if (configIndex === -1) {
|
|
395
|
+
this.#xoConfig.push(generatedConfig);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
this.#xoConfig[configIndex] = generatedConfig;
|
|
382
399
|
}
|
|
383
|
-
this.#virtualFiles.clear();
|
|
384
|
-
await fs.rm(tsconfigPath, { force: true });
|
|
385
400
|
}
|
|
386
401
|
catch (error) {
|
|
387
|
-
console.warn('XO: Failed to create tsconfig for
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
Add existing files to the config with an in-memory TypeScript Program.
|
|
392
|
-
*/
|
|
393
|
-
addExistingFilesToConfig(files, program) {
|
|
394
|
-
if (!this.#xoConfig || files.length === 0) {
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
const parserOptions = {
|
|
398
|
-
project: false,
|
|
399
|
-
projectService: false,
|
|
400
|
-
};
|
|
401
|
-
if (program) {
|
|
402
|
-
parserOptions.programs = [program];
|
|
403
|
-
}
|
|
404
|
-
const config = {
|
|
405
|
-
files: files.map(file => path.relative(this.#linterOptions.cwd, file)),
|
|
406
|
-
languageOptions: {
|
|
407
|
-
parser: typescriptParser,
|
|
408
|
-
parserOptions,
|
|
409
|
-
},
|
|
410
|
-
};
|
|
411
|
-
// IMPORTANT: All files intentionally share the same config object reference for memory efficiency.
|
|
412
|
-
// This prevents unbounded memory growth in long-running processes (e.g., language servers).
|
|
413
|
-
// The config is immutable after creation, so sharing is safe.
|
|
414
|
-
// Deduplication happens in setEslintConfig() via Set to avoid duplicate configs in the final array.
|
|
415
|
-
for (const file of files) {
|
|
416
|
-
this.#fileConfigs.set(file, config);
|
|
402
|
+
console.warn('XO: Failed to create tsconfig for unincluded files. Type-aware linting will be disabled for these files.', error instanceof Error ? error.message : String(error));
|
|
417
403
|
}
|
|
418
404
|
}
|
|
419
405
|
processReport(report, { rulesMeta = {} } = {}) {
|
|
@@ -426,17 +412,14 @@ export class Xo {
|
|
|
426
412
|
...this.getReportStatistics(report),
|
|
427
413
|
};
|
|
428
414
|
defineLazyProperty(result, 'usedDeprecatedRules', () => {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (!seenRules.has(rule.ruleId)) {
|
|
434
|
-
seenRules.add(rule.ruleId);
|
|
435
|
-
rules.push(rule);
|
|
436
|
-
}
|
|
415
|
+
const seenRuleIds = new Set();
|
|
416
|
+
return report.flatMap(({ usedDeprecatedRules }) => usedDeprecatedRules).filter(rule => {
|
|
417
|
+
if (seenRuleIds.has(rule.ruleId)) {
|
|
418
|
+
return false;
|
|
437
419
|
}
|
|
438
|
-
|
|
439
|
-
|
|
420
|
+
seenRuleIds.add(rule.ruleId);
|
|
421
|
+
return true;
|
|
422
|
+
});
|
|
440
423
|
});
|
|
441
424
|
return result;
|
|
442
425
|
}
|
|
@@ -488,21 +471,18 @@ export class Xo {
|
|
|
488
471
|
this.#tsFilesGlob.push(...tsGlob);
|
|
489
472
|
this.#tsFilesIgnoresGlob.push(...tsFilesIgnoresGlob);
|
|
490
473
|
}
|
|
491
|
-
setEslintConfig(cliIgnores = arrify(this.#baseXoConfig.ignores),
|
|
474
|
+
setEslintConfig(cliIgnores = arrify(this.#baseXoConfig.ignores), shouldStripDefaultIgnores = false) {
|
|
492
475
|
if (!this.#xoConfig) {
|
|
493
476
|
throw new Error('"Xo.setEslintConfig" failed');
|
|
494
477
|
}
|
|
495
|
-
// Combine base config with per-file configs from Map
|
|
496
|
-
// Deduplicate configs since multiple files can share the same config object
|
|
497
478
|
const [baseConfig = {}, ...resolvedConfigs] = this.#xoConfig;
|
|
498
479
|
const { ignores, ...configWithoutCliIgnores } = baseConfig;
|
|
499
480
|
const expandedResolvedConfigs = resolvedConfigs.map(config => expandGlobalIgnoreConfigForEslint(config));
|
|
500
|
-
const uniqueFileConfigs = [...new Set(this.#fileConfigs.values())];
|
|
501
481
|
const cliIgnoreConfig = cliIgnores.length > 0 ? [{ ignores: expandIgnoreNegationsForEslint(cliIgnores) }] : [];
|
|
502
|
-
const allConfigs = [configWithoutCliIgnores, ...expandedResolvedConfigs, ...cliIgnoreConfig
|
|
482
|
+
const allConfigs = [configWithoutCliIgnores, ...expandedResolvedConfigs, ...cliIgnoreConfig];
|
|
503
483
|
// Always regenerate to support instance reuse with new files
|
|
504
484
|
this.#eslintConfig = xoToEslintConfig(allConfigs);
|
|
505
|
-
if (
|
|
485
|
+
if (shouldStripDefaultIgnores) {
|
|
506
486
|
this.#eslintConfig = stripDefaultIgnoreConfigs(this.#eslintConfig);
|
|
507
487
|
}
|
|
508
488
|
}
|
|
@@ -527,39 +507,30 @@ export class Xo {
|
|
|
527
507
|
}
|
|
528
508
|
}
|
|
529
509
|
/**
|
|
530
|
-
Checks every TS file to ensure
|
|
510
|
+
Checks every TS file to ensure it is included in the tsconfig. Any that are not included are routed through a generated tsconfig (`tsconfig.generated.json`) so that autofix works correctly across multiple ESLint passes.
|
|
531
511
|
|
|
532
512
|
@param files - The TypeScript files being linted.
|
|
533
513
|
*/
|
|
534
514
|
async handleUnincludedTsFiles(files) {
|
|
535
|
-
if (!this.#linterOptions.ts
|
|
515
|
+
if (!this.#linterOptions.ts) {
|
|
536
516
|
return;
|
|
537
517
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if (allTsFiles.length === 0) {
|
|
541
|
-
this.#fileConfigs.clear();
|
|
542
|
-
if (this.#virtualFiles.size > 0) {
|
|
543
|
-
await this.addVirtualFilesToConfig([]);
|
|
544
|
-
}
|
|
518
|
+
if (!files || files.length === 0) {
|
|
519
|
+
await this.addUnincludedFilesToConfig([]);
|
|
545
520
|
return;
|
|
546
521
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
this
|
|
553
|
-
if (existingFiles.length > 0) {
|
|
554
|
-
this.addExistingFilesToConfig(existingFiles, program);
|
|
555
|
-
}
|
|
556
|
-
await this.addVirtualFilesToConfig(virtualFiles);
|
|
522
|
+
// Get ALL TypeScript files being linted
|
|
523
|
+
const allTsFiles = matchFilesForTsConfig(this.#linterOptions.cwd, files, this.#tsFilesGlob, this.#tsFilesIgnoresGlob);
|
|
524
|
+
const unincludedFiles = allTsFiles.length === 0
|
|
525
|
+
? []
|
|
526
|
+
: handleTsconfig({ files: allTsFiles, cwd: this.#linterOptions.cwd });
|
|
527
|
+
await this.addUnincludedFilesToConfig(unincludedFiles);
|
|
557
528
|
}
|
|
558
529
|
/**
|
|
559
530
|
Initializes the ESLint instance on the XO instance.
|
|
560
531
|
*/
|
|
561
|
-
async initEslint(files, cliIgnores = arrify(this.#baseXoConfig.ignores),
|
|
562
|
-
await this.prepareEslintConfig(files, cliIgnores,
|
|
532
|
+
async initEslint(files, cliIgnores = arrify(this.#baseXoConfig.ignores), shouldStripDefaultIgnores = false) {
|
|
533
|
+
await this.prepareEslintConfig(files, cliIgnores, shouldStripDefaultIgnores);
|
|
563
534
|
if (!this.#xoConfig) {
|
|
564
535
|
throw new Error('"Xo.initEslint" failed');
|
|
565
536
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "JavaScript/TypeScript linter (ESLint wrapper) with great defaults",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "xojs/xo",
|
|
@@ -77,30 +77,30 @@
|
|
|
77
77
|
"arrify": "^3.0.0",
|
|
78
78
|
"cosmiconfig": "^9.0.2",
|
|
79
79
|
"define-lazy-prop": "^3.0.0",
|
|
80
|
-
"eslint": "^10.
|
|
81
|
-
"eslint-config-xo": "^0.
|
|
80
|
+
"eslint": "^10.6.0",
|
|
81
|
+
"eslint-config-xo": "^0.57.0",
|
|
82
82
|
"eslint-formatter-pretty": "^7.1.0",
|
|
83
83
|
"find-cache-directory": "^6.0.0",
|
|
84
84
|
"get-stdin": "^10.0.0",
|
|
85
85
|
"get-tsconfig": "^4.14.0",
|
|
86
|
-
"globby": "^16.2.
|
|
86
|
+
"globby": "^16.2.1",
|
|
87
87
|
"jiti": "^2.7.0",
|
|
88
88
|
"meow": "^14.1.0",
|
|
89
89
|
"micromatch": "^4.0.8",
|
|
90
90
|
"open-editor": "^6.0.0",
|
|
91
91
|
"path-exists": "^5.0.0",
|
|
92
|
-
"prettier": "^3.
|
|
93
|
-
"type-fest": "^5.
|
|
92
|
+
"prettier": "^3.9.4",
|
|
93
|
+
"type-fest": "^5.8.0",
|
|
94
94
|
"typescript": "^6.0.3"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
97
97
|
"@types/micromatch": "^4.0.10",
|
|
98
|
-
"@types/node": "^
|
|
98
|
+
"@types/node": "^26.1.0",
|
|
99
99
|
"dedent": "^1.7.2",
|
|
100
100
|
"eslint-plugin-vue": "^10.9.2",
|
|
101
101
|
"execa": "^9.6.1",
|
|
102
102
|
"husky": "^9.1.7",
|
|
103
|
-
"np": "^11.2.
|
|
103
|
+
"np": "^11.2.2",
|
|
104
104
|
"temp-dir": "^3.0.0",
|
|
105
105
|
"xo": "file:."
|
|
106
106
|
},
|