@homer0/oxlint-config 1.0.3 → 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,14 +1,24 @@
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,
6
12
  typeAware as typeAwareRules,
7
13
  vitest as vitestRules,
14
+ noUnderscoreDangleRule,
15
+ noUnderscoreDangleOptions,
8
16
  } from './rules/index.js';
9
17
  import type {
10
18
  ConfigFragment,
11
- ConfigName,
19
+ ConfigOverrideOptions,
20
+ DangleRuleOptions,
21
+ DangleRuleSettings,
12
22
  CreateConfigSettings,
13
23
  GeneratedConfig,
14
24
  GeneratedConfigOverride,
@@ -17,7 +27,11 @@ import type {
17
27
  ResolveConfigComponentsOptions,
18
28
  ResolveConfigComponentsResult,
19
29
  ResolveTestConfigComponentsOptions,
30
+ ResolveTestConfigComponentsResult,
20
31
  CreateTestOverrideOptions,
32
+ ResolveGlobalsOptions,
33
+ ResolvedGlobalVars,
34
+ RuleSettings,
21
35
  } from './types.js';
22
36
 
23
37
  const mergeRules = (...fragments: (ConfigFragment | undefined)[]) =>
@@ -26,42 +40,139 @@ const mergeRules = (...fragments: (ConfigFragment | undefined)[]) =>
26
40
  return acc;
27
41
  }, {});
28
42
 
29
- const getConfig = (configs: ConfigName[]): ConfigName => {
30
- if (configs.length !== 1) {
31
- throw new Error(
32
- 'Exactly one environment config must be selected: "node" or "browser".',
33
- );
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
+ };
34
65
  }
35
66
 
36
- const [config] = configs;
37
- if (typeof config === 'undefined') {
38
- throw new Error(
39
- 'Exactly one environment config must be selected: "node" or "browser".',
40
- );
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
+
91
+ const allowDangleNames = (rules: RuleSettings, names: string[] = []): RuleSettings => {
92
+ if (names.length === 0) return rules;
93
+
94
+ const dangleRule = rules['no-underscore-dangle'] || noUnderscoreDangleRule;
95
+ if (!Array.isArray(dangleRule)) return rules;
96
+
97
+ const [severity, configuredOptions] = dangleRule as DangleRuleSettings;
98
+ const options: DangleRuleOptions =
99
+ typeof configuredOptions === 'object' && configuredOptions !== null
100
+ ? configuredOptions
101
+ : {};
102
+
103
+ return {
104
+ ...rules,
105
+ 'no-underscore-dangle': [
106
+ severity,
107
+ {
108
+ ...noUnderscoreDangleOptions,
109
+ ...options,
110
+ allow: Array.from(
111
+ new Set([
112
+ ...noUnderscoreDangleOptions.allow,
113
+ ...(options.allow || []),
114
+ ...names,
115
+ ]),
116
+ ),
117
+ },
118
+ ],
119
+ };
120
+ };
121
+
122
+ const resolveGlobals = ({
123
+ globals: globalVars = {},
124
+ doingTestsSetup,
125
+ }: ResolveGlobalsOptions): ResolvedGlobalVars => {
126
+ if (doingTestsSetup) {
127
+ return globalVars;
128
+ }
129
+
130
+ return Object.entries(globalVars).reduce<ResolvedGlobalVars>((acc, [name, mode]) => {
131
+ if (name.startsWith('$')) {
132
+ const groupName = name.slice(1);
133
+ if (!Object.hasOwn(globals, groupName)) {
134
+ throw new Error(`Unknown global group: "${groupName}".`);
135
+ }
136
+
137
+ Object.assign(acc, globals[groupName as keyof typeof globals]);
138
+ } else {
139
+ acc[name] = mode === true ? 'writable' : mode;
140
+ }
141
+
142
+ return acc;
143
+ }, {});
144
+ };
145
+
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}.`);
41
150
  }
42
151
 
43
- return config;
152
+ return env;
44
153
  };
45
154
 
155
+ /** Turns a file glob into a list. */
156
+ const resolveFiles = (files: string | string[]): string[] =>
157
+ Array.isArray(files) ? files.slice() : [files];
158
+
46
159
  const resolveTestFiles = (files: string | string[] | undefined): string[] => {
47
160
  if (typeof files === 'undefined') return [];
48
- if (Array.isArray(files)) return files.slice();
49
- if (Object.hasOwn(TEST_FILES, files))
161
+ if (typeof files === 'string' && Object.hasOwn(TEST_FILES, files)) {
50
162
  return TEST_FILES[files as TestConvention].slice();
163
+ }
51
164
 
52
- return [files];
165
+ return resolveFiles(files);
53
166
  };
54
167
 
55
168
  const resolveTestConfig = ({
56
169
  tests,
57
- productionConfig,
170
+ productionEnv,
58
171
  productionTs,
59
- }: ResolveTestConfigOptions): ResolvedTestConfig | undefined => {
60
- if (!tests) return undefined;
61
-
172
+ }: ResolveTestConfigOptions): ResolvedTestConfig => {
62
173
  if (tests === true) {
63
174
  return {
64
- configs: [productionConfig],
175
+ env: productionEnv,
65
176
  files: [...TEST_FILES.colocated, ...TEST_FILES.directory],
66
177
  framework: 'vitest',
67
178
  ts: productionTs,
@@ -70,7 +181,7 @@ const resolveTestConfig = ({
70
181
 
71
182
  if (typeof tests === 'string') {
72
183
  return {
73
- configs: [productionConfig],
184
+ env: productionEnv,
74
185
  files: TEST_FILES[tests].slice(),
75
186
  framework: 'vitest',
76
187
  ts: productionTs,
@@ -78,7 +189,7 @@ const resolveTestConfig = ({
78
189
  }
79
190
 
80
191
  return {
81
- configs: tests.configs ?? [productionConfig],
192
+ env: tests.env ?? productionEnv,
82
193
  files: resolveTestFiles(tests.files),
83
194
  framework: tests.framework || 'vitest',
84
195
  ts: tests.ts ?? productionTs,
@@ -86,11 +197,11 @@ const resolveTestConfig = ({
86
197
  };
87
198
 
88
199
  const resolveTestConfigComponents = ({
89
- config,
200
+ env,
90
201
  testConfig,
91
202
  tests,
92
- ts,
93
- }: ResolveTestConfigComponentsOptions) => {
203
+ ts = false,
204
+ }: ResolveTestConfigComponentsOptions): ResolveTestConfigComponentsResult => {
94
205
  if (testConfig) {
95
206
  return {
96
207
  doingTestsSetup: true,
@@ -109,8 +220,8 @@ const resolveTestConfigComponents = ({
109
220
  doingTestsSetup: false,
110
221
  testConfig: resolveTestConfig({
111
222
  tests,
112
- productionConfig: config,
113
- productionTs: ts ?? false,
223
+ productionEnv: env,
224
+ productionTs: ts,
114
225
  }),
115
226
  };
116
227
  };
@@ -119,8 +230,9 @@ const resolveConfigComponents = (
119
230
  options: ResolveConfigComponentsOptions,
120
231
  ): ResolveConfigComponentsResult => {
121
232
  const {
122
- configs,
233
+ env: environment,
123
234
  extensions,
235
+ globals: globalVars,
124
236
  jsdoc,
125
237
  nextjs,
126
238
  react,
@@ -132,22 +244,23 @@ const resolveConfigComponents = (
132
244
 
133
245
  const baseExtensionFragment = baseExtensionFragmentOption ?? extensions?.base;
134
246
 
135
- const config = getConfig(options.configs);
247
+ const env = getEnvironment(environment);
136
248
 
137
249
  const { doingTestsSetup, testConfig } = resolveTestConfigComponents({
138
- config,
250
+ env,
139
251
  testConfig: options.testConfig,
140
252
  tests: options.tests,
141
253
  ts,
142
254
  });
143
255
 
144
- const fragments: ConfigFragment[] = [baseFragment, extensionFragments[config]];
256
+ const fragments: ConfigFragment[] = [baseFragment, extensionFragments[env]];
257
+ const resolvedGlobals = resolveGlobals({ globals: globalVars, doingTestsSetup });
145
258
 
146
259
  const plugins = ['import'];
147
260
 
148
261
  const configGlobals: Array<Record<string, boolean | string>> = [];
149
262
 
150
- if (configs.includes('node')) {
263
+ if (env === 'node') {
151
264
  plugins.push('node');
152
265
  }
153
266
 
@@ -190,8 +303,8 @@ const resolveConfigComponents = (
190
303
  fragments.push(baseExtensionFragment);
191
304
  }
192
305
 
193
- if (extensions?.[config]) {
194
- fragments.push(extensions[config]);
306
+ if (extensions?.[env]) {
307
+ fragments.push(extensions[env]);
195
308
  }
196
309
 
197
310
  if (jsdoc && extensions?.jsdoc) {
@@ -207,8 +320,9 @@ const resolveConfigComponents = (
207
320
  }
208
321
 
209
322
  return {
210
- config,
323
+ env,
211
324
  fragments,
325
+ globals: resolvedGlobals,
212
326
  plugins,
213
327
  testConfig,
214
328
  configGlobals:
@@ -217,18 +331,31 @@ const resolveConfigComponents = (
217
331
  };
218
332
 
219
333
  const createTestOverride = ({
334
+ parentEnv,
220
335
  testConfig,
336
+ allowedDangleNames,
221
337
  extensions,
338
+ extraRules = [],
339
+ globals: globalVars,
222
340
  }: CreateTestOverrideOptions): GeneratedConfigOverride => {
223
- const { config, fragments, configGlobals } = resolveConfigComponents({
224
- configs: testConfig.configs,
341
+ const {
342
+ env,
343
+ fragments,
344
+ globals: resolvedGlobals,
345
+ configGlobals,
346
+ } = resolveConfigComponents({
347
+ env: testConfig.env,
225
348
  extensions,
349
+ globals: globalVars,
226
350
  testConfig,
227
351
  ts: testConfig.ts,
228
352
  baseFragment: extensionFragments.tests,
229
353
  baseExtensionFragment: extensions?.tests,
230
354
  });
231
- const rules = mergeRules(...fragments);
355
+ const rules = allowDangleNames(
356
+ mergeRuleSettings(fragments, extraRules),
357
+ allowedDangleNames,
358
+ );
232
359
 
233
360
  if (testConfig.files.length === 0) {
234
361
  throw new Error('Custom test configuration requires at least one file glob.');
@@ -237,29 +364,132 @@ const createTestOverride = ({
237
364
  return {
238
365
  files: testConfig.files,
239
366
  globals: {
240
- ...extensionFragments[config].globals,
367
+ ...disableGlobals(parentEnv),
368
+ ...extensionFragments[env].globals,
369
+ ...resolvedGlobals,
241
370
  ...configGlobals,
242
371
  },
243
372
  rules,
244
373
  };
245
374
  };
246
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
+
247
433
  /** Creates a native Oxlint configuration from one environment and optional policy layers. */
248
434
  export const createConfig = (options: CreateConfigSettings): GeneratedConfig => {
249
- const { fragments, plugins, config, testConfig } = resolveConfigComponents(options);
250
- const { extensions, ignores, typeAware } = options;
435
+ const {
436
+ fragments,
437
+ globals: resolvedGlobals,
438
+ plugins: resolvedPlugins,
439
+ env,
440
+ testConfig,
441
+ } = resolveConfigComponents(options);
442
+ const { allowedDangleNames, extensions, ignores, typeAware } = options;
443
+ const { configOverrides, ruleOverrides } = resolveUserOverrides(options.overrides);
444
+ const generatedOverrides: GeneratedConfigOverride[] = [];
445
+ const plugins = resolvedPlugins.slice();
446
+
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
+ }
251
477
 
252
- const rules = mergeRules(...fragments);
478
+ const rules = allowDangleNames(
479
+ mergeRuleSettings(fragments, ruleOverrides),
480
+ allowedDangleNames,
481
+ );
253
482
 
254
483
  return {
255
484
  globals: {
256
485
  ...extensionFragments.base.globals,
257
- ...extensionFragments[config].globals,
486
+ ...extensionFragments[env].globals,
487
+ ...resolvedGlobals,
258
488
  },
259
489
  ignorePatterns: [...DEFAULT_IGNORES, ...(ignores || [])],
260
490
  options: typeAware ? { typeAware: true } : undefined,
261
491
  plugins,
262
492
  rules,
263
- overrides: testConfig ? [createTestOverride({ testConfig, extensions })] : undefined,
493
+ overrides: generatedOverrides.length > 0 ? generatedOverrides : undefined,
264
494
  };
265
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,17 +1,13 @@
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
- ExtensionFragmentName,
14
8
  ExtensionFragments,
9
+ GlobalVarMode,
10
+ GlobalVars,
15
11
  GeneratedConfig,
16
12
  GeneratedConfigOverride,
17
13
  RuleSettings,
@@ -19,10 +15,4 @@ export type {
19
15
  TestFramework,
20
16
  TestsOption,
21
17
  } from './types.js';
22
-
23
- /** Creates a native Oxlint configuration with React and JSX accessibility policy. */
24
- export const createReactConfig = (options: CreateReactConfigOptions) =>
25
- createConfig({
26
- ...options,
27
- react: true,
28
- });
18
+ export type { ExtensionFragmentName, ConfigEnv } from './consts.js';
package/src/rules/base.ts CHANGED
@@ -1,4 +1,11 @@
1
- const maxComplexity = 30;
1
+ export const maxComplexity = 30;
2
+ export const noUnderscoreDangleOptions = {
3
+ allow: ['__'],
4
+ allowAfterThis: true,
5
+ allowAfterSuper: true,
6
+ enforceInMethodNames: false,
7
+ };
8
+ export const noUnderscoreDangleRule = ['error', noUnderscoreDangleOptions] as const;
2
9
 
3
10
  export const base = {
4
11
  /**
@@ -452,6 +459,12 @@ export const base = {
452
459
  * @see https://eslint.org/docs/latest/rules/no-useless-concat
453
460
  */
454
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',
455
468
  /**
456
469
  * Disallow redundant return statements.
457
470
  *
@@ -822,15 +835,7 @@ export const base = {
822
835
  *
823
836
  * @see https://eslint.org/docs/latest/rules/no-underscore-dangle
824
837
  */
825
- 'no-underscore-dangle': [
826
- 'error',
827
- {
828
- allow: ['__'],
829
- allowAfterThis: true,
830
- allowAfterSuper: true,
831
- enforceInMethodNames: false,
832
- },
833
- ],
838
+ 'no-underscore-dangle': noUnderscoreDangleRule,
834
839
  /**
835
840
  * Disallow ternary operators when simpler alternatives exist.
836
841
  *
@@ -1,4 +1,4 @@
1
- export { base } from './base.js';
1
+ export * from './base.js';
2
2
  export { browser } from './browser.js';
3
3
  export { jsdoc } from './jsdoc.js';
4
4
  export { nextjs } from './nextjs.js';