xo 1.2.3 → 2.0.1

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