xo 1.2.3 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 a generated tsconfig for type aware linting.
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
- this.eslintConfig ??= xoToEslintConfig([...this.xoConfig], { prettierOptions: this.prettierConfig });
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 a generated tsconfig for type aware linting.
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
- const tsFiles = matchFilesForTsConfig(this.linterOptions.cwd, files, this.tsFilesGlob, this.tsFilesIgnoresGlob);
213
- if (tsFiles.length === 0) {
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 { fallbackTsConfigPath, unincludedFiles } = await handleTsconfig({
292
+ const { program, existingFiles, virtualFiles } = handleTsconfig({
293
+ files: allTsFiles,
217
294
  cwd: this.linterOptions.cwd,
218
- files: tsFiles,
295
+ cacheLocation: this.cacheLocation,
219
296
  });
220
- if (!this.xoConfig || unincludedFiles.length === 0) {
221
- return;
297
+ this.fileConfigs.clear();
298
+ if (existingFiles.length > 0) {
299
+ this.addExistingFilesToConfig(existingFiles, program);
222
300
  }
223
- const config = {};
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
- this.eslint ??= new ESLint(eslintOptions);
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
- let files = await globby(globs, {
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
- files = '!**/*';
357
+ return this.processReport(ignoredResults);
281
358
  }
282
- const results = await this.eslint.lintFiles(files);
283
- const rulesMeta = this.eslint.getRulesMetaForResults(results);
284
- return this.processReport(results, { rulesMeta });
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": "1.2.3",
3
+ "version": "2.0.1",
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.17"
21
+ "node": ">=20.19"
22
22
  },
23
23
  "scripts": {
24
24
  "build": "npm run clean && tsc",
@@ -33,6 +33,8 @@
33
33
  "files": [
34
34
  "dist/lib/*.js",
35
35
  "dist/lib/*.d.ts",
36
+ "dist/lib/rules/*.js",
37
+ "dist/lib/rules/*.d.ts",
36
38
  "dist/*.js",
37
39
  "dist/*.d.ts"
38
40
  ],
@@ -65,53 +67,51 @@
65
67
  "typescript"
66
68
  ],
67
69
  "dependencies": {
68
- "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0",
69
- "@sindresorhus/tsconfig": "^7.0.0",
70
- "@stylistic/eslint-plugin": "^4.2.0",
71
- "@typescript-eslint/parser": "^8.37.0",
70
+ "@eslint-community/eslint-plugin-eslint-comments": "^4.7.1",
71
+ "@eslint/compat": "^2.0.2",
72
+ "@sindresorhus/tsconfig": "^8.1.0",
72
73
  "arrify": "^3.0.0",
73
74
  "cosmiconfig": "^9.0.0",
74
75
  "define-lazy-prop": "^3.0.0",
75
- "eslint": "^9.31.0",
76
- "eslint-config-prettier": "^10.1.5",
77
- "eslint-config-xo-react": "^0.28.0",
78
- "eslint-config-xo-typescript": "^7.0.0",
79
- "eslint-formatter-pretty": "^6.0.1",
80
- "eslint-plugin-ava": "^15.0.1",
76
+ "eslint": "^10.0.2",
77
+ "eslint-config-prettier": "^10.1.8",
78
+ "eslint-config-xo-react": "^0.29.0",
79
+ "eslint-config-xo-typescript": "^10.0.0",
80
+ "eslint-formatter-pretty": "^7.0.0",
81
+ "eslint-plugin-ava": "^16.0.0",
81
82
  "eslint-plugin-import-x": "^4.16.1",
82
- "eslint-plugin-n": "^17.21.0",
83
- "eslint-plugin-no-use-extend-native": "^0.7.2",
84
- "eslint-plugin-prettier": "^5.5.1",
85
- "eslint-plugin-promise": "^7.2.1",
86
- "eslint-plugin-unicorn": "^59.0.1",
83
+ "eslint-plugin-n": "^17.24.0",
84
+ "eslint-plugin-prettier": "^5.5.5",
85
+ "eslint-plugin-unicorn": "^63.0.0",
87
86
  "find-cache-directory": "^6.0.0",
88
- "get-stdin": "^9.0.0",
89
- "get-tsconfig": "^4.10.1",
90
- "globals": "^16.3.0",
91
- "globby": "^14.1.0",
92
- "meow": "^13.2.0",
87
+ "get-stdin": "^10.0.0",
88
+ "get-tsconfig": "^4.13.6",
89
+ "globals": "^17.3.0",
90
+ "globby": "^16.1.1",
91
+ "meow": "^14.1.0",
93
92
  "micromatch": "^4.0.8",
94
- "open-editor": "^5.1.0",
93
+ "open-editor": "^6.0.0",
95
94
  "path-exists": "^5.0.0",
96
- "prettier": "^3.6.2",
97
- "type-fest": "^4.41.0",
98
- "typescript-eslint": "^8.37.0"
95
+ "prettier": "^3.8.1",
96
+ "type-fest": "^5.4.3",
97
+ "typescript": "^5.9.3",
98
+ "typescript-eslint": "^8.56.1"
99
99
  },
100
100
  "devDependencies": {
101
- "@commitlint/cli": "^19.8.1",
102
- "@commitlint/config-conventional": "^19.8.1",
103
- "@types/eslint": "9.6.1",
104
- "@types/micromatch": "^4.0.9",
101
+ "@commitlint/cli": "^20.4.1",
102
+ "@commitlint/config-conventional": "^20.4.1",
103
+ "@types/micromatch": "^4.0.10",
104
+ "@types/node": "^25.2.1",
105
105
  "@types/prettier": "^3.0.0",
106
- "ava": "^6.4.1",
107
- "dedent": "^1.6.0",
108
- "execa": "^9.5.3",
106
+ "ava": "^7.0.0",
107
+ "dedent": "^1.7.1",
108
+ "execa": "^9.6.1",
109
109
  "husky": "^9.1.7",
110
- "lint-staged": "^16.0.0",
111
- "np": "^10.2.0",
112
- "npm-package-json-lint": "^9.0.0",
110
+ "lint-staged": "^16.3.4",
111
+ "np": "^11.0.2",
112
+ "npm-package-json-lint": "^9.1.0",
113
113
  "npm-package-json-lint-config-default": "^8.0.1",
114
- "prettier-plugin-packagejson": "^2.5.18",
114
+ "prettier-plugin-packagejson": "^3.0.0",
115
115
  "temp-dir": "^3.0.0",
116
116
  "xo": "file:."
117
117
  },
@@ -143,5 +143,10 @@
143
143
  "text",
144
144
  "lcov"
145
145
  ]
146
+ },
147
+ "xo": {
148
+ "rules": {
149
+ "ava/no-ignored-test-files": "off"
150
+ }
146
151
  }
147
152
  }
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/benmosher/eslint-plugin-import), [`ava`](https://github.com/avajs/eslint-plugin-ava), [`n`](https://github.com/eslint-community/eslint-plugin-n) and more.
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
- *You'll need [eslint-config-xo-vue](https://github.com/ChocPanda/eslint-config-xo-vue#use-with-xo) for specific linting in a Vue app.*
49
+ *For framework-specific linting, see [Astro](#astro), [Svelte](#svelte), and [Vue](#vue).*
50
50
 
51
51
  ## Usage
52
52
 
@@ -58,6 +58,7 @@ $ xo --help
58
58
 
59
59
  Options
60
60
  --fix Automagically fix issues
61
+ --fix-dry-run Automagically fix issues without saving the changes to the file system
61
62
  --reporter Reporter to use
62
63
  --space Use space indent instead of tabs [Default: 2]
63
64
  --config Path to a XO configuration file
@@ -136,16 +137,16 @@ export default [...] satisfies import('xo').FlatXoConfig
136
137
 
137
138
  ### files
138
139
 
139
- Type: `string | string[] | undefined`\
140
+ Type: `string | (string | string[])[]`\
140
141
  Default: `**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx,vue,svelte,astro}`
141
142
 
142
- A glob or array of glob strings which the config object will apply. By default `XO` will apply the configuration to [all files](lib/constants.ts).
143
+ 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
144
 
144
145
  > 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
146
 
146
147
  ### ignores
147
148
 
148
- Type: `string[]`
149
+ Type: `string | string[]`
149
150
 
150
151
  Some [paths](lib/constants.ts) are ignored by default, including paths in `.gitignore`. Additional ignores can be added here.
151
152
 
@@ -204,6 +205,66 @@ Default: `false`
204
205
 
205
206
  Adds `eslint-plugin-react`, `eslint-plugin-react-hooks`, and `eslint-config-xo-react` to get all the React best practices applied automatically.
206
207
 
208
+ ### Astro
209
+
210
+ To lint [Astro](https://astro.build) files, install [`eslint-plugin-astro`](https://github.com/ota-meshi/eslint-plugin-astro):
211
+
212
+ ```sh
213
+ npm install --save-dev eslint-plugin-astro
214
+ ```
215
+
216
+ Then spread its recommended config in your `xo.config.js`:
217
+
218
+ ```js
219
+ import astroPlugin from 'eslint-plugin-astro';
220
+
221
+ const xoConfig = [
222
+ ...astroPlugin.configs.recommended,
223
+ ];
224
+
225
+ export default xoConfig;
226
+ ```
227
+
228
+ ### Svelte
229
+
230
+ To lint [Svelte](https://svelte.dev) files, install [`eslint-plugin-svelte`](https://github.com/sveltejs/eslint-plugin-svelte):
231
+
232
+ ```sh
233
+ npm install --save-dev eslint-plugin-svelte
234
+ ```
235
+
236
+ Then spread its recommended config in your `xo.config.js`:
237
+
238
+ ```js
239
+ import sveltePlugin from 'eslint-plugin-svelte';
240
+
241
+ const xoConfig = [
242
+ ...sveltePlugin.configs.recommended,
243
+ ];
244
+
245
+ export default xoConfig;
246
+ ```
247
+
248
+ ### Vue
249
+
250
+ To lint [Vue](https://vuejs.org) files, install [`eslint-plugin-vue`](https://github.com/vuejs/eslint-plugin-vue):
251
+
252
+ ```sh
253
+ npm install --save-dev eslint-plugin-vue
254
+ ```
255
+
256
+ Then spread its recommended config in your `xo.config.js`:
257
+
258
+ ```js
259
+ import vuePlugin from 'eslint-plugin-vue';
260
+
261
+ const xoConfig = [
262
+ ...vuePlugin.configs['flat/recommended'],
263
+ ];
264
+
265
+ export default xoConfig;
266
+ ```
267
+
207
268
  ## TypeScript
208
269
 
209
270
  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 +367,12 @@ Show the world you're using XO → [![XO code style](https://shields.io/badge/co
306
367
  [![XO code style](https://shields.io/badge/code_style-5ed9c7?logo=xo&labelColor=gray&logoSize=auto)](https://github.com/xojs/xo)
307
368
  ```
308
369
 
370
+ Large badge: [![XO code style](https://shields.io/badge/code_style-5ed9c7?style=for-the-badge&logo=xo&labelColor=gray&logoSize=auto)](https://github.com/xojs/xo)
371
+
372
+ ```md
373
+ [![XO code style](https://shields.io/badge/code_style-5ed9c7?style=for-the-badge&logo=xo&labelColor=gray&logoSize=auto)](https://github.com/xojs/xo)
374
+ ```
375
+
309
376
  Or [customize the badge](https://github.com/xojs/xo/issues/689#issuecomment-1253127616).
310
377
 
311
378
  You can also find some nice dynamic XO badges on [badgen.net](https://badgen.net/#xo).