@homer0/oxlint-config 1.1.0 → 2.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.
@@ -1,5 +1,11 @@
1
1
  import globals from 'globals';
2
- import { DEFAULT_IGNORES, TEST_FILES, type TestConvention } from './consts.js';
2
+ import {
3
+ DEFAULT_IGNORES,
4
+ TEST_FILES,
5
+ CONFIG_ENVS,
6
+ type ConfigEnv,
7
+ type TestConvention,
8
+ } from './consts.js';
3
9
  import { extensionFragments } from './fragments.js';
4
10
  import {
5
11
  nextjs as nextjsRules,
@@ -10,7 +16,7 @@ import {
10
16
  } from './rules/index.js';
11
17
  import type {
12
18
  ConfigFragment,
13
- ConfigName,
19
+ ConfigOverrideOptions,
14
20
  DangleRuleOptions,
15
21
  DangleRuleSettings,
16
22
  CreateConfigSettings,
@@ -34,6 +40,54 @@ const mergeRules = (...fragments: (ConfigFragment | undefined)[]) =>
34
40
  return acc;
35
41
  }, {});
36
42
 
43
+ /** Combines config rules with user overrides. */
44
+ const mergeRuleSettings = (fragments: ConfigFragment[], settings: RuleSettings[] = []) =>
45
+ mergeRules(...fragments, ...settings.map((rules) => ({ rules })));
46
+
47
+ /** Marks environment globals as unavailable. */
48
+ const disableGlobals = (env: ConfigEnv): ResolvedGlobalVars =>
49
+ Object.fromEntries(
50
+ Object.keys(extensionFragments[env].globals).map((name) => [name, 'off']),
51
+ );
52
+
53
+ /** Checks if an override applies to files. */
54
+ const isConfigOverride = (
55
+ override: ConfigOverrideOptions | RuleSettings,
56
+ ): override is ConfigOverrideOptions => Object.hasOwn(override, 'files');
57
+
58
+ /** Splits rule overrides from file overrides. */
59
+ const resolveUserOverrides = (overrides: CreateConfigSettings['overrides']) => {
60
+ if (!overrides) {
61
+ return {
62
+ configOverrides: [],
63
+ ruleOverrides: [],
64
+ };
65
+ }
66
+
67
+ if (!Array.isArray(overrides)) {
68
+ return {
69
+ configOverrides: [],
70
+ ruleOverrides: [overrides],
71
+ };
72
+ }
73
+
74
+ return overrides.reduce<{
75
+ configOverrides: ConfigOverrideOptions[];
76
+ ruleOverrides: RuleSettings[];
77
+ }>(
78
+ (acc, override) => {
79
+ if (isConfigOverride(override)) {
80
+ acc.configOverrides.push(override);
81
+ } else {
82
+ acc.ruleOverrides.push(override);
83
+ }
84
+
85
+ return acc;
86
+ },
87
+ { configOverrides: [], ruleOverrides: [] },
88
+ );
89
+ };
90
+
37
91
  const allowDangleNames = (rules: RuleSettings, names: string[] = []): RuleSettings => {
38
92
  if (names.length === 0) return rules;
39
93
 
@@ -89,40 +143,36 @@ const resolveGlobals = ({
89
143
  }, {});
90
144
  };
91
145
 
92
- const getConfig = (configs: ConfigName[]): ConfigName => {
93
- if (configs.length !== 1) {
94
- throw new Error(
95
- 'Exactly one environment config must be selected: "node" or "browser".',
96
- );
146
+ const getEnvironment = (env: ConfigEnv): ConfigEnv => {
147
+ if (!CONFIG_ENVS.includes(env)) {
148
+ const humanReadableEnvs = CONFIG_ENVS.map((e) => `"${e}"`).join(' or ');
149
+ throw new Error(`Environment must be ${humanReadableEnvs}.`);
97
150
  }
98
151
 
99
- const [config] = configs;
100
- if (typeof config === 'undefined') {
101
- throw new Error(
102
- 'Exactly one environment config must be selected: "node" or "browser".',
103
- );
104
- }
105
-
106
- return config;
152
+ return env;
107
153
  };
108
154
 
155
+ /** Turns a file glob into a list. */
156
+ const resolveFiles = (files: string | string[]): string[] =>
157
+ Array.isArray(files) ? files.slice() : [files];
158
+
109
159
  const resolveTestFiles = (files: string | string[] | undefined): string[] => {
110
160
  if (typeof files === 'undefined') return [];
111
- if (Array.isArray(files)) return files.slice();
112
- if (Object.hasOwn(TEST_FILES, files))
161
+ if (typeof files === 'string' && Object.hasOwn(TEST_FILES, files)) {
113
162
  return TEST_FILES[files as TestConvention].slice();
163
+ }
114
164
 
115
- return [files];
165
+ return resolveFiles(files);
116
166
  };
117
167
 
118
168
  const resolveTestConfig = ({
119
169
  tests,
120
- productionConfig,
170
+ productionEnv,
121
171
  productionTs,
122
172
  }: ResolveTestConfigOptions): ResolvedTestConfig => {
123
173
  if (tests === true) {
124
174
  return {
125
- configs: [productionConfig],
175
+ env: productionEnv,
126
176
  files: [...TEST_FILES.colocated, ...TEST_FILES.directory],
127
177
  framework: 'vitest',
128
178
  ts: productionTs,
@@ -131,7 +181,7 @@ const resolveTestConfig = ({
131
181
 
132
182
  if (typeof tests === 'string') {
133
183
  return {
134
- configs: [productionConfig],
184
+ env: productionEnv,
135
185
  files: TEST_FILES[tests].slice(),
136
186
  framework: 'vitest',
137
187
  ts: productionTs,
@@ -139,7 +189,7 @@ const resolveTestConfig = ({
139
189
  }
140
190
 
141
191
  return {
142
- configs: tests.configs ?? [productionConfig],
192
+ env: tests.env ?? productionEnv,
143
193
  files: resolveTestFiles(tests.files),
144
194
  framework: tests.framework || 'vitest',
145
195
  ts: tests.ts ?? productionTs,
@@ -147,7 +197,7 @@ const resolveTestConfig = ({
147
197
  };
148
198
 
149
199
  const resolveTestConfigComponents = ({
150
- config,
200
+ env,
151
201
  testConfig,
152
202
  tests,
153
203
  ts = false,
@@ -170,7 +220,7 @@ const resolveTestConfigComponents = ({
170
220
  doingTestsSetup: false,
171
221
  testConfig: resolveTestConfig({
172
222
  tests,
173
- productionConfig: config,
223
+ productionEnv: env,
174
224
  productionTs: ts,
175
225
  }),
176
226
  };
@@ -180,7 +230,7 @@ const resolveConfigComponents = (
180
230
  options: ResolveConfigComponentsOptions,
181
231
  ): ResolveConfigComponentsResult => {
182
232
  const {
183
- configs,
233
+ env: environment,
184
234
  extensions,
185
235
  globals: globalVars,
186
236
  jsdoc,
@@ -194,23 +244,23 @@ const resolveConfigComponents = (
194
244
 
195
245
  const baseExtensionFragment = baseExtensionFragmentOption ?? extensions?.base;
196
246
 
197
- const config = getConfig(options.configs);
247
+ const env = getEnvironment(environment);
198
248
 
199
249
  const { doingTestsSetup, testConfig } = resolveTestConfigComponents({
200
- config,
250
+ env,
201
251
  testConfig: options.testConfig,
202
252
  tests: options.tests,
203
253
  ts,
204
254
  });
205
255
 
206
- const fragments: ConfigFragment[] = [baseFragment, extensionFragments[config]];
256
+ const fragments: ConfigFragment[] = [baseFragment, extensionFragments[env]];
207
257
  const resolvedGlobals = resolveGlobals({ globals: globalVars, doingTestsSetup });
208
258
 
209
259
  const plugins = ['import'];
210
260
 
211
261
  const configGlobals: Array<Record<string, boolean | string>> = [];
212
262
 
213
- if (configs.includes('node')) {
263
+ if (env === 'node') {
214
264
  plugins.push('node');
215
265
  }
216
266
 
@@ -253,8 +303,8 @@ const resolveConfigComponents = (
253
303
  fragments.push(baseExtensionFragment);
254
304
  }
255
305
 
256
- if (extensions?.[config]) {
257
- fragments.push(extensions[config]);
306
+ if (extensions?.[env]) {
307
+ fragments.push(extensions[env]);
258
308
  }
259
309
 
260
310
  if (jsdoc && extensions?.jsdoc) {
@@ -270,7 +320,7 @@ const resolveConfigComponents = (
270
320
  }
271
321
 
272
322
  return {
273
- config,
323
+ env,
274
324
  fragments,
275
325
  globals: resolvedGlobals,
276
326
  plugins,
@@ -281,18 +331,20 @@ const resolveConfigComponents = (
281
331
  };
282
332
 
283
333
  const createTestOverride = ({
334
+ parentEnv,
284
335
  testConfig,
285
336
  allowedDangleNames,
286
337
  extensions,
338
+ extraRules = [],
287
339
  globals: globalVars,
288
340
  }: CreateTestOverrideOptions): GeneratedConfigOverride => {
289
341
  const {
290
- config,
342
+ env,
291
343
  fragments,
292
344
  globals: resolvedGlobals,
293
345
  configGlobals,
294
346
  } = resolveConfigComponents({
295
- configs: testConfig.configs,
347
+ env: testConfig.env,
296
348
  extensions,
297
349
  globals: globalVars,
298
350
  testConfig,
@@ -300,7 +352,10 @@ const createTestOverride = ({
300
352
  baseFragment: extensionFragments.tests,
301
353
  baseExtensionFragment: extensions?.tests,
302
354
  });
303
- const rules = allowDangleNames(mergeRules(...fragments), allowedDangleNames);
355
+ const rules = allowDangleNames(
356
+ mergeRuleSettings(fragments, extraRules),
357
+ allowedDangleNames,
358
+ );
304
359
 
305
360
  if (testConfig.files.length === 0) {
306
361
  throw new Error('Custom test configuration requires at least one file glob.');
@@ -309,7 +364,8 @@ const createTestOverride = ({
309
364
  return {
310
365
  files: testConfig.files,
311
366
  globals: {
312
- ...extensionFragments[config].globals,
367
+ ...disableGlobals(parentEnv),
368
+ ...extensionFragments[env].globals,
313
369
  ...resolvedGlobals,
314
370
  ...configGlobals,
315
371
  },
@@ -317,38 +373,123 @@ const createTestOverride = ({
317
373
  };
318
374
  };
319
375
 
376
+ type CreateConfigOverrideOptions = {
377
+ override: ConfigOverrideOptions;
378
+ options: CreateConfigSettings;
379
+ parentEnv: ConfigEnv;
380
+ rootRuleOverrides: RuleSettings[];
381
+ };
382
+
383
+ /** Creates a file override and its test override. */
384
+ const createConfigOverride = ({
385
+ override,
386
+ options,
387
+ parentEnv,
388
+ rootRuleOverrides,
389
+ }: CreateConfigOverrideOptions) => {
390
+ const { files, ignores, rules: overrideRules, tests, ...overrideOptions } = override;
391
+ const {
392
+ env,
393
+ fragments,
394
+ globals: resolvedGlobals,
395
+ plugins,
396
+ testConfig,
397
+ } = resolveConfigComponents({
398
+ ...options,
399
+ ...overrideOptions,
400
+ env: override.env ?? parentEnv,
401
+ tests,
402
+ });
403
+ const extraRules = [...rootRuleOverrides, ...(overrideRules ? [overrideRules] : [])];
404
+
405
+ return {
406
+ override: {
407
+ excludeFiles: ignores,
408
+ files: resolveFiles(files),
409
+ globals: {
410
+ ...disableGlobals(parentEnv),
411
+ ...extensionFragments[env].globals,
412
+ ...resolvedGlobals,
413
+ },
414
+ rules: allowDangleNames(
415
+ mergeRuleSettings(fragments, extraRules),
416
+ options.allowedDangleNames,
417
+ ),
418
+ },
419
+ plugins,
420
+ testOverride: testConfig
421
+ ? createTestOverride({
422
+ parentEnv,
423
+ testConfig,
424
+ allowedDangleNames: options.allowedDangleNames,
425
+ extensions: overrideOptions.extensions ?? options.extensions,
426
+ extraRules,
427
+ globals: overrideOptions.globals ?? options.globals,
428
+ })
429
+ : undefined,
430
+ };
431
+ };
432
+
320
433
  /** Creates a native Oxlint configuration from one environment and optional policy layers. */
321
434
  export const createConfig = (options: CreateConfigSettings): GeneratedConfig => {
322
435
  const {
323
436
  fragments,
324
437
  globals: resolvedGlobals,
325
- plugins,
326
- config,
438
+ plugins: resolvedPlugins,
439
+ env,
327
440
  testConfig,
328
441
  } = resolveConfigComponents(options);
329
442
  const { allowedDangleNames, extensions, ignores, typeAware } = options;
443
+ const { configOverrides, ruleOverrides } = resolveUserOverrides(options.overrides);
444
+ const generatedOverrides: GeneratedConfigOverride[] = [];
445
+ const plugins = resolvedPlugins.slice();
330
446
 
331
- const rules = allowDangleNames(mergeRules(...fragments), allowedDangleNames);
447
+ if (testConfig) {
448
+ generatedOverrides.push(
449
+ createTestOverride({
450
+ parentEnv: env,
451
+ testConfig,
452
+ allowedDangleNames,
453
+ extensions,
454
+ extraRules: ruleOverrides,
455
+ globals: resolvedGlobals,
456
+ }),
457
+ );
458
+ }
459
+
460
+ for (const override of configOverrides) {
461
+ const generatedOverride = createConfigOverride({
462
+ override,
463
+ options,
464
+ parentEnv: env,
465
+ rootRuleOverrides: ruleOverrides,
466
+ });
467
+
468
+ generatedOverrides.push(generatedOverride.override);
469
+ if (generatedOverride.testOverride) {
470
+ generatedOverrides.push(generatedOverride.testOverride);
471
+ }
472
+
473
+ for (const plugin of generatedOverride.plugins) {
474
+ if (!plugins.includes(plugin)) plugins.push(plugin);
475
+ }
476
+ }
477
+
478
+ const rules = allowDangleNames(
479
+ mergeRuleSettings(fragments, ruleOverrides),
480
+ allowedDangleNames,
481
+ );
332
482
 
333
483
  return {
334
484
  globals: {
335
485
  ...extensionFragments.base.globals,
336
- ...extensionFragments[config].globals,
486
+ ...extensionFragments[env].globals,
337
487
  ...resolvedGlobals,
338
488
  },
339
489
  ignorePatterns: [...DEFAULT_IGNORES, ...(ignores || [])],
340
490
  options: typeAware ? { typeAware: true } : undefined,
341
491
  plugins,
342
492
  rules,
343
- overrides: testConfig
344
- ? [
345
- createTestOverride({
346
- testConfig,
347
- allowedDangleNames,
348
- extensions,
349
- globals: resolvedGlobals,
350
- }),
351
- ]
352
- : undefined,
493
+ overrides: generatedOverrides.length > 0 ? generatedOverrides : undefined,
353
494
  };
354
495
  };
@@ -5,12 +5,14 @@ const NEXTJS_IGNORES = ['.next/', 'out/', 'build/', 'next-env.d.ts'];
5
5
 
6
6
  /** Creates a native Oxlint configuration for a Next.js TypeScript project. */
7
7
  export const createNextjsConfig = ({
8
+ globals = {},
8
9
  ignores = [],
9
10
  ...options
10
11
  }: CreateNextjsConfigOptions) =>
11
12
  createConfig({
12
13
  ...options,
13
- configs: ['node'],
14
+ env: 'node',
15
+ globals: { $browser: true, ...globals },
14
16
  ignores: [...NEXTJS_IGNORES, ...ignores],
15
17
  nextjs: true,
16
18
  react: true,
package/src/index.ts CHANGED
@@ -1,15 +1,10 @@
1
- import { createConfig } from './createConfig.js';
2
- import type { CreateReactConfigOptions } from './types.js';
3
-
4
1
  export { createConfig } from './createConfig.js';
5
2
  export { createNextjsConfig } from './createNextjsConfig.js';
6
3
  export { extensionFragments } from './fragments.js';
7
4
  export type {
8
5
  ConfigFragment,
9
- ConfigName,
10
6
  CreateConfigOptions,
11
7
  CreateNextjsConfigOptions,
12
- CreateReactConfigOptions,
13
8
  ExtensionFragments,
14
9
  GlobalVarMode,
15
10
  GlobalVars,
@@ -20,11 +15,4 @@ export type {
20
15
  TestFramework,
21
16
  TestsOption,
22
17
  } from './types.js';
23
- export type { ExtensionFragmentName } from './consts.js';
24
-
25
- /** Creates a native Oxlint configuration with React and JSX accessibility policy. */
26
- export const createReactConfig = (options: CreateReactConfigOptions) =>
27
- createConfig({
28
- ...options,
29
- react: true,
30
- });
18
+ export type { ExtensionFragmentName, ConfigEnv } from './consts.js';
package/src/rules/base.ts CHANGED
@@ -459,6 +459,12 @@ export const base = {
459
459
  * @see https://eslint.org/docs/latest/rules/no-useless-concat
460
460
  */
461
461
  'no-useless-concat': 'error',
462
+ /**
463
+ * Allow escapes that may be unnecessary but improve regular expression readability.
464
+ *
465
+ * @see https://eslint.org/docs/latest/rules/no-useless-escape
466
+ */
467
+ 'no-useless-escape': 'off',
462
468
  /**
463
469
  * Disallow redundant return statements.
464
470
  *
package/src/types.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  import type globals from 'globals';
2
- import type { ExtensionFragmentName, TestConvention } from './consts.js';
3
-
4
- export type ConfigName = 'browser' | 'node';
2
+ import type { ExtensionFragmentName, TestConvention, ConfigEnv } from './consts.js';
5
3
 
6
4
  export type RuleSettings = Record<string, unknown>;
7
5
 
@@ -37,43 +35,52 @@ export type ExtensionFragments = Partial<Record<ExtensionFragmentName, ConfigFra
37
35
  /** Selects a test framework-specific native rule policy. */
38
36
  export type TestFramework = 'vitest';
39
37
 
38
+ type TestConfigBaseOptions = {
39
+ env?: ConfigEnv;
40
+ framework?: TestFramework;
41
+ ts?: boolean;
42
+ };
43
+
40
44
  /**
41
45
  * Narrows a test override and optionally selects its environment, framework, and
42
46
  * TypeScript policy.
43
47
  */
44
- export type TestConfigOptions = {
45
- configs?: ConfigName[];
46
- framework?: TestFramework;
48
+ export type TestConfigOptions = TestConfigBaseOptions & {
47
49
  /** Uses a built-in convention or custom glob(s). */
48
50
  files?: TestConvention | (string & {}) | string[];
49
- ts?: boolean;
50
51
  };
51
52
 
52
53
  /** Selects both conventions, one built-in convention, or a custom test override. */
53
54
  export type TestsOption = boolean | TestConvention | TestConfigOptions;
54
55
 
55
- export type CreateConfigOptions = {
56
- configs: ConfigName[];
57
- /** Adds exact identifiers allowed by `no-underscore-dangle`. */
56
+ type CreateConfigBaseOptions = {
57
+ env: ConfigEnv;
58
58
  allowedDangleNames?: string[];
59
59
  extensions?: ExtensionFragments;
60
60
  globals?: GlobalVars;
61
61
  ignores?: string[];
62
- /** Enables the native subset of JSDoc policy. */
63
62
  jsdoc?: boolean;
64
- tests?: TestsOption;
65
- /** Defaults to `true`. */
66
63
  ts?: boolean;
67
- /**
68
- * Enables Oxlint's root-only type-aware mode. Requires oxlint-tsgolint and TypeScript
69
- * 7+.
70
- */
71
- typeAware?: boolean;
72
64
  };
73
65
 
74
- export type CreateReactConfigOptions = CreateConfigOptions;
66
+ export type TestConfigOverrideOptions = TestConfigBaseOptions & {
67
+ files: string | string[];
68
+ };
69
+
70
+ export type ConfigOverrideOptions = {
71
+ files: string | string[];
72
+ rules?: RuleSettings;
73
+ tests?: TestConfigOverrideOptions;
74
+ } & Partial<CreateConfigBaseOptions>;
75
+
76
+ export type CreateConfigOptions = CreateConfigBaseOptions & {
77
+ tests?: TestsOption;
78
+ overrides?: RuleSettings | Array<ConfigOverrideOptions | RuleSettings>;
79
+ /** Enables Oxlint's root-only type-aware mode. Requires oxlint-tsgolint and TS 7+ */
80
+ typeAware?: boolean;
81
+ };
75
82
 
76
- export type CreateNextjsConfigOptions = Omit<CreateConfigOptions, 'configs' | 'ts'>;
83
+ export type CreateNextjsConfigOptions = Omit<CreateConfigOptions, 'env' | 'ts'>;
77
84
 
78
85
  export type CreateConfigSettings = CreateConfigOptions & {
79
86
  nextjs?: boolean;
@@ -82,18 +89,19 @@ export type CreateConfigSettings = CreateConfigOptions & {
82
89
 
83
90
  export type ResolveTestConfigOptions = {
84
91
  tests: Exclude<TestsOption, false>;
85
- productionConfig: ConfigName;
92
+ productionEnv: ConfigEnv;
86
93
  productionTs: boolean;
87
94
  };
88
95
 
89
96
  export type ResolvedTestConfig = {
90
- configs: ConfigName[];
97
+ env: ConfigEnv;
91
98
  files: string[];
92
99
  framework?: TestFramework;
93
100
  ts: boolean;
94
101
  };
95
102
 
96
103
  export type GeneratedConfigOverride = {
104
+ excludeFiles?: string[];
97
105
  files: string[];
98
106
  globals: ResolvedGlobalVars;
99
107
  rules: RuleSettings;
@@ -112,8 +120,10 @@ export type GeneratedConfig = {
112
120
  };
113
121
 
114
122
  export type CreateTestOverrideOptions = {
123
+ parentEnv: ConfigEnv;
115
124
  testConfig: ResolvedTestConfig;
116
125
  allowedDangleNames?: string[];
126
+ extraRules?: RuleSettings[];
117
127
  extensions?: ExtensionFragments;
118
128
  globals?: GlobalVars | ResolvedGlobalVars;
119
129
  };
@@ -126,7 +136,7 @@ export type ResolveConfigComponentsOptions = Omit<CreateConfigSettings, 'globals
126
136
  };
127
137
 
128
138
  export type ResolveTestConfigComponentsOptions = {
129
- config: ConfigName;
139
+ env: ConfigEnv;
130
140
  testConfig?: ResolvedTestConfig;
131
141
  tests?: TestsOption;
132
142
  ts?: boolean;
@@ -138,7 +148,7 @@ export type ResolveTestConfigComponentsResult = {
138
148
  };
139
149
 
140
150
  export type ResolveConfigComponentsResult = {
141
- config: ConfigName;
151
+ env: ConfigEnv;
142
152
  globals: ResolvedGlobalVars;
143
153
  testConfig?: ResolvedTestConfig;
144
154
  fragments: ConfigFragment[];