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.
@@ -0,0 +1,386 @@
1
+ const isPropertyContainer = (value) => typeof value === 'function' || (typeof value === 'object' && value !== null);
2
+ const isIdentifierNode = (node) => node.type === 'Identifier' && 'name' in node && typeof node.name === 'string';
3
+ const isLiteralNode = (node) => node.type === 'Literal';
4
+ const isMemberExpressionNode = (node) => node.type === 'MemberExpression'
5
+ && 'computed' in node
6
+ && typeof node.computed === 'boolean'
7
+ && 'object' in node
8
+ && 'property' in node;
9
+ const isBinaryExpressionNode = (node) => node.type === 'BinaryExpression'
10
+ && 'operator' in node
11
+ && typeof node.operator === 'string'
12
+ && 'left' in node
13
+ && 'right' in node;
14
+ const isNewExpressionNode = (node) => node.type === 'NewExpression' && 'callee' in node;
15
+ const createPropertyInfo = (value, extraProperties = []) => {
16
+ const all = new Set();
17
+ const callable = new Set();
18
+ for (const propertyName of extraProperties) {
19
+ all.add(propertyName);
20
+ }
21
+ for (let currentValue = value; isPropertyContainer(currentValue); currentValue = Object.getPrototypeOf(currentValue)) {
22
+ for (const propertyName of Object.getOwnPropertyNames(currentValue)) {
23
+ all.add(propertyName);
24
+ const descriptor = Object.getOwnPropertyDescriptor(currentValue, propertyName);
25
+ if (typeof descriptor?.value === 'function') {
26
+ callable.add(propertyName);
27
+ }
28
+ }
29
+ }
30
+ return {
31
+ all,
32
+ callable,
33
+ };
34
+ };
35
+ const emptyFunction = () => undefined;
36
+ // Keep the checked native object list explicit so rule behavior stays predictable.
37
+ const nativeObjectDefinitions = [
38
+ {
39
+ typeName: 'Array',
40
+ instance: [],
41
+ static: Array,
42
+ prototype: Array.prototype,
43
+ },
44
+ {
45
+ typeName: 'ArrayBuffer',
46
+ instance: new ArrayBuffer(0),
47
+ static: ArrayBuffer,
48
+ prototype: ArrayBuffer.prototype,
49
+ },
50
+ {
51
+ typeName: 'Boolean',
52
+ instance: Boolean.prototype,
53
+ static: Boolean,
54
+ prototype: Boolean.prototype,
55
+ },
56
+ {
57
+ typeName: 'DataView',
58
+ instance: new DataView(new ArrayBuffer(1)),
59
+ static: DataView,
60
+ prototype: DataView.prototype,
61
+ },
62
+ {
63
+ typeName: 'Date',
64
+ instance: new Date(),
65
+ static: Date,
66
+ prototype: Date.prototype,
67
+ },
68
+ {
69
+ typeName: 'Float32Array',
70
+ instance: new Float32Array(),
71
+ static: Float32Array,
72
+ prototype: Float32Array.prototype,
73
+ },
74
+ {
75
+ typeName: 'Float64Array',
76
+ instance: new Float64Array(),
77
+ static: Float64Array,
78
+ prototype: Float64Array.prototype,
79
+ },
80
+ {
81
+ typeName: 'Function',
82
+ instance: emptyFunction,
83
+ static: Function,
84
+ prototype: Function.prototype,
85
+ },
86
+ {
87
+ typeName: 'Int8Array',
88
+ instance: new Int8Array(),
89
+ static: Int8Array,
90
+ prototype: Int8Array.prototype,
91
+ },
92
+ {
93
+ typeName: 'Int16Array',
94
+ instance: new Int16Array(),
95
+ static: Int16Array,
96
+ prototype: Int16Array.prototype,
97
+ },
98
+ {
99
+ typeName: 'Int32Array',
100
+ instance: new Int32Array(),
101
+ static: Int32Array,
102
+ prototype: Int32Array.prototype,
103
+ },
104
+ {
105
+ typeName: 'Map',
106
+ instance: new Map(),
107
+ static: Map,
108
+ prototype: Map.prototype,
109
+ },
110
+ {
111
+ typeName: 'Number',
112
+ instance: Number.prototype,
113
+ static: Number,
114
+ prototype: Number.prototype,
115
+ },
116
+ {
117
+ typeName: 'Object',
118
+ instance: {},
119
+ static: Object,
120
+ prototype: Object.prototype,
121
+ },
122
+ {
123
+ typeName: 'Promise',
124
+ instance: Promise.resolve(),
125
+ static: Promise,
126
+ prototype: Promise.prototype,
127
+ },
128
+ {
129
+ typeName: 'RegExp',
130
+ instance: /./u,
131
+ static: RegExp,
132
+ prototype: RegExp.prototype,
133
+ },
134
+ {
135
+ typeName: 'Set',
136
+ instance: new Set(),
137
+ static: Set,
138
+ prototype: Set.prototype,
139
+ },
140
+ {
141
+ typeName: 'String',
142
+ instance: String.prototype,
143
+ instanceProperties: ['length'],
144
+ static: String,
145
+ prototype: String.prototype,
146
+ },
147
+ {
148
+ typeName: 'Uint8Array',
149
+ instance: new Uint8Array(),
150
+ static: Uint8Array,
151
+ prototype: Uint8Array.prototype,
152
+ },
153
+ {
154
+ typeName: 'Uint8ClampedArray',
155
+ instance: new Uint8ClampedArray(),
156
+ static: Uint8ClampedArray,
157
+ prototype: Uint8ClampedArray.prototype,
158
+ },
159
+ {
160
+ typeName: 'Uint16Array',
161
+ instance: new Uint16Array(),
162
+ static: Uint16Array,
163
+ prototype: Uint16Array.prototype,
164
+ },
165
+ {
166
+ typeName: 'Uint32Array',
167
+ instance: new Uint32Array(),
168
+ static: Uint32Array,
169
+ prototype: Uint32Array.prototype,
170
+ },
171
+ {
172
+ typeName: 'URL',
173
+ instance: new URL('https://example.com'),
174
+ static: URL,
175
+ prototype: URL.prototype,
176
+ },
177
+ {
178
+ typeName: 'URLSearchParams',
179
+ instance: new URLSearchParams(),
180
+ static: URLSearchParams,
181
+ prototype: URLSearchParams.prototype,
182
+ },
183
+ {
184
+ typeName: 'JSON',
185
+ static: JSON,
186
+ },
187
+ {
188
+ typeName: 'Math',
189
+ static: Math,
190
+ },
191
+ {
192
+ typeName: 'Reflect',
193
+ static: Reflect,
194
+ },
195
+ ];
196
+ const nativeObjects = new Map();
197
+ for (const nativeObjectDefinition of nativeObjectDefinitions) {
198
+ const nativeObjectInfo = {};
199
+ if (nativeObjectDefinition.instance) {
200
+ nativeObjectInfo.instance = createPropertyInfo(nativeObjectDefinition.instance, nativeObjectDefinition.instanceProperties);
201
+ }
202
+ if (nativeObjectDefinition.prototype) {
203
+ nativeObjectInfo.prototype = createPropertyInfo(nativeObjectDefinition.prototype);
204
+ }
205
+ if (nativeObjectDefinition.static) {
206
+ nativeObjectInfo.static = createPropertyInfo(nativeObjectDefinition.static);
207
+ }
208
+ nativeObjects.set(nativeObjectDefinition.typeName, nativeObjectInfo);
209
+ }
210
+ const getPropertyName = (memberExpression) => {
211
+ const { property } = memberExpression;
212
+ if (memberExpression.computed) {
213
+ return isLiteralNode(property) && typeof property.value === 'string' ? property.value : undefined;
214
+ }
215
+ return isIdentifierNode(property) ? property.name : undefined;
216
+ };
217
+ const resolveBinaryExpressionType = (binaryExpression) => {
218
+ if (binaryExpression.operator !== '+') {
219
+ return undefined;
220
+ }
221
+ const leftReference = resolveNativeObjectReference(binaryExpression.left);
222
+ const rightReference = resolveNativeObjectReference(binaryExpression.right);
223
+ if (leftReference?.usage !== 'instance' || rightReference?.usage !== 'instance') {
224
+ return undefined;
225
+ }
226
+ if (leftReference.typeName === 'String' || rightReference.typeName === 'String') {
227
+ return {
228
+ typeName: 'String',
229
+ usage: 'instance',
230
+ };
231
+ }
232
+ return undefined;
233
+ };
234
+ const resolveIdentifierReference = (node) => {
235
+ if (!nativeObjects.has(node.name)) {
236
+ return undefined;
237
+ }
238
+ return {
239
+ typeName: node.name,
240
+ usage: 'static',
241
+ };
242
+ };
243
+ const resolveLiteralReference = (node) => {
244
+ if (node.regex) {
245
+ return {
246
+ typeName: 'RegExp',
247
+ usage: 'instance',
248
+ };
249
+ }
250
+ if (typeof node.value === 'boolean') {
251
+ return {
252
+ typeName: 'Boolean',
253
+ usage: 'instance',
254
+ };
255
+ }
256
+ if (typeof node.value === 'number') {
257
+ return {
258
+ typeName: 'Number',
259
+ usage: 'instance',
260
+ };
261
+ }
262
+ if (typeof node.value === 'string') {
263
+ return {
264
+ typeName: 'String',
265
+ usage: 'instance',
266
+ };
267
+ }
268
+ return undefined;
269
+ };
270
+ const resolvePrototypeReference = (node) => {
271
+ if (getPropertyName(node) !== 'prototype' || !isIdentifierNode(node.object) || !nativeObjects.has(node.object.name)) {
272
+ return undefined;
273
+ }
274
+ return {
275
+ typeName: node.object.name,
276
+ usage: 'prototype',
277
+ };
278
+ };
279
+ const resolveNewExpressionReference = (node) => {
280
+ if (!isIdentifierNode(node.callee) || !nativeObjects.has(node.callee.name)) {
281
+ return undefined;
282
+ }
283
+ return {
284
+ typeName: node.callee.name,
285
+ usage: 'instance',
286
+ };
287
+ };
288
+ function resolveNativeObjectReference(node) {
289
+ if (!node) {
290
+ return undefined;
291
+ }
292
+ if (isMemberExpressionNode(node)) {
293
+ return resolvePrototypeReference(node);
294
+ }
295
+ if (isBinaryExpressionNode(node)) {
296
+ return resolveBinaryExpressionType(node);
297
+ }
298
+ if (isLiteralNode(node)) {
299
+ return resolveLiteralReference(node);
300
+ }
301
+ if (isIdentifierNode(node)) {
302
+ return resolveIdentifierReference(node);
303
+ }
304
+ if (isNewExpressionNode(node)) {
305
+ return resolveNewExpressionReference(node);
306
+ }
307
+ switch (node.type) {
308
+ case 'ArrayExpression': {
309
+ return {
310
+ typeName: 'Array',
311
+ usage: 'instance',
312
+ };
313
+ }
314
+ case 'ArrowFunctionExpression':
315
+ case 'FunctionExpression': {
316
+ return {
317
+ typeName: 'Function',
318
+ usage: 'instance',
319
+ };
320
+ }
321
+ case 'ObjectExpression': {
322
+ return {
323
+ typeName: 'Object',
324
+ usage: 'instance',
325
+ };
326
+ }
327
+ case 'TemplateLiteral': {
328
+ return {
329
+ typeName: 'String',
330
+ usage: 'instance',
331
+ };
332
+ }
333
+ default: {
334
+ return undefined;
335
+ }
336
+ }
337
+ }
338
+ const noUseExtendNativeRule = {
339
+ meta: {
340
+ type: 'problem',
341
+ docs: {
342
+ description: 'Disallow relying on non-standard properties on native objects',
343
+ },
344
+ messages: {
345
+ unexpected: 'Avoid relying on extended native objects.',
346
+ },
347
+ schema: [],
348
+ },
349
+ create(context) {
350
+ return {
351
+ // eslint-disable-next-line @typescript-eslint/naming-convention
352
+ MemberExpression(node) {
353
+ const propertyName = getPropertyName(node);
354
+ if (!propertyName) {
355
+ return;
356
+ }
357
+ const nativeObjectReference = resolveNativeObjectReference(node.object);
358
+ if (!nativeObjectReference) {
359
+ return;
360
+ }
361
+ const propertyInfo = nativeObjects.get(nativeObjectReference.typeName)?.[nativeObjectReference.usage];
362
+ if (!propertyInfo) {
363
+ return;
364
+ }
365
+ const isCall = node.parent.type === 'CallExpression' && node.parent.callee === node;
366
+ if (isCall) {
367
+ if (!propertyInfo.callable.has(propertyName)) {
368
+ context.report({
369
+ node,
370
+ messageId: 'unexpected',
371
+ });
372
+ }
373
+ return;
374
+ }
375
+ if (!propertyInfo.all.has(propertyName)) {
376
+ context.report({
377
+ node,
378
+ messageId: 'unexpected',
379
+ });
380
+ }
381
+ },
382
+ };
383
+ },
384
+ };
385
+ export default noUseExtendNativeRule;
386
+ //# sourceMappingURL=no-use-extend-native.js.map
@@ -71,9 +71,11 @@ export type XoConfigItem = Simplify<XoConfigOptions & Omit<Linter.Config, 'files
71
71
  /**
72
72
  An array of glob patterns indicating the files that the configuration object should apply to. If not specified, the configuration object applies to all files.
73
73
 
74
+ Accepts a single glob string, an array of globs, or ESLint's native format where nested arrays create AND patterns.
75
+
74
76
  @see [Ignore Patterns](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new#excluding-files-with-ignores)
75
77
  */
76
- files?: string | string[] | undefined;
78
+ files?: string | Array<string | string[]> | undefined;
77
79
  /**
78
80
  An array of glob patterns indicating the files that the configuration object should not apply to. If not specified, the configuration object applies to all files matched by files.
79
81
 
@@ -1,6 +1,6 @@
1
1
  import { type Linter } from 'eslint';
2
- import { type SetRequired } from 'type-fest';
3
2
  import { type XoConfigItem } from './types.js';
3
+ export declare const typescriptParser: Linter.Parser | undefined;
4
4
  /**
5
5
  Convert a `xo` config item to an ESLint config item.
6
6
 
@@ -11,7 +11,7 @@ Files and rules will always be defined and all other ESLint config properties ar
11
11
  @param xoConfig
12
12
  @returns eslintConfig
13
13
  */
14
- export declare const xoToEslintConfigItem: (xoConfig: XoConfigItem) => SetRequired<Linter.Config, "rules" | "files">;
14
+ export declare const xoToEslintConfigItem: (xoConfig: XoConfigItem) => Linter.Config;
15
15
  /**
16
16
  Function used to match files which should be included in the `tsconfig.json` files.
17
17
 
@@ -21,7 +21,7 @@ Function used to match files which should be included in the `tsconfig.json` fil
21
21
  @param ignores - The globs to ignore when matching the files.
22
22
  @returns An array of file paths that match the globs and do not match the ignores.
23
23
  */
24
- export declare const matchFilesForTsConfig: (cwd: string, files: string[], globs: string[], ignores: string[]) => string[];
24
+ export declare const matchFilesForTsConfig: (cwd: string, files: string[] | undefined, globs: string[], ignores: string[]) => string[];
25
25
  /**
26
26
  Once a config is resolved, it is pre-processed to ensure that all properties are set correctly.
27
27
 
package/dist/lib/utils.js CHANGED
@@ -3,6 +3,14 @@ import micromatch from 'micromatch';
3
3
  import arrify from 'arrify';
4
4
  import configXoTypescript from 'eslint-config-xo-typescript';
5
5
  import { allFilesGlob, jsExtensions, jsFilesGlob, } from './constants.js';
6
+ const typescriptParserConfig = configXoTypescript.find(config => {
7
+ const languageOptions = config.languageOptions;
8
+ return languageOptions?.parser;
9
+ });
10
+ export const typescriptParser = typescriptParserConfig?.languageOptions?.parser;
11
+ if (!typescriptParser) {
12
+ throw new Error('XO: Failed to locate TypeScript parser in eslint-config-xo-typescript');
13
+ }
6
14
  /**
7
15
  Convert a `xo` config item to an ESLint config item.
8
16
 
@@ -17,8 +25,8 @@ export const xoToEslintConfigItem = (xoConfig) => {
17
25
  const { files, rules, space, prettier, ignores, semicolon, react, ..._xoConfig } = xoConfig;
18
26
  const eslintConfig = {
19
27
  ..._xoConfig,
20
- files: arrify(xoConfig.files ?? allFilesGlob),
21
- rules: xoConfig.rules ?? {},
28
+ ...(xoConfig.files ? { files: arrify(xoConfig.files) } : {}),
29
+ ...(xoConfig.rules ? { rules: xoConfig.rules } : {}),
22
30
  };
23
31
  eslintConfig.ignores &&= arrify(xoConfig.ignores);
24
32
  return eslintConfig;
@@ -32,7 +40,7 @@ Function used to match files which should be included in the `tsconfig.json` fil
32
40
  @param ignores - The globs to ignore when matching the files.
33
41
  @returns An array of file paths that match the globs and do not match the ignores.
34
42
  */
35
- export const matchFilesForTsConfig = (cwd, files, globs, ignores) => micromatch(files.map(file => path.normalize(path.relative(cwd, file))),
43
+ export const matchFilesForTsConfig = (cwd, files, globs, ignores) => micromatch(files?.map(file => path.normalize(path.relative(cwd, file))) ?? [],
36
44
  // https://github.com/micromatch/micromatch/issues/217
37
45
  globs.map(glob => path.normalize(glob)), {
38
46
  dot: true,
@@ -47,11 +55,14 @@ This includes ensuring that user-defined properties can override XO defaults, an
47
55
  @param xoConfig - The flat XO config to pre-process.
48
56
  @returns The pre-processed flat XO config.
49
57
  */
58
+ // eslint-disable-next-line complexity
50
59
  export const preProcessXoConfig = (xoConfig) => {
51
60
  const tsFilesGlob = [];
52
61
  const tsFilesIgnoresGlob = [];
53
62
  const processedConfig = [];
54
63
  for (const [idx, { ...config }] of xoConfig.entries()) {
64
+ const languageOptions = config.languageOptions;
65
+ const parserOptions = languageOptions?.parserOptions;
55
66
  // We can skip the first config item, as it is the base config item.
56
67
  if (idx === 0) {
57
68
  processedConfig.push(config);
@@ -60,8 +71,10 @@ export const preProcessXoConfig = (xoConfig) => {
60
71
  // Use TS parser/plugin for JS files if the config contains TypeScript rules which are applied to JS files.
61
72
  // typescript-eslint rules set to "off" are ignored and not applied to JS files.
62
73
  if (config.rules
63
- && !config.languageOptions?.parser
64
- && !config.languageOptions?.parserOptions?.['project']
74
+ // eslint-disable-next-line @typescript-eslint/dot-notation
75
+ && !languageOptions?.['parser']
76
+ && parserOptions?.project === undefined
77
+ && parserOptions?.programs === undefined
65
78
  && !config.plugins?.['@typescript-eslint']) {
66
79
  const hasTsRules = Object.entries(config.rules).some(rulePair => {
67
80
  // If its not a @typescript-eslint rule, we don't care
@@ -77,7 +90,7 @@ export const preProcessXoConfig = (xoConfig) => {
77
90
  if (hasTsRules) {
78
91
  let isAppliedToJsFiles = false;
79
92
  if (config.files) {
80
- const normalizedFiles = arrify(config.files).map(file => path.normalize(file));
93
+ const normalizedFiles = arrify(config.files).flat().map(file => path.normalize(file));
81
94
  // Strip the basename off any globs
82
95
  const globs = normalizedFiles.map(file => micromatch.scan(file, { dot: true }).glob).filter(Boolean);
83
96
  // Check if the files globs match a test file with a js extension
@@ -89,25 +102,27 @@ export const preProcessXoConfig = (xoConfig) => {
89
102
  isAppliedToJsFiles = true;
90
103
  }
91
104
  if (isAppliedToJsFiles) {
92
- config.languageOptions ??= {};
105
+ const updatedLanguageOptions = languageOptions
106
+ ? { ...languageOptions, parser: typescriptParser }
107
+ : { parser: typescriptParser };
108
+ config.languageOptions = updatedLanguageOptions;
93
109
  config.plugins ??= {};
94
110
  config.plugins = {
95
111
  ...config.plugins,
96
112
  ...configXoTypescript[1]?.plugins,
97
113
  };
98
- config.languageOptions.parser = configXoTypescript[1]?.languageOptions?.parser;
99
- tsFilesGlob.push(...arrify(config.files ?? allFilesGlob));
114
+ tsFilesGlob.push(...arrify(config.files ?? allFilesGlob).flat());
100
115
  tsFilesIgnoresGlob.push(...arrify(config.ignores));
101
116
  }
102
117
  }
103
118
  }
104
- // If a user sets the `parserOptions.project` or `projectService` or `tsconfigRootDir`, we need to ensure that the tsFilesGlob is set to exclude those files,
105
- // as this indicates the user has opted out of the default TypeScript handling for those files.
106
- if (config.languageOptions?.parserOptions?.['project'] !== undefined
107
- || config.languageOptions?.parserOptions?.['projectService'] !== undefined
108
- || config.languageOptions?.parserOptions?.['tsconfigRootDir'] !== undefined) {
119
+ // If the config sets `parserOptions.project`, `projectService`, `tsconfigRootDir`, or `programs`, treat those files as opt-out for XO's automatic program wiring.
120
+ if (parserOptions?.project !== undefined
121
+ || parserOptions?.projectService !== undefined
122
+ || parserOptions?.tsconfigRootDir !== undefined
123
+ || parserOptions?.programs !== undefined) {
109
124
  // The glob itself should NOT be negated
110
- tsFilesIgnoresGlob.push(...arrify(config.files ?? allFilesGlob));
125
+ tsFilesIgnoresGlob.push(...arrify(config.files ?? allFilesGlob).flat());
111
126
  }
112
127
  processedConfig.push(config);
113
128
  }
@@ -4,13 +4,57 @@ import arrify from 'arrify';
4
4
  import configReact from 'eslint-config-xo-react';
5
5
  import pluginPrettier from 'eslint-plugin-prettier';
6
6
  import eslintConfigPrettier from 'eslint-config-prettier';
7
+ import { fixupConfigRules, fixupPluginRules } from '@eslint/compat';
7
8
  import { config } from './config.js';
8
9
  import { xoToEslintConfigItem } from './utils.js';
9
10
  /**
11
+ Merge all plugins from every config into a single config entry at the start of the array, ensuring user-provided plugins take precedence. This avoids ESLint's flat config rejecting duplicate plugin names.
12
+ */
13
+ const hoistPlugins = (configs, userPluginOverrides) => {
14
+ const plugins = {};
15
+ const configsWithoutPlugins = [];
16
+ for (const configItem of configs) {
17
+ const { plugins: configPlugins } = configItem;
18
+ if (!configPlugins) {
19
+ configsWithoutPlugins.push(configItem);
20
+ continue;
21
+ }
22
+ // ESLint flat config rejects duplicate plugin names, so merge all plugins into one config.
23
+ Object.assign(plugins, configPlugins);
24
+ const { plugins: _ignored, ...configWithoutPlugins } = configItem;
25
+ if (Object.keys(configWithoutPlugins).length > 0) {
26
+ configsWithoutPlugins.push(configWithoutPlugins);
27
+ }
28
+ }
29
+ for (const [pluginName, plugin] of userPluginOverrides) {
30
+ plugins[pluginName] = plugin;
31
+ }
32
+ if (Object.keys(plugins).length === 0) {
33
+ return configsWithoutPlugins;
34
+ }
35
+ return [
36
+ {
37
+ name: 'xo/plugins',
38
+ plugins,
39
+ },
40
+ ...configsWithoutPlugins,
41
+ ];
42
+ };
43
+ /**
10
44
  Takes a XO flat config and returns an ESlint flat config.
11
45
  */
12
46
  export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
13
47
  const baseConfig = [...config];
48
+ const userPluginOverrides = new Map();
49
+ for (const xoConfigItem of flatXoConfig ?? []) {
50
+ const { plugins } = xoConfigItem;
51
+ if (!plugins) {
52
+ continue;
53
+ }
54
+ for (const [pluginName, plugin] of Object.entries(plugins)) {
55
+ userPluginOverrides.set(pluginName, plugin);
56
+ }
57
+ }
14
58
  /**
15
59
  Since configs are merged and the last config takes precedence this means we need to handle both true AND false cases for each option. For example, we need to turn `prettier`, `space`, `semi`, etc. on or off for a specific file.
16
60
  */
@@ -35,6 +79,7 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
35
79
  */
36
80
  const eslintConfigItem = xoToEslintConfigItem(xoConfigItem);
37
81
  if (xoConfigItem.semicolon === false) {
82
+ eslintConfigItem.rules ??= {};
38
83
  eslintConfigItem.rules['@stylistic/semi'] = ['error', 'never'];
39
84
  eslintConfigItem.rules['@stylistic/semi-spacing'] = [
40
85
  'error',
@@ -43,6 +88,7 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
43
88
  }
44
89
  if (xoConfigItem.space) {
45
90
  const spaces = typeof xoConfigItem.space === 'number' ? xoConfigItem.space : 2;
91
+ eslintConfigItem.rules ??= {};
46
92
  eslintConfigItem.rules['@stylistic/indent'] = [
47
93
  'error',
48
94
  spaces,
@@ -54,12 +100,14 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
54
100
  else if (xoConfigItem.space === false) {
55
101
  // If a user sets this to false for a small subset of files for some reason,
56
102
  // then we need to set them back to their original values.
103
+ eslintConfigItem.rules ??= {};
57
104
  eslintConfigItem.rules['@stylistic/indent'] = configXoTypescript[1]?.rules?.['@stylistic/indent'];
58
105
  eslintConfigItem.rules['@stylistic/indent-binary-ops'] = configXoTypescript[1]?.rules?.['@stylistic/indent-binary-ops'];
59
106
  }
60
107
  if (xoConfigItem.react) {
61
108
  // Ensure the files applied to the React config are the same as the config they are derived from
62
- baseConfig.push({ ...configReact[0], files: eslintConfigItem.files });
109
+ // TODO: Remove `fixupConfigRules` wrapping when eslint-config-xo-react supports ESLint 10 natively.
110
+ baseConfig.push({ ...fixupConfigRules(configReact)[0], files: eslintConfigItem.files, name: 'xo/react' });
63
111
  }
64
112
  // Prettier should generally be the last config in the array
65
113
  if (xoConfigItem.prettier) {
@@ -68,7 +116,7 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
68
116
  }
69
117
  else {
70
118
  // Validate that Prettier options match other `xoConfig` options.
71
- /* eslint-disable-next-line */
119
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
72
120
  if ((xoConfigItem.semicolon && prettierOptions.semi === false) || (!xoConfigItem.semicolon && prettierOptions.semi === true)) {
73
121
  throw new Error(`The Prettier config \`semi\` is ${prettierOptions.semi} while Xo \`semicolon\` is ${xoConfigItem.semicolon}, also check your .editorconfig for inconsistencies.`);
74
122
  }
@@ -79,9 +127,10 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
79
127
  throw new Error(`The Prettier config \`tabWidth\` is ${prettierOptions.tabWidth} while Xo \`space\` is ${xoConfigItem.space}, also check your .editorconfig for inconsistencies.`);
80
128
  }
81
129
  // Add Prettier plugin
130
+ // TODO: Remove `fixupPluginRules` wrapping when eslint-plugin-prettier supports ESLint 10 natively.
82
131
  eslintConfigItem.plugins = {
83
132
  ...eslintConfigItem.plugins,
84
- prettier: pluginPrettier,
133
+ prettier: fixupPluginRules(pluginPrettier),
85
134
  };
86
135
  const prettierConfig = {
87
136
  singleQuote: true,
@@ -96,6 +145,7 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
96
145
  // Configure Prettier rules
97
146
  const rulesWithPrettier = {
98
147
  ...eslintConfigItem.rules,
148
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
99
149
  ...pluginPrettier.configs?.['recommended']?.rules,
100
150
  // eslint-disable-next-line @typescript-eslint/naming-convention
101
151
  'prettier/prettier': ['error', prettierConfig],
@@ -106,11 +156,16 @@ export function xoToEslintConfig(flatXoConfig, { prettierOptions = {} } = {}) {
106
156
  }
107
157
  else if (xoConfigItem.prettier === false) {
108
158
  // Turn Prettier off for a subset of files
159
+ eslintConfigItem.rules ??= {};
109
160
  eslintConfigItem.rules['prettier/prettier'] = 'off';
110
161
  }
162
+ if (Object.keys(eslintConfigItem).length === 0) {
163
+ continue;
164
+ }
111
165
  baseConfig.push(eslintConfigItem);
112
166
  }
113
- return baseConfig;
167
+ // User plugins should always win, even if XO injects plugins later in the config list.
168
+ return hoistPlugins(baseConfig, userPluginOverrides);
114
169
  }
115
170
  export default xoToEslintConfig;
116
171
  //# sourceMappingURL=xo-to-eslint.js.map